建議:博客中的例子都放在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源碼