作者注:[2016.11 更新]这篇文章是基于一个非常旧的 vuex api 版本而写的,代码来自于2015年12月。
但是,它仍能针对下面几个问题深入探讨:

  1. vuex 为什么重要
  2. vuex 如何工作
  3. vuex 如何使你的应用更容易维护

vuex 是 vue.js 作者开发的一个原型库,它帮助你创建更大、维护性更强的应用,类似于 Facebook 的 flux 库(以及由社区维护的 redux 库)。
这篇文章不直接跳到 vuex 教你如何使用它,而是从背后的故事开始说起,逐步解释它为什么是优雅的替代方法,以及将如何帮助你。

译者注:a git repo of vuex-tutorial use vue2.0

你想要创建什么应用?

一个拥有按钮计数器的简单应用,点击按钮计数器加1。这听起来非常容易理解和完成。

我们假设这个应用有两个组件:

  1. 按钮 (它是事件的来源)
  2. 计数器 (它必须按照事件来反映更新)

这两个组件不知道彼此的存在,也不能相互通信。即使是在最小的 web 应用中,这也是一种非常常见的模式。在更大点儿的应用中,十几个组件相互通信,并时刻关注对方的变化。不相信我?这里是一个基础的 TODOlist 应用的交互清单:

这篇文章的目标

我们将讨论解决同一个问题的3种方法:

  1. 组件之间使用事件广播来通信
  2. 使用一个共享的状态对象通信
  3. 使用 vuex 通信

读完这篇文章,希望你能理解:

  1. 在你的项目中使用 vuex 的一个基本工作流程
  2. 它解决了哪些问题
  3. 相对其他方法,为什么它是更好的(尽管有些冗长和严格)

准备工作

我们将使用3种不同的方法来解决同一个问题。在这之前,需要做一些共同的准备工作。如果你打算跟着我做,我建议你为这个教程创建一个 git repo,这一小节结束后提交一次代码,然后为不同的方法创建不同的分支。

1
2
3
4
5
6
$ 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 组件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<template>
<button @click.prevent="activate">+1</button>
</template>
 
<script>
export default {
methods: {
activate () {
console.log('+1 Pressed')
}
}
}
</script>
 
<style>
</style>

下一步,在文件 src/components/CounterDisplay.vue 中创建 CounterDisplay 组件来展示计数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<template>
Count is {{ count }}
</template>
 
<script>
export default {
data () {
return {
count: 0
}
}
}
</script>
 
<style>
</style>

使用下面的内容替换 App.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<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,在浏览器打开页面,你应该看到一个 按钮 和一个 计数器。点击按钮,控制台将显示一条信息,其它没什么变化。
现在我们已经来到了起点,开始吧。

方法1:事件广播

来修改组件的代码。
首先在 IncrementButton.vue 中,在按钮被点击时使用 $dispatch 给父组件发送一个消息。

1
2
3
4
5
6
7
8
export default {
methods: {
activate () {
// Send an event upwards to be picked up by App
this.$dispatch('button-pressed')
}
}
}

在 App.vue 中监听来自子组件的这个消息事件,然后广播一个新的事件 increment 给所有的子组件:

1
2
3
4
5
6
7
8
9
10
11
12
export default {
components: {
Counter,
Increment
},
events: {
'button-pressed': function () {
// Send a message to all children
this.$broadcast('increment')
}
}
}

在 CounterDisplay.vue 中,监听 increment 事件,并增加状态数据中的变量:

1
2
3
4
5
6
7
8
9
10
11
12
export default {
data () {
return {
count: 0
}
},
events: {
increment () {
this.count++
}
}
}

这个方法的缺点:

这个方法基本没有什么技术上的错误。此外,在一个文件里实现整个应用的逻辑,专门使用 goto 来跳转也没有错。这只与可维护性有关,这里会讲一下为什么这个方法在可维护性上是糟糕的。

  1. 对于每一个操作,父组件都需要将事件分发给正确的组件;
  2. 在大型应用中,可能很难理解事件是从哪儿来的;
  3. 业务逻辑没有明确的位置。this.count++ 是在 CounterDisplay 中,但业务逻辑可能到处都是,这会导致难以维护。

