系列文章:html
Vuex — The core of Vue application (本文)react
當今,談到狀態管理首先想到的確定是 Redux,而隨着 Vue 2.0 的發佈,Vuex 也伴隨着推出了最新版,本文就帶你對照 Redux 來看看剛剛出爐的 Vuex 2.0。github
有關 Redux 的基礎概念在本文中會簡要略過,如再一一贅述篇幅就太長了,不瞭解的能夠看一下本人以前寫的有關 Redux 的兩篇文章:web
衆所周知,一個應用的外觀能夠變幻無窮,但不管如何變化,它都須要同樣東西去支撐,那就是——數據。這個數據是廣義上的,能夠是數據庫中的數據,也能夠是當前應用所處的狀態,甚至能夠是 WebRTC, Web Bluetooth 等一系列實時數據。數據庫
在 vue 應用中,vuex 就充當了數據提供者的角色,vue 則只須要關注頁面的展現與交互。npm
既然,明確了以 vuex 爲核心,那麼就來看看如何在 vue 應用中使用 vuex?
隨着 Vue 2.0 的發佈,Vuex 在近期也隨之推出 2.0 版。在上一篇文章中有提到做者的博客是用 vue 2.0 搭建的,但以前並無添加 vuex,如今正能夠藉此機會將 vuex 添加到項目中。
本文將介紹 Vuex 2.0 的同時,分享一些本人在這個過程當中的一些心得。
首先,固然是核心的核心 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
是一個對象,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
也是一個對象,同 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
也是一個對象,用於註冊 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
。
醒醒!別想 Redux 啦,Vuex 也能夠劃分處理 state 樹,它就是接着就要提到的 modules
。
Modules
的做用就如它的名字,劃分模塊。
它的屬性也是一個對象,key 是對應的 module 名,在 state 中會建立相應的 key,而 value 是一個用於配置如何建立 module 的對象,該對象的屬性基本同建立 store 時的 options
對象同樣,只少了最後 2 個尚未講到的屬性 plugins
和 strict
。這二者是否是有什麼關係哪?
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 輸出正確,但你的組件中並無獲得正確的值。
由於,當 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
。
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:mapState
和 mapGetters
。
mapState
和 mapGetters
一樣接受一個數組或對象,並根據相應的值將 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。