手册网

本手册由手册网整理收藏

手册网（www.shouce.ren）提供最全最新的开发手册

和教程，免费提供在线手册阅读和手册下载。


Vuex 教程

什么是 Vuex

Vuex 是一个专门为 Vue.js 应用设计的 状态管理模型 + 库。它为应用内的所有组件提供集中式存储服务，其中的规则确保状态只能按预期方式变更。它可以与 Vue 官方开发工具扩展(devtools extension) 集成，提供高级特征，比如 零配置时空旅行般（基于时间轴）调试，以及状态快照 导出/导入。

什么是”状态管理模型“？

让我们从一个简单的 Vue 计数器应用开始：

new Vue({
  // state (状态)
  data () {
    return {
      count: 0
    }
  },
  // view (视图)
  template: `
    <div>{{ count }}</div>
  `,
  // actions (动作)
  methods: {
    increment () {
      this.count++
    }
  }
})

这是一个含有以下部分的自包含( self-contained )应用：

  • state (状态)，驱动我们应用的真实的源；
  • view (视图)，对应着 状态 的声明式映射；
  • actions (动作)，用户在 视图 上的输入引起状态的更改的可能方式。

这是一个非常简单的“单向数据流”思想的体现：

不管怎样，当我们有多组件共享公共状态时，简单快速地分解如下：

  • 多视图可能依赖于同一份状态。
  • 来自不同视图的动作可能需要变更同一份状态。

对于问题一，在深层嵌套组件间传递属性过于冗长，并且不能简单地在同级别的组件间传递。对于问题二，我们常常采取这样的解决方案：直接引用 父/子 实例，又或是通过事件来修改和同步多份状态副本。这样的模型是脆弱的，代码很快会变得不可维护。

所以我们为什么不从多个组件中把共享状态提取出来并作为全局单件管理呢？这样的话，我们的组件树就变成了一个大的”视图“，树中的任意一个组件，不管它们在哪个位置，都可以访问状态或者触发动作！

另外，通过定义和分离状态管理中的概念，强制使用某些规则，使得代码更结构化和高可维护性。

这是 Vuex 背后的基本思想，受到了 Flux，Redux 和 The Elm Architecture的启发。不像其他模型，Vuex 也是一个专门为 Vue.js 设计的库，充分利用了它微妙的响应式机制，从而实现高效更新。

什么时候应该使用?

尽管 Vuex 帮助我们处理共享状态管理，但是也带来了更多的思考和样板文件。这是一个短期效益和长期效益的权衡。

如果你没有开发过大型的单页应用(SPA)就立刻上 Vuex，可能会觉得繁琐然后排斥，这是很正常的 —— 如果是个简单的应用，大多数情况下，不用 Vuex 还好，你要的可能就是个简单的 全局事件总线(global event bus)。不过，如果你构建的是一个中到大型单页应用，当你在考虑如何更好的在 Vue 组件外处理状态时，Vuex 自然就是你的下一步选择。Redux 的作者有一句话说的不错：

原文：Flux libraries are like glasses: you’ll know when you need them.
译文：Flux 库就像眼镜：当你需要它们的时候你会懂的。

手册网


Vuex 安装

直接下载 / CDN

https://unpkg.com/vuex

Unpkg.com 提供基于 NPM 的 CDN 链接。以上链接总是保持在 NPM 上的最新版本。你也可以通过类似于这样的 URL https://unpkg.com/vuex@2.0.0 使用特殊的 版本/标签。

在包含 Vue 之后包含 vuex，它会自动安装：

<script src="/path/to/vue.js"></script>
<script src="/path/to/vuex.js"></script>

NPM

npm install vuex

当配合模块系统一起使用时，你必须通过 Vue.use() 显式安装到路由上：

import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)

当你使用全局脚本标签时不需要这样做。

使用开发版

你如果想使用最新的开发版，请直接去 GitHub 克隆，然后构建你自己的 vuex。

git clone https://github.com/vuejs/vuex.git node_modules/vuex
cd node_modules/vuex
npm install
npm run build


Vuex 起步

所有 Vuex 应用的中心就是 store（状态存储）。”store” 本质上是一个保存应用状态的容器。这里有两件要点，让 Vuex store 区别于普通全局对象：

  1. Vuex store 是响应式的。当 Vue 组件从 store 读取状态，如果 store 中的状态更新，它们也会高效地响应更新。

  2. 你不能直接更改 store 中的状态。更改状态的唯一的方法就是显式 提交更改 (committing mutations)。这样可以确保每次状态更改都留有可追踪的记录，并且可以使用工具帮助我们更好地理解我们的应用。

最简单的 Store

NOTE: 我们将会在以后的示例代码中使用 ES2015 语法。如果你还没有用过，你应该看看！

安装 Vuex 之后，我们来创建一个 store。这是一个非常易懂的例子 - 只含有一个初始化状态对象，以及一些更改：

