Vuex —— The core of Vue application

系列文章:

  1. Vue 2.0 升(cai)級(keng)之旅
  2. Vuex — The core of Vue application (本文)

當今,談到狀態管理首先想到的肯定是 Redux,而隨著 Vue 2.0 的發布,Vuex 也伴隨著推出了最新版,本文就帶你對照 Redux 來看看剛剛出爐的 Vuex 2.0。

有關 Redux 的基礎概念在本文中會簡要略過,如再一一贅述篇幅就太長了,不了解的可以看一下本人之前寫的有關 Redux 的兩篇文章:

  1. Redux 入門
  2. Redux 進階

為什么說 Vuex 是 Vue 應用的核心?

眾所周知,一個應用的外觀可以千變萬化,但無論如何變化,它都需要一樣東西去支撐,那就是——數據。這個數據是廣義上的,可以是數據庫中的數據,也可以是當前應用所處的狀態,甚至可以是 WebRTC, Web Bluetooth 等一系列實時數據。

在 vue 應用中,vuex 就充當了數據提供者的角色,vue 則只需要關注頁面的展示與交互。

既然,明確了以 vuex 為核心,那么就來看看如何在 vue 應用中使用 vuex?

隨著 Vue 2.0 的發布,Vuex 在近期也隨之推出 2.0 版。在上一篇文章中有提到作者的博客是用 vue 2.0 搭建的,但之前并沒有添加 vuex,現在正可以借此機會將 vuex 添加到項目中。

本文將介紹 Vuex 2.0 的同時,分享一些本人在這個過程中的一些心得。

首先,當然是核心的核心 Store。

Store

Store 用來存放整個應用的 state。

那怎么建立 store 哪?由于,Vuex 2.0 剛剛推出,最新的 API 還得看 Release Note

創建一個 Store 非常簡單只需 new Vuex.Store({ ...options }),其中,options 可以是一下幾種:

  • state Object:存放應用狀態
  • actions Object:注冊 action
  • mutations Object:注冊 mutation
  • getters Object:注冊 getter
  • modules Object:注冊 module
  • plugins Array<Function>:注冊中間件
  • strict Boolean:是否開啟嚴格模式,嚴格模式下所有對 state 的變化必須通過 mutation 來修改,反之拋出異常,默認不開啟。

或許你不了解這些屬性的含義,沒關系,之后每個還會分別解釋。

明白了屬性的含義,那么創建一個 store 的代碼就可能會是這樣

// store.js
import Vue from 'vue';
import Vuex from 'vuex';
import createLogger from 'vuex/logger';

import blog from './module/blog';

// 在 Vue 中,注冊 Vuex
Vue.use(Vuex);

export default new Vuex.Store({
    state: {},
    plugins: process.env.NODE_ENV !== 'production' ? [createLogger()] : [],
    modules: {
        blog
    }
});

store 創建完成之后,就可以在根組件中使用了。

import Vue from 'vue';
import store from '../vuex';
import router from './router';
import './blog';

new Vue({
    store,
    router,
    template: '<blog></blog>'
}).$mount('#app');

個人看來,一個狀態管理的應用,無論是使用 vuex,還是 redux,最困難的部分是在 store 的設計

究竟該如何設計一個 store,是根據組件的結構層次設計對應的 store,還是根據應用數據來設計 store?

由于,store 是存放整個應用狀態的地方,所以,起初我認為應該是前者按組件的層次結構去設計。這樣 store 中分別保存著每個組件的狀態,這對大型項目來說或許會造成大量的冗余數據存儲在 store 中,以及一些重復的工作,但這也提供了簡潔鮮明的層次結構,增強了項目的可維護性,這對大型項目來說更至關重要。

但伴隨著寫項目時的思考,我漸漸推翻了之前的想法。

假設這樣一個場景,項目中有兩個互不相關的組件,但它們倆卻依賴同一份數據源。如果,這時采用之前的設計方法,那么這同一份數據源會被存放在 store 的兩個不同的位置。那么此時,如果一個組件需要對數據源進行操作的話,它不但需要修改自己組件對應的 state,同時還要發起 action 來修改另一個組件的 state,這恰恰違背了組件的單一性。

