学习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/
            }
        ]
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章