Fork me on GitHub

vuex 要点

frontend/vuejs/vuex/vuex_banner

vuex是一个状态管理模式,通过用户的actions触发事件,然后通过mutations去更改数据(你也可以说状态啦 -> state),最后通过getters对状态进行获取,更改页面展示的内容。哈哈 😄 ,详细的内容请接着往下看,如有不妥请文末留言啊。原创文章,转载请注明出处。

注意 ⚠️ 文章中涉及到项目代码是使用Vue官方提供的脚手架vue-cli进行搭建的,如果看者感兴趣,可以自行用vue-cli搭建项目,并进行代码的验证。

Vuex是什么

官网介绍:Vuex是一个专门为Vuejs应用程序开发的状态管理模式。(类似react的redux)。Vuex采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。Vuex在构建中大型的应用比较适用,小型的应用用组件之间的通信就可以了,小型应用用上Vuex就显得比较臃肿了。

Vuex的安装

因为自己是使用npm来辅助开发的,所以我也只说下通过npm安装Vuex的方法。其他的安装方法,请戳传送门

进入你项目的根目录,然后执行:

1
2
3
$ npm install vuex --save 

$ npm install vuex --save-dev

然后在store主入口的javascript文件,一般是store/index.js中通过use进行引用,前提是你已经安装了vue :

1
2
3
4
import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

为了方便在各个组件中使用store,需要在程序的根组件中将其注入到每个子组件。我们需要在实例化Vue的时候将store引入(实例化Vue的文件一般是main.js主入口文件)。

1
2
3
4
5
6
import Vue from 'vue'
import store from '/path/to/store/index.js'

const initApp =new Vue({
store: store
}).$mount('#app')

核心概念

在使用Vuex进行开发的过程中,你可以理解核心的概念只有StateActionMutation三个,就像本文章开篇给出的截图流程那样简单明了。但是,我们使用Vuex开发一个应用,肯定是想要方便管理等等。这里自己按照五个核心概念来谈谈,五个核心概念也是官网推荐使用的。Vuex的五个核心概念除了上面指出的三个之外,还包括GetterModule两个。先一句话来概括下它们 :

  • State : 数据源的存放地

  • Getter : store的计算属性

  • Mutation : 处理数据逻辑,使得数据和视图分离(同步)

  • Action : 类似Mutation(异步),改变状态的话,还得触发Mutation

  • Module : 将store分解成模块

下面来详细讲解各个核心概念咯 😊

State

Vuex是使用单一状态树,一个对象就包含了全部的应用层级状态。这也就表明,一个应用仅仅包含一个store的实例。

状态State对应于Vue对象中的data,因为两者是对应的关系,所以在这里可以称状态==数据的。如下代码指出:

1
2
3
4
5
6
7
8
9
10
<script>
export default {
name: '',
data() { // state对应的地方
return {
...
}
}
}
</script>

State里面存放的数据是响应式的,Vue组件从store中读取数据,若是store中的数据发生改变,依赖这个数据的组件也会发生更新。也就是说数据和视图是同步的。

局部状态

虽然说VuexStore仓库让我们统一管理数据变得更加方便,但是代码一多也会变得冗长和不直观。有些组件的数据是自己严格使用,我们可以将state放在组件自身,作为局部数据,专供此组件使用。比如现在只想在一个组件中使用emotion: happiness,那就不必要在storestate中进行定义了,只在本组件初始化就行了:

1
2
3
4
5
data () {
return {
emotion: 'happiness'
}
}

获取状态

Vue组件中获取store中的数据(状态),最直接的就是通过计算属性获取。因为在上面我将store注册到根组件上了,所以在这里直接通过this.$store就可以调用了。比如我获取状态(state)中的count: 100 :

1
2
3
4
5
computed: {
count: function (){
return this.$store.state.count;
}
}

mapState辅助函数

mapState辅助函数把全局的State映射到当前组件computed计算属性中,即是帮助我们生成计算属性。简化我们的代码操作,不需要使用this.$store.state获取了。以上面状态(state)中的count: 100为例子 :

