近几年前端发展迅猛,Vue、Angular、React 等 MVVM 框架兴起,同时 ES6 引入大量新语言特性,而浏览器对这些新东西的支持又十分有限,这就有了 Webpack、Gulp 等构建工具的崛起。
然而,MVVM、ES6 的新语言特性、构建工具等这些前端新崛起的东西,在后端领域其实已经十分成熟,前端只不过是逐渐靠近后端。这似乎也让后端更容易靠近前端。因此,现在是后端学习前端的一个好时机(虽然有点晚)。这也是我最近开始学习前端的原因之一。
同时,由于前端发展实在太快,前端已然是一个十分庞大的体系。仅就 Vue 的生态而言,它包含了 vue-core、vue-loader、vue-cli、vue-router、vue-vuex、vue-devtools 以及大量的插件等。光看完这串名词都要花不少时间,要深入了解这些以及相关的依赖,显然要花费很多时间。
最近我对 vue-core、vue-loader、vue-cli、vue-router、vue-vuex、vue-devtools 都有涉足,这是个持续的过程。这篇博文就先对 Vuex 的学习做一些总结,后续再谈谈其他的。
What’s Vuex
一句话,Vuex 是一个专为 Vue.js 应用程序开发的,基于 Flux 架构模式的状态管理库。简单来说,Vuex 管理了组件之间的状态,让组件之间不必再通过参数传递来通信。有的人把它理解成一个全局变量,这确实比较容易理解,但不够准确。把它理解成一个数据库更为准确。为什么理解成一个数据库,这就得从 Flux 说起了。
Flux
Flux 是一种架构模式,而不是一个具体的工具或框架。它从 MVC 模式改良而来,解决了 Model 层和 View 层耦合性太强,难以维护和理解的问题,创新式的提出了从 Action => Dispatcher => Store => View 的单向数据流。
由一个 Store 对象代替 MVC 中的 Model 层,让 Store 和 View 层耦合更松散。
引用官方的一段介绍:
All data flows through the dispatcher as a central hub. Actions are provided to the dispatcher in an action creator method, and most often originate from user interactions with the views. The dispatcher then invokes the callbacks that the stores have registered with it, dispatching actions to all stores. Within their registered callbacks, stores respond to whichever actions are relevant to the state they maintain. The stores then emit a change event to alert the controller-views that a change to the data layer has occurred. Controller-views listen for these events and retrieve data from the stores in an event handler. The controller-views call their own setState() method, causing a re-rendering of themselves and all of their descendants in the component tree.
- Store 相当于 Model 层,包含了状态和逻辑;
- Dispatcher 相当于 Controller 层,显然它是唯一的,它本质上是一个注册中心,Store会把 callback 注册到它上面,当应用发起 action 时,注册中心回调用相应的 callback。
- View 层比较特殊,我们称它为 controller-view, 它可以自动监听 Store 的变更而自动更新视图。
Facebook 证明了,这个架构模式在前端领域是非常成功的。
至此,我们知道了,基于 Flux 实现的 Vuex 其实就是一个改进版的 MVC(或者说 MVVM)程序,专注于提供、保存和加工处理数据,所以,数据库是更接近它本质的一种理解。
store 模式
既然提到了 Flux,就顺带提一下 store 模式吧。store 模式可以理解为 Flux 的阉割版,相比于 Flux 少了 action 的 dispatch,仅仅是简单的提供 mutation 方法。store 模式在中小型应用中十分有用,因为这种情况下 Flux 将显得繁琐和冗余。
var store = {
debug: true,
state: {
message: 'Hello!'
},
setMessageAction (newValue) {
if (this.debug) console.log('setMessageAction triggered with', newValue)
this.state.message = newValue
},
clearMessageAction () {
if (this.debug) console.log('clearMessageAction triggered')
this.state.message = ''
}
}
上述是一个 store 模式的实例,比较简单。
Vuex 的作用
既然知道了什么是 Vuex,我们再来看看它有什么作用,有什么意义。其实就是看它解决了什么问题。
Vuex 主要解决了两个问题:
1、组件间信息传递引起的大量参数传递问题
这个问题在组件多层嵌套,参数需要层层传递的时候尤为明显,虽然可以通过 inject 来缓解,但是使用 inject 的同时也带来了结构上的耦合,并不是最佳方案,而且在兄弟组件之间需要传递参数时 inject 也无能为力。this.$root
、this.$parent
和 this.$refs.refName
等这些一般不推荐使用,它们会使程序变得十分脆弱。
2、全局状态改变时,组件自动更新问题
Vuex 提供了数据双向绑定的支持,数据变更时,组件会自动更新。
Vuex 的使用场景
我们什么情况下需要用 Vuex 呢?如果是小型应用,一个简单的 store 模式就足够了。但是,如果你需要构建一个中大型单页应用,你很可能要考虑如何更好地在组件外部管理状态,那么 Vuex 将会成为你自然而然的选择。引用 Redux 的作者 Dan Abramov 的话说就是:
Flux 架构就像眼镜:您自会知道什么时候需要它。
Vuex 的最佳实践
我们怎么用 Vuex 呢?或者说使用 Vuex 的最佳实践是什么。接下来我们来探讨这个问题。
核心概念
首先我们要明白几个核心概念:
- State:状态对象,数据实际存储的地方
- Getter:可以认为是一个计算属性,返回从 State 中获取,经过计算(过滤、排序、查找等)后的数据
- Mutation:类似事件。提交 Mutation 后,Store 自动调用对应的 callback 函数
- Action:类似 Mutation,但不直接变更状态,通过提交 Mutation 变更。可以包含异步操作,调用后端 API
- Module:模块对象,它可以为每个模块(Vue Component)定义局部的 Store
规则
Vuex 并不限制你的代码结构。但是,它规定了一些需要遵守的规则。
1、应用层级的状态应该集中到单个 store 对象中。
单个 store 对象和模块化并不冲突,我们可以用 Module
将状态和状态变更事件分布到各个子模块中。
const moduleA = {
state: { ... },
mutations: { ... },
actions: { ... },
getters: { ... }
}
cont moduleB = {
state: { ... },
mutations: { ... },
actions: { ... }
}
const store = new Vuex.Store({
modules: {
a: moduleA,
b: moduleB
}
})
2、 提交 mutation 是更改状态的唯一方法,并且这个过程是同步的。
也就是说,不能定义异步的 mutation 函数。
mutations: {
someMutation (state) {
api.callAsyncMethod(() => {
state.count++
})
}
}
在上述代码里,state.count
的变更在异步的回调函数中,我们无法确定它什么时候被调用,这让我们难以追踪状态的改变。难以追踪状态的改变将是程序调试时的噩梦。
3、mutation 需要遵守 Vue 的响应规则。
既然 Vuex 的 store 中的状态是响应式的,那么当我们变更状态时,监视状态的 Vue 组件也会自动更新。这也意味着 Vuex 中的 mutation 也需要与使用 Vue 一样遵守一些注意事项。详细请参考我之前关于 Vue 的博文。
4、异步逻辑都应该封装到 action 里面。
store.dispatch
可以处理被触发的 action 的处理函数返回的 Promise,并且 store.dispatch
仍旧返回 Promise,所以 action 可以用来处理异步逻辑。
// 假设 getData() 和 getOtherData() 返回的是 Promise
actions: {
async actionA ({ commit }) {
commit('gotData', await getData())
},
async actionB ({ dispatch, commit }) {
await dispatch('actionA') // 等待 actionA 完成
commit('gotOtherData', await getOtherData())
}
}
Async 函数运行结果可能是 rejected
, 所以最好把 store.dispatch
放在 try...catch
代码块中。
try {
this.$store.dispatch('actionB');
} catch (err) {
console.log(err);
}
// 另一种写法
this.$store.dipatch('actionB')
.then((result) => console.log(result))
.catch(err => console.log(err));
这些是官方建议遵守的规则,也是最佳实践要遵守的规则。只要你遵守以上规则,如何组织代码随你便。如果你的 store 文件太大,可以将 action、mutation 和 getter 分割到单独的文件。
import Vue from 'vue'
import Vuex from 'vuex'
import actions from './actions'
import mutations from './mutations'
import getters from './getters'
Vue.use(Vuex)
const store = new Vuex.Store({
state: {
...
},
actions,
mutations,
getters
})
export default store
严格模式
为了能检查开发成员是否遵守了上述的规则,官方提供了严格模式。
开启严格模式,只需要在创建 Store 的时候传入 strict: true
:
const store = new Vuex.Store({
// ...
strict: true
})
在严格模式下啊,如果状态变更不是由 mutation 触发,将会抛出错误。不要在正式环境下启用严格模式!否则会造成性能损耗。 在使用 Webpack 时,我们可以这样配置:
const store = new Vuex.Store({
// ...
strict: process.env.NODE_ENV !== 'production'
})
结束语
Vuex 还有一些其他内容,如:测试、热重载等,它们都比较简单,实践模式也比较单一,我就不再赘述了。
(done)
参考文档
[1] Facebook Flux
[2] Vuex Document
[3] State Management