--- title: 'VueJS 2.0 教學筆記: Vuex 狀態管理' disqus: hackmd --- VueJS 2.0 教學筆記: Vuex 狀態管理 === 綱要 [TOC] 為什麼需要狀態管理 --- 當你的`data()`中關於布林值、特定字元、數值用來改變 UI 狀態的變數定義太多,開始在維護上感到有些吃力或覺得太過冗長,便需要一個全局狀態管理的機制來幫助你將這些狀態拆分出來。並在這些狀態改變時,一併將接下來需要執行的程式內容按著這些變化,判別該回傳什麼樣的資料。 而**Vuex中儲存的狀態經過Refresh後會還原**的關係,必須視情況來決定要不要將資料寫入State的同時也記錄在`LocalStorange`或`Cookies` Vuex Data Flow --- ![](https://i.imgur.com/riv00cC.png) Vue CLI 架設好新專案後,在store路徑下會看見: ```javascript= import Vue from 'vue' import Vuex from 'vuex' Vue.use(Vuex) const store = new Vuex.Store({ state: { }, gatters: { }, mutations: { }, actions: { } }) export default store ``` Vuex 所使用的有: ==**State**== 用來存放狀態值 ==**Getters**== 用來在外部取得`State`中的值 ==**Mutations**== 對`State`發起`Mutate`,更動`State`中的值 ==**Action**== 對`Mutation`提交`commit`後,執行對象的Mutate 封裝Vuex的方法 --- 前面說過為了實踐拆分store,用來檔案管理不同頁面的狀態,在這裡需要進行二次封裝。 首先在store路徑下,先建立一個`modules`的路徑,以及裡頭的`index.js`: ![](https://i.imgur.com/w5yaY7Y.png) **modules/index.js** ```javascript= // 引入lodash中的camelCase,稍後會使用到它來過濾所有的檔案名稱,並轉為駝峰命名 import camelCase from 'lodash/camelCase' // 檢查載進來的modules檔案是否都是any.js const requireModule = require.context('.', false, /\.js$/) const modules = {} // 遍歷requireModule中所有Modules Name requireModule.keys().forEach(fileName => { // 如果是index.js就跳出遍歷 if (fileName === './index.js') return // 使不同命名方式的檔案載進來後,最後定義的變數都能變成駝峰式命名 // 簡單的說就是把 import { abc_def } from './path' 變成 import { abcDef } const moduleName = camelCase( fileName.replace(/(\.\/|\.js)/g, '') ) modules[moduleName] = { ...requireModule(fileName).default } }) export default modules ``` **store/index.js** 接著將這個封裝好的`modules`置入`store`下的`index.js` ```javascript= import Vue from 'vue' import Vuex from 'vuex' import modules from './modules' // 引入modules Vue.use(Vuex) const store = new Vuex.Store({ modules, // 放到這裡 state: {}, getters: {}, mutations: {}, actions: {} }) export default store ``` 以後只需要把新的store檔案加入modules路徑即可。 用Vuex來建置一個迷你型的文字冒險遊戲 --- 在`store/modules`下建立一個`temp.js` 這個例子當中我們用選項之間的加總計分來決定最後結局會跑哪一個,所以需要幾個狀態: 1. total:分數加總 2. flag:取得的flag 3. epilogue:結局 [Github範例](https://github.com/fortes1219/vue_0803/blob/master/src/store/modules/temp.js) ```javascript= const state = { total: 0, flag: '', epilogue: '' } ``` 然後為了讓`mapGetters()`可以被外部調用,需要定義`getters` ```javascript= // store/modules/temp.js const getters = { mapGetTotal: state => { console.log('vuex total: ', state.total) return state.total }, mapGetFlag: state => { console.log('vuex flag: ', state.flag) return state.flag }, mapGetEpilogue: state => { console.log('vuex epilogue: ', state.epilogue) return state.epilogue } } ``` 設置`Mutations`和`Actions` ```javascript= const mutations = { changeTotal(state, val) { state.total = val console.log('vuex total is: ', state.total) }, // 選項的總分區間來決定 FLAG changeFlag(state, val) { if (state.total <= 5) { val = 'a' } else if (state.total > 5 && state.total <= 8) { val = 'b' } else if (state.total > 8 && state.total <= 12) { val = 'c' } else if (state.total > 12) { val = 'd' } state.flag = val }, // 根據 FLAG 來決定結局走哪一條線 changeEpilogue(state) { switch (state.flag) { case 'a': state.epilogue = '日常 END' break case 'b': state.epilogue = 'True END' break case 'c': state.epilogue = '聖堂教会 END' break case 'd': state.epilogue = 'イリヤ添い寝 END' break } } }, const actions = { setFlag(val) { val.commit('changeFlag') } } export default { state, getters, mutations, actions } ``` 完成了Vuex的基本設置,我們先把頁面component建立起來,之後再回過頭來處理`Mutations` **VuexTemp.vue** [Github範例: VuexTemp.vue](https://github.com/fortes1219/vue_0803/blob/master/src/components/home/VuexTemp.vue) ```htmlmixed= <template> <div class="page vuex_temp"> <h1>冬木市で聖杯戦争の痕跡を探し出せ!</h1> <br /> <div class="row vertical"> <!-- <p data-space-next="1rem">{{ mapGetMsg }}</p><el-button @click="setMsg">Get State</el-button> --> <p>いつ頃</p> <br /> <el-radio-group v-model="flagItem.selectA" @change="onRadioChange"> <el-radio-button v-for="(item, index) in flagItem.partA" :key="index" :label="item.label" >{{ item.name }}</el-radio-button> </el-radio-group> <br /> <p>どこへ</p> <br /> <el-radio-group v-model="flagItem.selectB" @change="onRadioChange"> <el-radio-button v-for="(item, index) in flagItem.partB" :key="index" :label="item.label" >{{ item.name }}</el-radio-button> </el-radio-group> <br /> <p>誰と</p> <br /> <el-radio-group v-model="flagItem.selectC" @change="onRadioChange"> <el-radio-button v-for="(item, index) in flagItem.partC" :key="index" :label="item.label" >{{ item.name }}</el-radio-button> </el-radio-group> </div> <br /> <el-button @click="$router.push({name: 'VuexDetail'})">結果へ</el-button> <div class="row vertical v_center" data-space="space-vertical"> <p>スコア:{{ mapGetTotal }}</p><br> <p>フラグ:{{ mapGetFlag }}</p><br> <p>可能なエンディング:{{ mapGetEpilogue }}</p> </div> </div> </template> ``` ```javascript= <script> export default { data() { return { flagItem: { selectA: "", selectB: "", selectC: "", partA: [ { label: 0, name: "朝で" }, { label: 1, name: "昼で" }, { label: 2, name: "夜で" } ], partB: [ { label: 1, name: "衛宮家" }, { label: 8, name: "私立穂群原学園" }, { label: 3, name: "柳洞寺" }, { label: 5, name: "聖堂教会" }, { label: 2, name: "アインツベルン城" } ], partC: [ { label: 1, name: "セイバー" }, { label: 6, name: "大河先生" }, { label: 2, name: "遠坂 凛" }, { label: 3, name: "間桐 桜" }, { label: 0, name: "イリヤスフィール" } ] } }; }, } </script> ``` 為了顯示可能的路徑和當前豎起的FLAG,我們需要`computed`來幫助獲取`mapGetters`的內容 並且將當前分數的加總設置一個新的計算屬性`getTotalScore()` ```javascript= computed: { ...mapGetters(["mapGetTotal", "mapGetFlag", "mapGetEpilogue"]), getTotalScore() { let result = this.flagItem.selectA + this.flagItem.selectB + this.flagItem.selectC return result } } ``` 最後,由`mapActions`來提交一個`setFlag`、`mapMutations`則用來變更總分以及結局的狀態 ```javascript= methods: { ...mapActions(["setFlag"]), ...mapMutations(["changeTotal", "changeFlag", "changeEpilogue"]), onRadioChange() { this.changeTotal(this.getTotalScore) this.setFlag() this.changeEpilogue() } } ``` **VuexTempDetail.vue** [Github範例: VuexTempDetail.vue](https://github.com/fortes1219/vue_0803/blob/master/src/components/home/VuexTempDetail.vue) 這個檔案我們用來顯示結局圖片和總分、flag等資料 注意一下,這裡的文字資訊欄位可以使用自己在`data()`定義的參數,並且`create()`階段就讓`mapGetters`取回的`state`寫入`data()` ```htmlmixed= <template> <div class="page vuex_temp"> <h1>Vuex Detail</h1> <br> <el-button @click="$router.go(-1)">Go Back</el-button> <br> <div class="row vertical" data-space="space-vertical"> <p>スコア:{{ total }}</p><br> <b>フラグ:{{ flag }}</b><br> <b>エンディング:{{ endding }}</b><br> <div v-if="flag == 'a'"> <img src="img/end_1.jpg" style="width: 100%;"> </div> <div v-if="flag == 'b'"> <img src="img/end_2.jpg" style="width: 100%;"> </div> <div v-if="flag == 'c'"> <img src="img/end_3.jpg" style="width: 100%;"> </div> <div v-if="flag == 'd'"> <img src="img/end_4.jpg" style="width: 100%;"> </div> </div> </div> </template> ``` ```javascript= <script> import { mapState, mapGetters, mapActions, mapMutations } from 'vuex' export default { name: "VuexTemp", data() { return { total: 0, flag: '', endding: '' } }, computed: { ...mapGetters([ 'mapGetTotal', 'mapGetFlag', 'mapGetEpilogue' ]) }, methods: { ...mapActions([ 'setMsg' ]), ...mapMutations([ 'changeMsg' ]) }, created() { this.total = this.mapGetTotal this.flag = this.mapGetFlag this.endding = this.mapGetEpilogue } }; </script> ``` 大致上完成後頁面會長這樣: ![](https://i.imgur.com/vzushNz.png) ![](https://i.imgur.com/a9uGlsu.png) 為何不直接調用 Mutations 就好? --- `Mutations` 本身是同步處理,當有特定動作需要戳 API 等待回應或者運算資源消耗較多的功能時,我們會需要用到非同步處理,`Actions` 就是為了此提交 commit 給 `Mutations`。 所以建議盡量還是使用官方提供的流程,由 `Actions` 去呼叫`Mutations`、再由`Mutations`改變`State`的內容喔。 ###### tags: `VueJS` `Vuex`