// 如果使用模块系统，确保之前调用过 Vue.use(Vuex)
const store = new Vuex.Store({
  state: {
    count: 0
  },
  mutations: {
    increment (state) {
      state.count++
    }
  }
})

现在，你可以通过 store.state 访问状态对象，以及通过 store.commit 触发状态的更改：

store.commit('increment')
console.log(store.state.count) // -> 1

再次说明，我们提交 mutation 而不是直接更改 store.state.count，是想要显式地追踪它。这个简单的转换使你的意图更加明显。当你阅读代码时，可以更好地了解应用中的状态更改。另外，这也让我们有机会去实现一些工具，记录修改日志、保存状态快照、甚至是进行基于时间轴的调试。

因为 store 的状态是响应式的，要在组件中使用 store 的状态，只需在 computed 属性中返回 store 的状态就行。而触发状态改变可以简单的理解为在组件方法中提交 mutation。

这是一个 最基本的 Vuex 计数应用 的例子。

接下来，我们将在更多的细微细节中讨论每个核心思想，让我们从 State 开始吧。


Vuex State

单一状态树

Vuex 使用 单一状态树 - 是的，用一个对象就包含了全部的应用层级状态，然后作为一个『唯一数据源(SSOT)』而存在。这也意味着，每一个应用将只有一个 store 实例。单一状态树让我们能够直接地定位任一特定的状态片段，在调试的过程中也能轻易地取得整个当前应用状态的快照。

单状态树和模块化并不冲突 - 在后面的章节里我们会讨论如何将状态(state)和状态变更事件(mutation)分布到各个子模块中。

在 Vue 组件中获得 Vuex 状态

那么我们如何在 Vue 组件中展示状态呢？由于 Vuex 的状态存储是响应式的，从 store 实例中读取状态最简单的方法，就是在计算属性的函数中直接返回某个 store 的状态：

// 创建一个计数器组件
const Counter = {
  template: `<div>{{ count }}</div>`,
  computed: {
    count () {
      return store.state.count
    }
  }
}

当 store.state.count 发生变化，将会引发重新对计算属性取值，并且相关联的 DOM 将触发更新。

然而，这种模式导致组件依赖于全局状态单例。当使用模块系统时，还需要在每个组件都去引入 store，才能使每个组件都能使用 store 的状态，同时测试组件时也需要模拟出 store。

Vuex 提供一个机制，设置 store 选项（启用Vue.use(Vuex)）将 store 从根组件『注入』到每一个子组件中：

const app = new Vue({
  el: '#app',
  // 使用 "store" 选项后，可以注册 store 对象。将会把 store 实例注入到所有子组件。
  store,
  components: { Counter },
  template: `
    <div class="app">
      <counter></counter>
    </div>
  `
})

通过在根实例中注册 store 选项，该 store 实例会被注入到根组件下的所有子组件中，并且子组件可以通过 this.$store 来访问。让我们一起调整刚才 计数器 的实现：

const Counter = {
  template: `<div>{{ count }}</div>`,
  computed: {
    count () {
      return this.$store.state.count
    }
  }
}

mapState 工具

当一个组件需要引用 store 的多个 state 属性或 getter 函数时，声明列举出所有计算属性会变得重复且繁琐。为了解决这个问题，我们可以使用 mapState 工具，它为我们生成 computed 所需的很多个 getter 函数，帮助我们节省一些键盘按键(^_^)：

// vuex 提供了独立的构建工具函数 Vuex.mapState
import { mapState } from 'vuex'
export default {
  // ...
  computed: mapState({
    // 箭头函数可以让代码非常简洁
    count: state => state.count,
    // 传入字符串 'count' 等同于 `state => state.count`
    countAlias: 'count',
    // 想访问局部状态，就必须借助于一个普通函数，函数中使用 `this` 获取局部状态
    countPlusLocalState (state) {
      return state.count + this.localCount
    }
  })
}

当计算属性名称和状态子树名称对应相同时，我们可以向 mapState 工具函数传入一个字符串数组。

computed: mapState([
  // 映射 state.count 到 store.this.count
  'count'
])

对象扩展运算符

注意，mapState 返回一个对象。我们如何使用 mapState 合并其他局部的计算属性呢？通常地，为了将多个对象合并为一个对象，再把这个合并好的最终对象传入到 computed 属性去，我们不得不使用一个工具函数来实现。然而有了对象扩展运算符（ECMAScript 提案 stage-3），我们可以大大简化语法：

computed: {
  localComputed () { /* ... */ },
  // 使用对象扩展运算符，将 mapState 返回的对象和外层其他计算属性混合起来
  ...mapState({
    // ...
  })
}

组件仍然可以具有局部状态

使用 Vuex 并不意味你应该把 所有 状态都放在 Vuex 中去管理。尽管把更多的状态放到 Vuex 管理，会让状态变化变得更加清晰和可调试，但有时也能使代码变得冗余和不直观。如果某部分状态严格属于一个单独的组件，那就只把这部分状态作为局部状态就好了。你应该权衡利弊，做适合您的 App 开发需求的决策。


