作者:滴滴公共前端团队
前言:
最早我们在设计《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'
// 在 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 中存储了 editor 和 page 两个对象,用于存储不同模块的状态。需要说明的是,这里完全可以使用模块机制将其拆开,在 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 操作,实现撤销重做功能。具体实现逻辑此处不作赘述。
后续我们也会深入地给大家分享 vuex 应用相关的内容
附:
Vuex 2.0 源码分析知乎地址:zhuanlan.zhihu.com/p/23921964
「掘金技术征文」活动:gold.xitu.io/post/58522d…
欢迎关注DDFE
GITHUB:github.com/DDFE
微信公众号:微信搜索公众号“DDFE”或扫描下面的二维码