建议:博客中的例子都放在vue_blog_project工程中,推荐结合工程实例与博客一同窗习javascript
上一篇博客(Vue组件通讯深刻)中,介绍了多种方法来实现组件之间的通讯,可是涉及到深层嵌套和非直接关联组件之间的通讯时,都会遇到没法追踪数据和调试的问题,而vuex就是为解决此类问题而生的。html
这篇博客将简要的介绍vuex的基本用法和最佳实践,而后完成下面的demovue
声明:在此仅介绍Vuex精华知识,更详尽的知识请参考Vuex中文官网java
Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的全部组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化
Vuex 解决了多个视图依赖于同一状态
和来自不一样视图的行为须要变动同一状态
的问题,将开发者的精力聚焦于数据的更新而不是数据在组件之间的传递上git
(1)state
:用于数据的存储,是store中的惟一数据源github
// 定义 new Vuex.Store({ state: { allProducts: [] } //... }) // 组件中获取 this.$store.state.allProducts
(2)getters
:如vue中的计算属性同样,基于state数据的二次包装,经常使用于数据的筛选和多个数据的相关性计算vue-router
// 定义 getters: { cartProducts(state, getters, rootState) => (getters.allProducts.filter(p => p.quantity)) } // 组件中获取 this.$store.getters.cartProducts
(3)mutations
:相似函数,改变state数据的惟一途径,且不能用于处理异步事件(重点!!!)vuex
// 定义 mutations: { setProducts (state, products) { state.allProducts = products } } // 组件中使用 this.$store.commit('setProducts', {//..options})
(4)actions
:相似于mutation,用于提交mutation来改变状态,而不直接变动状态,能够包含任意异步操做shell
// 定义(shop为api) actions: { getAllProducts ({ commit }, payload) { shop.getProducts((res) => { commit('setProducts', res) }) } } // 组件中使用 this.$store.dispatch('getAllProducts', {//..payload})
(5)modules
:相似于命名空间,用于项目中将各个模块的状态分开定义和操做,便于维护npm
// 定义 const moduleA = { state: { ... }, mutations: { ... }, actions: { ... }, getters: { ... } } const moduleB = { state: { ... }, mutations: { ... }, actions: { ... } } const store = new Vuex.Store({ modules: { a: moduleA, b: moduleB } }) // 组件中使用 store.state.a // -> moduleA 的状态 store.state.b // -> moduleB 的状态
注意:默认状况下,模块内部的 action、mutation 和 getter 是注册在全局命名空间的——这样使得多个模块可以对同一 mutation 或 action 做出响应,仅有state是局部做用。所以,经常使用getters将state包装后输出,这样能够直接经过this.$store.getters.
的方式拿到数据,而不用去访问某个模块下的state
在组件中使用store中的数据或方法时,按照上面的说法,每次都要this.$store.
的方式去获取,有没有简单一点的方式呢?辅助函数就是为了解决这个问题
// 组件中注册 import { mapState, mapGetters, mapMutations, mapActions } from 'vuex' export default { computed: { // 数组形式,当映射的计算属性的名称与 state 的子节点名称相同时使用 ...mapState(['allProducts']) // 对象形式,可重命名 state 子节点名称 ...mapState({ products: state => state.allProducts }) // 下面为了简便,均以数组形式使用 ...mapGetters(['cartProducts']) }, methods: { ...mapMutations(['setProducts']), ...mapActions(['getAllProducts']) } } // 组件中使用 // 变量 this.allProducts this.products // 方法 this.setProducts() this.getAllProducts()
因为上面提到,经常使用的作法是将state中数据使用getter包装后输出,所以,mapState在项目中较少遇到,其余三个却是常常使用,另外,有两个注意项和两个最佳实践:
注意:
最佳实践(后面的demo中会引导使用):
store ├── index.js # 导出 store 的地方 ├── state.js # 根级别的 state ├── getters.js # 二次包装state数据 ├── actions.js # 根级别的 action ├── mutations.js # 根级别的 mutation ├── mutation-types.js # 全部 mutation 的常量映射表 └── modules # 若是有. ├── ...
(1)在项目中安装Vuex
:
npm install vuex --save
(2)在src目录下新建store/index.js
,其中代码以下:
import Vue from 'vue' import Vuex from 'vuex' // 修改state时在console打印,便于调试 import createLogger from 'vuex/dist/logger' Vue.use(Vuex) const debug = process.env.NODE_ENV !== 'production' const state = {} const getters = {} const mutataions = {} const actions = {} export default new Vuex.Store({ state, getters, mutataions, actions, // 严格模式,非法修改state时报错 strict: debug, plugins: debug ? [createLogger()] : [] })
(3)在入口文件main.js
中添加:
// ... import router from './router' import store from './store' new Vue({ el: '#app', router, store, // ... })
能够对比vue-router和vuex的安装方式:它们均为vue插件,并在实例化组件时引入,在该实例下的全部组件都可由this.$router
和this.$store
的方式查询到对应的插件实例
需求:完成在文章开头看到的动图功能【注:demo源码】,api数据和功能以下:
// 商品列表 [ { 'id': 1, 'title': 'iPad 4 Mini', 'price': 500, 'inventory': 2 }, { 'id': 2, 'title': 'H&M T-Shirt White', 'price': 10, 'inventory': 10 }, { 'id': 3, 'title': 'Charli XCX - Sucker CD', 'price': 20, 'inventory': 5 } ]
功能1: 商品增减时,库存变化,购物车列表和金额变化
功能2: 清空购物车时,全部数据还原
分析:
组件结构:一个父组件包裹两个子组件商品列表和购物车;数据方面:商品列表数据来自于api接口+加入购物车数目标志,加入购物车商品列表来自商品列表的筛选;
基于上面的分析,可以下组织代码
(1)store中代码
const state = { all: [] } const getters = { // 总商品列表 allProducts: state => state.all, // 购物车商品列表 cartProducts: (state, getters) => (getters.allProducts.filter(p => p.quantity)), // 购物车商品总价 cartTotalPrice: (state, getters) => { return getters.cartProducts.reduce((total, product) => { return total + product.price * product.quantity }, 0) } } const mutations = { setProducts (state, products) { state.all = products }, clearCartProducts (state) { state.all.forEach(p => { p.quantity = 0 }) } } const actions = { // 获取数据后,加入选取数量quantity的标识,以区分是否被加入购物车 getAllProducts ({ commit }) { shop.getProducts((res) => { const newRes = res.map(p => Object.assign({}, p, {quantity: 0})) commit('setProducts', newRes) }) } }
(2)商品列表组件ProductList.vue
<template> <ul class="product-wrapper"> <li class="row header"> <div v-for="(th,i) in tHeader" :key="i">{{ th }}</div> </li> <li class="row" v-for="product in currentProducts" :key="product.id"> <div>{{ product.title }}</div> <div>{{ product.price }}</div> <div>{{ product.inventory - product.quantity }}</div> <div> <el-input-number :min="0" :max="product.inventory" v-model="product.quantity" @change="handleChange"> </el-input-number> </div> </li> </ul> </template> <script> import { mapGetters, mapMutations, mapActions } from 'vuex' export default { data () { return { tHeader: ['名称', '价格', '剩余库存', '操做'], currentProducts: [] } }, computed: { ...mapGetters(['allProducts']) }, // 为了不表单直接修改store中的数据,须要使用watch模拟双向绑定 watch: { allProducts: { handler (val) { this.currentProducts = JSON.parse(JSON.stringify(this.allProducts)) }, deep: true } }, created () { this.getAllProducts() }, methods: { handleChange () { this.setProducts(this.currentProducts) }, ...mapMutations(['setProducts']), ...mapActions(['getAllProducts']) } } </script>
(3)购物车列表组件ShoppingCart.vue
<template> <div class="cart"> <p v-show="!products.length"><i>Please add some products to cart.</i></p> <ul> <li v-for="product in products" :key="product.id"> {{ product.title }} - {{ product.price }} x {{ product.quantity }} </li> </ul> <p>Total: {{ total }}</p> <el-button @click="clearCartProducts">CLEAR</el-button> </div> </template> <script> import { mapGetters, mapMutations } from 'vuex' export default { computed: { ...mapGetters({ products: 'cartProducts', total: 'cartTotalPrice' }) }, methods: { ...mapMutations(['clearCartProducts']) } } </script>
(4)结合上面所说的最佳实践优化:
首先,按照上面的tree结构将store文件夹拆分;接下来:
在store中新建mutation-types.js
文件,
export const SET_PRODUCTS = 'SET_PRODUCTS' export const CLEAR_CART_PRODUCTS = 'CLEAR_CART_PRODUCTS'
mutations.js
做以下更改:
import * as types from './mutation-types' export default { [types.SET_PRODUCTS] (state, products) { state.all = products }, [types.CLEAR_CART_PRODUCTS] (state) { state.all.forEach(p => { p.quantity = 0 }) } }
actions.js
做以下更改:
import shop from '@/api/shop' import * as types from './mutation-types' export default { // 获取数据后,加入选取数量quantity的标识,以区分是否被加入购物车 getAllProducts ({ commit }) { shop.getProducts((res) => { const newRes = res.map(p => Object.assign({}, p, {quantity: 0})) commit(types.SET_PRODUCTS, newRes) }) }, // 这里将mutation中的方法以action的形式输出,主要是组件中有使用mutation的方法,到时仅需引用mapActions便可,可按实际状况使用 setProducts ({ commit }, products) { commit(types.SET_PRODUCTS, products) }, clearCartProducts ({ commit }) { commit(types.CLEAR_CART_PRODUCTS) } }
另外,在组件引用mutation部分也须要做相应修改
在此仅将demo中的核心部分列出,完整的代码请查看demo源码