---
# System prepended metadata

title: Vuex 狀態管理與Nginx 反向代理
tags: [資料庫系統]

---

---
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（元件或頁面）就會跟著改變，就是這一連串的行為是不可逆的，因此稱為：單向資料流。

![](https://i.imgur.com/w4EM9pv.png)


其中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;
}
```

---