让我来举例说明一下这个方法会怎样导致bug:

  1. 你雇了两个实习生: Alice 和 Bob。你告诉 Alice 你需要为另外一个组件实现另一个计数器,告诉 Bob 写一个重置按钮;
  2. Alice 写了一个新的组件 FormattedCounterDisplay,它能够监听增量,并增加自己的状态数据。Alice 开心的提交了代码;
  3. Bob 写了一个新的 Reset 组件,它向应用发出一个 reset 事件,并重新分发它。他在 CounterDisplay 中将 count 重置为0,但是他没有意识到 Alice 的组件也订阅了这个变化;
  4. 你的用户点击 +1 按钮后看到应用工作正常。但是当他点击 重置 按钮,只有一个计数器被重置了。这看起来是一个非常简单的例子,仅仅为了说明状态和业务逻辑绑在一起可能会导致错误。

方法2: 共享状态

撤销方法1中的改动,创建一个新文件 src/store.js

1
2
3
4
5
export default {
state: {
counter: 0
}
}

首先修改 CounterDisplay.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<template>
Count is {{ sharedState.counter }}
</template>
 
<script>
import store from '../store'
 
export default {
data () {
return {
sharedState: store.state
}
}
}
</script>

这里我们做了一些有趣的事情:

  1. 获取到一个 store 对象,它仅仅是一个对象常量,但是在不同的文件中定义的;
  2. 在本地数据中,我们创建了一个叫 sharedState 的数据,它映射到 store.state
  3. vue 使用 store.state 作为当前组件的一部分数据,这意味着 store.state 有任何变化,vue 都会自动更新 sharedState

到目前为止它还不能工作,现在我们来修改 IncrementButton.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import store from '../store'
 
export default {
data () {
return {
sharedState: store.state
}
},
methods: {
activate () {
this.sharedState.counter += 1
}
}
}
  1. 在这里,我们引入 store,并像之前的例子一样监听了数据的状态变化;
  2. 当 activate 方法被调用时,指向 store.state 的 sharedState 的计数器 counter 增加;
  3. 监听了计数器的所有组件和计算属性都会被更新。

它为什么比方法1更好

我们来回顾一下两个实习生 Alice 和 Bob 的问题:

  1. Alice 写的用来监听共享数据的 FormattedComponentDisplay 组件将会始终显示最新的 counter 数据;
  2. Bob 的重置按钮组件将共享数据的 counter 置为0,这将同时影响 CounterDisplay 和 Alice 写的 FormattedCounterDisplay
  3. 重置按钮符合预期。

为什么这样仍然不够好

  1. 在 Alice 和 Bob 的实习期内,他们使用不同的格式写了许多计数器、重置按钮,以及增量按钮,它们更新的是同一份共享的数据,生活很美好;
  2. 一旦他们回到学校,你需要维护他们的代码;
  3. 新任经理 Carol 进来之后说:“我不想看到计数器的数字超过100”

你现在该做什么?

  1. 你去十几个组件的代码里找到所有更新数据的地方吗?这让人沮丧;
  2. 你找到显示数据的地方然后添加一个 filter/formatter 来格式化数据吗?这同样让人沮丧;
  3. 这里就是这个问题,业务逻辑分散在应用的各个角落,原则上一个很简单的问题,但是维护和调试起来却特别痛苦。

稍好一点儿的方法

现在来重构你的代码,重写 store.js 如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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 后代码看起来清晰了许多。然而,一个新实习生不知道这背后的理论,他发现在应用的其他部分直接写入 store.state.counter 更容易,于是一切变得难于调试。
然后,你制定大量严格的规则和代码审查,以确保没有人在 store.js 中不使用函数的情况下修改状态数据。如果这都不起作用,那你可以告诉hr结束他的实习了。

方法3:vuex

回滚方法2里的修改,原则上 vuex 的工作原理与方法2有些相似。给你看一张稍稍有些可怕的图:

首先来创建 src/store.js,这次用下面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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

现在来看看这段代码做了什么:

  1. 获取 Vuex 模块,然后使用 Vue.use 安装这个插件;
  2. store 不再是一个普通的 JSON 对象,而是 Vuex.Store 的一个实例;
  3. 在 state 中创建一个计数器 counter,设置为0;
  4. 创建一个新的变异对象,包含 INCREMENT 方法:获取一个状态数据,然后改变它。

看看这段代码里有哪些有趣的东东:

  1. 所有通过 require('../store.js') 或 import store from '../store.js' 引入的 store 将使用同一个 store 实例;
  2. 我们不会修改 store.state.counter,但是我们有一份 state 的拷贝用来做修改,这在接下来会很重要。

