Vue元件通訊深入Vuex
建議:部落格中的例子都放在vue_blog_project工程中,推薦結合工程例項與部落格一同學習
上一篇部落格(Vue元件通訊深入)中,介紹了多種方法來實現元件之間的通訊,但是涉及到深層巢狀和非直接關聯元件之間的通訊時,都會遇到無法追蹤資料和除錯的問題,而vuex就是為解決此類問題而生的。
這篇部落格將簡要的介紹vuex的基本用法和最佳實踐,然後完成下面的demo
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中會引導使用):
- 使用常量替代 Mutation 事件型別,這樣可以使 linter 之類的工具發揮作用,同時把這些常量放在單獨的檔案中可以讓你的程式碼合作者對整個 app 包含的 mutation 一目瞭然
- 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.$router
和this.$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原始碼