然而,使用應用數據來設計 store 就不會有這樣的問題。鑒于這個原因,我現在更傾向于第二個理念來設計整個應用的 store。

所以,當項目開始時,要考慮到整個應用的數據模型來設計 store 真是相當麻煩啊。

談完了 store,就再一個個來看剛剛創建 store 時所提到的屬性,state 就是用來保存狀態的,沒啥好說的,直接來看看第二個 actions

Actions

actions 是一個對象,key 就是 action 的名字,value 就是對應的 action。此處的 action,無論從名字,還是作用都和 redux 中的 action 相同,用于激發 state 的變更。但是,它們的用法卻不相同。

Redux 中的 action 需要返回一個 JS 對象,即使加了 thunk 中間件之后,能夠返回一個函數,但這個函數最終返回的還是一個 JS 對象,最后通過,store.dispatch 該對象來觸發 state 的變更。

然而,Vuex 中的 action 它本身就是一個方法,并且這個方法并不需要任何的返回,而是,通過 store.commit 來觸發 mutation

Vuex 2.0 中,已將原先的 store.dispatch 改名為了 store.commit 來觸發 mutation
Vuex 2.0 中,并沒有移除 store.dispatch,而是改為用于觸發 action

所有 action 方法接受當前 store 的實例作為第一個參數,調用傳遞的參數會作為第二個參數傳入(暫不支持多參數)。

Mutations

mutations 也是一個對象,同 actions 類似,key 就是 mutation 的名字,value 就是對應的 mutation。

mutation 用于更新應用的 state。Redux 中雖然沒有 mutation 這個詞,但從上面的解釋就明白,這同 redux 中的 reduce 起著相同的作用。

但兩者在寫法上又有著不同,由于 vuex 中的 mutations 是一個對象,并借用 ES6 對象方法可以使用變量省略的特點,調用 mutation 可以直接通過命名找到相應的處理方法,這使得它比 redux 的一系列 switch/case 語句要更簡單、更優雅。

更大的不同之處在于 redux 的 reduce 是要求返回一個新的 state,而 vuex 就如它的命名 mutations(變異)是對當前 state 進行操作,而不能返回一個新的 state,這里就和 FP 的理念有所沖突了。

// mutations.js
export default {
    // work
    [LOAD_SOCIAL_LINK](state = {}, mutation = {}) {
        state.socialLinkList = mutation.payload
            .filter(item => !!item.link)
            .map(item => ({
                ...item,
                svgPath: svgPath + '#' + item.name
            }));
    }
    
    // not work
    [LOAD_SOCIAL_LINK](state = {}, mutation = {}) {
        state = {
            ...state,
            socialLinkList: mutation.payload
                .filter(item => !!item.link)
                .map(item => ({
                    ...item,
                    svgPath: svgPath + '#' + item.name
                }))
        };
    }
};

單就這點來看,redux 略勝一籌。

Getters

Getters 也是一個對象,用于注冊 getter,每個 getter 都是一個 function 用于返回一部分的 state。

getter 方法接受 state 作為第一個參數,一個簡單的 getters 就可能是這樣:

export default {
    // 省略...
    getters: {
        socialLinkList: state => state.socialLinkList
    }
};

掌握了 Store, Actions, Mutations 以及 Getters 這幾個概念,那你就掌握了 vuex 的核心,已經完全可以創建一個完整的 store,并可以使用了。

但隨著項目的增長,你會發現將 Actions, Mutations, Getters 全都寫在一起非常難以維護,這時你會想念 Redux 中將 state 劃分處理的 combineReducers

Wake up!

醒醒!別想 Redux 啦,Vuex 也可以劃分處理 state 樹,它就是接著就要提到的 modules

Modules

Modules 的作用就如它的名字,劃分模塊。

它的屬性也是一個對象,key 是對應的 module 名,在 state 中會創建相應的 key,而 value 是一個用于配置如何創建 module 的對象,該對象的屬性基本同創建 store 時的 options 對象一樣,只少了最后 2 個還沒有講到的屬性 pluginsstrict。這兩者是不是有什么關系哪?

