1 Vuex 简介
官方定义:Vuex 是一个专为 Vue.js 应用程序开发的状态(state)管理模式。它采用集中式存储管理应用的所有组件的状态(state),并以相应的规则保证状态以一种可预测的方式发生变化。
(1)为什么需要Vuex
传统的vue,每个组件都会维护自己的state,因此在组件树中,整个程序的state被分散在个组件中,这些state可能会冗余重复,可能会相互依赖,以致state的变更会引发复杂的组件的连锁更新甚至是骨牌效应。Vuex把“State”从每个组件中抽离出来,统一维护,这样做可以清晰的实现组件之间数据流的统一管理。
(2)Vuex如何运作
Vuex取消了每一个组件内部的State,取而代之的是全局定义一个 state,统一保存整个应用程序状态;
State必须通过Mutations变更,组件需要变更State值时,不能直接修改,只能向Mutations发送Commit请求,因此State的改变可以通过Mutations追踪,轻松调试;
实际中,State变更前往往要先执行异步操作,从后端获取最新信息,而Mutations只能做同步操作,所以还需要一个Actions中间件用于执行异步操作;
于是,实际的执行流是 :首先,Components(组件)向 Actions 发送 Dispatch 以实现异步后端交互;然后,Actions 异步回调后向Mutations 发送 Commit 请求修改State;最后Mutations 修改 State 以实现界面(Components)的更新。具体过程如下图所示,其中的“Backend API”是后端服务,“Devtools”是调试工具,用于监控 Mutations 的变化。
(3)vuex 的官方文档
vuex准备了不错的官方文档,需要详细学习的可以去参考:vuex.vuejs.org/zh/
2 使用vuex的计数器
2.1 一个简单的vuex计数器Components+Mutations+State
(1)创建vue项目,安装vuex(在vue项目目录下执行npm安装)
npm install vuex ‐‐save
(2)在main.js中添加 vuex 的并创建 state 和 mutations
import Vue from 'vue'
import App from './App.vue'
import Vuex from 'vuex' //导入Vuex
Vue.use(Vuex) //全局使用Vuex
Vue.config.productionTip = false
const store=new Vuex.Store({
state:{ //创建 store
count:0,
},
mutations:{ //创建 mutations
increment(state){
state.count++;
},
descrement(state){
state.count‐‐;
}
}
});
new Vue({
store, //保存到根对象的 $store
render: h => h(App),
}).$mount('#app')
(3)界面组件(Counter.vue)
值得注意的是:
- 由于在前面的Vue根对象中传入了store,所以在所有子组件中,都可以通过“this.$store”获取到vuex的store。
- store中的mutations方法是不能直接调用的!需要通过 store.commit('mutations类型') 的方式,类似引发事件的方式来实现调用。
<template>
<div>
<h1>count: {{$store.state.count}}</h1>
<button @click="$store.commit('increment')">count++</button>
<button @click="$store.commit('descrement')">count‐‐</button>
</div>
</template>
<script>
export default {
name: 'counter'
}
</script>
2.2 改进计数器:mutation 传参
(1)store.commit(MUTATION_TYPE, payload) 方法除了可以传入mutation类型(MUTATION_TYPE)之外,还可以传入额外一个对象型参数 payload。
(2)组件通常使用计算属性(computed)来读取state中的数据以免过于冗长于是Counter.vue修改为:
<template>
<div>
<h2>点击次数:{{count}}</h2>
<button @click="increment">+</button>
<button @click="descrement">‐</button>
</div>
</template>
<script>
export default {
name:"counter",
computed:{ //使用计算属性
count(){
return this.$store.state.count;
},
},
methods:{
increment(){
this.$store.commit('increment', {number:2}); //使用payload传递额外的数据
},
descrement(){
this.$store.commit('descrement',{number:3});
}
}
}
</script>
这时,main.js 中的mutations应修改为:

2.3 改进计数器:使用actions处理异步请求
在vuex中,mutations用于修改state,是不能执行异步操作的,因为mutations是程序用于跟踪调试变化的关键步骤,如果执行了异步操作,state的变化前后势必被分割在不同的时机和代码中执行,这样就很难在复杂的异步调用中弄清楚state的变化究竟是谁造成的了。 例如: 上述示例中的两个mutation类型increment和descrement,代码如果是同步修改state的话,使用devtools可以跟踪state的变化。
mutations:{
increment(state, payload){ state.count+=payload.number; },
descrement(state, payload){ state.count‐=payload.number; }
}
如果变成了异步修改state的话,虽然应用的结果是正确的,但devtools就无法跟踪state的变化了。
mutations:{
increment(state, payload){
setTimeout(()=>state.count+=payload.number, 1000); //1秒后执行的异步操作
},
descrement(state, payload){
setTimeout(()=>state.count‐=payload.number, 1000); //1秒后执行的异步操作
}
但是在实际应用中,state的修改往往是需要以后调用后端的API,根据异步返回结果再执行的,所以异步修改state的场景非常普遍!为此,vuex引入了actions组件,把所有异步操作都交给actions实现,actions执行后再通过mutations同步去修改state!下面代码演示了如何使用actions执行异步操作,然后在调用mutations修改state。
(1) main.js
import Vue from 'vue'
import App from './App.vue'
import Vuex from 'vuex';
Vue.use(Vuex);
Vue.config.productionTip = false
const store=new Vuex.Store({
state:{
count:0,
},
mutations:{ //使用mutations同步修改state
increment(state, payload){ state.count+=payload.number; },
descrement(state, payload){ state.count‐=payload.number; }
},
actions:{ //使用actions执行异步操作
increment(context, param){
setTimeout(()=>context.commit('increment', param), 1000); //异步返回后调用mutation
},
descrement(context, param){
setTimeout(()=>context.commit('descrement', param), 1000);
}
}})
new Vue({
store,
render: h => h(App),
}).$mount('#app')
(2)在界面组件中调用actions 和mutations一样,actions方法也不应该直接调用,需要通过 store.dispatch('action类型') 的方式,类似于事件引发的方式来调用
<template>
<div>
<h2>点击次数:{{count}}</h2>
<button @click="increment">+</button>
<button @click="descrement">‐</button>
</div>
</template>
<script>
export default {
name:"counter",
computed:{
count(){ return this.$store.state.count; },
},
methods:{
increment(){ this.$store.dispatch('increment', {number:2}); }, //调用action
descrement(){ this.$store.dispatch('descrement',{number:3}); } //调用action
}
}
</script>
3 vuex版本的基于MySQL存储的Todos
下面使用 前端(Vuex)和 后端(MySQL+JEE)重写之前的 Todos 项目。 实际上要创建包含Vuex的项目,只需要在 vuecli 的项目创建向导中选择vuex组件即可:
3.1 创建后端 todosbackend
1)提供如下的 RESTful API 接口:
GET: /api/todos #查询所有todos
POST: /api/todos #添加一个新的todo
DELETE: /api/todos/{id} #删除id对应的todo
PUT: /api/todos/{id} #改变id对应的todo的completed状态
(2)为vue项目添加axios支持
vue add axios
(3)配置后端代理(添加:vue.config.js)
module.exports = {
devServer: {
port: 3000,
proxy: {
'/api': {
target: 'http://localhost:9090' // target host
}
}
}
}
3.2 实现vue部分(vue+vuex+axios)
(1)修改 store下的index.js,实现 state、mutations、actions 由于mutations 是 vuex 的核心,实践者建议把各种 mustaions类型定义为常量,以便标识和调用。
import Vue from 'vue'
import Vuex from 'vuex'
import axios from 'axios' // 导入异步axios模块
Vue.use(Vuex)
const FIND_TODOS = 'findTodos' // 定义mutations类型
export default new Vuex.Store({
state: { // 定义全局state
todos: []
},
getters: { // 可以在store中添加getters来定义可重用的计算属性
todos (state) {
return state.todos
},
todoRows (state) {
return state.todos.length
}
},
mutations: { // 定义修改全局state的mutations
[FIND_TODOS] (state, todos) { // 使用mutation类型常量作为mutation函数名,ES2015特性
state.todos = todos
}
},
actions: { // 定义处理异步请求的action
findTodos (context) {
axios.get('/api/todos').then(resp => context.commit(FIND_TODOS, resp.data))
},
deleteTodo (context, id) {
axios.delete('/api/todos/' + id).then(resp => context.dispatch('findTodos'))
},
changeTodoState (context, id) {
axios.put('/api/todos/' + id).then(resp => context.dispatch('findTodos'))
},
addTodo (context, todo) { // 若action执行后需要回调界面组件,可以考虑返回Promise对象包装异步操作
return new Promise((resolve) => {
axios.post('/api/todos', todo).then(resp => {
context.dispatch('findTodos')
resolve(true)
})
})
}
}
})
(2)App.vue
<template>
<div id="app">
<h2>Todos App</h2>
<todo‐input></todo‐input>
<hr>
<todo‐list></todo‐list>
</div>
</template>
<script>
import TodoList from './components/TodoList'
import TodoInput from './components/TodoInput'
export default {
name: 'app',
components: {
TodoList, TodoInput
}
}
</script>
(3)TodoList.vue
<template>
<table>
<thead>
<tr><th>完成</th><th>待办事项</th><th>删除</th></tr>
</thead>
<tbody>
<tr v‐for="item in $store.getters.todos" :key="item.id">
<td><input type="checkbox" :checked="item.completed" @click="changeState(item.id)" /></td>
<td :class="{'finished':item.completed}">{{item.title}}</td>
<td><button @click="remove(item.id)">删除</button></td>
</tr>
<tr><td colspan="3" style="text‐align:center">待办事项共:{{$store.getters.todoRows}}项</td></tr>
</tbody>
</table>
</template>
<script>
export default {
name: 'todo‐list',
mounted () {
this.$store.dispatch('findTodos')
},
methods: {
remove (id) {
this.$store.dispatch('deleteTodo', id)
},
changeState (id) {
this.$store.dispatch('changeTodoState', id)
}
}
}
</script>
(4)TodoInput.vue
<template>
<div>
新事项:<input type="text" v‐model="title" />
<button @click="add">添加</button>
</div>
</template>
<script>
export default {
name: 'todo‐input',
data () { //组件内部依然可以包含无需被外部重用的自己的data
return { title: '' }
},
methods: {
add () {
this.$store.dispatch('addTodo', { 'title': this.title, completed: false })
.then((ok) => { //如果action中返回Promise对象,则可以使用then继续执行action后的回调
if (ok) {
this.title = ''
}
})
}
}
}
</script>
4 Vuex的模块化开发
实际的项目中,可能有非常多的功能要实现,如果用一个 store.js 来实现所有的vuex功能,将会很难维护,为此,vuex引入了模块的概念,允许我们把每一个独立的功能编写成一个模块,然后在汇总到一起来实现分工合作。
4.1 模块化开发的实践建议
(1)编写单独的 mutation-types.js;
export const TODOS = {
FIND_TODOS : 'findTodos'
}
(2)拆分模块,实现分工合作( store/index.js )
import Vuex from 'vuex'
import axios from 'axios'
import todos from './modules/todos.js'
Vue.use(Vuex)
const store = new Vuex.Store({
modules:{
todos,
}
});
export default store;
(3)分模块编写 state、mutations和actions,启用命名空间
import {模块常量} from '../mutation_types';
const state={
todos:[],
}
const mutations={
[模块常量.MUTATION](state, param){
},
}
const actions={
action方法(context, param){
},
}
export default{
namespaced: true, //启用命名空间
state,
mutations,
actions
}
4.2 使用模块化修改 todos 示例
1)创建store的文件夹结构