1
2
3
4
5
6
7
import { mapState } from 'vuex' // 注意别漏了引入
export default {
computed:
mapState({
count: state => state.count
}),
}

Getter

上面的state中我们了解到,在store仓库里,state是用来存储数据的。在多个组件中要进行使用同一种状态的话,对数据进行简单操作,我们可以通过在组件的computed中进行获取this.$store.state.theDataName。简单操作没问题,但是,我们进行其他的操作,比如过滤操作,我们就得写一堆的代码 :

1
2
3
4
5
6
7
computed: {
filterData: function () {
this.$store.state.theDataName.filter(function(item){
// do something ...
})
}
}

然后在每个组件中复制这一大堆的代码,或者你单独新建一个文件把代码写进入,每个组件都引入(如果你不觉得很麻烦的话)。

Getter可以把组件中共享状态抽取出来,这也是Getter存在的意义。我们可以认为,GetterStore的计算属性。

如何使用Getter

为了方便管理,需要一个单独的getters.js的文件,假如已经有对数据进行过滤的函数了:

1
2
3
4
5
export default {
filterDatas (state,getter,rootState) {
// do something ...
}
}

那么只要在相关的组件的computed中引入就可以了,是不是很方便啊 :

1
2
3
4
5
computed: {
filterItems: function () {
return this.$store.getters.filterDatas;
}
}

mapGetters辅助函数

mapGetters辅助函数仅仅是将store中的getter映射到局部计算属性,看情况使用,类似mapState。下面使用mapGetter改写上面的filterItems

1
2
3
4
5
6
7
import { mapGetters } from 'vuex' // 记得引入
export default {
computed:
mapGetters({
filterItems: 'filterDatas'
})
}

Mutation

Vuex的中文官网中明确指出更改Vuex的store中的状态(state)的唯一的方法是提交mutation

Mutation可以理解为:在Mutation里面装着一些改变数据方法的集合。即把处理数据逻辑方法全部放在Mutation里面,使得数据和视图分离。

使用Mutation

Mutation的结构:每个mutation都有一个字符串的事件类型(type)和一个回调函数(handler)也可以理解为{type:handler()},这和订阅发布有点类似。先是注册事件,当触发响应类型的时候调用handle(),调用type的时候需要用到store.commit('typeName')方法。比如我想在要触发mutations.js中的INCREASE处理函数:

1
2
3
4
5
6
7
// mutations.js
const INCREASE = 'INCREASE'; // 不能漏
export default {
[INCREASE](state,data){
// change the state ...
}
}

因为我注册了store到根组件,那么在.vue组件中就可以通过this.$store.commit('INCREASE')触发这个改变相关状态的处理函数了。如果在actions.js中调用,直接使用提供的commit参数进行commit('INCREASE')触发处理函数。

提交载荷(Payload)

可以向store.commit传入额外的参数,参数一般为object类型。我这里接着上面的示例,组件触发的时候传入一个100的数字到data里面 :

1
2
3
4
5
methods:{
increase: function (){
this.$store.commit('INCREASE',100);
}
}

使用mutation-types.js

使用mutation-types.js(名称可根据爱好随便取)是为了方便管理项目mutation的类型。我在知乎上也回答过为什么要使用mutation-types.js,当然你完全没必要使用它,不过我自己喜欢使用它。将使用mutation内容中的mutations.js代码拆分为两部分,一部分是mutation-types.js,另一部分是mutations.js,示范如下 :

1
2
3
4
5
6
7
8
9
10
// mutation-types.js
export const INCREASE = 'INCREASE';

// mutations.js
import {INCREASE} from '/path/to/mutation-type.js'
export default {
[INCREASE](state,data){
// change the state ...
}
}

mapMutations辅助函数

为了简化你的代码量,使得代码看起来逼格更高点,你可以使用mapMutations辅助函数将组件中的methods映射为store.commit调用(需要在根节点注入store哦)。demo来映射上面的increase

