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'

// 在 Vue 实例的初始化中声明 store。
new Vue({
  el: 'body',
  components: {
    App
  },
  store
})

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

然后看一下 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 中的内容:

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

具体业务逻辑这里不展开,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'

export function delLayer( { dispatch }, index) {
  dispatch(types.DEL_LAYER, index)
}

export function delScene( { dispatch }, index) {
  dispatch(types.DEL_SCENE, index)
}

export function removeFromArr( { dispatch }, arr, itemToRemove) {
  dispatch(types.REMOVE_FROM_ARR, arr, itemToRemove)
}

export function addToArr( { dispatch }, arr, itemToAdd) {
  dispatch(types.ADD_TO_ARR, arr, itemToAdd)
}

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 操作,实现撤销重做功能。具体实现逻辑此处不作赘述。

支付宝扫码打赏 微信打赏

坚持原创技术分享,您的支持将鼓励我继续创作!

扫描二维码,分享此文章

逆葵's Picture
逆葵

网名逆葵。北邮土著,CS 硕士在读。《Vue.js 权威指南》作者之一。学习、思考、沉淀中, 向成为顶级 JavaScript 技术栈开发者努力。