學習Vue的狀態管理模式Vuex

學習Vue的狀態管理模式Vuex

什麼是狀態管理模式State Management Pattern?

組成:View視圖,State狀態,Actions行動

基本的情況參考官方文檔

爲什麼要用Vuex?

  1. 多個視圖可能依賴相同的狀態片段

    比如兄弟組件sibling components

  2. 從不同視圖的行動可能會需要改變相同的狀態

    文檔說情況一: 想要伸手去拿直系父/子實例引用。我的理解,狀態是存在父組件的,比如一個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:

  1. 偏向於初始化你的,帶有所有前面upfront期望desired的字段fields,倉庫初始狀態。什麼意思?是說如果我想在某個組件裏要某個字段如count,那麼這裏會初始化是嗎?

  2. 當添加新的屬性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')
        })
    }
}

移交行動

On to Actions

異步性與狀態變異結合讓你的程序非常難推出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。

發送行動

Dispatching Actions

行動是用帶有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')`
    })
    }
}

編寫行動

Composing Actions

行動通常都是異步的,所以那麼我我們又怎麼知道什麼時候一個行動結束了呢?並且更重要的是,我們又怎麼編寫多個行動一起去處理多個複雜的異步流呢。

第一個要去了解的是,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 狀態

模塊本地狀態

Module Local State

在一個模塊的變異和獲得者中,這個第一個接收的實參會是模塊本地的狀態。

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
        }
    }
}

命名空間

Namespacing

默認情況下,在模塊內的行動,變異和獲得者仍然是在全局命名空間下注冊的-這個允許多個模塊去響應相同的變異/行動類型。這裏文檔中沒有提到狀態,這麼說在默認情況下,狀態是在命名空間裏的。

如果你想要你的模塊去做更獨立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,dispatchcommit。換句話說,你能不用在相同模塊下寫前綴的情況下使用模塊資產。在命名空間或沒命名空間之間切換,並不影響模塊下的代碼。

訪問在命名空間模塊裏的全局資產

如果你想要使用全局狀態和獲得者,rootStaterootGetters作爲第三和第四個實參傳入給獲得者函數,同時也作爲暴露在context環境對象的屬性於行動函數

爲了在全局命名空間裏發送行動和提交變異,傳遞{ root: true }作爲第三個實參給dispatchcommit

在命名空間的模塊中註冊全局行動

如果你想要在命名空間模塊裏註冊全局行動,你可以用root: true來標記,並且將行動的定義替換成函數的處理器。例如:

{
  actions: {
    someOtherActions((dispatch)) {
      dispatch('someAction')
    }
  },
  modules: {
    foo: {
      namespaced: true,
      
      actions: {
      	someAction: {
      		root: true,
      		handler (namespacedContext, payload) {}  // -> 'someAction'
      	}
      }
          }
  }

}

將助手們和命名空間綁定在一起

當使用mapState,mapGetters,mapActionsmaputations助手,將一個命名空間模塊綁定到組件中時,它能變得有點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.myModulestore.state.nested.myModule而暴露出來。

對於其他Vuex插件,那些插件爲了狀態管理, 通過附加一個模塊到應用的商店,也去leverage利用Vuex, 動態的模塊註冊讓這個變成可能。例如,在一個動態的附加的模塊裏,通過管理這個應用的路由狀態,這個vuex-router-sync庫將vuex和vue-router整合到一起。

使用`store.unregisterModule(moduleName),你也可以移除一個動態的註冊的模塊。注意使用這個方法,你不能移除靜態的模塊(在商店創造時聲明的)。

維持狀態

Preserving state

當註冊一個新的模塊時,你想要保持之前的狀態,例如維持從一個服務器端渲染的應用的狀態, 是可能的,那你可以通過使用這個preserveState的選項來實現:store.registerModule('a', module, { preserveState: true })

當你設置了preserveState: true的時候,這個模塊就是註冊的了,行動,變異和獲得者都被添加倉庫裏,但是狀態不會。它假設了你的倉庫已經包含了給這個模塊的狀態,並且你不想要覆蓋它。

模塊重利用

有時候我們可能需要創建多個一個模塊的實例,例如:

如果我們使用一個普通的對象去聲明模塊的狀態,那麼這個狀態的對象就會通過引用而被分享,並且當它被變異的時候,造成跨倉庫/模塊的狀態污染。

這確切就是使用data`在Vue組件裏一樣的問題。所以這個污染也是一樣-爲了聲明模塊狀態使用一個函數(2.3.0+支持)

const MyReusableModule = {
    state () {
        return {
            foo: 'bar'
        }
    }
    // mutations, actions, getters
}

應用結構

Vuex並不真的限制你怎麼結構化你的代碼。不如說rather,它強制enforce一套高等級的原則:

  1. 應用級別的狀態是集中在倉庫裏的。
  2. 這個唯一去變異狀態的方式時通過提交變異,這是同步的交易transactions。
  3. 異步邏輯應該被封進內部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/
            }
        ]
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章