1. 程式人生 > >Vue元件通訊深入Vuex

Vue元件通訊深入Vuex

建議:部落格中的例子都放在vue_blog_project工程中,推薦結合工程例項與部落格一同學習

上一篇部落格(Vue元件通訊深入)中,介紹了多種方法來實現元件之間的通訊,但是涉及到深層巢狀和非直接關聯元件之間的通訊時,都會遇到無法追蹤資料和除錯的問題,而vuex就是為解決此類問題而生的。

這篇部落格將簡要的介紹vuex的基本用法和最佳實踐,然後完成下面的demo

clipboard.png

1. Vuex 簡介

宣告:在此僅介紹Vuex精華知識,更詳盡的知識請參考Vuex中文官網

1.1 初識Vuex

Vuex 是一個專為 Vue.js 應用程式開發的狀態管理模式。它採用集中式儲存管理應用的所有元件的狀態,並以相應的規則保證狀態以一種可預測的方式發生變化

Vuex 解決了多個檢視依賴於同一狀態來自不同檢視的行為需要變更同一狀態的問題,將開發者的精力聚焦於資料的更新而不是資料在元件之間的傳遞上

1.2 Vuex各個模組

(1)state:用於資料的儲存,是store中的唯一資料來源

// 定義
new Vuex.Store({
    state: {
        allProducts: []
    }
    //...
})
// 元件中獲取
this.$store.state.allProducts

(2)getters:如vue中的計算屬性一樣,基於state資料的二次包裝,常用於資料的篩選和多個數據的相關性計算

// 定義
getters: {
    cartProducts(state, getters, rootState) 
        => (getters.allProducts.filter(p => p.quantity))
}
// 元件中獲取
this.$store.getters.cartProducts

(3)mutations:類似函式,改變state資料的唯一途徑,且不能用於處理非同步事件(重點!!!)

// 定義
mutations: {
    setProducts (state, products) {
        state.allProducts = products
    }
}

// 元件中使用
this.$store.commit('setProducts', {//..options})

(4)actions:類似於mutation,用於提交mutation來改變狀態,而不直接變更狀態,可以包含任意非同步操作

// 定義(shop為api)
actions: {
    getAllProducts ({ commit }, payload) {
        shop.getProducts((res) => {
            commit('setProducts', res)
        })
    }
}

// 元件中使用
this.$store.dispatch('getAllProducts', {//..payload})

(5)modules:類似於名稱空間,用於專案中將各個模組的狀態分開定義和操作,便於維護

// 定義
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在專案中較少遇到,其他三個倒是經常使用,另外,有兩個注意項和兩個最佳實踐:

注意

最佳實踐(後面的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原始碼