Vuex其他知识点。
插件
Vuex的插件就是一个函数,接收store
作为唯一参数,通过subscribe
对store
每次的mutation
进行监听:
1 2 3 4 5 6 7
| const myPlugin = store => { store.subscribe((mutation, state) => { }) }
|
插件需要使用plugins
选项引入:
1 2 3 4
| const store = new Vuex.Store({ plugins: [myPlugin] })
|
插件内同样不允许直接修改state
,只能通过只能通过提交mutation
来触发变化
严格模式
开启严格模式,当不通过mutation
而直接修改state
时,Vuex都会抛出错误。
不要再发布环境下启用严格模式,严格模式会深度检测状态树来检测不合规的状态变更,造成性能上的损失:
1 2 3 4
| const store = new Vuex.Store({ strict: process.env.NODE_ENV !== 'production' })
|
表单处理
在开启了严格模式后,把Vuex的state
使用到v-model
会报错:
1
| <input v-model="subTitle.message" type="text" />
|
上面的代码中的subTitle
是属于Vuex的store的对象,用户输入时相当于没有通过mutation
直接修改了state
,Vuex会抛出错误:
1
| Error: [vuex] do not mutate vuex store state outside mutation handlers.
|
解决方法有两个,一种是利用了v-model
的语法糖的本质,将obj.message
作为value
,再input
方法中手动触发commit
方法,然后在mutation
中修改state
值
1
| <input :value="subTitle.message" type="text" @input="inputHandler"/>
|
1 2 3 4 5 6 7 8 9 10
| export default { methods: { inputHandler(e) { this.$store.commit('changeSubTitle', { message: e.target.value }) }, }, computed: { ...mapState(['subTitle']), } }
|
在Store中:
1 2 3 4 5 6 7
| export default new Vuex.Store({ mutations: { changeSubTitle(state, { message }) { state.subTitle.message = message; } }, })
|
另一种方法就是使用带有setter的双向绑定计算属性:
1
| <input v-model="title" type="text" />
|
1 2 3 4 5 6 7 8 9 10 11 12 13
| computed: { title: { get() { return this.$store.state.title }, set(value) { this.$store.commit('changeTitle', { message: value }) } }, }
|
测试
Mutation和Getter测试时思路相同,将Mutation或者Getter单独导出来,在测试文件中模拟一个state
,来进行断言:
1 2 3 4 5 6 7 8 9 10 11 12 13
| const state = { count: 0, }
export const mutations = { increment: state => state.count++ }
export default new Vuex.Store({ state, mutations })
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| import { expect } from 'chai' import { mutations } from './store'
const { increment } = mutations
describe('mutations', () => { it('INCREMENT', () => { const state = { count: 0 } increment(state) expect(state.count).to.equal(1) }) })
|
测试Action比较麻烦,因为它们可能会调用外部的API,需要将外部的API调用进行Mock,可以使用webpack和inject-loader打包测试文件:
1 2 3 4 5 6 7 8 9
| import shop from '../api/shop'
export const getAllProducts = ({ commit }) => { commit('REQUEST_PRODUCTS') shop.getProducts(products => { commit('RECEIVE_PRODUCTS', products) }) }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59
|
import { expect } from 'chai' const actionsInjector = require('inject-loader!./actions')
const actions = actionsInjector({ '../api/shop': { getProducts (cb) { setTimeout(() => { cb([ ]) }, 100) } } })
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() } }
action({ commit, state }, ...args)
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: { } } ], done) }) })
|
还是挺复杂的,实际上actionsInjector
模块mock的仅仅是API部分(../api/shop
),而actions.getAllProducts
执行的还是原来的action
,但是commit
和state
已经都被我们替换了。
如果可以使用Sinon.JS,那么可以使用它来替换上面的辅助函数testAction
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| describe('actions', () => { it('getAllProducts', () => { const commit = sinon.spy() const state = {} actions.getAllProducts({ commit, state }) expect(commit.args).to.deep.equal([ ['REQUEST_PRODUCTS'], ['RECEIVE_PRODUCTS', { }] ]) }) })
|
如果需要给Vuex写单元测试的时候,还是需要到这里对照着例子来实现一下。
执行测试可以在Node环境下,也可以在浏览器环境下,在Node环境下执行的时候需要创建以下webpack配置(需要配置好.babelrc
):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| module.exports = { entry: './test.js', output: { path: __dirname, filename: 'test-bundle.js' }, module: { loaders: [ { test: /\.js$/, loader: 'babel-loader', exclude: /node_modules/ } ] } }
|
执行的时候:
1 2
| $ webpack $ mocha test-bundle.js
|
在浏览器中测试可以参考文档。
热重载
Vue-cli脚手架针对Vuex提供了热刷新的功能,当更改Store的数据,页面会自动刷新,但是相比于Vue组件的热重载功能,体验还是略逊一筹。
Vuex想要实现热重载,也是借助了webpack的Hot Module Replacement API,以前曾经学习过它的实现原理(注意,面试的时候的高频题目)
实现热重载的前提就是,必须将代码模块化,所以Store中的Mutation/Module/Action/Getter必须导出为单独的JS文件,才可以实现热重载
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| if (module.hot) { module.hot.accept(['./modules/todo-list'], () => { const newTodoList = require('./modules/todo-list').default;
console.log(newTodoList);
store.hotUpdate({ modules: { store_todoList: newTodoList, } }) }) }
|
注意:热重载的目标只能是Mutation/Module/Action/Getter,手动对state
的修改不能触发HMR,可以参考这个issue。所以这就导致了一个问题:
如果配置了热重载,那么如果改动state时就必须手动刷新,热刷新也没有了;如果不配置热重载,修改任何文件都是热刷新。这样的话热重载我感觉意义不大