Vuex Getters

有时，我们可能需要基于 store 中的状态来计算推导状态，例如过滤一个项目列表，并对过滤结果进行计数：

computed: {
  doneTodosCount () {
    return this.$store.state.todos.filter(todo => todo.done).length
  }
}

如果多个组件需要用到这个推导后的状态，那我们就必须到处重复写该计算属性函数；或者将其提取到一个公共的工具函数中，并将公共函数多处导入 - 两者都不太理想。

Vuex 允许我们在 store 定义 “getters” （将它们视为 store 的计算属性）。getter 函数将接收 state 作为第一个参数：

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)
    }
  }
})

这些 getter 函数将导出在 store.getters 对象上：

store.getters.doneTodos // -> [{ id: 1, text: '...', done: true }]

getter 函数也接收其他 getter 作为第二个参数：

getters: {
  // ...
  doneTodosCount: (state, getters) => {
    return getters.doneTodos.length
  }
}
store.getters.doneTodosCount // -> 1

现在我们可以轻松地在任意组件中使用 getter 了：

computed: {
  doneTodosCount () {
    return this.$store.getters.doneTodosCount
  }
}

mapGetters 工具函数

mapGetters 工具函数会将 store 中的 getter 映射到局部计算属性中。

import { mapGetters } from 'vuex'
export default {
  // ...
  computed: {
    // 使用对象扩展操作符把 getter 混入到 computed 中
    ...mapGetters([
      'doneTodosCount',
      'anotherGetter',
      // ...
    ])
  }
}

如果你要将 getter 映射为不同的名称，请使用一个对象：

    mapGetters({

    // 映射 this.doneCount 到 store.getters.doneTodosCount

    doneCount: 'doneTodosCount'

    })


Vuex Mutations

在 Vuex store 中，实际改变 状态(state) 的唯一方式是通过 提交(commit) 一个 mutation。 Vuex 的 mutation 和事件系统非常相似：每个 mutation 都有一个字符串 类型(type) 和 一个 回调函数(handler)。回调函数是我们执行实际修改状态的地方，它将接收 状态(state) 作为第一个参数。

const store = new Vuex.Store({
  state: {
    count: 1
  },
  mutations: {
    increment (state) {
      // 改变 state
      state.count++
    }
  }
})

你不能直接调用 mutation 的回调函数。选项 mutations 在这里更像是注册事件：“当触发类型为 increment 的 mutation 时，执行其回调函数。”所以你需要调用该类型的 store.commit 才能执行 mutation 的回调函数。

store.commit('increment')

Commit 传入 Payload

向 store.commit 传递一个额外的参数，这个参数被称为 payload ：

// ...
mutations: {
  increment (state, n) {
    state.count += n
  }
}
store.commit('increment', 10)

多数情况下，payload 应该是一个对象，以便它可以包含多个字段，这样 mutation 记录中有了 payload 字段名，可描述性会变得更好。

// ...
mutations: {
  increment (state, payload) {
    state.count += payload.amount
  }
}
store.commit('increment', {
  amount: 10
})

对象风格的 Commit

提交 mutation 的另一种替代方式，是直接使用具有 type 属性的对象：

store.commit({
  type: 'increment',
  amount: 10
})

当使用对象风格的 commit，整个对象都会被作为 payload 参数传入到对应类型的 mutation 的回调函数中，不过回调函数还保持不变：

mutations: {
  increment (state, payload) {
    state.count += payload.amount
  }
}

静默的 Commit

注意：一旦我们实现了 devtools 中过滤 mutation，此特性可能会被弃用。

默认情况下，每个提交过的 mutation 都会被发送到插件（如 devtools）。然而在某些情况下，你可能不希望插件去记录每个状态更改。像是在短时间多次提交到 store 或轮询，并不总是需要跟踪。在这种情况下你可以在 store.commit 中传入第三个参数，来指定插件中的 mutation 是否“静默”。

store.commit('increment', {
  amount: 1
}, { silent: true })
// 使用对象风格的 dispatch
store.commit({
  type: 'increment',
  amount: 1
}, { silent: true })

遵循 Vue 响应式规则

由于 Vue 中 Vuex store 的状态是响应式的，当我们改变状态，Vue 组件观察到状态改变将自动更新。这也意味着 Vuex mutation 同样遵循纯 Vue 响应式规则。

  1. 推荐预先初始化 store 中你所需的初始状态。

  2. 向对象添加新的属性时，你应该这样做：

    • 使用 Vue.set(obj, 'newProp', 123)

    • 用新的对象替换该对象。例如，使用 stage-2 对象扩展语法 我们可以这样写：

      state.obj = { ...state.obj, newProp: 123 }

用常量命名 Mutation

在各种 Flux 实现中，使用常量作为 mutation 类型是一种常见的模式。这允许代码利用工具如 linters，将所有常量放在一个单独文件中，尽可能使协作者对整个应用的 mutation 一目了然。