2)分模块定义 mutation types 名称 ~/store/mutationtypes.js
export const TODOS = {
FIND_TODOS: 'findTodos' // 定义mutations类型
}
3) 设置store的入口 ~/store/index.js
import Vue from 'vue'
import Vuex from 'vuex'
import todos from './modules/todos'
Vue.use(Vuex)
export default new Vuex.Store({
modules: {
todos
}
})
4)定义模块
~/store/modules/todos.js
import { TODOS } from '../mutation‐types'
import axios from 'axios'
const todos = {
// 命名空间(namespaced)设为true,则当前的actions和getters在外部访问时需要加上模块名称(如 “findTodos” ‐> “todos/findTodos”)
namespaced: true,
state: {
todos: []
},
getters: { //
items (state) { // 外部访问时为 store.getters['todos/items']
return state.todos
},
todoRows (state) { // 外部访问时为 store.getters['todos/todoRows']
return state.todos.length
}
},
mutations: {
[TODOS.FIND_TODOS] (state, param) { // 注意:FIND_TODOS ‐> TODOS.FIND_TODOS
state.todos = param
}
},
actions: { // 定义处理异步请求的action
findTodos (context) {
axios.get('/api/todos').then(resp => context.commit(TODOS.FIND_TODOS, resp.data))
},
deleteTodo (context, id) {
axios.delete('/api/todos/' + id).then(resp => context.dispatch('findTodos')) // 内部访 问action不需要加模块名
},
changeTodoState (context, id) {
axios.put('/api/todos/' + id).then(resp => context.dispatch('findTodos'))
},
addTodo (context, todo) {
return new Promise((resolve) => {
axios.post('/api/todos', todo).then(resp => {
context.dispatch('findTodos')
resolve(true)
})
})
}
}
}
export default todos
(4)在界面组件中调用store的todos模块 需要注意的是: 1)模块下的getters需要以 this.store.dispatch('模块/action名称', 参数) 方式调用
<template>
<table>
<thead>
<tr><th>完成</th><th>待办事项</th><th>删除</th></tr>
</thead>
<tbody>
<tr v‐for="item in $store.getters['todos/items']" :key="item.id">
<td><input type="checkbox" :checked="item.completed" @click="changeState(item.id)" /></td>
<td :class="{'finished':item.completed}">{{item.title}}</td>
<td><button @click="remove(item.id)">删除</button></td>
</tr>
<tr><td colspan="3" style="text‐align:center">待办事项共:{{$store.getters.todoRows}}项</td></tr>
</tbody>
</table>
</template>
<script>
export default {
name: 'todo‐list',
mounted () {
this.$store.dispatch('todos/findTodos')
},
methods: {
remove (id) {
this.$store.dispatch('todos/deleteTodo', id)
},
changeState (id) {
this.$store.dispatch('todos/changeTodoState', id)
}
}
} </script>
5 Vuex 核心概念小结
| 部件 | 描述 | 用法 | 组件中的快捷包装器 |
|---|---|---|---|
| state | 提供一个全局的响应式数据,供所有组件读取 | this.$store.state.xxx 取值 | mapState |
| getters | 借助Vue的计算属性来实现计算数据共享,减少各组 件中计算属性的重复编写 | this.$store.getters.xxx 取值 | mapGetters |
| mutations | 用于同步修改state的方法 | 通过 this.$store.commit('xxx', param) 调用 | mapMutations |
| action | 用于执行异步操作,并最终触发mutation方法 | 通过 this.$store.dispatch('xxx', param) 调 用 | mapActions |
| modules | 分模块实现state、getters、actions与mutations,实 现程序中的模块隔离,在复杂项目中便于分工合作 |
简化后的示例TodoList.vue:
<template>
<div>
<hr/>
<table>
<tr>
<td>状态</td>
<td>待办事项</td>
<td>删除</td>
</tr>
<tr v-for="item in items" :key="item.id">
<td><input type="checkbox" :checked="item.completed" @change="change(item.id)"/></td>
<td>{{item.title}}</td>
<td><button @click="del(item.id)">删除</button></td>
</tr>
</table>
<hr/>
<h2>总记录数:{{todoRows}}</h2>
</div>
</template>
<script>
import { mapState, mapGetters ,mapActions} from 'vuex'
export default {
name: 'TodoList',
computed:{
//映射getter(getter和state映射为计算属性)
...mapGetters('todos',['todoRows','items'])
},
mounted(){
this.findTodos();
},
methods:{
//映射Action(mutations和action映射为方法)
...mapActions('todos',['deleteTodo','changeTodo','findTodos']),
del(id){
this.deleteTodo(id);
},
change(id){
this.changeTodo(id);
}
}
}
</script>