1
2
3
4
5
6
7
8
import {mapMutations} from 'vuex' // 不能漏哦
export default {
methods: {
...mapMutations([
'INCREASE'
])
}
}

Action

Action 类似于 Mutation,不同点是 :

  • Action提交的是 mutation,而不是直接变更状态

  • Action是异步的,而Mutation是同步的

详细的相似点可以回滚看Mutation的啦,或者直接戳vue官网Store

组件内分发Action

因为我在全局组件中挂载了store,所以引用就可以这样写 -> this.$store.dispatch('dispatchEvent'),当然你可以传参过去啦。比如:this.$store.dispatch('dispatchEvent',param),param一般是obj类型的。

mapActions辅助

为了简化操作,Action像Mutaion一样有一个映射的函数mapActions。使用方法也类似Mutation,demo如下 :

1
2
3
4
5
6
7
8
9
10
11
12
import {mapActions} from 'vuex' // 不能漏哦
export default {
methods: {
...mapActions([
'INCREASE'
])

...mapActions([
increase: 'INCREASE'
])
}
}

Module

由于vue中使用单一的状态树,当管理的项目中大型的时候,所有的状态都集中在一个对象中会变得比较复杂,难以管理,显得项目比较臃肿。为了解决这些问题,我们可以使用vuex提供的Module功能,将store分割成模块。每个模块都有自己的state、mutation、action、getter。现在假设你的应用的功能包括登录和音乐两个功能模块页面,那么store的结构可以这样写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

- module
- music
actions.js
getters.js
index.js // music module 的入口文件
mutations.js
state.js
- user
actions.js
getters.js
index.js // user module的入口文件
mutations.js
state.js
actions.js
index.js // store 的入口文件
mutation-types.js // 管理所有的mutations
mutations.js
state.js

模块的局部状态

对于模块内部的mutation,接收的第一个参数是state,也就是接收本模块的局部状态,比如上面的music模块,我在其state.js中写上 :

1
2
3
4
5
6
export default {
music: {
list: [],
total: 100
}
}

我在同级的mutations.js中有 :

1
2
3
4
5
6
7
import * as types from '../../mutation-types'
export default {
[types.UPDATE_MUSIC](state,data){
console.log(state.music.total); // 打印出100
...other handle
}
}

命名空间

默认情况下,模块内部的action、mutation 和 getter是注册在全局命名空间的 -> 这样使得多个模块能够对mutation和action作出响应。

如果看者希望你写的模块具有更高的封装度和复用性,你可以通过添加namespaced:true的方式使其成为命名空间模块。当模块被注册后,它的所有 getter、action 及 mutation 都会自动根据模块注册的路径调整命名。比如上面的music模块 :

1
2
3
4
5
6
7
8
9
10
11
12
13
import state from './state'     //state
import getters from './getters' //getters
import * as actions from './actions' //actions
import mutations from './mutations' //mutations

//modules
export default {
namespaced: true, // 添加命名空间
state,
getters,
actions,
mutations
}

详细的情况请戳vuex官网modules

store结构

vuex的官网谈项目结构,我这里谈store结构,因为我觉得每个人的项目的结构布局有所不同,但是vuex可以是一个模版化的使用。当然,这模版化的使用遵循了官网所定的规则:

  • 应用层级的状态应该集中在单个 store对象中

  • 提交mutation是更改状态(state)的唯一方法,并且这个过程是同步的

  • 异步逻辑都应该封装到action里面

整理的store结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
.
├── ...

└── store
├── actions.js // 根级别的 action
├── index.js // 我们组装模块并导出 store 的地方
├── mutation-types.js // store所有mutations管理
├── mutations.js // 根级别的 mutation
├── state.js // 根级别的 state
└── modules
├── moduleA
├── moduleB
└── moduleC
├── actions.js // moduleC 的 action
├── getters.js // moduleC 的 getter
├── index.js // moduleC 的 入口
├── mutations.js // moduleC 的 mutation
└── state.js // moduleC 的 state

