[Vue.js] 筆記 - Vuex

筆記 - Vuex

Vuex是針對 Vue.js,實現不同組件間共享狀態的工具,比如說,當使用者登入後,如何讓各個組件共用“登入者名稱“這個資訊,又或是購物平台上購物車的實現,可以使用Vuex簡單的實現目的.

範例使用 vue-cli 建立,對官網的教學文章筆記下Vuex的使用方式

vue create vuexsample
  1. State - store 內儲存的狀態
  1. mutations - 相檔於 method
    • 可以直接操作 state
    • 只能處理同步方法
  1. getters - 相當於 computed
    • 可以直接操作 state
  1. actions - 也是類似 method
    • 可以處理非同步方法
    • 無法直接修改 state,必須透過 mutations
    • 可以取得當前的 state(store or module)

     

Vuex 基本使用


安裝 vuex

npm i vuex -s

建立一個 store.js,用來存放 Vuex 相關內容

  1. 必須加上 Vue.use(Vuex),若不是使用webpack之類的打包方式,可以不用這行
        import Vuex, {Store} from "vuex";
        import Vue from "vue";
        
        Vue.use(Vuex);
        
        const store = new Store({
            state: {
                count: 0
            },
        });
        
        export default store;

     

在建立 vue 實體時把 store 一併給入

    import Vue from 'vue'
    import App from './App.vue'
    import store from "./Store";
    
    Vue.config.productionTip = false
    
    new Vue({
        store,
        render: h => h(App),
    }).$mount('#app') 

調整 store.js 加上 state 和 mutatuins,稍後會在 .vue 使用到

  1. State 相當於在 vuex 上保存的變數
  1. mutations 相當於 method,負責修改 state
    • mutations 的第一個參數是 state,用來存取 store 內的 state
const store = new Store({
        state: {
            count: 0
        },
        mutations: {
            add(state) {
                state.count++;
            }
        }
    });

在 .vue 上開始使用 vuex

  1. 前面在 new Vue() 時傳入了 store進去,因此底下的各個組件都可以透過 this.$store 存取 store 的內容
  1. store 內的 state 可以使用 this.$store.state.count,從取得 count 的值
  1. store 內的 mutations 可以使用 this.$store.commit("<name>") 來呼叫
<template>
        <div id="app">
            <div>count: {{count}}</div>
            <button @click="add">Add</button>
        </div>
    </template>
    
    <script>
        export default {
            name: 'App',
            computed: {
                count() {
                    return this.$store.state.count;
                }
            },
            methods: {
                add() {
                    this.$store.commit("add");
                }
            }
        }
    </script>

現在已經完成簡單的計數器,也可以透過 DevTools 看到 vuex 內的內容

接著為 add 添加一個參數,讓計數器不是累加1

  • mutations 的第二個參數是使用端傳入的參數,這邊有幾種寫法
1. 
    // store.js
    mutations: {
            add(state, count) {
                state.count += count;
            }
        }
    
    // .vue
    methods: {
                add() {
                    this.$store.commit("add", 2);
                }
            }
2.
    // store.js
    mutations: {
            add(state, payload) {
                state.count += (payload.count);
            }
        }
     
    // .vue
    methods: {
                add() {
                    this.$store.commit("add", {
                                        count: 2
                                    });
                }
            }

​
3.
    // store.js
    mutations: {
            add(state, payload) {
                state.count += (payload.count);
            }
        }
     
    // .vue
    methods: {
                add() {
                    this.$store.commit({
                                        type: "add"
                                        count: 2
                                    });
                }
            }

​

Getters

若某些邏輯是必須對 state 運算取得的,可以使用 getters

  • 第一個參數是 state
  • 透過 this.$store.getters.<name> 取得
// store.js
        getters: {
            length(state) {
                return state.count + 100;
            }
        }
// .vue
            computed: {
                length() {
                    return this.$store.getters.length;
                }
            },

gatters 第二個參數是 getters,代表可以從裡面取得其他的 getter

        getters: {
            length(state, getters) {
                return state.count + getters.test + 100;
            },
            test(state) {
                return state.count + 5;
            }
        }