class Store {
  constructor (options = {}) {
    // 省略...
    
    // init root module.
    // this also recursively registers all sub-modules
    // and collects all module getters inside this._wrappedGetters
    installModule(this, state, [], options)
    
    // 省略...
  }

從 vuex 創建的源碼中可以看到,其實,store 它本身就是一個 module。

既然,modules 中能配置 modules 那就意味著:模塊是可以嵌套的。那么,使用 modules 就可以將 state 劃分為各個模塊,同 combineReducers 一樣可以化繁為簡,這對中大型項目來說必不可少。

一個 module 的定義就可以是這樣。

// nav module
import mutations from './mutations';
import actions from './actions';

export default {
    state: {},
    getters: {
        navList: state => state.navList
    },
    actions,
    mutations
};

警報!前方第 6 行有坑,請速速繞行。

第 6 行?

state: {}, 初始化 state 能有什么問題啊?

當你運行你的應用的時候,你會發現,如果 navList 的變化是由一個同步的方法返回的就沒有問題,但如果,它是通過異步方法返回的,你會發現雖然控制臺上的 mutation log 輸出正確,但你的組件中并沒有得到正確的值。

What happened?

因為,當 action 調用之后會計算一次 getter,如果是同步的,那么此時 getter 的 state 中已經保存著最新的數據。

但如果是異步的,那么此時 getter 中的 state 是一個空對象,那么上例中的 state.navList 就會返回一個 undefined。然而,undefined 就不會進入 vue 的 watch 系統,所以當異步請求結束后,即使 state 中對應字段變為了目標值,但也不會再調用 getter 了,組件中的值自然也不會更新了。

那怎么解決哪?那就是給 state 中的每個屬性設初始值,這樣在第一次計算 getter 的值時就會返回對應的初始值,而這個初始值是在 vue 的系統中的,所以當異步請求結束后調用 mutation 改變 state 中對應的值后,getter 會自動觸發更新,此時,組件中對應的值也就被修改了。

所以,一定要記得:

為每個屬性設置初始化 state !!!

為每個屬性設置初始化 state !!!

為每個屬性設置初始化 state !!!

重要的話,說三遍!!!

最后,在使用 modules 還需要注意,在不同 modules 下,注冊的 action 或 mutation 的名字重復并不會報錯,但都會被調用,所以要注意命名

好,modules 講完了,繼續看下一個屬性 plugins

Plugins

vuex 自 1.0 版開始就將原先的 middlewares 替換成了 plugins。也就是說,現在使用的 plugins 就是中間件。

plugins 的參數終于同之前的有所不同了,是一個數組,數組中的每一項都是一個方法,方法接受一個參數就是當前 store 的實例。

    // vuex source code: apply plugins
    plugins.concat(devtoolPlugin).forEach(plugin => plugin(this))

vuex 中間件的編寫理解起來也十分容易,就是通過 store.subscribe 來訂閱 mutation 的變化,這比 redux 中間件的工作原理更容易理解。

最后的 strict 屬性之前已經提到了,就是用來設置時候開啟嚴格模式的,嚴格模式下,state 只能通過 mutation 來修改。

至此,創建 vuex store 的所有屬性都講完了,store 也就完成了,那么,vue 的組件該如何和 vuex 的 store 鏈接起來哪?

連接到組件

vuex 1.0 之前如何將 vuex 連接到組件在這里就不說了,有興趣可以上官網上看看。

主要來看看如何使用 vue 2.0 新增的 4 個 helper 方法優雅地將 vuex 連接到組件。

這 4 個 helper 方法,分別是:

