Vuex白話教程第六講:Vuex的管理員Module(實戰篇)

 

 

寫在前面

這一講是 Vuex 基礎篇的最後一講,也是最爲複雜的一講。如果按照官方來的話,對於新手可能有點難以接受,所以想了下,決定乾脆多花點時間,用一個簡單的例子來講解,順便也複習一下之前的知識點。

首先還是得先了解下 Module 的背景。我們知道,Vuex 使用的是單一狀態樹,應用的所有狀態會集中到一個對象中。如果項目比較大,那麼相應的狀態數據肯定就會更多,這樣的話,store 對象就會變得相當的臃腫,非常難管理。

這就好比一家公司只有老闆一個人來管理一樣。如果小公司倒還好,公司要是稍微大一點,那就麻煩了。這個時候,老闆就會成立各大部門,並給各大部門安排一個主管,把管理的任務分派下去,然後有什麼事情需要處理的話,只需要跟這幾個主管溝通,由主管再把任務分配下去就行了,這就大大提高了工作效率,也減輕了老闆的負擔。

那麼同樣的道理,Module 其實就承擔了部門管理員的角色,而 store 就是老闆。理解了這一層,那麼後面就好辦多了,接下來,咱們就一步一步動起手來開始實踐。

一、準備工作

這裏我們使用官方提供的 vue-cli 來建一個項目「vuex-test」。當然,先得安裝 vue-cli:

npm install -g @vue/cli
# OR
yarn global add @vue/cli

安裝完成後,就可以使用以下命令來創建項目了:

vue create vuex-test

還可以使用圖形化界面來創建:

vue ui

具體關於 vue-cli 的使用方法可以去官方查看,戳此進入 。

項目創建完成以後,找到項目安裝的目錄,並打開控制檯執行:

// 先定位到項目的目錄下
cd vuex-test

// 然後安裝 vuex
npm install vuex --save

// 運行一下
npm run serve

運行完成,可以打開 http://localhost:8080/ 看下效果。

最後大家找一個自己比較比較喜歡的 IDE 來打開這個項目,方便查看和編輯。我個人比較喜歡用 WebStore,這裏也推薦給大家。

二、簡單入手

項目的默認結構圖

這裏,我們只看 src 目錄,其他的暫時不管。組件 components 不在這一講的範圍內,所以也可以忽視,資源 assets 也沒什麼可說的,就是如果有圖片或者視頻什麼的話,都放在這個文件夾裏面就是了。

我們打開 App.vue 文件,去掉組件的相關代碼,並編寫一點簡單的 vue 代碼。修改如下:

<template>
    <div id="app">
        <img alt="Vue logo" src="./assets/logo.png" />
        <h1>{{name}}</h1>
        <button @click="modifyNameAction">修改名字</button>
    </div>
</template>

<script>
    export default {
        data() {
            return {
                name: 'Lucy'
            }
        },

        methods: {
            modifyNameAction() {
                this.name = "bighone"
            }
        }
    }
</script>

現在我們引入 Vuex ,用它來管理狀態數據,比如這裏的 name。首先在 src 中新建一個 store.js 文件,並寫下如下熟悉的代碼:

import Vue from 'vue';
import Vuex from 'vuex';

Vue.use(Vuex);

export default new Vuex.Store({
    state: {
        name: 'Lucy',
    },
    mutations: {
        setName(state, newName) {
            state.name = newName;
        }
    },
    actions: {
        modifyName({commit}, newName) {
            commit('setName', newName);
        }
    }
});

然後,在 main.js 中導入 store,並全局注入:

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

最後修改 App.vue 中的代碼如下:

<script>
    import {mapState, mapActions} from 'vuex';
    export default {
        computed: {
            ...mapState(['name'])
        },

        methods: {
            ...mapActions(['modifyName']),

            modifyNameAction() {
                this.modifyName('bighone');
            }
        },
    }
</script>

想必弄懂這些代碼,應該都是沒啥問題的,因爲這些都是 Vuex 很基礎的知識點,這裏實操來簡單回顧一下,加深印象。如果看不懂,那證明之前的基礎知識還沒掌握。

三、引入 Module

在前言裏面,我們已經了 Module 的基本職責,那麼具體如何使用呢?

Vuex 允許我們將 store 分割成大大小小的對象,每個對象也都擁有自己的 state、getter、mutation、action,這個對象我們把它叫做 module(模塊),在模塊中還可以繼續嵌套子模塊、子子模塊 ……

現在在 src 裏面建個文件夾,命名爲 module,然後再裏面新建一個 moduleA.js 文件,並編寫如下代碼:

export default {
    state: {
        text: 'moduleA'
    },
    getters: {},
    mutations: {},
    actions: {}
}

如上,再建一個 moduleB.js 文件,這裏就不重複了。

然後打開 store.js 文件,導入這兩個 module :

import moduleA from './module/moduleA';
import moduleB from './module/moduleB';