getters 也可以返回一個方法

        getters: {
            length(state, getters) {
                return state.count + getters.test + getters.func(0) + 100;
            },
            test(state) {
                return state.count + 5;
            },
            func() {
                return (value) => value + 50;
            }
        }

actions

類似於 method,跟 mutations 不同的是 action 可以處理非同步操作,代表可以加上 async/await 之類的相關邏輯,並且呼叫 mutations 改變 state

  • 第一個參數是 context,代表的是當下位置的 store 或是 module
  • 可以從 context.commit(<name>) 呼叫 mutations
  • 透過 this.$store.dispatch("<name>") 呼叫 action
// store.js
        actions: {
            cut(context) {
                context.commit("add", {
                    count: -1
                });
            }
        }
// .vue
    methods: {
        cut() {
            this.$store.dispatch("cut");
        }
    }

也可以對 context 解構,僅取得 commit 來使用

        actions: {
            cut({commit}) {
                commit("add", {count: -1});
            }
        }

與 mutations 同樣的,有各種呼叫方式

    cut() {
            this.$store.dispatch({
            type: "cut",
            count: -2
        });
    }

Map 系列


一旦需要從 store 取用的東西一多時,每個都要寫一次 this.$stroe... 挺煩人的,vuex 針對這部份提供了幾個工具可以使用

MapState

從 vuex 引入 mapState,並且調整下 .vue,mapState讓取用 state 可以更方便

import {mapState} from "vuex";
    
        export default {
            name: 'App',
            computed: mapState({
                count: state => state.count,
            }),
            methods: {
                add() {
                    this.$store.commit("add", 2);
                }
            }
        }

除了上面的方式外,mapState也有其他的寫法

computed: mapState(["count"]),
computed: {
                ...mapState(["count"]),
            },
computed: {
                ...mapState({
                    count: 'count'
                })
            },

​

其他的 computed 繼續往後添加即可

computed: {
                ...mapState({
                    count: 'count'
                }),
                test() {
                    return "";
                }
            },

mapMutations

相對於 mapState,mutations 也有 mapMutations 可以使用,同樣的也有 state 的各種寫法

import {mapMutations, mapState} from "vuex";
    
        export default {
            name: 'App',
            computed: {
                ...mapState({
                    count: 'count'
                }),
            },
            methods: {
                ...mapMutations({
                    add: 'add'
                }),
            }
        }

mapGetters

使用方式同上

import {mapGetters, mapState} from "vuex";
    
        export default {
            name: 'App',
            computed: {
                ...mapGetters({
                    length: 'length',
                    test: 'test',
                    func: 'func',
                }),
            }
        }

mapActions

使用方式同上

import {mapActions, mapGetters, mapState} from "vuex";
    
        export default {
            name: 'App',
            methods: {
                ...mapActions({
                    cutAction: "cut"
                }),
            }
        }

 

Modules


當 store 的內容越來越多,在管理上就會越不便,這時候可以使用 modeuls 來拆分 store,把各個區塊拆分獨立管理

 

把 Store 所有內容都拆出一個 Modules

  • 在 store 內使用 modules,來指定要加入的 modules
  • modules 加上 namespace: true,在後續使用會比較方便
  • modules 的內容跟在 store 時幾乎一樣,部分差異會在後面提到

修改使用端

  • 取得 state 需要多加上 namespace,變成 this.$store.<namespace>.<name>
    • 這邊的 my 則是在 store 的 modules 前定義的名稱
    • 並且在 modules 內加上 namespace=true 來指定需要 namespace
    • getters、mutations、action,則是也是在前面加上 namespace,變成 ["<namespace>/<name>"]
// store.js
    import Vuex, {Store} from "vuex";
    import Vue from "vue";
    
    Vue.use(Vuex);
    
    const myStore = {
        namespaced: true,
        state: {
            count: 0,
            name:"Test",
        },
        mutations: {
            add(state, payload) {
                state.count += payload;
            }
        },
        actions: {
            cut(context, payload) {
                context.commit("add", payload.count);
            }
        },
        getters: {
            length(state) {
                return state.name.length;
            }
        }
    }
    
    const store = new Store({
        modules: {
            my: myStore
        },
    });
    
    export default store;
