Vuex 实战

5,915 阅读6分钟
原文链接: mp.weixin.qq.com

前言
    最早我们在设计《Vue.js权威指南》这本书的时候也一直思考要不要加入 Vuex 相关的内容,也有很多同学抱怨说我们没有加入这个章节。    
    其实整体我们应用的还是比较早,也在 1.0 和 2.* 都踩了一些坑,但是也不期望大家在任何复杂不复杂的场景里面滥用 Vuex。

    后面我们在 vue 2.0 全家桶源码分享系列里面也分享了一篇《Vuex 2.0 源码分析》,没有看过的同学可以在文末链接查看

正文

    Vuex 作为中大型 Vue 应用中的“御用”集中数据管理工具,在滴滴很早就得到了广泛使用。
本文旨在以尽可能简洁的文字向读者展示:
如何在一个颇具规模的 Vue 应用中组织和管理 Vuex 的代码

注:虽然目前 Vuex 的最新版本已经来到 2.x。2.x 在1.0 的基础上进行了一些优化,提升了命名的语义化以及 增强了模块的可移植性和可组合性,但基本思想和架构并没有改变。
本文基于 Vuex 1.0 版本,读者大可不必担心出现类似 Angular 1.x  升级到 2.x 式的断崖式更新。

首先,介绍一下项目的背景:
一个采用 Vue.js 编写的富交互的 H5 编辑器,由于各个组件中的数据交互繁多,页面的生成也极度依赖存储的状态,使用 Vuex 进行管理便势在必行。
项目引入 Vuex 的方式如下:

import App from 'components/home/App'
import store from 'vuex/editor/store'
new Vue({
  el: 'body',
  components: {
    App
  },
  ‍store
 })

在根实例中注册 store 选项,这样该 store 实例会注入到根组件下的所有子组件中,方便后面我们在每个子组件中调用 store 中 state 里存储的数据。

然后看一下 vuex 文件夹下的目录,后面我们会逐个分析每个文件的作用:

 vuex
 └── editor
    ├── mutation-types.js
    ├── actions
    │   └── index.js
    ├── mutations
    │   └── index.js
    ├── plugins
    │   └── index.js
    ├── state
    │   └── index.js
    └── store
        └── index.js

创建 store 对象的代码放在 vuex/editor/store/index.js 中,如下所示:


// vuex/editor/store/index.js
import Vuex from 'vuex'
import state from 'vuex/editor/state'
import mutations from 'vuex/editor/mutations'
import { actionLogPlugin } from 'vuex/editor/plugins'
const store = new Vuex.Store({
  state,
  mutations,
  plugins: [actionLogPlugin]
})
export default store

这里又声明了 state 和 mutations 对象,以及声明使用到的 plugins。plugins 后面再说,先看 state 和 mutations,相信各位读者已经对 Vuex 中各个部件的作用已经了如指掌,但是为防遗忘,还是贴一下这张图吧:
state 是用于存储各种状态的核心仓库,让我们一瞥 vuex/editor/state/index.js 中的内容:


// 编辑器相关状态
const editor = {
  ...
}
// 页面相关状态
let page = {
  ...
}
const state = {
  editor,
  page
}
export default state

state 中存储了 editorpage 两个对象,用于存储不同模块的状态。需要说明的是,这里完全可以使用模块机制将其拆开,在 editor.js 里存储编辑器相关的 state 和 mutations,在 page.js 中存储页面相关的 state 和 mutations,以使结构更加清晰。不过这里没有使用模块机制,由于模块数量并不多,也是完全可以接受的。

这些 state 需要反映到组件中。
跳过官方文档中对为何不使用计算属性的解释,我们直接来看最佳实践:在子组件中通过 vuex.getters 来获取该组件需要用到的所有状态:


// src/components/h5/Navbar.vue
...
export default {
    data () {
      return {
        ...
      }
    },
    methods: {
      ...
    },
    vuex: {
      actions: {
        ...
      },
      getters: {
        editor(state) {
          return state.editor
        },
        page(state) {
          return state.page
        },
        ...
      }
    }
}

vuex.getters 对象中,每个属性对应一个 getter 函数,该函数仅接收 store 中 state,也就是总的状态树作为唯一参数,然后返回 state 中需要的状态,然后在组件中就可以以 this.editor 的方式直接调用,类似计算属性。

再看一下 vuex/editor/mutations/index.js 中的内容:

// src/components/h5/Navbar.vue
...
export default {
    data () {
      return {
        ...
      }
    },
    methods: {
      ...
    },
    vuex: {
      actions: {
        ...
      },
      getters: {
        editor(state) {
          return state.editor
        },
        page(state) {
          return state.page
        },
        ...
      }
    }
}

具体业务逻辑这里不展开,mutations 中主要就是定义各种对 state 的状态修改。每个 mutation 函数接收第一个参数为 state 对象,其余参数则为一路从组件中触发 action 时传过来的 payload。所有的 mutation 函数必须为同步执行,否则无法追踪状态的改动。
注意到,这里引入了 mutation-types.js。该文件主要作用为放置所有的命名 Mutations 的常量,方便合作开发人员厘清整个 app 包含的 mutations。在采用模块机制时,可以在每个模块内只引入相关的 mutations,也可以像本项目一样使用 import * as types 简单粗暴地引入全部。
mutation-types.js 中内容大致如下:

export const CHANGE_LAYER_ZINDEX = 'CHANGE_LAYER_ZINDEX'
export const DEL_LAYER = 'DEL_LAYER'

 然后我们来到 actions,照例先看一下 vuex/editor/actions/index.js 中的内容:

import * as types from '../mutation-types'
const mutations = {
  [types.CHANGE_LAYER_ZINDEX] (state, dir, index) {
    ...
  },
  [types.DEL_LAYER] (state, index) {
    ...
  },
  [types.REMOVE_FROM_ARR] (state, arr, itemToRemove) {
    ...
  },
  [types.ADD_TO_ARR] (state, arr, itemToAdd) {
    ...
  },
  [types.DEL_SCENE] (state, index) {
    ...
  },
  ...
}
  
export default mutations

actions 的主要工作就是 dispatch (中文译为分发)mutations。初入门的同学可能觉得这是多此一举,actions 这一步看起来完全可以省略。
事实上,actions 的出现是为了弥补 mutations 无法实现异步操作的缺陷。所有的异步操作都可以放在 actions 中,比如如果想在调用 delScene 函数 5 秒后再分发 mutations,可以写成这样:

function delScene ({ dispatch }, index) {
  setTimeout(() => {
    dispatch(types.DEL_SCENE, index)
  }, 5000)
 }

触发 mutations 的代码不会在组件中出现,但 actions 会出现在每个需要它的组件中,其也是连接组件和 mutations 的桥梁(额,另一条桥梁是 state,见上面那张经典老图)。在子组件中引入 actions 的方式类似 state,也是注册在 vuex 选项下:


// src/components/h5/Navbar.vue
...
import { 
  undoAction, 
  redoAction,
  togglePreviewStatus,
  ...
} from 'vuex/editor/actions'
export default {
    data () {
      return {
        ...
      }
    },
    methods: {
      ...
    },
    vuex: {
      actions: {
        undoAction,
        redoAction,
        togglePreviewStatus,
        ...
      },
      getters: {
        ...
      }
    }
}

这样,组件中可以直接调用各个 actions,比如 this.togglePreviewStatus(status),等价于this.togglePreviewStatus( this.$store, status)(还记得我们在 actions 中定义的各个函数的第一个参数是 store 吗?)。这是最基本的使用 actions 的方式,在此基础上你还可以玩出别的花样来,比如给 actions 取别名、定义内联 actions、绑定所有 actions 等,具体用法参见官方文档。

回过头去看 vuex 文件夹下的目录结构,发现还有一个 plugins 我们没有介绍。老规矩,先看一下  vuex/editor/plugins/index.js 中的内容:


...
export function actionLogPlugin(store) {
  store.subscribe((mutation, state) => {
    // 每次 mutation 之后调用
    // mutation 的格式为 { type, payload }
    ...
  })
}


核心部分在于采用 store.subscribe 注册了一个函数。

该函数会在每次 mutation 之后被调用。这里 actionLogPlugin 函数完成的是记录每次 mutation 操作,实现撤销重做功能。具体实现逻辑此处不作赘述。


后续我们也会深入地给大家分享 vuex 应用相关的内容

好消息:

DDFE 进驻了知乎,很多知乎的小伙伴也可以在知乎关注我们哦:

专栏地址:https://zhuanlan.zhihu.com/ddfe-weekly

Vuex 2.0 源码分析知乎地址:https://zhuanlan.zhihu.com/p/23921964