// 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) {
      // 改变状态
    }
  }
})

是否使用常量在很大程度上是一个偏好 - 在多人合作开发的大型项目中它很有用，但如果你不喜欢使用，它也是完全可选的。

Mutation 必须是同步函数

一个重要的原则就是牢记 mutation 必须是同步函数。为什么？考虑下面的例子：

mutations: {
  someMutation (state) {
    api.callAsyncMethod(() => {
      state.count++
    })
  }
}

现在想象我们正在调试应用程序，并查看 devtool 的 mutation 记录。每个 mutation 记录，devtool 将需要捕获每个状态“之前”和“之后”的快照。然而，上面的示例中 mutation 内部的异步回调使得这是不可能的：当 mutation 被提交后，回调函数还未被调用，也没有办法让 devtool 知道回调函数在何时被调用 - 即在回调函数中执行任意状态变更，实际上都无法跟踪。

组件中提交 Mutation

可以在组件中使用 this.$store.commit('xxx') 提交 mutation，或者使用 mapMutations 工具遍历组件方法到 store.commit 的回调上(需要把 store 注入根组件)

import { mapMutations } from 'vuex'
export default {
  // ...
  methods: {
    ...mapMutations([
      'increment' // 映射 this.increment() 到 this.$store.commit('increment')
    ]),
    ...mapMutations({
      add: 'increment' // 映射 this.add() 到 this.$store.commit('increment')
    })
  }
}

下一步：Actions

在 mutation 中混合异步调用会导致你的程序很难调试。例如当你调用两个都含有异步回调的方法去改变状态，你如何知道他们何时被调用和哪个回调被首先调用？这正是我们分离 Mutation 和 Action 这两个概念的原因。在 Vuex，Mutation 必须是同步事务：

store.commit('increment')
// 类型为 "increment" 的 mutation 提交后，可能引起的任意状态变化，都应该在此时同步完成

为了处理异步操作，接下来我们介绍 Actions。


Vuex Actions

action 和 mutation 类似，区别在于：

  • action 不改变状态，只提交(commit) mutation。
  • action 可以包含任意异步操作。

让我们注册一个简单的 action：

const store = new Vuex.Store({
  state: {
    count: 0
  },
  mutations: {
    increment (state) {
      state.count++
    }
  },
  actions: {
    increment (context) {
      context.commit('increment')
    }
  }
})

Action 处理函数接收一个上下文对象(context object)，该对象提供了跟 store 实例相同的方法/属性，因此你可以调用 context.commit 提交一个 mutation，或者通过 context.state 和 context.getters 访问 state 和 getters。稍后我们会介绍 Modules，我们将看到为什么上下文对象 (context object) 不是 store 实例自身。

在实践中，我们经常用到 ES2015 参数解构 来简化代码（特别是我们要多次调用 commit 的时候）：

actions: {
  increment ({ commit }) {
    commit('increment')
  }
}

分发(Dispatch) Action

使用 store.dispatch 方法触发 action。

store.dispatch('increment')

这看起来很蠢：如果我们想增加 count，为什么我们不直接调用 store.commit('increment')？回想起mutation 必须是同步函数 了吗？action 可以不是同步函数。我们可以在 action 回调函数中执行 异步操作：

actions: {
  incrementAsync ({ commit }) {
    setTimeout(() => {
      commit('increment')
    }, 1000)
  }
}

action 同样支持 payload 格式和对象风格的 dispatch：

// dispatch 传入 payload
store.dispatch('incrementAsync', {
  amount: 10
})
// dispatch 传入一个对象
store.dispatch({
  type: 'incrementAsync',
  amount: 10
})

日常生活行为中，更实际的例子是购物车结帐，涉及调用异步 API和分发多重 mutations：

actions: {
  checkout ({ commit, state }, payload) {
    // 把当前购物车的商品备份起来
    const savedCartItems = [...state.cart.added]
    // 发送结帐请求，并愉快地清空购物车
    commit(types.CHECKOUT_REQUEST)
    // 购物 API 接收一个成功回调和一个失败回调
    shop.buyProducts(
      products,
      // 成功操作
      () => commit(types.CHECKOUT_SUCCESS),
      // 失败操作
      () => commit(types.CHECKOUT_FAILURE, savedCartItems)
    )
  }
}

注意，我们执行的是一个异步操作的流程，并通过提交 action 来记录其副作用（状态变化）。

组件中分发 Action

使用 this.$store.dispatch('xxx')（需要根组件中注入 store ）在组件中分发 action，使用 mapActions 工具函数，映射组件方法到调用 store.dispatch：

import { mapActions } from 'vuex'
export default {
  // ...
  methods: {
    ...mapActions([
      'increment' // 映射 this.increment() 到 this.$store.dispatch('increment')
    ]),
    ...mapActions({
      add: 'increment' // map this.add() to this.$store.dispatch('increment')
    })
  }
}