export default new Vuex.Store({
    modules: {
        moduleA, moduleB,
    },
    // ...
}

這個時候,store 中已經注入了兩個子模塊 moduleA moduleB,我們可以在 App.vue 中通過 this.$store.state.moduleA.text 這種方式來直接訪問模塊中的 state 數據。如下修改:

// ...
computed: {
    ...mapState({
        name: state => state.moduleA.text
    }),
},
// ...

由此可知,模塊內部的 state 是局部的,只屬於模塊本身所有,所以外部必須通過對應的模塊名進行訪問。

但是注意了:

模塊內部的 action、mutation 和 getter 默認可是註冊在全局命名空間的,這樣使得多個模塊能夠對同一 mutation 或 action 作出響應。

這裏以 mutation 的響應爲例,給 moduleA 和 moduleB 分別新增一個 mutations,如下:

mutations: {
    setText(state) {
        state.text = 'A'
    }
},

moduleB 和上面一樣,把文本名稱修改一下即可,這裏就不重複了。然後回到 App.vue 中,修改如下:

<script>
    import {mapState, mapMutations} from 'vuex';
    export default {
        computed: {
            ...mapState({
                name: state => (state.moduleA.text + '和' + state.moduleB.text)
            }),
        },
        methods: {
            ...mapMutations(['setText']),
            modifyNameAction() {
                this.setText();
            }
        },
    }
</script>

運行然後點擊修改,我們會發現模塊 A 和 B 中的 text 值都改變了。當然,action 的用法一模一樣,大家也可以試試。

如果模塊之間的數據有交集的話,那麼我們其實就可以通過這種方式,來同步更新模塊之間的數據,雖然看起來非常的方便,但是用的時候可一定要謹慎,這種處理方式一旦沒用好,遇到錯誤,排查起來還是比較有難度的。

四、訪問根節點

我們已經知曉,模塊內部的 state 是局部的,只屬於模塊本身所有。那麼如果我們要想在模塊中訪問 store 根節點的數據 state,怎麼辦呢?

很簡單,我們可以在模塊內部的 getter 和 action 中,通過 rootState 這個參數來獲取。接下來,我們給 modelA.js 文件添加一點代碼。

export default {
    // ...
    getters: {
        // 注意:rootState必須是第三個參數
        detail(state, getters, rootState) {
            return state.text + '-' + rootState.name;
        }
    },
    actions: {
        callAction({state, rootState}) {
            alert(state.text + '-' + rootState.name);
        }
    }
}

然後修改 App.vue :

<script>
    import {mapActions, mapGetters} from 'vuex';
    export default {
        computed: {
            ...mapGetters({
                name: 'detail'
            }),
        },
        methods: {
            ...mapActions(['callAction']),
            modifyNameAction() {
                this.callAction();
            }
        },
    }
</script>

然後運行你會發現,根節點的數據已經被我們獲取到了。這裏需要注意的是在 getters 中,rootState 是以第三個參數暴露出來的,另外,還有第四個參數 rootGetters,用來獲得根節點的 getters 信息,這裏就不演示了,感興趣自己可以去嘗試。唯一要強調的就是千萬不要弄錯參數的位置了。

當然,action 中也能接收到 rootGetters,但是在 action 中,由於它接收過來的數據都被包在 context 對象中的,所以解包出來沒有什麼順序的限制。

五、命名空間

前面我們已經知道了,模塊內部的 action、mutation 和 getter 默認是註冊在全局命名空間的。如果我們只想讓他們在當前的模塊中生效,應該怎麼辦呢?

通過添加 namespaced: true 的方式使其成爲帶命名空間的模塊。當模塊被註冊後,它的所有 getter、action 及 mutation 都會自動根據模塊註冊的路徑調整命名。

我們在 moduleA.js 中添加 namespaced: true

export default {
    namespaced: true,
    // ...
}

這個時候再去運行代碼,你會發現如下錯誤:

[vuex] unknown getter: detail

在全局 getter 中已經找不到 detail 的這個方法了,因爲它的路勁已經改變了,不再屬於全局,僅僅只屬於 moduleA 了。所以,這個時候,如果我們想要訪問它,必須帶上路勁纔行。修改 App.vue 如下:

<script>
    import {mapActions, mapGetters} from 'vuex';
    export default {
        computed: {
            ...mapGetters({
                name: 'moduleA/detail'
            }),
        },
        methods: {
            ...mapActions({
                call: 'moduleA/callAction'
            }),
            modifyNameAction() {
                this.call();
            }
        },
    }
</script>

注意,如果一個模塊啓用了命名空間,那麼它裏面的 getter 和 action 中收到的 getter,dispatch 和 commit 也都是局部化的,不需要在同一模塊內額外添加空間名前綴。也就是說,更改 namespaced 屬性後不需要修改模塊內的任何代碼。

那麼我們如何在帶命名空間的模塊內訪問全局內容呢?

通過前面的學習,我們已經瞭解到:

如果你希望使用全局 state 和 getter,rootState 和 rootGetter 會作爲第三和第四參數傳入 getter,也會通過 context 對象的屬性傳入 action。

現在如果想要在全局命名空間內分發 action 或提交 mutation 的話,那麼我們只需要將 將 { root: true } 作爲第三參數傳給 dispatch 或 commit 即可。

export default {
    namespaced: true,
    // ...
    actions: {
        callAction({state, commit, rootState}) {
            commit('setName', '改變', {root: true});
            alert(state.text + '-' + rootState.name);
        }
    }
}

接下來看看如何在帶命名空間的模塊內註冊全局 action

若需要在帶命名空間的模塊註冊全局 action,你可添加 root: true,並將這個 action 的定義放在函數 handler 中。

寫法稍微有點變化,我們來看看,修改 moduleA.js,如下:

export default {
    namespaced: true,
    // ...
    actions: {
        callAction: {
            root: true,
            handler (namespacedContext, payload) {
                let {state, commit} = namespacedContext;
                commit('setText');
                alert(state.text);
            }
        }
    }
}

簡單解釋下,這裏的 namespacedContext 就相當於當前模塊的上下文對象,payload 是調用的時候所傳入的參數,當然也叫載荷。

示例就講到這裏,接下來看看帶命名空間的綁定函數

關於 mapState, mapGetters, mapActions 和 mapMutations 這些函數如何來綁定帶命名空間的模塊,上面示例代碼中其實已經都寫過了,這裏再看看另外幾種更簡便的寫法,先看看之前的寫法。

這裏就用官方的示例代碼舉例說明:

computed: {
    ...mapState({
        a: state => state.some.nested.module.a,
        b: state => state.some.nested.module.b
    })
},
methods: {
    ...mapActions([
        // -> this['some/nested/module/foo']()
        'some/nested/module/foo', 
        // -> this['some/nested/module/bar']()
        'some/nested/module/bar' 
    ])
}

更優雅的寫法:

computed: {
    ...mapState('some/nested/module', {
        a: state => state.a,
        b: state => state.b
    })
},
methods: {
    ...mapActions('some/nested/module', [
        'foo', // -> this.foo()
        'bar' // -> this.bar()
    ])
}

將模塊的空間名稱字符串作爲第一個參數傳遞給上述函數,這樣所有綁定都會自動將該模塊作爲上下文。

我們還可以通過使用 createNamespacedHelpers 創建基於某個命名空間輔助函數。它返回一個對象,對象裏有新的綁定在給定命名空間值上的組件綁定輔助函數:

import { createNamespacedHelpers } from 'vuex'

const { mapState, mapActions } = createNamespacedHelpers('some/nested/module')

export default {
  computed: {
    // 在 `some/nested/module` 中查找
    ...mapState({
      a: state => state.a,
      b: state => state.b
    })
  },
  methods: {
    // 在 `some/nested/module` 中查找
    ...mapActions([
      'foo',
      'bar'
    ])
  }
}

六、模塊的動態註冊

這一章節,官網講得比較清楚,所以直接搬過來了。

在 store 創建之後,可以使用 store.registerModule 方法動態的註冊模塊:

// 註冊模塊 `myModule`
store.registerModule('myModule', {
  // ...
})
// 註冊嵌套模塊 `nested/myModule`
store.registerModule(['nested', 'myModule'], {
  // ...
})

之後就可以通過 store.state.myModule 和 store.state.nested.myModule 訪問模塊的狀態。

模塊動態註冊功能使得其他 Vue 插件可以通過在 store 中附加新模塊的方式來使用 Vuex 管理狀態。例如,vuex-router-sync 插件就是通過動態註冊模塊將 vue-router 和 vuex 結合在一起,實現應用的路由狀態管理。

你也可以使用 store.unregisterModule(moduleName) 來動態卸載模塊。注意,你不能使用此方法卸載靜態模塊(即創建 store 時聲明的模塊)。

在註冊一個新 module 時,你很有可能想保留過去的 state,例如從一個服務端渲染的應用保留 state。你可以通過 preserveState 選項將其歸檔:store.registerModule('a', module, { preserveState: true })

七、模塊重用

就一點,重用會導致模塊中的數據 state 被污染,所以和 Vue 中的 data 一樣,也使用一個函數來申明 state 即可。

const MyReusableModule = {
  state () {
    return {
      foo: 'bar'
    }
  },
  //...
}

寫在最後

演示代碼寫的沒啥邏輯,還請見諒,主要還是爲了幫助大家更好的理解 Module 的用法,如果有不理解的地方,歡迎留言告知。

那麼到這裏,Vuex 的核心概念就已經全部講完了。不知道大家掌握的如何,雖然這套框架有點抽象,但其實只要我們真的用心去學了,要弄懂它也是很容易的。不過光看懂還是不夠,一定要在項目中多運用,多實操才能夠掌握的更加牢固。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章