译文地址
更新于2016年11月:这篇文章的代码写于2015年12月,并且使用的是非常老的vuex api版本。
但是,这篇文章仍然很有价值,它深入分析了为什么vuex是如此的重要,vuex的工作原理以及vuex是如何让你的应用更加出色并易于保存。
Vuex是由Vue.js作者创作的一种开发中的原型库,它可以帮助你以一种更加可持续的方式构建大型应用,它的规则类似于Facebook的Flux库(随后被redux等社区迭代)。
相对于直接跳入vuex并开始使用它,在本文中,我会解释为什么vuex在可替代方案中是如此的受欢迎和vuex对你而言有什么价值。
我们要构建的是什么?
我们要构建的简单应用包含了一个按钮和一个计数器。按下按钮会增加计数器。这个任务可以很容易的帮助我们理解以下概念。
在这个应用中有两个组件:
- 一个按钮(这是事件源)
- 一个计数器(这里会根据原始事件反映更新)
这两个组件互不相识并且无法通信,哪怕在一些非常小的web应用中,这都是一个非常常见的模型。在大型应用中有大量的组件需要互相通信并且保持彼此连接,以下是一个基本的todo list的流程互动:
这篇文章的目标
我们将使用三种方式来探索解决相同的问题:
- 使用事件广播以使得组件间通信
- 使用共享的state对象
- 使用vuex
读完本文后,希望你能够理解:
- 如何使用vuex的一个基本的工作流程
- vuex解决的是什么类型的问题
- 为什么vuex会优于其他的策略,尽管它有点复杂和严格
建立一个开始点
我们将用3种不同的解决同一个问题,在开始之前,我们需要我有一个相同的开始点,如果你想一道开始,我建议你为本教程建立一个git repo,在开始之后创建一个commit并且为不同方法创建分支。
$ npm install -g vue-cli
$ vue init webpack vuex-tutorial
$ cd vuex-tutorial
$ npm install
$ npm install --save vuex
$ npm run dev
现在你可以看到基础的vue脚手架页面,让我们创建并更新一些文件以使其达到我们想要的效果。
首先,我们在src/components/IncrementButton.vue
中建立一个IncrementButton
组件:
<template>
<button @click="activate">+1</button>
</template>
<script>
export default {
methods: {
activate () {
console.log('+1 Pressed')
}
}
}
</script>
<style>
</style>
接下来我们创建一个CounterDisplay
组件来展示计数器,让我们在src/components/CounterDisplay.vue
中创建一个新的基础vue组件。
<template>
Count is {{ count }}
</template>
<script>
export default {
data () {
return {
count: 0
}
}
}
</script>
<style>
</style>
替换App.vue
文件:
<template>
<div id="app">
<h3>Increment:</h3>
<increment></increment>
<h3>Counter:</h3>
<counter></counter>
</div>
</template>
<script>
import Counter from './components/CounterDisplay.vue'
import Increment from './components/IncrementButton.vue'
export default {
components: {
Counter,
Increment
}
}
</script>
<style>
</style>
现在你可以使用npm run dev
命令并且在浏览器中打开页面,你会看到一个按钮和一个计数器,点击按钮会在console中输出一个信息,现在我们已经抵达开始点,接下来就是处理过程了。
方法一:事件广播
让我们在组件中进行修改吧。首先,在
IncrementButton.vue
中我们使用$dispatch
来发送一个信息给父节点以表明按钮被触发了。
export default {
methods: {
activate () {
// 发送一个事件给App父节点
this.$dispatch('button-pressed')
}
}
}
在App.vue
中我们由子节点监听事件并重新广播一个新的增量事件给所有节点。
export default {
components: {
Counter,
Increment
},
events: {
'button-pressed': function () {
// 发送一个信息给所有子节点
this.$broadcast('increment')
}
}
}
在CounterDisplay.vue
中我们收听increemnt
事件并增加state的值。
export default {
data () {
return {
count: 0
}
},
events: {
increment () {
this.count ++
}
}
}
该方法的一些缺点
在本方法中并没有什么技术错误,对于在一个文件中书写你的所有应用逻辑,该方法也是没有技术问题的,但问题是可维护性,以下说明了这种方法是多么的难以维护。
- 对于每次行为,父组件必须与正确的组件连接并调度事件。
- 对于一个大型应用很难分析事件是从何发出的。
- 没有明确的空间给"业务逻辑",
this.count++
在CounterDisplay
中,但业务逻辑可能在任何地方,这就造成了难以维护性。
我来给出这个应用可能引起bug的一个例子:
- 你聘请了两个实习生:Alice和Bob,你告诉Alice你需要一个新的组件中的计数器,你告诉Bob区写一个Reset按钮。
- Alice写下一个新的组件
FormattedCounterDisplay
并订阅了increment
事件来增加他自己的state,Alice很高兴并提交了代码。 - Bob写了一个新的"Reset"组件并添加一个
reset
事件到App
来重新调度,他在CounterDisplay
实现了reset
以使得计数重置为0,但他不知道Alice的组件也订阅了该计数。 - 你的用户按下"+1",发现应用可以正常工作,但当他们按下"reset",只有一个计数重置了。
这是一个很简单的例子,但它说明了使用事件将分散state和业务逻辑整合在一起时会发生错误。
方法二:共享state
让我们还原到方法一开始前,我们新建一个新的文件src/store.js
export default {
state: {
counter: 0
}
}
我们第一次修改CounterDisplay.vue
:
<template>
Count is {{ sharedState.counter }}
</template>
<script>
import store from '../store'
export default {
data () {
return {
sharedState: store.state
}
}
}
</script>
我在在这个做了一些有趣的事情:
- 我们取得
store
对象,这是一个不变的对象,但它是其他文件定义的。 - 在我们的本地
data
中我们创建了一个名为sharedState
的新变量,映射到store.state
。 - vue使得
data
反映了store.state
,意味着vue会在store.state
改变时自动更新sharedState
。
至此该工程还没法工作,但现在,我们可以修改IncrementButton.vue
import store from '../store'
export default {
data () {
return {
sharedState: store.state
}
},
methods: {
activate () {
this.sharedState.counter += 1
}
}
}
- 在这里,我们输入store并添加了它的映射就像之前的例子。
- 当
activate
函数被调用时,经过sharedState
反映到store.state
并增加计数器。 - 所有订阅
counter
的组件和计算属性现在会更新。
这种方法比第一种好在哪
让我们重新回到两位实习生的问题-Alice和Bob。
- Alice写下
FormattedComponentDisplay
并订阅Shared State Counter,这个计数器会始终展示最新的计数结果。 - Bob的
ResetButton
组件设置Shared State Counter为0,这会影响CounterDisplay
和Alice写的FormattedCounterDisplay
。 - 用户发现reset按钮能够正常工作了。
这种方法还有哪些不足
- 在实习的过程中,Alice和Bob写下更多的计数器展示、reset按钮、以及不同类型的增加按钮,所有这些都会更新同一个共享计数器。Life is good.
- 一旦他们回到学校,你需要维护他们的代码。
- Carol-新来的领导,他要求"我不希望计数器超过100"。
现在你怎么办?
- 你会找出所有更新计数器的组件?那真令人沮丧。
- 你想增加一个filter或formatter?那也很麻烦。
现在就出现了问题,业务逻辑分散在应用的各个部分,这个问题在原理上很简单,但对于维护来说就是一个大问题。
一个稍微好一点的方法
现在你重构了所有格原始代码并重写了store.js
:
var store = {
state: {
counter: 0
},
increment: function () {
if (store.state.counter < 100) {
store.state.counter += 1;
}
},
reset: function () {
store.state.counter = 0;
}
}
export default store
这样做可以使得代码更清晰,你可以明确的调用increment
,并且所有的业务逻辑都存储在其中,但是,一个新的实习生并不知道这些,他发现从该应用的其他部分可以很容易的修改store.state.counter
,这时就很难定位错误了。
之后你又加了很多严格的规则、指南、代码review来确保所有人都使用store.js
中的函数来修改state,当这些都不管用后,你不得不让人力砍掉这个实习生计划。
方法三:Vuex
让我们还原到方法二开始之前的状态,从原理上vuex有点类似方法二,以下是一个有点复杂的图:
我们首先重新新建一个
src/store.js
,这次我们的代码改为:
import Vuex from 'vuex'
import Vue from 'vue'
Vue.use(Vuex)
var store = new Vuex.Store({
state: {
counter: 0
},
mutations: {
INCREMENT (state) {
state.counter ++
}
}
})
export default store
我看看代码中发生了什么:
- 我们获得
Vuex
模块并指导Vue
来使用它启动插件。 - 我们的存储不再是一个问题json对象,而是一个
Vuex.Store
的实例。 - 我们在state中创建一个
counter
并设置为0。 - 我们有一个新的
mutations
对象,它有一个INCREMENT
方法,输入一个state并修改这个值。
这段代码中有一些有趣的东西:
- 所有使用
require('../store.js')
或import store from '../store.js'
的代码将由相同的store实例。 - 我们从不编辑
store.state.counter
,但我们得到了这个state的一个可更新和修改的拷贝,这将会非常重要。
现在我们已经解决了store问题,让我们修改IncrementButton.vue
import store from '../store'
export default {
methods: {
activate () {
}
}
}
<template>
Count is {{ counter }}
</template>
<script>
import store from '../store'
export default {
computed: {
counter () {
return store.state.counter
}
}
}
</script>
这时事情变得有趣了,我们不再订阅共享state,我们使用vue的computed属性从store中获得计数。
Vue清楚counter
的computed属性依赖于store.state.counter
,因此当store更新时,它会更新所有的相关项,这就是我们要的!
如果你要重载页面,你会发现计数器正常工作,这里是一步步发生的事情:
- Vue的事件处理器成为
activate
,我们写的这个函数是store.dispatch('INCREMENT')
。 - 在这里,
INCREMENT
是一个行为的名称,它代表“这是一种可改变state的类型”的标示符,同时,我们可以将额外的参数传递给调度函数。 - Vue能够清楚计数器的调度,同样的,我们也能够让它更加复杂从而服务于更大的应用。
- 调度器接收state的拷贝并更新它,Vue保存了一份state的旧的拷贝,而这会被用于之后更高级的特征。
- state更新时,vue自动更新所有依赖于该state的组件。
- 如果你这样做了,这会让你的代码更具可测试性。
为什么这种方法比方法二更好:
- 如果所有states的一个拷贝存在于开发的整个流程中,vue开发者就可以潜在的构建一个"Time Travelling Debugger",除了有一个很cool的名字,它还允许你在应用中撤销行为并修改逻辑,提高开发效率。
- 你可以构建一个中间件并使其工作在state改变时,例如,你可以构建一个logger,它可以留存用户的所有行为日志。如果发现了bug,你可以调出日志来查看到底是哪一步出了问题。
- 对比让你自己去监视所有的行为,让你的团队成员都能改变应用的state会变得更加高效。
仍有很多工作要做
这里只展示了vuex的皮毛,现在仅仅只是构建的开始,我相信这种开发模式要发展成熟仍然需要很多年。
你可以发现更多资料去组织你的store,在vuex的文档中学习更多的信息,消化所有的概念可能需要一些时间,我们可能也要做出一些错误的尝试才能找出正确的方法。
结语:处理实习生的代码
你可以移植你的应用到vue.js,你的实习生发现可以在他的组件中很便捷的写入store.state.counter
,你现在已经拿到了最后一根稻草,现在在你的store.js
中加一行。
var store = new Vuex.Store({
state: {
counter: 0
},
mutations: {
INCREMENT (state) {
state.counter ++
}
},
strict: true // Vuex申请专利的反实习装置
})
现在所有人可以直接写入store,这也会引起一个问题,注意这会降低你的应用的速率,你可以在产品发布时安全的去掉它,去看看文档中例子是怎么做的。