组合多个 Action

Action 通常是异步的，所以我们如何知道一个 action 何时完成？更重要的是，我们如何组合多个 action 一起操作复杂的异步流程？

第一件事是知道 store.dispatch 返回『action 回调函数被触发后的返回值』，所以你可以在 action 中返回一个 Promise 对象。

actions: {
  actionA ({ commit }) {
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        commit('someMutation')
        resolve()
      }, 1000)
    })
  }
}

现在你可以这么做：

store.dispatch('actionA').then(() => {
  // ...
})

然后在另一个 action ：

actions: {
  // ...
  actionB ({ dispatch, commit }) {
    return dispatch('actionA').then(() => {
      commit('someOtherMutation')
    })
  }
}

最后，如果我们使用 async / await，很快落地的 JavaScript 特性，我们可以这样组合我们的 action：

// 假定 getData() and getOtherData() 返回的是 Promise
actions: {
  async actionA ({ commit }) {
    commit('gotData', await getData())
  },
  async actionB ({ dispatch, commit }) {
    await dispatch('actionA') // 等待 actionA 结束
    commit('gotOtherData', await getOtherData())
  }
}

store.dispatch 可能会在不同模块中，触发多个 action 回调函数。在这种情况下，返回值是一个 Promise 对象，该对象在所有被触发的回调都 resolve 之后自己再 resolve。


Vuex Modules

由于使用了单一状态树，应用的所有状态都包含在一个大对象内。但是，随着我们应用规模的不断增长，这个Store变得非常臃肿。

为了解决这个问题，Vuex 允许我们把 store 分 module（模块）。每一个模块包含各自的状态、mutation、action 和 getter，甚至是嵌套模块， 如下就是它的组织方式：

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 state
store.state.b // -> moduleB's state

模块本地状态

模块的 mutations 和 getters方法第一个接收参数是模块的本地状态。

const moduleA = {
  state: { count: 0 },
  mutations: {
    increment: (state) {
      // state 是模块本地的状态。
      state.count++
    }
  },
  getters: {
    doubleCount (state) {
      return state.count * 2
    }
  }
}

相似地，在模块的 actions 中，context.state 暴露的是本地状态， context.rootState暴露的才是根状态。

const moduleA = {
  // ...
  actions: {
    incrementIfOdd ({ state, commit }) {
      if (state.count % 2 === 1) {
        commit('increment')
      }
    }
  }
}

在模块的 getters 内，根状态也会作为第三个参数暴露。

const moduleA = {
  // ...
  getters: {
    sumWithRootCount (state, getters, rootState) {
      return state.count + rootState.count
    }
  }
}

命名空间

要注意，模块内的 actions、mutations 以及 getters 依然注册在全局命名空间内 —— 这就会让多个模块响应同一种 mutation/action 类型。你可以在模块的名称中加入前缀或者后缀来设定命名空间，从而避免命名冲突。如果你的 Vuex 模块是一个可复用的，执行环境也未知的，那你就应该这么干了。例如，我们想要创建一个 todos 模块：

// types.js
// 定义  getter、 action 和 mutation 的常量名称
// 并且在模块名称上加上 `todos` 前缀
export const DONE_COUNT = 'todos/DONE_COUNT'
export const FETCH_ALL = 'todos/FETCH_ALL'
export const TOGGLE_DONE = 'todos/TOGGLE_DONE'
// modules/todos.js
import * as types from '../types'
// 用带前缀的名称来定义 getters, actions and mutations
const todosModule = {
  state: { todos: [] },
  getters: {
    [types.DONE_COUNT] (state) {
      // ...
    }
  },
  actions: {
    [types.FETCH_ALL] (context, payload) {
      // ...
    }
  },
  mutations: {
    [types.TOGGLE_DONE] (state, payload) {
      // ...
    }
  }
}

注册动态模块

你可以用 store.registerModule 方法在 store 创建之后注册一个模块：

store.registerModule('myModule', {
  // ...
})

模块的 store.state.myModule 暴露为模块的状态。

其他的 Vue 插件可以为应用的 store 附加一个模块，然后通过动态注册就可以使用 Vuex 的状态管理功能了。例如，vuex-router-sync 库，通过在一个动态注册的模块中管理应用的路由状态，从而将 vue-router 和 vuex 集成。

你也能用 store.unregisterModule(moduleName) 移除动态注册过的模块。但是你不能用这个方法移除静态的模块（也就是在 store 创建的时候声明的模块）。


Vuex 应用结构

实际上，Vuex 在怎么组织你的代码结构上面没有任何限制，相反，它强制规定了一系列高级的原则：

  1. 应用级的状态集中放在 store 中。

  2. 改变状态的唯一方式是提交mutations，这是个同步的事务。

  3. 异步逻辑应该封装在action 中。

只要你遵循这些规则，怎么构建你的项目的结构就取决于你了。如果你的 store 文件非常大，仅仅拆分成 action、mutation 和 getter 多个文件即可。