上面的结构比较通用,模版化,我在接下来的完整小项目中就是使用上面的store结构来管理啦 😝

完整小项目

自己在上面讲了一大推的废话,嗯哈,为了证明那不是废话,下面就结合上面讲的知识点来一个综合的min-demo吧,欢迎指正啊! @~@

是什么项目呢

思来想去,自己还是觉得做一个简单版本的todo项目好点,理由如下:

  • 个人时间精力邮箱(main reason)

  • todo项目 -> 麻雀虽小,五脏俱全

项目包含一个简单的登录页面,然后跳转到todo小项目的页面。如图所示:

frontend/vuejs/vuex/min-demo-login

frontend/vuejs/vuex/min-demo-todo

在登录页面,会要求你填写非空的内容进入,我这里填了自己的名字啦。在todo页面,你就需要在输入框输入你要做的事情啦,事情的添加默认是未做的状态。当然,允许进行时间的状态进行设置和事件的删除啦。成品可查看下面最终的效果gif动效,就酱 @~@

项目的初始化

⚠️ 本项目在mac系统上使用vue-cli的基础上搭建(搭建日期2018.01.14)的小项目,其完整的覆盖了vue的全家桶了 -> 使用的vue版本是^2.5.2,vuex的版本是^3.0.1,vue-router的版本也是^3.0.1。如果你使用低版本,请参考低版本的相关说明。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# 全局安装 vue-cli
$ npm install --global vue-cli
# 进入桌面
$ cd desktop
# 初始化项目min-demo
$ vue init webpack min-demo

? Project name min-demo # 项目名称
? Project description A Vue.js project # 项目描述
? Author reng99 # 项目作者
? Vue build standalone
? Install vue-router? Yes # 是否使用路由
? Use ESLint to lint your code? No # 是否启动语法检查
? Set up unit tests No # 是否配置单元测试
? Setup e2e tests with Nightwatch? No # 是否配置集成测试
? Should we run `npm install` for you after the project has been created? (recom
mended) npm # 选择那种包管理工具进行安装依赖,共三种选择:npm,yarn,no thanks 我选择了npm

vue-cli · Generated "min-demo".
# 等待安装依赖的完成
...
# 进入项目
$ cd min-demo
# 启动项目
$ npm run dev

# 如果一切正常,就会在浏览器的http://localhost:8080的地址页面有相关的vue界面展示出来

当然,使用脚手架搭建的项目,没有自动集成vuex,这就需要你进入项目的根目录,执行npm install vuex --save命令来安装啦。

项目的实现

嗯嗯,下面我将改写在vue-cli搭建的项目,以符合我自己期望。改写的代码就不全给出来了啊,关键的项目代码还是会贴一下的。😝

这个项目的结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
.
├── build/ #webpack 的配置项
│ └── ...
├── config/
│ ├── index.js # 项目的主要配置
│ └── ...
├── node_modules/ # 相关依赖
│ └── ...
├── src/
│ ├── main.js # 应用的主入口
│ ├── App.vue # 引用的根组件
│ ├── components/
│ │ ├── Login.vue # 登录组件
│ │ └── Todo.vue # todo组件
│ ├── store/
│ │ ├── modules/ # todo组件
│ │ │ └── todo
│ │ │ ├── actions.js # todo的actions
│ │ │ ├── getters.js # todo的getters
│ │ │ ├── index.js # todo的入口
│ │ │ ├── mutations.js # todo的mutations
│ │ │ └── state.js # todo的状态
│ │ ├── actions.js # 根actions
│ │ ├── index.js # store入口文件
│ │ ├── mutation-types.js # 整个store中的mutation的管理
│ │ ├── mutations.js # 根mutations
│ │ └── state.js # 根的状态
│ ├── router/
│ │ └── index.js # 路由文件
│ └── assets/ # 模块的资源
│ └── ...
├── static/ # 静态资源存放的地方
│ └── ...
├── .babelrc # 语法转换babel的相关配置
├── .editorconfig # 编辑器IDE的相关配置
├── .gitignore # 提交到github忽略的内容配置
├── .postcssrc.js # css的处理配置postcssrc
├── index.html # index html模版
├── package.json # 相关的执行命令和依赖配置
└── README.md # 项目的说明文件

