學習Vue的狀態管理模式Vuex
什麼是狀態管理模式State Management Pattern?
組成:View視圖,State狀態,Actions行動
基本的情況參考官方文檔
爲什麼要用Vuex?
-
多個視圖可能依賴相同的狀態片段
比如兄弟組件sibling components
-
從不同視圖的行動可能會需要改變相同的狀態
文檔說情況一: 想要伸手去拿直系父/子實例引用。我的理解,狀態是存在父組件的,比如一個form在父組件裏,那麼子組件裏有input,比如有關表單提交驗證,我們想直接直接拿從對方的實例拿數據,之前這樣的狀態都是通過$emit這樣的事件來傳遞的。
情況二:通過事件,嘗試改變並同步狀態的多個副本。我的理解是比如有一個父組件狀態,然後有兩個子組件都要用這個狀態,然後在子組件上觸發的事件,改變從父組件拷貝到組件的狀態,並且同步它到父組件。
比如** 感覺還是得有個例子比較好理解啊
因爲這兩種情況會造成項目代碼維護困難,項目會變得很臃腫哦。所以爲什麼不專門拿一個倉庫,大家那狀態直接從倉庫拿不更方便?
官方文檔上有一些圖,最好記一下。方便理解。
核心概念
狀態State
案例看一下GetWifi.vue, GetGirls.vue, GetMoney.vue和store.js
將Vuex State植入Vue的組件裏
mapState映射狀態助手
當我們需要利用多個倉庫狀態屬性或者獲得者時,如果一個一個去聲明這些computed計算的屬性的話,就重複和繁瑣。使用mapState生成“計算獲得者函數”,讓我們省去敲鍵盤的時間。
記一下在調用mapState()這個方式時傳入時的兩種情況。一種是傳入一個對象{},另一種是傳入一個數組[]。記得第一種情況下,使用方法要傳入state作爲參數,然後可以使用es6語法箭頭函數,可以設置別名,或新建一個方法來處理本組件裏的狀態和store裏的狀態的關係。第二種方法,要求的是一個映射的計算屬性的名字要和狀態子樹名相同
對象擴展運算符Object Spread Operator
mapState返回的是一個對象,然後看一下object spread operator對象擴展運算符是在歐洲電腦製造協會腳本ECMAScript提議裏事設呢麼說的。把這個對象合併到另一個對象裏面。爲啥這麼用呢,你看computed: mapState({})
如果這樣的話,那本地的計算屬性放到哪到哪?
組件依然可以使用本地狀態
就是說可能某個狀態只屬於某個單一的組件,那麼放到本地狀態可能就更合適。
Getters獲得者
案例看一下FindJob.vue和store.js
有時候需要基於倉庫狀態去計算導出的狀態,例如過濾一組物品並且計算他們
computed: {
doneTodosCount () {
return this.$store.state.todos.filter(todo => todo.done).length
}
}
有兩種不太理想的處理方法。第一種情況,如果超過一個組件需要使用它的話,那麼就不得不重複這個函數了,即A組件寫一個這個函數,B組件也寫一個這個函數。第二種情況,將它到導出到一個分享的助手裏並且導入到不同的地方, 什麼意思?之前學到mapState助手,是這個?那到底怎麼實現這個導入和導出?
Vuex允許我們定義Getters,可以認爲是給倉庫的計算屬性,就像計算屬性一樣,一個Getters的結果是基於依賴緩存的,並且當依賴改變的時候這個結果會重新求值
Getters會接受狀態作爲第一個實參。
const store = new Vuex.Store({
state: {
todos: [
{ id: 1, text: '...', done: true },
{ id: 2, text: '...', done: false }
]
},
getters: {
doneTodos: state => {
return state.todos.filter(todo => todo.done)
}
}
})
知道了怎麼定義,那麼具體怎麼用呢?
屬性風格的訪問
getters獲得者暴露在store.getters這個對象上,你可以作爲屬性來訪問值。
store.getters.doneTodos // -> [{ id: 1, text: '...', done: true }]
getters獲得者也會接受其他的getters獲得者,作爲第二個是實參
getters: {
// ...
doneTodosCount: (state, getters) => {
// 測試一下這個getters包不包含這個doneTodosCount, 但是我覺得應該不包括
// 經過測試console.log發現這個store.getters對象裏包含doneTodosCount鍵
return getters.doneTodos.length
}
}
在任何組件裏使用
computed: {
doneTodosCount () {
return this.$store.getters.doneTodosCount
}
}
要注意一下,作爲屬性訪問的獲得者是作爲Vue反應系統的部分。意思是不是就是說,這個值是反應的。
方法風格訪問
通過返回一個函數來傳遞實參給getters獲得者。當你想要在倉庫裏查詢一個數組時就很有用。
getters: {
// ...
getTodosById: (state) => (id) => {
return state.todos.find(todo => todo.id === id)
}
}
store.getters.getTodoById(2) // -> { id: 2, text: '...', done: false }
// 有個疑問,爲什麼在上面的例子裏,id沒有作爲第二個參數傳入呢,就是在state旁邊?
// 嘗試console.log一下,看一下這個store.getters
通過console.log(this.$store.getters),發現這樣
getTodosById: (id) => {...}
arguments: [],
caller: []
....
通過console.log(this.$store.getters.getTodosById),發現
(id) => {
return state.todos.filter(todo => todo.id === id)
}
所以vuex的代碼做了處理,所以纔可以直接傳入id這個實參。不要太糾結了,只有看源碼才能明白,總之要記住這個方法。
mapGetters獲得者助手
簡單來說就是映射這倉庫裏的獲得者到本地計算屬性
這裏貌似像getTodosById這樣如果用這個mapGetters的話,那麼在view視圖中顯示的就是這個函數的代碼
Mutations變異
在Vuex store這個倉庫裏,唯一改變state狀態的方式就是提交commit一個變異mutation。mutation變異很像事件event:每個變異都有一個字符串類型type和一個處理器handler。處理器函數是我們實施實際狀態修改的地方,並且它會接受狀態作爲第一個實參。
cosnt store = new Vuex.Store({
state: {
count: 1
},
mutations: {
increment (state) {
// mutate state
state.count++
}
}
})
然而你不能直接調用call一個變異處理器。想一想就像事件註冊一樣: “當一個帶有increment類型的變異觸發了,調用這個處理器。”爲了調用這個變異處理器,你需要調用帶有它的類型的store.commit。
store.commit('increment')
帶着負載提交
你可以傳遞一個額外的實參給store.commit。這個也被叫做給變異的負載。
// ...
mutations: {
increment (state, n) {
state.count += n
}
}
store.comit('increment', 10)
在大多數情況下,負載應該是一個對象,以便於它能夠包含多個字段fields,並且記錄的變異也會更
具有描述性descriptive。
// ...
mutations: {
increment (state, payload) {
state.count += payload.amount
}
}
store.commit('increment', {
amount: 10
})
對象風格提交
一個替代的方法是,通過直接使用一個帶有類型屬性的對象,去提交一個變異
store.commit({
type: 'increment',
amount: 10
})
當使用這種對象風格的提交方式,這整個對象都會被作爲負載傳遞給給變異處理器,所以變異處理器保持一樣
mutations: {
increment (state, payload) {
state.count += payload.amount
}
}
變異遵循Vue的反應性規則
Mutations Follow Vue’s Reactivity Rules
既然Vuex倉庫的狀態是由Vue才變成反應的,當我們變異狀態的時候,Vue的觀察狀態的組件們會自動更新。這也意味着, 當與簡單的plain的Vue一次作用時,變異是服從於subject to相同反應性的說明caveats:
-
偏向於初始化你的,帶有所有前面upfront期望desired的字段fields,倉庫初始狀態。什麼意思?是說如果我想在某個組件裏要某個字段如count,那麼這裏會初始化是嗎?
-
當添加新的屬性Properties給一個對象時,你應該要麼
-
使用Vue.set(obj, ‘newProps’, 123), 要麼
-
用一個新的對象來替代這個對象。看下面這個對象擴展語法object spread syntax
state.obj = {...state.boj, nweProp: 123 }
-
給變異類型使用常數Constants
Using Constants for Mutation Types
這是一個很常見的模式,爲變異類型去使用常數,在各種各樣的流量Flux實現implementations。??翻譯有問題?這就允許代碼可以利用好像linters這樣的工具,並且將書有的常數放到一個單一的文件中,允許你的合作者collaborators去獲得一個瞟一眼就知道什麼變異在整個應用中是可行的。
// mutation-types.js
export const SOME_MUTATION = 'SOME_MUTATION'
// store.js
import Vuex from 'vuex'
import { SOME_MUTATION } from './mutation-types'
const store = new Vuex.Store({
state: {...},
mutations: {
// 可以使用ES2015計算屬性名特色
// 使用一個常數作爲函數名
[SOME_MUTATION] (state) {
// mutate state
}
}
})
是否使用常數很大程度上是一種偏向。在有很多開發者的大項目中可以有幫助,但是這完全是可選的,如果你不喜歡的話。
變異必須是同步的
一個重要的規則要記住就是變異處理器函數必須是同步的。爲什麼呢?
mutations: {
someMutation (state) {
api.callAsyncMethod(() => {
state.count++
})
}
}
現在想象我們在調試這個app,並且看着開發者工具的變異日誌。對於所有的變異輸出日誌,開發者工具都會需要捕獲一個之前和之後狀態的截圖。然而,在這個變異案例中,這個異步的回調就會讓這個變得不可能了:當變異被提交的時候,回調函數還沒有被調用,並且沒有其他的方法去了解當什麼時候這個回調纔會被調用 - 在這個回調中任何狀態變異被執行基本上是不可追蹤的。
說的是什麼意思?哪個異步的回調?api.callAsyncMethod,這個名字的意思是api調用異步方法,那麼異步的方法應該是這個吧() => { state.count++ }。比如去讀取一個文件,當文件讀取成功的時候在去執行裏面的代碼。這裏變異someMutation被使用commit提交時,這個回調並沒有執行,因爲要等到文件讀取成功纔去調用。假如調用失敗了呢?順變想一下這個截圖到底怎麼截得?
在組件中提交變異
可以使用this.$store.commit(‘xxx’)在組件中提交變異,或者使用這個mapMutations助手,這個助手映射組件方法給store.commit調用(要求根store注射)。什麼意思?
import { mapMutations } from 'vuex'
export default {
// ...
methods: {
...mapMutations([
'increment', // 映射 this.increment() 到 this.$store.commit('increment')
// mapMutations也支持負載
'incrementBy' // 映射 this.incrementBy(amount) 到 this.$store.commit('incrementBy', amount)
]),
...mapMutations({
add: 'increment' // 隱射 this.add() 到 this.$store.commit('increment')
})
}
}
移交行動
異步性與狀態變異結合讓你的程序非常難推出reason about。例如,當你使用變異狀態的異步回調,去一起調用兩個方法時,你又怎麼知道什麼時候他們被調用了,並且那個回調被先調用了?確切的這就是我們想要分離這兩個概念。在Vuex中,變異是同步的約定transactions。
store.commit('increment')
// 任何這個increment變異可能造成的狀態改變
// 都應該在這個時候做完
其實有一個疑問,那麼怎麼樣才能讓這個狀態改變不在這個時候做完呢?
行動Actions
行動和變異相似,區別在於:
- 代替變異狀態,行動提交變異
- 行動能包含任意的arbitrary異步操作
const store = new Vuex.Store({
state: {
count: 0
},
mutations: {
increment (state) {
state.count++
}
},
actions: {
increment (context) {
context.commit('increment')
}
}
})
行動處理器接受一個環境context對象,這個對象將倉庫實例中相同的一套方法或屬性暴露出,所以你可以調用context.commit去提交一個變異,或者通過context.state和context.getters去訪問狀態和獲得者。我們能夠用context.dispatch去調用其他的行動。當我們後面介紹模塊Modules時,我們會明白爲什麼這個環境對象不是倉庫實例自身。
在練習中,我們通常使用ES2015的實參解構argument destructuring去簡化一些代碼(尤其是當我們需要去調用commit多次時):
actions: {
increment ({ commit }) {
commit('increment')
}
}
怎麼突然即有了一個{ commit }?看起來有點像是 const { commit } = context,就是 const commit = context.commit。
發送行動
行動是用帶有store.dispatch的方法觸發的:
store.dispatch('increment')
這個第一眼看有點傻乎乎的:如果我們想要增加這個count,爲什麼不就直接調用store.commit(‘increment’)呢?還記得那個變異必須是同步的嗎?行動不必。我們可以在行動中實施異步的操作。
actions: {
incrementAsync ({ commit }) {
setTimeout(() => {
commit('increment')
}, 1000)
}
}
行動支持一樣的負載格式和對象風格的發送。
// 帶着一個負載發送
store.dispatch('incrementAsync', {
amount: 10
})
// 帶着一個對象發送
store.dispatch({
type: 'incrementAsync',
amount: 10
})
一個更有實際意義的真實世界行動的列子,會是一個行動去結帳checkout一個購物車,這個會涉及調用一個異步API和提交多個變異。
actions: {
checkout ({ commit, state }, products) {
// 保存當前在購物欄中的物品
const savedCartItems = [...state.cart.added]
// 發送結帳請求,並且不出意外的話optimistically
// 清空購物欄
commit(types.CHECKOUT_REQUEST)
// 這個商店的API接受一個成功的回調和一個失敗的回調
// 這個types是啥回事?是一個對象是吧。CHECKOUT_REQUEST是這個對象的一個屬性
// 那麼倒是有點像之前專門給這些變異取常量名,並存到單個文件中,然後Import進來
shop.buyProducts(
products,
// 處理成功
() => commit(types.CHECKOUT_SUCCESS),
// 處理失敗
() => commit(types.CHECKOUT_FAILURE, savedCartitems)
)
}
}
現在我們執行了源源不斷a flow of的異步操作了,並且通過提交他們來記錄了行動的副作用(狀態變異)。
還是沒弄明白上面的案例到底在說什麼,chekcout到底是在做啥?是結賬吧。products裏面有啥?應該是這個購物車裏的東西state.cart.added。這些變異CHECKOUT_REQUEST,CHECKOUT_SUCCESS到底都會做些什麼?shop.buyProducts又會做些什麼?到底怎麼模擬上面的情況呢?
在組件中發送行動
Dispatching Actions in Components
你可以用this.$store.dispatch('xxx')
在組件中發送行動,或者使用mapActions
助手,這個助手映射組件方法到store.dispatch
調用(要求在根倉庫植入)
import { mapActions } from 'vuex'
export default {
methods: {
...mapActions([
'increment', //映射`this.increment()`給`this.$store.dispatch('increment')`
// `mapActions`也支持負載
'incrementBy' //映射`this.incrementBy(amount)`到`this.$store.dispatch('incrementBy', amount)`
]),
...mapActions({
add: 'increment' // 映射`this.add()`到`this.$store.dispatch('increment')`
})
}
}
編寫行動
行動通常都是異步的,所以那麼我我們又怎麼知道什麼時候一個行動結束了呢?並且更重要的是,我們又怎麼編寫多個行動一起去處理多個複雜的異步流呢。
第一個要去了解的是,store.dispatch
能夠處理,由觸發的行動處理器返回的Promise,並且
它也返回Promise:
actions: {
actionA ({ commit }) {
return new Promise((resolve, reject) => {
setTimeout(() => {
commit('someMutation')
resolve()
}, 1000)
})
}
}
現在我們可以做:
store.dispatch('actionA').then(() => {
// ...
})
並且在另一個行動中:
actions: {
// ...
actionB ({ dispatch, commit }) {
return dispatch('actionA').then(() => {
commit('someOtherMutation')
})
}
}
最終,如果我們使用了async / await,那麼我們可以像這樣編寫行動:
// 假設`getData() 和 `getOtherData()` 返回Promises
actions: {
async actionA ({ commit }) {
commit('gotData', await getData())
},
async actionB ({ dispatch, commit }) {
await dispatch('actionA') // 等待`actionA`完成
commit('gotOtherData', await getOtherData())
}
}
對於一個store.dispatch
在不同的模塊中去觸發不同的行動處理器是可能的。在這樣一個例子中
返回的值會是一個Promise,這個Promise當所有觸發的處理器都已經被解析的時候才解析。
你只管努力,其他的一切交給天意!
模塊Modules
由於使用一個單一的狀態樹,我們應用的所有狀態被包含在一個大對象裏面。然而, 如同as我們的應用在規模上逐漸增長,倉庫可以變得真的很臃腫bloated。
爲了幫忙解決這個問題,Vuex允許我們將我們的倉庫劃分成模塊。每個模塊能包含它自己的狀態,變異,行動,獲得者,並且甚至是嵌套的模塊-一路下來它都是分形的fractal。
const moduleA = {
state: { ... },
mutations: { ... },
actions: { ... },
getters: { ... }
}
const moduleB = {
state: { ... },
mutations: { ... },
actions: { ... }
}
const store = new Vuex.Store({
modules: {
a: moduleA,
b: moduleB
}
})
store.state.a // => `moduleA`'s 狀態
store.state.b // => `moduleB`'s 狀態
模塊本地狀態
在一個模塊的變異和獲得者中,這個第一個接收的實參會是模塊本地的狀態。
const moduleA = {
state: { count: 0 },
mutations: {
increment (state) {
// `state` is the local module state
state.count++
}
},
getters: {
doubleCount (state) {
return state.count * 2
}
}
}
類似的,在模塊的行動中,context.state
會暴露本地狀態,並且根狀態會作爲context.rootState
被暴露:
const moduleA = {
// ...
actions: {
incrementIfOddOnRootSum ({ state, commit, rootState }) {
if ((state.count + rootState.count) % 2 === 1){
commit('increment')
}
}
}
}
同樣,在模塊的獲得者中,根狀態會作爲第三個的實參而暴露
const moduleA = {
// ...
getters: {
sumWithRootCount (state, getters, rootState) {
return state.count + rootState.count
}
}
}
命名空間
默認情況下,在模塊內的行動,變異和獲得者仍然是在全局命名空間下注冊的-這個允許多個模塊去響應相同的變異/行動類型。這裏文檔中沒有提到狀態,這麼說在默認情況下,狀態是在命名空間裏的。
如果你想要你的模塊去做更獨立self-contained或者可重複使用,你可以使用namespaced: true
去把它標記爲命名空間的。當這個模塊註冊了,所有它的獲得者,行動和變異會,基於這個模塊註冊所在的路徑,自動命名空間的。例如:
const store = new Vuex.Store({
modules: {
account: {
namespaced: true,
// 模塊資產assets
state: {}, // 模塊狀態已經嵌套了,並且不會受到命名空間選項的影響
getters: {
isAdmin () {}, // -> getters['account/isAdmin']
},
actions: {
login () {} // -> dispatch('account/login')
},
mutations: {
login () {} // -> commit('account/login')
},
// 嵌套的模塊
modules: {
// 繼承從父母模塊的命名空間
myPage: {
state: {},
getters: {
profile () {} // -> getters['account/profile']
}
},
// 進一步嵌套命名空間
posts: {
namespaced: true,
state: {},
getters: {
popular () {} // -> getters['account/posts/popular']
}
}
}
}
}
})
命名空間的獲得者和行動會接收本地化的getters
,dispatch
和commit
。換句話說,你能不用在相同模塊下寫前綴的情況下使用模塊資產。在命名空間或沒命名空間之間切換,並不影響模塊下的代碼。
訪問在命名空間模塊裏的全局資產
如果你想要使用全局狀態和獲得者,rootState
和rootGetters
作爲第三和第四個實參傳入給獲得者函數,同時也作爲暴露在context
環境對象的屬性於行動函數
爲了在全局命名空間裏發送行動和提交變異,傳遞{ root: true }
作爲第三個實參給dispatch
和commit
。
在命名空間的模塊中註冊全局行動
如果你想要在命名空間模塊裏註冊全局行動,你可以用root: true
來標記,並且將行動的定義替換成函數的處理器。例如:
{
actions: {
someOtherActions((dispatch)) {
dispatch('someAction')
}
},
modules: {
foo: {
namespaced: true,
actions: {
someAction: {
root: true,
handler (namespacedContext, payload) {} // -> 'someAction'
}
}
}
}
}
將助手們和命名空間綁定在一起
當使用mapState
,mapGetters
,mapActions
和maputations
助手,將一個命名空間模塊綁定到組件中時,它能變得有點verbose冗長:
computed: {
...mapState({
a: state => state.some.nested.module.a,
b: state => state.some.nested.module.b
})
},
methods: {
...mapActions([
'some/nested/module/foo', // -> this['some/nested/module/foo']()
'some/nested/module/bar' // -> this['some/nested/module/bar'])()
])
}
在這樣的情況下,你可以傳遞這個模塊的命名空間字符串作爲第一個實參給助手們,以便於所有的綁定是使用模塊作爲環境而完成的。上面的代碼可以簡化成:
computed: {
...mapState('some/nested/module', {
a: state => state.a,
b: state => state.b
})
},
methods: {
...mapActions('some/nested/modules', [
'foo', // -> this.foo()
'bar' // -> this.bar()
])
}
更進一步,你可以通過使用createNamespacedHelpers
來創建命名空間助手。它返回一個對象,讓新的組件綁定助手們,那些助手們是和這個給定的命名空間值綁定一起的:
import { createNamespacedHelpers } from 'vuex'
const { mapState, mapActions } = createNamespacedHelpers('some/nested/module')
export default {
computed: {
// look up 查閱 in `some/nested/module`
...mapState({
a: state => state.a,
b: state => state.b
})
},
methods: {
// look up in `some/nested/module`
...mapActioins([
'foo',
'bar'
])
}
}
給插件開發者的警告caveats
你可能會關心,當你創建一個,提供了模塊們,並且讓用戶將他們添加到Vuex倉庫的插件時,對你的模塊那不可預測的命名空間。如果這個插件的使用者在命名空間模塊下添加你的模塊們, 你的模塊們也會命名空間的。
// get namespace value via plugin option
// and return Vuex plugin function
export function createPlugin (options = {}) {
return function (store) {
// add namespace to plugin module's types
const namespace = options.namespace || ''
store.dispatch(namespace + 'pluginAction')
}
}
動態模塊註冊
你可以在倉庫使用store.registerModule
方法創建後,註冊一個模塊:
// register a module `myModule`
store.registerModule('myModule', {
/// ...
})
// register a nested module `nested/myModule`
store.registerModule(['nested', 'myModule'], {
// ...
})
這個模塊的狀態會作爲store.state.myModule
和store.state.nested.myModule
而暴露出來。
對於其他Vuex插件,那些插件爲了狀態管理, 通過附加一個模塊到應用的商店,也去leverage利用Vuex, 動態的模塊註冊讓這個變成可能。例如,在一個動態的附加的模塊裏,通過管理這個應用的路由狀態,這個vuex-router-sync
庫將vuex和vue-router整合到一起。
使用`store.unregisterModule(moduleName),你也可以移除一個動態的註冊的模塊。注意使用這個方法,你不能移除靜態的模塊(在商店創造時聲明的)。
維持狀態
當註冊一個新的模塊時,你想要保持之前的狀態,例如維持從一個服務器端渲染的應用的狀態, 是可能的,那你可以通過使用這個preserveState
的選項來實現:store.registerModule('a', module, { preserveState: true })
當你設置了preserveState: true
的時候,這個模塊就是註冊的了,行動,變異和獲得者都被添加倉庫裏,但是狀態不會。它假設了你的倉庫已經包含了給這個模塊的狀態,並且你不想要覆蓋它。
模塊重利用
有時候我們可能需要創建多個一個模塊的實例,例如:
- 創建多個使用相同模塊的倉庫(例如e.g. 當這個
runInNewContext
選項是false
或者once
的時候,去避免在服務器渲染中有狀態的單例模式) - 在相同的倉庫註冊相同的模塊多次
如果我們使用一個普通的對象去聲明模塊的狀態,那麼這個狀態的對象就會通過引用而被分享,並且當它被變異的時候,造成跨倉庫/模塊的狀態污染。
這確切就是使用data`在Vue組件裏一樣的問題。所以這個污染也是一樣-爲了聲明模塊狀態使用一個函數(2.3.0+支持)
const MyReusableModule = {
state () {
return {
foo: 'bar'
}
}
// mutations, actions, getters
}
應用結構
Vuex並不真的限制你怎麼結構化你的代碼。不如說rather,它強制enforce一套高等級的原則:
- 應用級別的狀態是集中在倉庫裏的。
- 這個唯一去變異狀態的方式時通過提交變異,這是同步的交易transactions。
- 異步邏輯應該被封進內部encapsulated in,並且也能由行動組成。
只要你遵從規則,這取決於你如何構造你的項目。如果你的倉庫文件變得很大,簡單的開始分解你的行動,變異和獲得者成separate單獨的文件。
對於任何不是微不足道的non-trivial的應用,需要去利用leverage模塊。這就是一個樣本項目結構:
├── index.html
├── main.js
├── api
│ └── ... # abstractions for making API requests
├── components
│ └── App.vue
│ └── ...
└── store
├── index.js # where we assemble modules and export the store
├── actions.js # root actions
├── mutations.js # root mutations
└── modules
├── cart.js # cart module
└── products.js # products module
作爲參考,查看這個購物車案例。
這個要看一下這個項目的, 項目寫的挺好的。之後商城都能用用這個。
插件
Vuex商店接受這個plugins
插件選項,這個插件選項爲每一個變異暴露鉤子。一個Vuex插件簡單的就是一個函數,這個函數接受倉庫作爲唯一的實參。
const myPlugin = store => {
// 當倉庫初始化時被調用
store.subscribe((mutation, state) => {
// 在每一次變異後被調用
// 這個變異是以`{ type, payload}`這樣的形式存在comes in。
})
}
而且也能像這樣使用
const store = new Vuex.Store({
// ...
plugins [myPlugin]
})
在插件在提交變異
插件不允許去直接變異狀態 - 類似於你的組件,通過提交變異,他們只能觸發改變。
通過提交變異,一個插件能夠被使用去同步一個數據來源到這個倉庫。例如,去同步一個websocket數據源到這個倉庫(這只是一個人爲的contrived案例,在現實中這個createWebSocketPlugin
函數能夠爲了更復雜的任務採取一些額外的選項。
export defualt function createWebSocketPlugin (socket) {
return store => {
socket.on('data', data => {
store.commit('receiveData', data)
})
store.subscribe(mutation => {
if (mutation.type === 'UPDATE_DATA') {
socket.emit('update', mutation.payload)
}
})
}
}
const plugin = createWebSocketPlugin(socket)
cosnt store = new Vuex.Store({
state,
mutations,
plugins: [plugin]
})
拍狀態快照
有時候一個可能想要去接收狀態的快照,並且也將預變異狀態pre-mutation和發佈狀態post-mutation進行比較。爲了實現這個,你會在要狀態對象上實施一個深層拷貝。
const myPluginWithSnapshot = store => {
let prevState = _.cloneDeep(store.state)
store.subscribe((mutation, state) => {
let nextState = _.cloneDeep(state)
// compare `prevState` and `nextState`..
// save state for next mutation
prevState = nextState
})
}
拍狀態快照的插件應該只在開發的過程中使用。當使用webpack或者Browserify時,我們可以讓我們的構建工具爲我們處理那個
const store = new Vuex.Store({
// ...
plugins: process.env.NODE_ENV !== 'production'
? [myPluginWithSnapshot]
: []
})
默認情況下這個插件會被使用。對於生成環境,你會需要DefinePlugin給webpack定義插件,或者爲Browserify用envify,爲我們最終的構建,去轉換這個process.env.NODE_ENV !== 'production'
爲false
。
內嵌的記錄器插件
如果你正在使用vue-devtools的話,有可能不需要這個。
爲了常見的調試用處,Vuex伴隨着一個日誌記錄器插件
import createLogger from 'vuex/dist/logger'
const store = new Vuex.Store({
plugins: [createLogger()]
})
這個createLogger
函數takes有一些選項
const logger = createLogger({
collapsed: false, // auto-expand logged mutations
filter (mutation, stateBefore, stateAfter) {
// return `true` if a mutation should be logged
// `mutation` is a `{type, payload}`
return mutation.type !== 'aBlacklistedMutation'
},
transformer (state) {
// transform the state before loggin it
// for example return only a specific sub-tree
return state.subTree
},
mutationTransformer (mutation) {
// mutations are logged in the format of `{ type, payload }`
// we can format it any way we want
return mutation.type
},
logger: console, //implementation of the `console` API, default `console`
})
這個日誌記錄器文件通過<script>
標籤也能被直接包含,並且會全局暴露這個createVuexLogger
函數。
留意這個日誌記錄器插件拍狀態快照,所以只在開發時使用它。
嚴格模式
爲了激活嚴格模式,當創建一個Vuex倉庫的時候,簡單的傳入strict: true
:
const store = new Vuex.Store({
// ...
strict: true
})
在個模式中,無論何時Vuex狀態在超出變異處理器之外的情況下被變異,一個錯誤將會被拋出。這就確保了所有的狀態變異能夠明確的被調試工具追蹤。
開發 vs 生產
當給生產情況部署時,不要激活嚴格模式!。嚴格模式爲了檢測不恰當的變異,在狀態樹上,運行一個異步的深度觀察者,並且當你給狀態做大量的變異時,這能夠是相當的昂貴。確保在生產環境將它關閉去避免性能損耗。
和插件相似,我們能讓我們的構建工具處理那個
const store = new Vuex.Store({
// ...
strict: process.env_NODE_ENV !== 'production'
})
表單處理
當在嚴格模式下使用Vuex時,在一個屬於Vuex的一塊狀態上,去使用v-model
,可能會有一些棘手。
<input v-model="obj.message">
假設obj
是一個計算的屬性,這個屬性是從倉庫返回的一個對象,當用戶在input中輸入時,這裏的這個v-model
會嘗試去直接變異obj.message
。在嚴格模式下,這個會造成一個錯誤,因爲這個變異並不是在一個明確的Vuex變異處理器中執行。
這個"Vuex 方式"去處理這個問題是綁定這個<input>
的值,並且在這個input
上調用一個行動,或者change
事件
<input :value="message" @input="updateMessage">
// ...
computed: {
...mapState({
message: state => state.obj.message
})
},
methods: {
updateMessage (e) {
this.$store.commit('updateMessage', e.target.value)
}
}
並且這裏是變異處理器
// ...
mutations: {
updateMessage (state, message) {
state.obj.message = message
}
}
雙向計算屬性
應當承認,上面的是相當的繁瑣,鄉杜雨v-model
+本地狀態,並且我們也失去了一些從v-model
來的有用的特色。一個交替的方式是使用一個帶有一個設值函數的雙向計算屬性
<input v-model="message">
// ...
computed: {
message: {
get() {
return this.$store.state.obj.message
},
set (value) {
this.$store.commit('updateMessage', value)
}
}
}
測試
這個主要的部分我們想要在Vuex中進行單元測試的是變異和行動。
測試變異
變異是非常簡單straightforward去測試,因爲他們只是函數,哪個完全依賴於他們自己的實參。一個小技巧是如果你使用ES2015模塊,並且將你的變異放到store.js
文件裏,除了默認的輸出外,你也應該導出變異作爲一個命名的輸出
const state = { }
//export const mutations = {}
export default new Vuex.Store({
state,
mutations
})
例子使用Mocha + Chai測試一個變異(你能使用任何的你喜歡的框架/斷言assertion庫)
// mutations.js
export const mutations = {
increment: state => state.count++
}
// mutations.spec.js
import { expect } from 'chai'
import { mutations } from './store'
// destructure assign `mutations`
const { increment } = mutations
describe('mutations', () => {
it('INCREMENT', () => {
// mock state
const state = { count: 0 }
// apply mutation
increment(state)
// assert result
expect(state.count).to.equal(1)
})
})
這裏要安裝安裝好Mocha, Chai, 測試用的是在瀏覽器中直接運行的。不過案例中的用到了Impot和export那基本上要用上babel。
測試行動
行動能是更棘手一點,因爲他們可能召喚call out to外部的APIs。當測試行動時,我們通常需要做某種程度的some level of模擬mocking。例如,我們能將API調用抽象成一個服務,並且模擬在我們測試中的那個服務。爲了輕鬆模擬依賴dependencies,我們能使用webpack和inject-loader注射加載器去打包bundle我們的測試文件。
例子測試一個異步行動:
// actions.js
import shop from '..api/shop'
export const getAllProducts = ({ commit }) => {
commit('REQUEST_PRODUCTS')
shop.getProducts(products => {
commit('RECEIVE_PRODUCTS', products)
})
})
// actions.spec.js
// use require syntax for inline loaders
// with inject-loader, this returns a module factory
// that allows us to inject mocked dependencies
import { export } from 'chai'
const actionsInjector = require('inject-loader!./actions')
// create the module with our mocks
const actions = actionsInjector({
'../apishop': {
getProducts (cb) {
setTimeout(() => {
cb([ /* mocked response */])
}, 100)
}
}
})
// helper for testing action with expected mutations
const testAction = (action, payload, state, expectedMutations, done) => {
let count = 0
// mock commit
const commit = (type, payload) => {
const mutation = expectedMutations[count]
try {
expect(type).to.equal(mutation.type)
if (payload) {
expect(payload).to.deep.equal(mutation.payload)
}
} catch (error) {
done(error)
}
count++
if (count >= expectedMutations.length) {
done()
}
}
// call the action with mocked store and arguments
action({ commit, state }, payload)
// check if no mutations should have been dispatched
if (expectedMutations.length === 0) {
expect(count).to.equal(0)
done()
}
}
describe('actions', () => {
it('getAllProducts', done => {
testAction(actions.getAllProducts, null, {}, [
{ type: 'REQUEST_PRODUCTS' },
{ TYPE: 'RECEIVE_PRODUCTS', payload: { /* mocked response */}}
], done)
})
})
如果在你的測試環境中(例如使用Sinon.JS)你有間諜spies可利用,你能使用他們代替testAction
助手。
describe('actions', () => {
it('geteAllProducts', () => {
const commit = sinon.spy()
const state = {}
actions.getAllProducts({ commit, state })
expect(commit.args).to.deep.equal([
['REQUEST_PRODUCTS'],
['RECEIVE_PRODUCTS', { /* mocked response */}]
])
})
})
測試的話如果只用browser的話,那還好配置,都放到一個js文件就可以。如果是多個文件,然後用到Import和export的話,最好用vue-cli搭建一下環境,一個入口文件test.js隨便取什麼名, store.js裏面放上const store和export const mutations,這裏參考一下main.js。
調試的過程中出現問題了,我們先往下面看,後面來回來弄!
測試獲得者
運行測試
如果你的變異和行動都正確的寫出了,在適當的模擬後,在瀏覽器APIs上,這個測試應該沒有直接的依賴。因此thus你能簡單的使用webpack打包這些測試,並且直接在Node中運行。二選一的Alternatively,你能使用mocha-loader
或者Karma+karma-webpack
去在真實的瀏覽器中運行這些測試。
在Node中運行
創建下列的webpack配置(和恰當的.babelrc一起)
// webpack.config.js
module.exports = {
entry: './test.js',
output: {
path: __dirname,
filename: 'test-bundle.js'
},
module: {
loaders: [
{
test: /\.js$/,
loader: 'babel-loader',
exclude: /node_modules/
}
]
}
}