对于稍微复杂点的应用，我们可能都需要用到模块。下面是一个简单的项目架构：

├── index.html
├── main.js
├── api
│   └── ... # 这里发起 API 请求
├── components
│   ├── App.vue
│   └── ...
└── store
    ├── index.js          # 组合 modules 、export store
    ├── actions.js        # 根 action
    ├── mutations.js      # 根 mutations
    └── modules
        ├── cart.js       # cart 模块
        └── products.js   # products 模块

关于更多，查看 购物车实例。


Vuex 插件

Vuex 的 store 接收 plugins 选项，这个选项暴露出每个 mutation 的钩子。一个 Vuex 的插件就是一个简单的方法，接收 store 作为唯一参数：

const myPlugin = store => {
  // 当 store 在被初始化完成时被调用
  store.subscribe((mutation, state) => {
    // mutation 之后被调用
    // mutation 的格式为 {type, payload}。
  })
}

然后像这样使用：

const store = new Vuex.Store({
  // ...
  plugins: [myPlugin]
})

在插件内提交 Mutations

插件不能直接修改状态 - 这就像你的组件，它们只能被 mutations 来触发改变。

通过提交 mutations，插件可以用来同步数据源到 store。例如， 为了同步 websocket 数据源到 store (这只是为说明用法的例子，在实际中，createPlugin 方法会附加更多的可选项，来完成复杂的任务)。

export default 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)
const store = new Vuex.Store({
  state,
  mutations,
  plugins: [plugin]
})

生成状态快照

有时候插件想获取状态 “快照” 和状态的改变前后的变化。为了实现这些功能，需要对状态对象进行深拷贝：

const myPluginWithSnapshot = store => {
  let prevState = _.cloneDeep(store.state)
  store.subscribe((mutation, state) => {
    let nextState = _.cloneDeep(state)
    // 对比 prevState 和 nextState...
    // 保存状态，用于下一次 mutation
    prevState = nextState
  })
}

** 生成状态快照的插件只能在开发阶段使用，使用 Webpack 或 Browserify，让构建工具帮我们处理：

const store = new Vuex.Store({
  // ...
  plugins: process.env.NODE_ENV !== 'production'
    ? [myPluginWithSnapshot]
    : []
})

插件默认会被起用。为了发布产品，你需要用 Webpack 的 DefinePlugin 或者 Browserify 的 envify 来转换 process.env.NODE_ENV !== 'production' 的值为 false。

内置 Logger 插件

如果你正在使用 vue-devtools，你可能不需要。

Vuex 带来一个日志插件用于一般的调试:

import createLogger from 'vuex/dist/logger'
const store = new Vuex.Store({
  plugins: [createLogger()]
})

createLogger 方法有几个配置项：

const logger = createLogger({
  collapsed: false, // 自动展开记录 mutation
  transformer (state) {
    // 在记录之前前进行转换
    // 例如，只返回指定的子树
    return state.subTree
  },
  mutationTransformer (mutation) {
    // mutation 格式 { type, payload }
    // 我们可以按照想要的方式进行格式化
    return mutation.type
  }
})

日志插件还可以直接通过 <script> 标签，然后它会提供全局方法 createVuexLogger 。

要注意，logger 插件会生成状态快照，所以仅在开发环境使用。


Vuex 严格模式

要启用严格模式，只需在创建 Vuex store 的时候简单地传入 strict: true。

const store = new Vuex.Store({
  // ...
  strict: true
})

在严格模式下，只要 Vuex 状态在 mutation 方法外被修改就会抛出错误。这确保了所有状态修改都会明确的被调试工具跟踪。

开发阶段 vs. 发布阶段

  • 不要在发布阶段开启严格模式！ 严格模式会对状态树进行深度监测来检测不合适的修改 —— 确保在发布阶段关闭它避免性能损耗。

跟处理插件的情况类似，我们可以让构建工具来处理：

const store = new Vuex.Store({
  // ...
  strict: process.env.NODE_ENV !== 'production'
})


Vuex 表单处理

严格模式下的Vuex，在属于 Vuex 的 state （状态）上使用 v-model时会比较棘手：

<input v-model="obj.message">

假设 obj 计算的属中返回一个对象，在用户输入时，v-model会尝试直接修改obj.message。在严格模式下，因为修改不在Vuex mutation handler中执行,将会抛出一个错误。

用“Vuex思维”去处理是给<input>绑定value（值），然后帧听<input>或change事件，并在回调中调用action：

<input :value="message" @input="updateMessage">
// ...
computed: {
  ...mapState({
    message: state => state.obj.message
  })
},
methods: {
  updateMessage (e) {
    this.$store.commit('updateMessage', e.target.value)
  }
}

在这里使用 mutation handler(变更句柄):

// ...
mutations: {
  updateMessage (state, message) {
    state.obj.message = message
  }
}

双向计算属性