// .vue
        export default {
            name: 'App',
            computed: {
                count() {
                    return this.$store.state.my.count;
                },
                length() {
                    return this.$store.getters["my/length"];
                },
            },
            methods: {
                add() {
                    this.$store.commit("my/add", 2);
                },
                cut() {
                    this.$store.dispatch("my/cut", {
                        count: -2
                    });
                }
            }
        }

從 modules 呼叫 root 的方法

若需要從 modules 呼叫 root 的 mutations 時可以在加上第三的參數,指定呼叫 root

const myStore = {
        namespaced: true,
        actions: {
            addRoot(context) {
                context.commit("add", 3, {root: true});
            }
        }
    }

從 modules 取得 root 的 state 或 getters

getters - 在modules的 getters 提供了幾個參數,最後面兩個分別是 rootState 和 rootGetters,從名字來看就可以知道用法了

getters: {
            rootCount(state, getters, rootState, rootGetters) {
                return rootState.rootCount;
            },
        }

actions - 與 getters 不一樣的是,action 的參數 context 本身就包含了 rootState 和 rootGetters 可以使用

actions: {
            cutRoot(context) {
                context.commit("add", -context.rootState.count);
            }
        },

接著我們把前面的範例的 vue 全部依照 modules 做調整後

<template>
        <div id="app">
            <div>count: {{count}}</div>
            <button @click="add(2)">Add</button>
            <button @click="cut(-2)">Cut</button>
            <button @click="cutRoot">Cut Root</button>
            <div>length: {{length}}</div>
            <hr>
            <button @click="addRoot(2)">Add Root</button>
            <div>rootCount: {{rootCount}}</div>
            <hr>
            <button @click="addTest(3)">Add Root</button>
        </div>
    </template>
    
    <script>
        import {mapActions, mapGetters, mapMutations, mapState} from "vuex";
    
        export default {
            name: 'App',
            computed: {
                ...mapState({
                    count: state => state.my.count
                }),
                ...mapGetters({
                    length: "my/length",
                    rootCount: "my/rootCount"
                }),
            },
            methods: {
                ...mapMutations({
                    add: "my/add",
                    addRoot: "add",
                }),
                ...mapActions({
                    cut: "my/cut",
                    cutRoot: "my/cutRoot",
                    addTest: "my/addRoot"
                }),
            }
        }
    </script>
import Vuex, {Store} from "vuex";
    import Vue from "vue";
    
    Vue.use(Vuex);
    
    const myStore = {
        namespaced: true,
        state: {
            count: 0,
            name: "Test",
        },
        mutations: {
            add(state, payload) {
                state.count += payload;
            }
        },
        actions: {
            cut(context, payload) {
                context.commit("add", payload);
            },
            cutRoot(context) {
                context.commit("add", -context.rootState.rootCount);
            },
            addRoot(context) {
                context.commit("add", 3, {root: true});
            }
        },
        getters: {
            length(state) {
                return state.name.length;
            },
            rootCount(state, getters, rootState) {
                return rootState.rootCount;
            },
        }
    }
    
    const store = new Store({
        modules: {
            my: myStore
        },
        state: {
            rootCount: 0
        },
        mutations: {
            add(state, payload) {
                state.rootCount += payload;
            }
        },
    });
    
    export default store;

namespace 看得很煩人,可以拿掉嗎?

每一個使用的地方都要掛上 namespace 在前面,使用上挺煩人的,有幾種方式可以讓他更好看一些

 

map系列的參數

當第一個參數改為一個字串時,代表的則是 namespace,結果會變成

import {mapActions, mapGetters, mapMutations, mapState} from "vuex";
    
        export default {
            name: 'App',
            computed: {
                ...mapState({
                    count: state => state.my.count
                }),
                ...mapGetters("my", {
                    length: "length",
                    rootCount: "rootCount"
                }),
            },
            methods: {
                ...mapMutations("my", {
                    add: "add",
                }),
                ...mapMutations({
                    addRoot: "add",
                }),
                ...mapActions("my", {
                    cut: "cut",
                    cutRoot: "cutRoot",
                    addTest: "addRoot"
                }),
            }
        }

