插件
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时就必须手动刷新,热刷新也没有了;如果不配置热重载,修改任何文件都是热刷新。这样的话热重载我感觉意义不大