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