必须承认，上述比v-model+本机状态(local state)啰嗦得多，以及从v-model中弃用了一些有用的功能。使用 setter 双向计算属性的另一种方法：

// ...
computed: {
  message: {
    get () {
      return this.$store.state.obj.message
    },
    set (value) {
      this.$store.commit('updateMessage', value)
    }
  }
}


Vuex 测试

我们主要针对Vuex中的mutaions 和actions进行单元测试。

测试 Mutations

Mutations 很容易被测试，因为它们仅仅是一些完全依赖参数的函数。小技巧是，如果你在 store.js 文件中定义了 mutations，并且使用 ES2015 模块功能默认输出，那么你仍然可以给 mutations 取个变量名然后把它输出去：

const state = { ... }
//取个变量名并输出mutations
export const mutations = { ... }
export default new Vuex.Store({
  state,
  mutations
})

以下为使用 Mocha + chai 测试 mutation 的例子（实际上你可以用任何你喜欢测试框架）

// mutations.js
export const mutations = {
  increment: state => state.count++
}
// mutations.spec.js
import { expect } from 'chai'
import { mutations } from './store'
// 解构赋值mutations（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)
  })
})

测试 Actions

Actions 可能会更加棘手一些，因为他们可能要求请求外部API.

当测试actions时，我们通常需要增加mocking服务层——例如，我们可以把API调用抽象成服务，然后我们在测试中模拟这种服务。为了便于解决mock的依赖关系，可以用 Webpack 和 inject-loader 打包测试文件。

异步action测试示例：

// actions.js
import shop from '../api/shop'
export const getAllProducts = ({ dispatch }) => {
  dispatch('REQUEST_PRODUCTS')
  shop.getProducts(products => {
    dispatch('RECEIVE_PRODUCTS', products)
  })
}
// actions.spec.js
// 使用 require 语法处理内联loaders
//inject-loader,返回一个模块工厂
//让我们能够注入mocked的依赖关系。
import { expect } from 'chai'
const actionsInjector = require('inject!./actions')
//使用mocks创建模块
const actions = actionsInjector({
  '../api/shop': {
    getProducts (cb) {
      setTimeout(() => {
        cb([ /* mocked response */ ])
      }, 100)
    }
  }
})
//用指定的mutatios测试action的辅助函数
const testAction = (action, args, state, expectedMutations, done) => {
  let count = 0
  // mock 提交
  const commit = (type, payload) => {
    const mutation = expectedMutations[count]
    expect(mutation.type).to.equal(type)
    if (payload) {
      expect(mutation.payload).to.deep.equal(payload)
    }
    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)
  })
})

测试 Getters

如果你的getters方法很复杂，那么你得测试他们。测试Getter 方法和测试mutations一样非常简单！

测试getter实例：

// getters.js
export const getters = {
  filteredProducts (state, { filterCategory }) {
    return state.products.filter(product => {
      return product.category === filterCategory
    })
  }
}
// getters.spec.js
import { expect } from 'chai'
import { getters } from './getters'
describe('getters', () => {
  it('filteredProducts', () => {
    // mock 状态
    const state = {
      products: [
        { id: 1, title: 'Apple', category: 'fruit' },
        { id: 2, title: 'Orange', category: 'fruit' },
        { id: 3, title: 'Carrot', category: 'vegetable' }
      ]
    }
    // mock getter
    const filterCategory = 'fruit'
    // 从getter中取回值
    const result = getters.filteredProducts(state, { filterCategory })
    // 声明返回值
    expect(result).to.deep.equal([
      { id: 1, title: 'Apple', category: 'fruit' },
      { id: 2, title: 'Orange', category: 'fruit' }
    ])
  })
})

运行测试

如果你的 mutations 和 actions 已经正确，后面应该在适合的mocking上浏览器测试API的依赖关系。

在Node上运行

创建下面的webpack配置（加上适合.babelrc）：

// webpack.config.js
module.exports = {
  entry: './test.js',
  output: {
    path: __dirname,
    filename: 'test-bundle.js'
  },
  module: {
    loaders: [
      {
        test: /\.js$/,
        loader: 'babel',
        exclude: /node_modules/
      }
    ]
  }
}

然后:

webpack
mocha test-bundle.js

在浏览器上运行

  1. 安装 mocha-loader
  2. 把上述 webpack 配置中的 entry 改成 ‘mocha!babel!./test.js’
  3. 用以上配置启动 webpack-dev-server
  4. 访问 localhost:8080/webpack-dev-server/test-bundle.

使用Karma + karma-webpack在浏览器中测试

详细的安装咨询见vue-loader documentation.


Vuex 热重载

Vuex 支持在开发中使用webpack的Hot Module Replacement API 热重载 mutations，modules ，actions 和 getters，你也可以在 Browserify 里使用 browserify-hmr 插件来实现同样的功能。

对于mutations 和 modules，你只需要简单地调用 store.hotUpdate():