现在我们已经改好了 store,来继续修改 IncrementButton.vue

1
2
3
4
5
6
7
8
9
import store from '../store'
 
export default {
methods: {
activate () {
store.dispatch('INCREMENT')
}
}
}

这个组件没有任何数据,但是点击的时候调用 store.dispatch('INCREMENT'),一会儿再返回来看。

下面更新一下 CounterDisplay.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<template>
Count is {{ counter }}
</template>
 
<script>
import store from '../store'
 
export default {
computed: {
counter () {
return store.state.counter
}
}
}
</script>

事情从这儿才真正有趣!我们不再订阅共享的状态数据的变化,而是使用 vue 的计算属性来给 counter 同步 store 中的数据。
Vue 足够聪明来计算出基于 store.state.counter 的计算属性 counter,无论 store 何时被更新,它将更新所有的关联项。That’s it!

如果你刷新这个页面,你将看到计数器依然正确工作。下面将逐步解释发生了什么:

  1. vue 的事件处理函数是 activate,这个方法调用了 store.dispatch('INCREMENT')
  2. 在这里,INCREMENT 是一个动作的名称。它表示 “这是 state 应该做出的那种改变”。我们还可以传递额外的其他参数给分发函数;
  3. vue 指明了分发事件时应该调用哪个函数。现在我们只有一个,但是我们可以为大型应用定制的更复杂;
  4. 这个函数接收状态数据的拷贝,并对它进行更新。vue 保留一份旧数据的拷贝用于后续的高级功能;
  5. 当状态更新之后,vue 自动更新所有组件;
  6. 这些使得你的代码可测试性更强,如果你做了这些的话。

这里是比办法2更好的原因

假如在开发过程中所有状态的拷贝都被保存下来,vue 开发者建立起所谓的“时间旅行调试器”是非常有可能的。除了一个听起来超酷的超级英雄的名字,它将允许你在应用中撤销行为、改变逻辑,以及开发的更快。
只要状态改变,你就可以构建中间件。例如,你可以创建一个 logger 来记录用户执行的所有操作。如果他们发现了一个bug,你可以获取到用户日志,重新播放所有的行为,并正确的重现他们的bug。
通过强制你在一个地方(store)进行所有的动作,这是一个很好的参考,你团队中的每一个人都可以使用你应用中所有修改状态数据的方法。

还有很长的路要走

这里仅仅接触到了 vuex 表面可以做的事情,它自身仍然是一个早期版本,我相信这将成为未来许多年里最成熟的模式之一。
你可以去网上找到关于如何组织 store 以及 vuex 文档的更多信息。你可能需要花一些时间来理解所有的概念,甚至可能需要一些尝试和错误才能找出正确的方法。

结语:处理实习生的代码

你将应用移植到 vue.js,你的实习生仍旧可以找到方法在自己的组件中重写 store.state.counter。你明白的,这是最后一根稻草。然后继续在你的 store.js 中增加一行代码:

1
2
3
4
5
6
7
8
9
10
11
var store = new Vuex.Store({
state: {
counter: 0
},
mutations: {
INCREMENT (state) {
state.counter++
}
},
strict: true // Vuex's patent pending anti-intern device
})

现在无论何时何人直接修改 store,将会抛出一个错误。请注意这会减慢你的应用运行的时间,这个配置可以在生产环境移除,相关示例请查文档。