⚠️ 项目重点在src文件夹内

/src/components/Login.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
<template>
<div id="login">
<div class='login'>
<div class='login-title'>简单模拟登录</div>
<div class='login-body'>
<div class='hint' v-show='hintFlag'>输入的文字不能为空</div>
<input placeholder='请输入任意文字...' type='text' v-model='loginTxt'/>
<div class="btn" @click='login'>登录</div>
</div>
</div>
</div>
</template>

<script>
export default {
name: 'Login',
data () {
return {
hintFlag: false,
loginTxt: ''
}
},
methods: {
login () {
var vm = this;
if(vm.loginTxt.trim()==''){
vm.hintFlag = true;
}else{
// 进入todo的页面
vm.hintFlag = false;
// 触发获取登录名
vm.$store.dispatch('createUsername',vm.loginTxt);
vm.$router.push('/todo');
}
}
},
watch:{
loginTxt(curVal){
var vm = this;
if(curVal.trim()==''){
vm.hintFlag = true;
}else{
vm.hintFlag = false;
}
}
}
}
</script>

<style scoped lang='less'>
#login{
margin-top: 100px;
.login{
width: 400px;
margin: 0 auto;
&-title{
color: #999;
font-size: 22px;
text-align: center;
margin-bottom: 20px;
}
&-body{
width: 360px;
padding: 40px 20px 60px 20px;
background: #ededed;
input{
width: 100%;
display: block;
height: 40px;
text-indent: 10px;
}
.btn{
width: 100%;
text-align: center;
height: 40px;
line-height: 40px;
background: #09c7d1;
color: #fff;
margin-top: 20px;
cursor: pointer;
}
.hint{
color: red;
font-size: 12px;
text-align: center;
padding-bottom: 10px;
}
}
}
}
</style>

在上面的组件中,自己原封不动的将里面的代码复制过来了,你应该可以看出,这个.vue文件中结合了三块的东西,分别是html的模版、javascript代码和运用less预处理器编写的css代码。

/src/components/Todo.vue组件的结构依旧是这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
<template>
<div id="todo">
<div class='username'>欢迎您!<span>{{username}}</span></div>
<div class="main">
<div class="input">
<input placeholder='请输入要做的事情...' type='text' v-model='eventTxt'/>
<button @click="addEvent">增加</button>
</div>
...
</div>
</div>
</template>

<script>
export default {
name: 'ToDo',
data () {
return {
noDataFlag: true,
...
}
},
created(){
var vm = this;
if(vm.username == ''){
vm.$router.push('/');
}
},
computed: {
username(){
return this.$store.getters.username;
},
...
},
methods: {
delEvent (id) {
this.$store.dispatch('delEvent',id);
},
...
},
watch:{
...
}
}
</script>

<style scoped lang='less'>
#todo{
margin-top: 100px;
...
}
</style>

在路由的文件中,因为知识涉及了两个页面的路由跳转,这里也全贴出来吧 –

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import Vue from 'vue'
import Router from 'vue-router'
import Login from '@/components/Login'
import ToDo from '@/components/ToDo'

Vue.use(Router)

export default new Router({
routes: [
{
path: '/',
name: 'Login',
component: Login
},
{
path: '/todo',
name: 'ToDo',
component: ToDo
}
]
})

关于store,这是一个重点,我打算详细说啦。首先当然得从整个store的入口文件讲起啦。在store/index.js中,我是这样引用的 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import Vue from 'vue'		// 引入vue依赖
import Vuex from 'Vuex' // 引入vuex依赖

import state from './state' // 引入根状态
import * as actions from './actions' // 引入根actions
import mutations from './mutations' // 引入根mutations

