学习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/
}
]
}
}