  • mapState
  • mapMutations
  • mapGetters
  • mapActions

常言道:口說無憑。

我們就來看一個博客升級中的簡單例子,沒有加入 vuex 前,本人博客的首頁是這樣設定的:

// home.js
import Vue from 'vue';

import PostService from '../../../common/service/PostService';

import img from '../../../assets/img/home-bg.jpg';
import template from './home.html';

const Home = Vue.extend({
    template,
    data: () => {
        return {
            header: {
                img,
                title: 'D.D Blog',
                subtitle: 'Share More, Gain More.'
            },
            postList: []
        };
    },
    created() {
        const postService = new PostService();
        postService.queryPostList().then(({postList}) => (this.postList = postList));
    }
});

這里我們回顧一下之前的所講,為 home 組件創建對應的 store module。

// index.js
// mutation types
const INIT_HOME_PAGE = 'INIT_HOME_PAGE';
const LOAD_POST_LIST = 'LOAD_POST_LIST';

// actions
const initHomePage = ({dispatch, commit}) => {
    commit(createAction(INIT_HOME_PAGE, {
        header: {
            image,
            title: 'D.D Blog',
            subtitle: 'Share More, Gain More.'
        }
    }));
    dispatch('loadPostList');
};

const loadPostList = ({commit}) => {
    new PostService().queryPostList()
        .then((result = {}) => {
            commit(createAction(LOAD_POST_LIST, {
                postsList: result.postsList
            }));
        });
};

const actions = {initHomePage, loadPostList};

// mutations
const mutations = {
    [INIT_HOME_PAGE](state = {}, mutation = {}) {
        state.header = mutation.payload.header;
    },

    [LOAD_POST_LIST](state = {}, mutation = {}) {
        state.postsList = mutation.payload.postsList;
    }
};

export default {
    state: {
        header: {},
        postsList: []
    },
    getters: {
        postsList: state => state.postsList
    },
    actions,
    mutations
};
const createAction = (typeName = '', data = '') => ({ type: typeName, payload: data });

這里的 createAction 是自己創建的一個簡單函數,用于格式化 mutation 獲得的參數,這并不是必須的,vuex 的 commit 方法是接受參數為 (type, data) 的。

OK。對應的 store module 也創建好了,就來改組件吧。

首先,應用的狀態都來自于 store,那么組件中的 data 屬性自然就不用了,直接刪除。爽~

const Home = Vue.extend({
    template,
    created() {
        const postService = new PostService();
        postService.queryPostList().then(({postList}) => (this.postList = postList));
    }
});

其次,原先在 created hooks 里直接去查數據,現在用了 vuex 自然要通過調用 action 來獲取數據,這里就要用到 4 大金剛之一——mapActions 來獲取 vuex 中設定好的 action。

mapActions 接受一個數組或對象,根據相應的值將對應的 action 綁定到組件上。

import {mapActions} from 'vuex';

const Home = Vue.extend({
    template,
    methods: mapActions(['initHomePage']),
    created() {
        this.initHomePage();
    }
});

數據拿到了,怎么綁定到組件上哪?這就可以用到另兩個 helper:mapStatemapGetters

mapStatemapGetters 同樣接受一個數組或對象,并根據相應的值將 store 中的 state 或 getter 綁定到組件上。

import vue from 'vue';
import { mapState, mapGetters, mapActions } from 'vuex';

import template from './home.html';

const Home = vue.extend({
    template,
    computed: {
        ...mapState({
            header: state => state.home.header
        }),
        ...mapGetters(['postsList'])
    },
    methods: mapActions(['initHomePage']),
    created() {
        this.initHomePage();
    }
});

哈哈,這樣模板不用改變一分一毫,升級就完成啦~

是不是很簡潔,很優雅~

容器組件和展示組件

容器組件和展示組件這個概念在 Redux 入門一文中已有提到。然而,這個概念并不只服務于 react,在 vue 中也可以用到。

簡單來說,容器組件就是用于包裹展示組件的組件,它和界面展示無關,它負責數據的獲取和傳遞,之前的 home 組件就是一個容器組件,再來看看它的 template,你會發現它除了根元素以外,不包含其他任何的 html 標簽。

<section>
    <!-- Content Header -->
    <content-header :board-img="header.image" :title="header.title" :subtitle="header.subtitle"></content-header>

