---
title: 'Vuex 狀態管理與Nginx 反向代理'
disqus: hackmd
---
Vuex 狀態管理與Nginx 反向代理
===
###### tags: `資料庫系統`
## 本章節目標
* Vue Router Navigation Guards
* Vuex 介紹
* Vuex 實驗基礎 (commit & mutations)
* Nginx 反向代理 - 基礎
* Vue.js 網頁前端編譯和壓縮 (應用於production環境)
* 使用 Nginx 做 Load Balancer 實驗
---
## Vue Router Navigation Guards
在使用Vue.js時,會用到vue-router進行路由管理。為了在用戶訪問每個頁面之前能判斷是否有該頁面的權限(例如獲得JWT的token),我們可以用vue-router的beforeEach方法,來定義一個方法進行權限判斷,決定允許或拒絕用戶訪問,或者是跳轉到登錄界面。
[官方Navigation Guards介紹](https://router.vuejs.org/guide/advanced/navigation-guards.html#global-before-guards)
```javascript=
// main.js
import Vue from 'vue'
import App from './App'
import router from './router'
import Element from 'element-ui'
import 'element-ui/lib/theme-chalk/index.css'
import locale from 'element-ui/lib/locale/lang/zh-TW'
import axios from 'axios'
Vue.use(Element, { locale })
Vue.config.productionTip = false
Vue.prototype.$axios = axios
// 增加 claims的判斷
router.beforeEach((to, from, next) => {
console.log(to)
console.log(from)
const claims = localStorage.getItem('claims')
if (!claims && to.path !== '/login') {
next('/login')
} else {
// if (claims.exp < new Date().getTime()/1000) {
// localStorage.removeItem('claims')
// next('/login')
// } else {
// next()
// }
next()
}
})
/* eslint-disable no-new */
new Vue({
el: '#app',
router,
components: { App },
template: '<App/>'
})
```
LocalStorage:
備註:以上範例是一個簡單展示的登入功能,所以並沒有在安全性上做任何設定,要注意儲存在瀏覽器中的資料不能是機敏資訊,否則會有安全性上的問題。
---
## Vuex 介紹
到目前學習Vue.js為止,可以觀察到資料都是存放在各自 components(或page) 的 data 裡,進而完成一個頁面呈現。在一個小小專案當中還看不到問題,但在一個大型系統當中,重複使用到的資料一定很多,而且可能存放在各個不同的components,這樣各個頁面切換之間如何拿到同一筆資料? 像是驗證token、使用者資訊 (user profile)、購物車(shopping cart)等等。
分久必合,合久必分,因此我們需要一個集中所有資料的地方:"store"
#### 單向資料流設計模式 (Flux design petter)
[延伸閱讀:從 Flux 與 MVC 的差異來簡介 Flux](https://blog.techbridge.cc/2016/04/29/introduce-flux-from-flux-and-mvc/)
> Flux是一種讓你很容易做到 one-way data flow 的概念,讓你View中的每個component的state都能夠predictable
Vue + flux = vuex
Who use vuex ?
* 大型專案都會建議使用
* React Flux 框架有:Flux, Redex
* 統一管理應用程式所有狀態
* 官方推薦項目之一
Vuex 是專為 Vue.js 做「狀態管理」模式套件,它作為應用程序中所有組件「集中」存儲區,並在規則下確保只能以可預測的方式對狀態進行變更。
#### 在vue-todo專案底下安裝 vuex
```shell=
npm install vuex --save
```
下圖為vuex 官方流程圖:所有的動作都是從 action 出發,接著到了 store 把結果存起來改變 state的值 (所有狀態變數都是在這邊做儲存) 然後因為 state 改變了,所以 view(元件或頁面)就會跟著改變,就是這一連串的行為是不可逆的,因此稱為:單向資料流。

其中action、mutations和state實際運做為何,我們直接用實做的方式來實驗,首先先來套用 vuex 框架:
在 src 下,新增一個 store 的資料夾,裡面分別有5隻.js檔案。
├─src
│ ├─components
│ └─store
│ ├─actions.js
│ ├─getters.js
│ ├─index.js
│ ├─mutations.js
│ ├─state.js
```javascript=
// index.js
import Vue from 'vue'
import Vuex from 'vuex'
import state from './state'
import actions from './actions'
import mutations from './mutations'
import getters from './getters'
Vue.use(Vuex)
const store = new Vuex.Store({
state,
actions,
mutations,
getters
})
export default store
```
actions.js、getters.js、mutations.js、state.js都先給空物件
```javascript=
export default {
}
```
在main.js當中 加入store
```javascript=
import store from './store'
//---
new Vue({
el: '#app',
router,
store,
components: { App },
template: '<App/>'
})
```
---
## Vuex 實驗基礎 (commit & mutations)
我們在Main.Vue的header中增加一個欄位顯示count 數據
```htmlmixed=
<el-header style="text-align: right; font-size: 12px">
<h2 style="display: inline">count: {{ count }}</h2>
<a href="javascript:void(0)" @click.prevent="onLogoutClick">登出</a>
</el-header>
```
在TodoList.vue當中增加兩個按鈕,用來做數值的增加和減少
```htmlmixed=
<el-row>
<el-button type="primary" @click="onCreateTodoClick($event)">新增待辦事項</el-button>
<el-button type="primary" @click="onDonwloadTodoClick($event)">下載Excel</el-button>
<el-button type="primary" @click="onIncreaseClick($event)">+</el-button>
<el-button type="primary" @click="onDecreaseClick($event)">-</el-button>
</el-row>
methods: {
onIncreaseClick (event) {
},
onDecreaseClick (event) {
},
}
```
試想,我們現在點集增加或減少按鈕,要怎樣更新到Main.vue中的count值呢?
在 state.js中增加 count參數
```javascript=
export default {
count: 0
}
```
在Main.js中增加
```javascript=
...mapState({
count: state => state.count
})
```
透過 mapState來取的state.js中的 count變數
```javascript=
// Main.js
<template>
<el-container style="height: 100%; border: 1px solid #eee">
<el-aside width="200px">
<el-menu :router="true" default-active="/todo/list" background-color="#545c64" text-color="#fff" active-text-color="#ffd04b">
<el-submenu index="1">
<template slot="title"><i class="el-icon-message"></i>功能選項</template>
<el-menu-item-group>
<el-menu-item index="/todo/list">事項列表</el-menu-item>
</el-menu-item-group>
</el-submenu>
</el-menu>
</el-aside>
<el-container>
<el-header style="text-align: right; font-size: 12px">
<h2 style="display: inline">count: {{ count }}</h2>
<a href="javascript:void(0)" @click.prevent="onLogoutClick">登出</a>
</el-header>
<el-main>
<router-view/>
</el-main>
</el-container>
</el-container>
</template>
<script>
import { mapState } from 'vuex'
export default {
data () {
return {
// count: 0
}
},
methods: {
onLogoutClick () {
console.log('login')
localStorage.removeItem('claims')
this.$router.replace({name: 'Login'})
}
},
computed: {
...mapState({
count: state => state.count
})
}
}
</script>
<style>
.el-header {
background-color: #B3C0D1;
color: #333;
line-height: 60px;
}
.el-aside {
color: #333;
position: relative;
float: left;
top: 0px;
bottom: 0px;
background-color: rgb(238, 241, 246);
height: 100vh;
}
</style>
```
[mapState語法說明參考](https://vuex.vuejs.org/zh/guide/state.html)
在mutations.js當中增加兩個方法,用來變動state count的值
```javascript=
export default {
increase (state, payload) {
state.count += payload.num
},
decrease (state, payload) {
state.count -= payload.num
}
}
```
修改TodoList的按鈕事件
```javascript=
onIncreaseClick (event) {
this.$store.commit('increase', {num: 1})
},
onDecreaseClick (event) {
this.$store.commit('decrease', {num: 1})
},
```
---
### Nginx 反向代理 - 基礎
反向代理會根據用戶端(瀏覽器)的請求,從後端的伺服器(如Web伺服器)上取得資源,然後再將這些資源返回給用戶端,用戶端通過反向代理存取不同後端伺服器上的資源,而不需要知道這些後端伺服器的存在,而以為所有資源都來自於這個反向代理伺服器。
在實作的部分,流程上大致是:
1. 架設一個Web Server (可以用nodejs, python, etc),並在某個port作起動 (e.g. port 3000)
2. 安裝 Nginx
3. 撰寫Nginx反向代理相關設定(reverse proxy)
在 nginx 的設定資料夾(/etc/nginx)中,有兩個資料夾分別是sites-available和sites-enabled
修改設定 (設定檔預設路徑: /etc/nginx/sites-available/default)
```shell=
sudo vim /etc/nginx/sites-available/default
```
```nginx=
sudo vim /etc/nginx/sites-available/default
```
nginx 的設定檔案是一種巢狀的格式,左邊為 key 中間用空白隔開,右邊為 value,最後分號是一個段落的結束。
增加 location /api/v1 的路徑配置
```nginx=
server_name _;
location /api/v1 {
proxy_pass http://127.0.0.1:8090;
proxy_http_version 1.1;
proxy_set_header Host $host;
}
location / {
# First attempt to serve request as file, then
# as directory, then fall back to displaying a 404.
try_files $uri $uri/ =404;
}
```
其中proxy_pass 是反向代理的Web Server位置,這裡設定的是伺服器本地端的nodejs express server http://127.0.0.1:8090
設定完成後有兩種方法讓設定生效:
```shell=
# 當沒有請求時,重新加載配置文件,很安全。
sudo service nginx reload
#強制重新啟動nginx,相當於強制關閉nginx,所有正在請求的uri將會流失。
sudo service nginx restart
```
接著使用流覽器打開 (不需要port, IP請換成你自己的IP位子),看是否有抓到回應
http://IP/api/v1/todos
---
## Vue.js 網頁前端編譯和壓縮 (應用於production環境)
在網頁前端專案中 (vue-todo)執行 npm run build 指令
```shell=
npm run build
```
接著會看到專案開始進行 building for production...
```shell=
> vue-todo@1.0.0 build /home/xiaoding_edu/vue-webapp/vue-todo
> node build/build.js
⠴ building for production...^C
xiaoding_edu@nkust-ding01:~/vue-webapp/vue-todo$ npm run build
> vue-todo@1.0.0 build /home/xiaoding_edu/vue-webapp/vue-todo
> node build/build.js
Hash: 5d1afd606818cff11c7d
Version: webpack 3.12.0
Time: 84969ms
Asset Size Chunks Chunk Names
static/css/app.0b9d14cd33baeb731b64061fc2bedcbe.css 198 kB 2 [emitted] app
static/fonts/element-icons.535877f.woff 28.2 kB [emitted]
static/js/0.19fbb46a7452c2d96f21.js 689 kB 0 [emitted] [big]
static/js/vendor.5b228d7fa835c667f692.js 760 kB 1 [emitted] [big] vendor
static/js/app.2900f5819ba61884f569.js 9.22 kB 2 [emitted] app
static/js/manifest.01e2e25742d0856d64e7.js 1.46 kB 3 [emitted] manifest
static/fonts/element-icons.732389d.ttf 56 kB [emitted]
static/css/app.0b9d14cd33baeb731b64061fc2bedcbe.css.map 290 kB [emitted]
static/js/0.19fbb46a7452c2d96f21.js.map 2.14 MB 0 [emitted]
static/js/vendor.5b228d7fa835c667f692.js.map 3 MB 1 [emitted] vendor
static/js/app.2900f5819ba61884f569.js.map 41.5 kB 2 [emitted] app
static/js/manifest.01e2e25742d0856d64e7.js.map 7.75 kB 3 [emitted] manifest
index.html 510 bytes [emitted]
Build complete.
Tip: built files are meant to be served over an HTTP server.
Opening index.html over file:// won't work.
```
如果專案沒有任何bug,以及通過語法測試,會出現 Build complete.就代表完成了
執行build指令後,會將專案編譯和壓縮,並將構建完成的檔案產生至專案項目下的 dist 目錄中,因此,可直接將dist/底下的所有檔案複製到網站伺服器的根目錄中即可 (例如Nginx預設的目錄/var/www/html/)
目前路徑/var/www/html/因該有下列三個檔案or資料夾
```shell=
-rw-r--r-- 1 root root 612 May 15 13:28 index.nginx-debian.html
-rw-r--r-- 1 root root 20 May 15 13:33 info.php
lrwxrwxrwx 1 root root 21 May 15 13:57 phpmyadmin -> /usr/share/phpmyadmin
```
除了phpmyadmin 是我們資料庫管理需要的工具之外,index.nginx-debian.html和info.php兩個檔案可以先移除
```shell=
sudo rm /var/www/html/index.nginx-debian.html
sudo rm /var/www/html/info.php
```
將 dist/index.html 和dist/static 資料夾複製到 /var/www/html/
```shell=
sudo cp -r dist/* /var/www/html/
```
可以在package.json中的scripts中增加 deploy指令
```json=
"deploy": "rm -rf /var/www/html/index.html && rm -rf /var/www/html/static/* && cp -r dist/* /var/www/html/"
```
>前端網業的axios url參數可以直接改成 http://IP/api/v1/ (不需要有port的參數了!因為nginx會將 /api/v1 的url導到 8090 port)
---
## 使用 Nginx 做 Load Balancer 實驗
當服務流量達到一定程度時,一台 Web Server 可能會有一些風險,例如因不夠力而導致服務中斷,或是想要在服務沒有中止的情況下更新應用程式,這時就需要 Load Balancer。
Nginx 也有內建的 Load Balancer的相關設定,並支援三種模式:
* round-robin:標準輪詢方式(預設)
* least-connected:當連線進來時會把 Request 導向連線數較少的 Server
* ip-hash:依據 Client IP 來分配到不同台 Server
再次修改nginx的設定 (設定檔預設路徑: /etc/nginx/sites-available/default)
頂部多增加一個upstream的定義
```nginx=
upstream webserver {
server 127.0.0.1:8090;
server 127.0.0.1:8091;
}
```
然後將原本 Nginx location /api/v1的配置改成如下:
```nginx=
location /api/v1 {
proxy_pass http://webserver;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
```
設定檔/api/v1有幾個地方不一樣:
1. 第一個是導向的網址是在 upstream 所自訂的名稱 “webserver”
2. 另外設定三個 proxy_set_header是因為透過 Load Balancer 重導request後,對 於兩台Web Server 1 以及 Web Server 2 來說,會拿到Load Balancer 的 IP而不是真正用戶端的IP,所以利用 proxy_set_header 將真正的 Client IP 設定在 Header
#### 測試
經過以上簡單的設定後,一個 Load Balancer 機制就完成了,我們可以在專案資料夾中再創建一個server2,把server內除了 node_modules資料夾和package-lock.json檔案之外複製到server2資料夾當中,如下
```shell=
xiaoding_edu@nkust-ding01:~/vue-webapp/server2$ ls -l
total 48
-rw-r--r-- 1 xiaoding_edu xiaoding_edu 221 May 19 14:23 conf.js
drwxr-xr-x 2 xiaoding_edu xiaoding_edu 4096 Jun 5 02:49 config
-rw-r--r-- 1 xiaoding_edu xiaoding_edu 1888 Jun 5 08:59 index.js
-rw-r--r-- 1 xiaoding_edu xiaoding_edu 407 Jun 5 02:31 package.json
drwxr-xr-x 2 xiaoding_edu xiaoding_edu 4096 Jun 4 08:58 routes
xiaoding_edu@nkust-ding01:~/vue-webapp/server2$
```
記得要先在server2資料夾下使用 npm install安裝套件
```shell=
npm install
```
在server2/index.js當中增加一個/api/v1/test的Get,並回應 Hello Server2,
最後PORT改成8091
```javascript=
// -
app.get('/api/v1/test', function (req, res) {
res.send('Hello Server2!');
});
// ---
app.listen(8091, function () {
console.log('Example app listening on port 8091!');
})
```
在server/index.js當中增加一個/api/v1/test的Get ,並回應 Hello Server1
```javascript=
// -
app.get('/api/v1/test', function (req, res) {
res.send('Hello Server1!');
});
```
接著重啟Nginx進行測試
> 注意: 若流覽器沒有變化可能為暫存有關,可開無痕測試
>
也可以自行設定權重(weight),讓效能較好的 Server 吃比較更多的負載,另外也可以將round-robin的方式改成least_conn的平衡方式
```nginx=
upstream webserver {
least_conn;
server 127.0.0.1:8090 weight=3;
server 127.0.0.1:8091 weight=2;
}
```
---