import todo from './modules/todo/index' // 引入todo模块

Vue.use(Vuex) // 引入vuex

// 初始化store
export default new Vuex.Store({
state,
actions,
mutations,
modules:{
todo
}
})

在根的storemutation-types.js文件中,管理着整个项目的状态管理函数 –> 包括创建用户名、添加要做的事情、删除创建的事情、显示事件的状态(全部,已经做,没有做)和标记事件(已经做的事件标记为未做,未做的事件标记为已经做)。代码展示如下 :

1
2
3
4
5
6
7
8
export const CREATE_USERNAME = 'CREATE_USERNAME'  // 创建用户名
export const ADD_EVENT = 'ADD_EVENT' // 添加事件
export const DEL_EVENT = 'DEL_EVENT' // 删除事件
export const ALL_EVENT = 'ALL_EVENT' // 全部事件
export const UNDO_EVENT = 'UNDO_EVENT' // 没做事件
export const DONE_EVENT = 'DONE_EVENT' // 已做事件
export const MARK_UNDONE = 'MARK_UNDONE' // 标记为未做
export const MARK_DONE = 'MARK_DONE' // 标记为已做

store/state.js的作用在你听完store/todo/state.js的讲解后你应该会明白。在模块todo的state中,自己定义了此模块的相关的数据结构,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
export default {
// 事件列表
list:[
// {
// id: 0, 相关的id
// content:'', // 事件的内容
// flag: 1 // 是否完成,1是完成,0是未完成
// }
],
allList:[],
increase: 0,
total: 0,
done: 0
}

定义的这些数据结构,你可以说是状态吧,是为了给mutation和getters进行操作。对了,你也许注意到了store根目录中没有getters.js文件。因为,这是分散模块管理项目,为什么还需要呢,如果你想保留,你可以自己新建一个,按照自己的习惯进行管理项目呗。

上个段落以及前面某部分内容已经谈及了mutations的作用,本项目中使用mutation就是为了改变自己在todo/state.js定义的状态,比如改变allList:[]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import * as types from '../../mutation-types'

export default {
// 添加事件
[types.ADD_EVENT] (state,data){
var obj = {
id: state.increase++,
content: data,
flag: 0
}
state.allList.push(obj);
state.list = state.allList;
state.total = state.allList.length;
},
...
}

todo/getter.js就是为了将vuex中的状态获取,方便显示在页面的啦,在本项目中,自己超级简单的使用了下:

1
2
3
4
5
6
7
8
9
export default {
list (state,getters,rootState) {
return state.list;
},
username (state,getters,rootState) {
return rootState.username;
},
...
}

最后一个是关于todo/actions.js,这是页面中的用户的事件去发送事件,使得产生mutations去改变状态(state.js),最终使得页面展示的内容(getters)发生改变。这里以一个派遣添加事件为例子 :

1
2
3
4
5
import * as types from '../../mutation-types'

export const addEvent = ({commit,state,rootState},query) => {
commit(types.ADD_EVENT,query);
}

嗯,整篇文章都说整个store是挂载在根组件上的,那么是在哪里呢?答案就是src/main.js文件啦,文件内的代码如下 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import Vue from 'vue'
import App from './App'
import router from './router'

import store from './store'

Vue.config.productionTip = false

new Vue({
el: '#app',
router,
store,
components: { App },
template: '<App/>'
})

最终的效果

好吧,自己利用了一个下午搭建项目并简单思考了相关的逻辑,简单实现项目,其最终的效果如下gif动图啦 :

frontend/vuejs/vuex/min-demo

嗯,项目是不是很简单,所以就不放源码上去了 😂 。其实自己觉得源码实现不够严谨啦,毕竟只是花了短短一个下午和晚上从设计到实现… 逃:)

参考内容

vuex官网

( 完 @~@ )

<-- 本文已结束  感谢您阅读 -->
客官,且步,赏一个呗 (@ ~ @)