插件
Vuex的插件就是一個函數,接收store
作爲唯一參數,通過subscribe
對store
每次的mutation
進行監聽:
const myPlugin = store => {
// 當store初始化後調用
store.subscribe((mutation, state) => {
// 每次 mutation 之後調用
// mutation 的格式爲 { type, payload }
})
}
插件需要使用plugins
選項引入:
const store = new Vuex.Store({
// ...
plugins: [myPlugin]
})
插件內同樣不允許直接修改state
,只能通過只能通過提交mutation
來觸發變化
嚴格模式
開啓嚴格模式,當不通過mutation
而直接修改state
時,Vuex都會拋出錯誤。
不要再發布環境下啓用嚴格模式,嚴格模式會深度檢測狀態樹來檢測不合規的狀態變更,造成性能上的損失:
const store = new Vuex.Store({
// ...
strict: process.env.NODE_ENV !== 'production'
})
表單處理
在開啓了嚴格模式後,把Vuex的state
使用到v-model
會報錯:
<input v-model="subTitle.message" type="text" />
上面的代碼中的subTitle
是屬於Vuex的store的對象,用戶輸入時相當於沒有通過mutation
直接修改了state
,Vuex會拋出錯誤:
Error: [vuex] do not mutate vuex store state outside mutation handlers.
解決方法有兩個,一種是利用了v-model
的語法糖的本質,將obj.message
作爲value
,再input
方法中手動觸發commit
方法,然後在mutation
中修改state
值
<input :value="subTitle.message" type="text" @input="inputHandler"/>
export default {
methods: {
inputHandler(e) {
this.$store.commit('changeSubTitle', { message: e.target.value })
},
},
computed: {
...mapState(['subTitle']),
}
}
在Store中:
export default new Vuex.Store({
mutations: {
changeSubTitle(state, { message }) {
state.subTitle.message = message;
}
},
})
另一種方法就是使用帶有setter的雙向綁定計算屬性:
<input v-model="title" type="text" />
computed: {
// 帶有 setter 的雙向綁定的計算書行
title: {
get() {
return this.$store.state.title
},
set(value) {
this.$store.commit('changeTitle', {
message: value
})
}
},
}
測試
Mutation和Getter測試時思路相同,將Mutation或者Getter單獨導出來,在測試文件中模擬一個state
,來進行斷言:
const state = {
count: 0,
}
// mutations 作爲命名輸出對象
export const mutations = {
increment: state => state.count++
}
export default new Vuex.Store({
state,
mutations
})
// mutations.spec.js
import { expect } from 'chai'
import { mutations } from './store'
const { increment } = mutations
describe('mutations', () => {
it('INCREMENT', () => {
// 模擬狀態
const state = { count: 0 }
// 應用 mutation
increment(state)
// 斷言結果
expect(state.count).to.equal(1)
})
})
測試Action比較麻煩,因爲它們可能會調用外部的API,需要將外部的API調用進行Mock,可以使用webpack和inject-loader打包測試文件:
// actions.js
import shop from '../api/shop'
export const getAllProducts = ({ commit }) => {
commit('REQUEST_PRODUCTS')
shop.getProducts(products => {
commit('RECEIVE_PRODUCTS', products)
})
}
// actions.spec.js
// 使用 require 語法處理內聯 loaders。
// inject-loader 返回一個允許我們注入 mock 依賴的模塊工廠
import { expect } from 'chai'
const actionsInjector = require('inject-loader!./actions')
// 使用 mocks 創建模塊
const actions = actionsInjector({
'../api/shop': {
getProducts (cb) {
setTimeout(() => {
cb([ /* mocked response */ ])
}, 100)
}
}
})
// 用指定的 mutations 測試 action 的輔助函數
const testAction = (action, args, state, expectedMutations, done) => {
let count = 0
// 模擬提交
const commit = (type, payload) => {
const mutation = expectedMutations[count]
try {
expect(mutation.type).to.equal(type)
if (payload) {
expect(mutation.payload).to.deep.equal(payload)
}
} catch (error) {
done(error)
}
count++
if (count >= expectedMutations.length) {
done()
}
}
// 用模擬的 store 和參數調用 action
action({ commit, state }, ...args)
// 檢查是否沒有 mutation 被 dispatch
if (expectedMutations.length === 0) {
expect(count).to.equal(0)
done()
}
}
describe('actions', () => {
it('getAllProducts', done => {
testAction(actions.getAllProducts, [], {}, [
{ type: 'REQUEST_PRODUCTS' },
{ type: 'RECEIVE_PRODUCTS', payload: { /* mocked response */ } }
], done)
})
})
還是挺複雜的,實際上actionsInjector
模塊mock的僅僅是API部分(../api/shop
),而actions.getAllProducts
執行的還是原來的action
,但是commit
和state
已經都被我們替換了。
如果可以使用Sinon.JS,那麼可以使用它來替換上面的輔助函數testAction
:
describe('actions', () => {
it('getAllProducts', () => {
// 一步直接模擬 commit
const commit = sinon.spy()
const state = {}
actions.getAllProducts({ commit, state })
expect(commit.args).to.deep.equal([
['REQUEST_PRODUCTS'],
['RECEIVE_PRODUCTS', { /* mocked response */ }]
])
})
})
如果需要給Vuex寫單元測試的時候,還是需要到這裏對照着例子來實現一下。
執行測試可以在Node環境下,也可以在瀏覽器環境下,在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/
}
]
}
}
執行的時候:
$ webpack
$ mocha test-bundle.js
在瀏覽器中測試可以參考文檔。
熱重載
Vue-cli腳手架針對Vuex提供了熱刷新的功能,當更改Store的數據,頁面會自動刷新,但是相比於Vue組件的熱重載功能,體驗還是略遜一籌。
Vuex想要實現熱重載,也是藉助了webpack的Hot Module Replacement API,以前曾經學習過它的實現原理(注意,面試的時候的高頻題目)
實現熱重載的前提就是,必須將代碼模塊化,所以Store中的Mutation/Module/Action/Getter必須導出爲單獨的JS文件,纔可以實現熱重載
if (module.hot) {
module.hot.accept(['./modules/todo-list'], () => {
// 獲取更新後的模塊
// 因爲 babel 6 的模塊編譯格式問題,下面需要加上 .default
const newTodoList = require('./modules/todo-list').default;
console.log(newTodoList);
// 加載新模塊
store.hotUpdate({
modules: {
store_todoList: newTodoList,
}
})
})
}
注意:熱重載的目標只能是Mutation/Module/Action/Getter,手動對state
的修改不能觸發HMR,可以參考這個issue。所以這就導致了一個問題:
如果配置了熱重載,那麼如果改動state時就必須手動刷新,熱刷新也沒有了;如果不配置熱重載,修改任何文件都是熱刷新。這樣的話熱重載我感覺意義不大