Vuex 简介

136 阅读3分钟

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)

值得注意的是:

  1. 由于在前面的Vue根对象中传入了store,所以在所有子组件中,都可以通过“this.$store”获取到vuex的store。
  2. 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应修改为:

![image-20200524145640578](Vuex 简介.assets/image-20200524145640578.png)

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的项目,只需要在 vue­cli 的项目创建向导中选择vuex组件即可:

3.1 创建后端 todos­backend

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 vfor="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的文件夹结构

![image-20200524151537973](Vuex 简介.assets/image-20200524151537973.png)

2)分模块定义 mutation types 名称 ~/store/mutation­types.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.getters[模块/getter]方式调用2)模块下的action需要以this.store.getters['模块/getter名称'] 方式调用 2)模块下的action 需要以 this.store.dispatch('模块/action名称', 参数) 方式调用

<template>
<table>
<thead>
<tr><th>完成</th><th>待办事项</th><th>删除</th></tr>
</thead>
<tbody>
<tr vfor="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>