Vue组件通讯深刻Vuex

建议:博客中的例子都放在vue_blog_project工程中,推荐结合工程实例与博客一同窗习javascript

上一篇博客(Vue组件通讯深刻)中,介绍了多种方法来实现组件之间的通讯,可是涉及到深层嵌套和非直接关联组件之间的通讯时,都会遇到没法追踪数据和调试的问题,而vuex就是为解决此类问题而生的。html

这篇博客将简要的介绍vuex的基本用法和最佳实践,而后完成下面的demovue

clipboard.png

1. Vuex 简介

声明:在此仅介绍Vuex精华知识,更详尽的知识请参考Vuex中文官网java

1.1 初识Vuex

Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的全部组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化

Vuex 解决了多个视图依赖于同一状态来自不一样视图的行为须要变动同一状态的问题,将开发者的精力聚焦于数据的更新而不是数据在组件之间的传递上git

1.2 Vuex各个模块

(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

1.3 辅助函数

在组件中使用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在项目中较少遇到,其余三个却是常常使用,另外,有两个注意项和两个最佳实践:

注意

  1. Mutation 需遵照 Vue 的响应规则见Vuex官网Mutation部分
  2. 表单处理时引起的直接修改state中数据问题,见Vuex官网表单处理部分

最佳实践(后面的demo中会引导使用):

  1. 使用常量替代 Mutation 事件类型,这样可使 linter 之类的工具发挥做用,同时把这些常量放在单独的文件中可让你的代码合做者对整个 app 包含的 mutation 一目了然
  2. store 结构使用以下方式
store
    ├── index.js             # 导出 store 的地方
    ├── state.js             # 根级别的 state
    ├── getters.js           # 二次包装state数据
    ├── actions.js           # 根级别的 action
    ├── mutations.js         # 根级别的 mutation
    ├── mutation-types.js    # 全部 mutation 的常量映射表
    └── modules              # 若是有.
        ├── ...

2. Vuex 安装

(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.$routerthis.$store的方式查询到对应的插件实例

3. Vuex 项目实践

需求:完成在文章开头看到的动图功能【注: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源码

上一篇:Vue组件通讯深刻

相关文章
相关标签/搜索