vuex 基础:教程和说明的更多相关文章

  1. vuex入门教程和思考 [转] 里面有几个实例

    Vuex基础概念 vuex中涉及的概念主要有下面几点,下面做个简单的介绍和理解. Vuex 官方文档:https://vuex.vuejs.org/zh-cn/ 官网有介绍,也有个demo shopp ...

  2. matlab基础教程——根据Andrew Ng的machine learning整理

    matlab基础教程--根据Andrew Ng的machine learning整理 基本运算 算数运算 逻辑运算 格式化输出 小数位全局修改 向量和矩阵运算 矩阵操作 申明一个矩阵或向量 快速建立一 ...

  3. <<Bootstrap基础教程>> 新书出手,有心栽花花不开,无心插柳柳成荫

    并非闲的蛋疼,做技术也经常喜欢蛋疼,纠结于各种技术,各种需求变更,还有一个很苦恼的就是UI总是那么不尽人意.前不久自己开源了自己做了多年的仓储项目(开源地址:https://github.com/he ...

  4. Memcache教程 Memcache零基础教程

    Memcache是什么 Memcache是danga.com的一个项目,来分担数据库的压力. 它可以应对任意多个连接,使用非阻塞的网络IO.由于它的工作机制是在内存中开辟一块空间,然后建立一个Hash ...

  5. Selenium IDE 基础教程

    Selenium IDE 基础教程 1.下载安装     a 在火狐浏览其中搜索附件组件,查找 Selenium IDE     b 下载安装,然后重启firefox 2.界面讲解      在菜单- ...

  6. html快速入门(基础教程+资源推荐)

    1.html究竟是什么? 从字面上理解,html是超文本标记语言hyper text mark-up language的首字母缩写,指的是一种通用web页面描述语言,是用来描述我们打开浏览器就能看到的 ...

  7. 转发-UI基础教程 – 原生App切图的那些事儿

    UI基础教程 – 原生App切图的那些事儿 转发:http://www.shejidaren.com/app-ui-cut-and-slice.html 移动APP切图是UI设计必须学会的一项技能,切 ...

  8. 【Unity3D基础教程】给初学者看的Unity教程(四):通过制作Flappy Bird了解Native 2D中的RigidBody2D和Collider2D

    作者:王选易,出处:http://www.cnblogs.com/neverdie/ 欢迎转载,也请保留这段声明.如果你喜欢这篇文章,请点[推荐].谢谢! 引子 在第一篇文章[Unity3D基础教程] ...

  9. oracle基础教程(8)oracle修改字符集

    oracle基础教程(8)oracle修改字符集 1.用dba连接数据库 -->sqlplus / as sysdba 2.查看字符集 -->SELECT parameter, value ...

  10. 改写《python基础教程》中的一个例子

    一.前言 初学python,看<python基础教程>,第20章实现了将文本转化成html的功能.由于本人之前有DIY一个markdown转html的算法,所以对这个例子有兴趣.可仔细一看 ...

随机推荐

  1. Linux中普通用户提权为超级用户

    首先创建一个普通用户,并且给普通用户设置一个密码,保证能用su 命令能用普通用户登录 [root@ahu ~]# useradd test [root@ahu ~]# passwd test New ...

  2. ssh命令集锦

    [前提] ssh命令其实平时工作会比较少能够用到(因为直接用远程客户端来连接) 但是偶尔还是需要利用ssh临时的连接到某个服务器,所以当遇到的时候来总结一下 [集锦] 一.ssh以某个用户名连接到某个 ...

  3. express-partials使用方法

    1.安装express-partials 方法一:运行cmd用npm install express-partials 方法二:在package.json里面的dependencies添加" ...

  4. MongoDB学习笔记-1

    mongod --dbpath D:\MogonDB3.4.10\db //开启数据库,无端口号mongod --dbpath D:\MogonDB3.4.10\db --port=10086 //开 ...

  5. InnoDB Lock浅谈

    数据库使用锁是为了支持更好的并发,提供数据的完整性和一致性.InnoDB是一个支持行锁的存储引擎,锁的类型有:共享锁(S).排他锁(X).意向共享(IS).意向排他(IX).为了提供更好的并发,Inn ...

  6. android4.0后无法向Servlet发送请求解决办法

    从4.0开始,强制性地规定网络堵塞任务都不能放在ui线程,不然直接报错. 个办法,在oncreate下面加入 StrictMode.setThreadPolicy(new StrictMode.Thr ...

  7. 关于.NetCore 2.0 迁移到2.1的一些建议和问题

    最近手欠升级了下VS2017到15.7版本 然后更新了下sdk到2.1.300版本 那么麻烦就来了,原有项目就带来了很多问题,更新所有包到最新就不用说了 下面说明下最主要存在的2问题: 第一个问题:调 ...

  8. Java中的String问题

    方式一:String a = “aaa” ; 方式二:String b = new String(“aaa”); 两种方式都能创建字符串对象,但方式一要比方式二更优.因为字符串是保存在常量池中的,而通 ...

  9. python程序后台运行的实现

    后台运行work()方法. work.py # -*- coding:utf-8 -*- def work(): print "running" import time time. ...

  10. 如何用node.js批量给图片加水印

    上一篇我们讲了如何用node.js给图片加水印,但是只是给某一张图片加,并没有涉及到批量处理.这一篇,我们学习如果批量进行图片加水印处理. 一.准备工作: 首先,你要阅读完这篇文章:http://ww ...