vuex
是一个状态管理模式,通过用户的actions
触发事件,然后通过mutations
去更改数据(你也可以说状态啦 -> state),最后通过getters
对状态进行获取,更改页面展示的内容。哈哈 😄 ,详细的内容请接着往下看,如有不妥请文末留言啊。原创文章,转载请注明出处。
原文请戳传送门
注意 ⚠️ 文章中涉及到项目代码是使用Vue
官方提供的脚手架vue-cli
进行搭建的,如果看者感兴趣,可以自行用vue-cli
搭建项目,并进行代码的验证。
Vuex是什么
官网介绍:Vuex是一个专门为Vuejs应用程序开发的状态管理模式
。(类似react的redux)。Vuex
采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。Vuex在构建中大型的应用比较适用,小型的应用用组件之间的通信就可以了,小型应用用上Vuex
就显得比较臃肿了。
Vuex的安装
因为自己是使用npm
来辅助开发的,所以我也只说下通过npm
安装Vuex
的方法。其他的安装方法,请戳传送门。
进入你项目的根目录,然后执行:
$ npm install vuex --save
或
$ npm install vuex --save-dev
然后在store
主入口的javascript文件,一般是store/index.js
中通过use
进行引用,前提是你已经安装了vue
:
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
为了方便在各个组件中使用store
,需要在程序的根组件中将其注入
到每个子组件。我们需要在实例化Vue
的时候将store
引入(实例化Vue
的文件一般是main.js主入口文件)。
import Vue from 'vue'
import store from '/path/to/store/index.js'
const initApp =new Vue({
store: store
}).$mount('#app')
核心概念
在使用Vuex
进行开发的过程中,你可以理解核心的概念只有State
、Action
和Mutation
三个,就像本文章开篇给出的截图流程那样简单明了。但是,我们使用Vuex
开发一个应用,肯定是想要方便管理等等。这里自己按照五个核心概念来谈谈,五个核心概念也是官网推荐使用的。Vuex
的五个核心概念除了上面指出的三个之外,还包括Getter
和Module
两个。先一句话来概括下它们 :
State : 数据源的存放地
Getter : store的计算属性
Mutation : 处理数据逻辑,使得数据和视图分离(同步)
Action : 类似Mutation(异步),改变状态的话,还得触发Mutation
Module : 将store分解成模块
下面来详细讲解各个核心概念咯 😊
State
Vuex
是使用单一状态树,一个对象就包含了全部的应用层级状态。这也就表明,一个应用仅仅包含一个store
的实例。
状态State
对应于Vue
对象中的data,因为两者是对应的关系,所以在这里可以称状态==数据
的。如下代码指出:
<script>
export default {
name: '',
data() { // state对应的地方
return {
...
}
}
}
</script>
State
里面存放的数据是响应式的,Vue
组件从store
中读取数据,若是store
中的数据发生改变,依赖这个数据的组件也会发生更新。也就是说数据和视图是同步的。
局部状态
虽然说Vuex
的Store
仓库让我们统一管理数据变得更加方便,但是代码一多也会变得冗长和不直观。有些组件的数据是自己严格使用,我们可以将state
放在组件自身,作为局部数据,专供此组件使用。比如现在只想在一个组件中使用emotion: happiness
,那就不必要在store
的state
中进行定义了,只在本组件初始化就行了:
data () {
return {
emotion: 'happiness'
}
}
获取状态
在Vue
组件中获取store
中的数据(状态),最直接的就是通过计算属性获取。因为在上面我将store
注册到根组件上了,所以在这里直接通过this.$store
就可以调用了。比如我获取状态(state)中的count: 100
:
computed: {
count: function (){
return this.$store.state.count;
}
}
mapState辅助函数
mapState
辅助函数把全局的State
映射到当前组件computed
计算属性中,即是帮助我们生成计算属性。简化我们的代码操作,不需要使用this.$store.state
获取了。以上面状态(state)中的count: 100
为例子 :
import { mapState } from 'vuex' // 注意别漏了引入
export default {
computed:
mapState({
count: state => state.count
}),
}
Getter
上面的state
中我们了解到,在store
仓库里,state
是用来存储数据的。在多个组件中要进行使用同一种状态的话,对数据进行简单操作,我们可以通过在组件的computed中进行获取this.$store.state.theDataName
。简单操作没问题,但是,我们进行其他的操作,比如过滤操作
,我们就得写一堆的代码 :
computed: {
filterData: function () {
this.$store.state.theDataName.filter(function(item){
// do something ...
})
}
}
然后在每个组件中复制这一大堆的代码,或者你单独新建一个文件把代码写进入,每个组件都引入(如果你不觉得很麻烦的话)。
而Getter
可以把组件中共享状态抽取出来,这也是Getter
存在的意义。我们可以认为,Getter
是Store
的计算属性。
如何使用Getter
为了方便管理,需要一个单独的getters.js
的文件,假如已经有对数据进行过滤的函数了:
export default {
filterDatas (state,getter,rootState) {
// do something ...
}
}
那么只要在相关的组件的computed
中引入就可以了,是不是很方便啊 :
computed: {
filterItems: function () {
return this.$store.getters.filterDatas;
}
}
mapGetters辅助函数
mapGetters
辅助函数仅仅是将store
中的getter
映射到局部计算属性,看情况使用,类似mapState
。下面使用mapGetter
改写上面的filterItems
:
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
处理函数:
// 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
里面 :
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
,示范如下 :
// 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
:
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如下 :
import {mapActions} from 'vuex' // 不能漏哦
export default {
methods: {
...mapActions([
'INCREASE'
])
或
...mapActions([
increase: 'INCREASE'
])
}
}
Module
由于vue中使用单一的状态树,当管理的项目中大型的时候,所有的状态都集中在一个对象中会变得比较复杂,难以管理,显得项目比较臃肿。为了解决这些问题,我们可以使用vuex提供的Module功能
,将store分割成模块。每个模块都有自己的state、mutation、action、getter。现在假设你的应用的功能包括登录和音乐
两个功能模块页面,那么store的结构可以这样写:
- 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中写上 :
export default {
music: {
list: [],
total: 100
}
}
我在同级的mutations.js
中有 :
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模块 :
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结构如下:
.
├── ...
│
└── 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结构
来管理啦 😝
完整小项目
自己在上面讲了一大推的<del>废话</del>,嗯哈,为了证明那不是<del>废话</del>,下面就结合上面讲的知识点来一个综合的min-demo
吧,欢迎指正啊! @~@
是什么项目呢
思来想去,自己还是觉得做一个简单版本的todo
项目好点,理由如下:
个人时间精力邮箱(main reason)
todo项目 -> 麻雀虽小,五脏俱全
项目包含一个简单的登录页面,然后跳转到todo小项目
的页面。如图所示:
在登录页面,会要求你填写非空的内容进入,我这里填了自己的名字啦。在todo页面
,你就需要在输入框输入你要做的事情啦,事情的添加默认是未做的状态。当然,允许进行时间的状态进行设置和事件的删除啦。成品可查看下面最终的效果gif动效
,就酱 @~@
项目的初始化
⚠️ 本项目在mac系统上使用vue-cli
的基础上搭建(搭建日期2018.01.14)
的小项目,其完整的覆盖了vue的全家桶了 -> 使用的vue版本是^2.5.2
,vuex的版本是^3.0.1
,vue-router的版本也是^3.0.1
。如果你使用低版本,请参考低版本的相关说明。
# 全局安装 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
搭建的项目,以符合我自己期望。改写的代码就不全给出来了啊,关键的项目代码还是会贴一下的。😝
这个项目的结构如下:
.
├── 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
中
<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
组件的结构依旧是这样:
<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>
在路由的文件中,因为知识涉及了两个页面的路由跳转,这里也全贴出来吧 --
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
中,我是这样引用的 :
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
}
})
在根的store
的mutation-types.js
文件中,管理着整个项目的状态管理函数 --> 包括创建用户名、添加要做的事情、删除创建的事情、显示事件的状态(全部,已经做,没有做)和标记事件(已经做的事件标记为未做,未做的事件标记为已经做)。代码展示如下 :
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中,自己定义了此模块的相关的数据结构,如下:
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:[]
:
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
中的状态获取,方便显示在页面的啦,在本项目中,自己超级简单的使用了下:
export default {
list (state,getters,rootState) {
return state.list;
},
username (state,getters,rootState) {
return rootState.username;
},
...
}
最后一个是关于todo/actions.js
,这是页面中的用户的事件去发送事件,使得产生mutations
去改变状态(state.js),最终使得页面展示的内容(getters)发生改变。这里以一个派遣添加事件为例子 :
import * as types from '../../mutation-types'
export const addEvent = ({commit,state,rootState},query) => {
commit(types.ADD_EVENT,query);
}
嗯,整篇文章都说整个store是挂载在根组件上的,那么是在哪里呢?答案就是src/main.js
文件啦,文件内的代码如下 :
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动图
啦 :
嗯,项目是不是很简单,所以就不放源码上去了 😂 。其实自己觉得源码实现不够严谨啦,毕竟只是花了短短一个下午和晚上从设计到实现... 逃:)
参考内容
( 完 @~@ )