透過 vuex 內的 createNamespacedHelpers

vuex 提供了一個工具可以直接取得指定的 namespace 的 map

const {mapState, mapMutations, mapGetters, mapActions} = createNamespacedHelpers("my");

其調整結果會變成如下,這樣看就簡潔了很多

import {createNamespacedHelpers} from "vuex"
    
        const {mapState, mapMutations, mapGetters, mapActions} = createNamespacedHelpers("my");
    
        export default {
            name: 'App',
            computed: {
                ...mapState({
                    count: state => state.count
                }),
                ...mapGetters({
                    length: "length",
                    rootCount: "rootCount"
                }),
            },
            methods: {
                addRoot(payload) {
                    this.$store.commit("add", payload);
                },
                ...mapMutations({
                    add: "add",
                }),
                ...mapActions({
                    cut: "cut",
                    cutRoot: "cutRoot",
                    addTest: "addRoot"
                }),
            }
        }

 

[非強制] 根據官方建議,可以把各個命名都改用 const 取代,方便後續的維護

直接貼上結果

    export const MyLength = "length";
    export const MyRootCount = "rootCount";
    export const RootAdd = "add";
    export const MyAdd = "add";
    export const MyCount = "count";
    export const MyName = "name";
    export const RootCount = "rootCount";
    export const MyCut = "cut";
    export const MyCutRoot = "cutRoot";
    export const MyAddRoot = "addRoot";
        import {createNamespacedHelpers} from "vuex"
        import {MyAdd, RootAdd} from "./MutationTypes";
        import {MyLength, MyRootCount} from "./GetterTypes";
        import {MyAddRoot, MyCut, MyCutRoot} from "./ActionTypes";
        import {MyCount} from "./StateTypes";
    
        const {mapState, mapMutations, mapGetters, mapActions} = createNamespacedHelpers("my");
    
        export default {
            name: 'App',
            computed: {
                ...mapState({
                    count: MyCount
                }),
                ...mapGetters({
                    length: MyLength,
                    rootCount: MyRootCount
                }),
            },
            methods: {
                addRoot(payload) {
                    this.$store.commit(RootAdd, payload);
                },
                ...mapMutations({
                    add: MyAdd,
                }),
                ...mapActions({
                    cut: MyCut,
                    cutRoot: MyCutRoot,
                    addTest: MyAddRoot
                }),
            }
        }
​
    
import Vuex, {Store} from "vuex";
    import Vue from "vue";
    import {MyAdd, RootAdd} from "./MutationTypes";
    import {MyAddRoot, MyCut, MyCutRoot} from "./ActionTypes";
    import {MyCount, MyName, RootCount} from "./StateTypes";
    import {MyLength, MyRootCount} from "./GetterTypes";
    
    Vue.use(Vuex);
    
    const myStore = {
        namespaced: true,
        state: {
            [MyCount]: 0,
            [MyName]: "Test",
        },
        mutations: {
            [MyAdd](state, payload) {
                state.count += payload;
            }
        },
        actions: {
            [MyCut](context, payload) {
                context.commit("add", payload);
            },
            [MyCutRoot](context) {
                context.commit("add", -context.rootState.rootCount);
            },
            [MyAddRoot](context) {
                context.commit("add", 3, {root: true});
            }
        },
        getters: {
            [MyLength](state) {
                return state.name.length;
            },
            [MyRootCount](state, getters, rootState) {
                return rootState.rootCount;
            },
        }
    }
    
    const store = new Store({
        modules: {
            my: myStore
        },
        state: {
            [RootCount]: 0
        },
        mutations: {
            [RootAdd](state, payload) {
                state.rootCount += payload;
            }
        },
    });
    
    export default store;

​

拖了幾天才把全部都玩過一輪,把內容都筆記一下

Vuex https://vuex.vuejs.org/zh/

Sample Code https://github.com/ianChen806/VuexSample