    <!-- Main Content -->
    <main-content>
        <post-list :post-list="postsList"></post-list>
    </main-content>
</section>

與此相反的是,展示組件單單用于展示,自己不獲取任何數據,數據都通過 props 傳遞,比如 content-header。

const template = `<header class="intro-header" :style="{ backgroundImage: 'url(' + boardImg + ')' }">
    <div class="container">
        <div class="row">
            <div class="col-lg-8 col-lg-offset-2 col-md-10 col-md-offset-1">
                <div class="site-heading">
                    <h1>{{ title }}</h1>
                    <hr class="small">
                    <span class="subheading">{{ subtitle }}</span>
                </div>
            </div>
        </div>
    </div>
</header>`;

export default Vue.component('contentHeader', {
    template,
    props: {
        boardImg: {
            type: String,
            default: _defaultImg
        },
        title: {
            type: String,
            required: true
        },
        subtitle: {
            type: String
        }
    }
});

這樣明確地區分容器組件和展示組件會使得項目結構變得更清晰,追蹤 bug ,以及維護也變得輕而易舉。

管理路由

是不是覺得這樣就完了?

No, No, No. 路由系統還沒處理,那么如何將 vue-router 納入到 vuex 的管理中哪?

這里又得感謝尤大大為我們造好了一個小工具 vuex-router-sync

首先,安裝

npm install vuex-router-sync@next --save

然后,在項目初始化的時候將 router 同 store 聯系起來就行,簡單到都不知道說啥好。

不知道說啥,就說說原理,看看源碼吧。

這個工具的原理也非常好理解,主要是 2 點:

一是,給 vuex 的 store 注冊一個 router 的 module。

function patchStore (store) {
  // 略...
  var routeModule = {
    mutations: {
      'router/ROUTE_CHANGED': function (state, to) {
        store.state.route = to
      }
    }
  }

  // add module
  if (store.registerModule) {
    store.registerModule('route', routeModule)
  } else if (store.module) {
    store.module('route', routeModule)
  } else {
    store.hotUpdate({
      modules: {
        route: routeModule
      }
    })
  }
}

另一個,就是使用 vue-router 的 afterEach hooks 來觸發 mutation。

exports.sync = function (store, router) {
  patchStore(store)
  store.router = router

  var commit = store.commit || store.dispatch
  // 略...
  
  // sync store on router navigation
  router.afterEach(function (transition) {
    if (isTimeTraveling) {
      isTimeTraveling = false
      return
    }
    var to = transition.to
    currentPath = to.path
    commit('router/ROUTE_CHANGED', to)
  })
}

項目中使用:

import { sync } from 'vuex-router-sync';
import store from '../vuex';
import router from './router';

sync(store, router);

new Vue({
    store,
    router,
    template: '<blog></blog>'
}).$mount('#app');

OK,這樣就大功告成了。

寫在最后

加入了 vuex 后,我的博客終于讓 vue 它們一家子(vue + vuex + vue-router)團圓了。

總的來看,vuex 同 vue 一樣使用起來相當方便,集成了許多方法,但似乎缺少了 redux 的那份優雅,而我喜歡比較優雅的...(看在全篇我都在安利 vue 的情面上,尤大大請不要打我~)

逃~

PS: 一下把 vuex 有關的一股腦都過了,可能過得太快,如有不明白的就留言吧。

最后的最后,當然是繼續安利下自己的 Blog,以及 Source Code

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,505評論 6 533
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,556評論 3 418
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,463評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,009評論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,778評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,218評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,281評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,436評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,969評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,795評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,993評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,537評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,229評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,659評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,917評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,687評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,990評論 2 374

推薦閱讀更多精彩內容

  • Vuex 是一個專為 Vue.js 應用程序開發的狀態管理模式。它采用集中式存儲管理應用的所有組件的狀態,并以相應...
    白水螺絲閱讀 4,678評論 7 61
  • 安裝 npm npm install vuex --save 在一個模塊化的打包系統中,您必須顯式地通過Vue.u...
    蕭玄辭閱讀 2,960評論 0 7
  • Vuex是什么? Vuex 是一個專為 Vue.js應用程序開發的狀態管理模式。它采用集中式存儲管理應用的所有組件...
    蕭玄辭閱讀 3,130評論 0 6
  • Vuex 的學習記錄 資料參考網址Vuex中文官網Vuex項目結構示例 -- 購物車Vuex 通俗版教程Nuxt....
    流云012閱讀 1,466評論 0 7
  • vuex 場景重現:一個用戶在注冊頁面注冊了手機號碼,跳轉到登錄頁面也想拿到這個手機號碼,你可以通過vue的組件化...
    sunny519111閱讀 8,029評論 4 111