// store.js
import Vue from 'vue'
import Vuex from 'vuex'
import mutations from './mutations'
import moduleA from './modules/a'
Vue.use(Vuex)
const state = { ... }
const store = new Vuex.Store({
  state,
  mutations,
  modules: {
    a: moduleA
  }
})
if (module.hot) {
  // 使 actions 和 mutations 成为可热重载模块
  module.hot.accept(['./mutations', './modules/a'], () => {
    // 获取更新后的模块
    // 因为 babel 6 的模块编译格式问题，这里需要加上 .default
    const newMutations = require('./mutations').default
    const newModuleA = require('./modules/a').default
    // 成新的 mutation 和 mudule
    store.hotUpdate({
      mutations: newMutations,
      modules: {
        a: newModuleA
      }
    })
  })
}

查看热重载的例子 来开始使用热重载


Vuex API 参考

Vuex.Store

import Vuex from 'vuex'
const store = new Vuex.Store({ ...options })

Vuex.Store 构造选项

  • state

    • 类型： Object

      Vuex store 的根state对象。

      Details

  • mutations

    • 类型： { [type: string]: Function }

      在 store 上注册 mutation，Handler function总是接收 state 作为第一个参数（如果定义在模块中，则作为模块的本地状态），如果有则接收 payload 作为第二个参数。

  • actions

    • 类型： { [type: string]: Function }

      在 store 上注册 action。Handler function接收一个 context 对象，这个对象提供以下属性：

      {
  state,     // 等同于 store.state，如果在模块中则是本地状态
  rootState, // 等同于 store.state，模块中才有
  commit,    // 等同于 store.commit
  dispatch,  // 等同于 store.dispatch
  getters    // 等同于 store.getters
}

  • getters

    • 类型： { [key: string]: Function }

      在 store 上注册 getter，getter 方法接收一下参数：

      state,     // 如果在模块中定义则为模块的本地状态
getters,   // 等同于 store.getters
rootState  // 等同于 store.state

      注册的 getter 被暴露在 store.getters。

  • modules

    • 类型： Object

      包含了子模块的对象，会被合并到 store，形如：

      {
  key: {
    state,
    mutations,
    actions?,
    getters?,
    modules?
  },
  ...
}

      每个模块可以包含 state 和 mutations，就像根模块的配置。一个模块的状态使用模块的 key 附加到 store 的根状态上。模块的 mutation 和 getter 将接收 module 的本地状态作为第一个参数，而不是根状态，并且模块 action 的 context.state 也指向本地状态。

  • plugins

    • 类型： Array<Function>

      一个插件方法的数组被应用于 store 上。这些插件直接接收 store 作为唯一参数，也可以监听 mutation（用于外部地数据持久化、记录或调试）或者提交 mutation （用于内部数据，例如 websocket 或 某些观察者）。

  • strict

    • 类型： Boolean

    • 默认值： false

      强制 Vuex store 进入严格模式，在严格模式下，在 mutation 处理器以外修改 Vuex state 则会抛出错误。

Vuex.Store Instance Properties

  • state

    • 类型： Object

      The root state. Read only.

  • getters

    • 类型： Object

      暴露注册的 getter，只读。

Vuex.Store 实例方法

  • commit(类型： string, payload?: any) | commit(mutation: Object)

    提交一个 mutation.

  • dispatch(type: string, payload?: any) | dispatch(action: Object)

    分发一个 action。返回 action 方法的返回值，如果多个处理器被触发，那么返回一个 Pormise。

  • replaceState(state: Object)

    替换 store 的根状态，仅用于调试 state。

  • watch(getter: Function, cb: Function, options?: Object)

    响应式的监测一个 getter 方法的返回值，当值改变时调用回调。getter 接收 store 的 state 作为唯一参数。接收类似 vm.$watch 方法的参数的可选对象。

    要停止检测，调用该方法的返回值 （watch 的返回值是个方法）。

  • subscribe(handler: Function)

    订阅（注册监听） store 的 mutation。handler 会再每个 mutation 完成后调用，接收 mutation 的描述对象和 经过 mutation 后的状态作为参数：

    store.subscribe((mutation, state) => {
  console.log(mutation.type)
  console.log(mutation.payload)
})

    这是插件中常见的用法。

  • registerModule(path: string | Array<string>, module: Module)

    注册一个动态模块。

  • unregisterModule(path: string | Array<string>)

    注销一个动态模块。

  • hotUpdate(newOptions: Object)

    热加载新的 action 和 mutation。

组件绑定助手

  • mapState(map: Array<string> | Object): Object

    创建一个组件的计算属性选项，该选项会返回 Vuex store 的子树。

  • mapGetters(map: Array<string> | Object): Object

    创建一个组件的计算属性选项，该选项会返回 getter的计算后的值。

  • mapActions(map: Array<string> | Object): Object

    创建一个组件的methods选项，该选项返回一个 action。

  • mapMutations(map: Array<string> | Object): Object

    创建一个组件的methods选项，该选项提交一个 mutation。