最近使用Vue全家桶手擼了一個pc版小米商城的前端項目,對於組件通訊和狀態管理有了一個更加深入的認識。由於組件劃分的比較細,開始我使用的是基本的props和emit傳值,後來發現一旦嵌套過深就變得很繁瑣,同時考慮到有多個組件存在須要共同管理的狀態,基本的傳值已經沒有辦法知足需求了,因此使用到了vuex來劃分模塊管理狀態。這裏須要提一點就是,若是不存在多組件共同管理的狀態,最好是不用vuex管理,vuex是用來管理多組件共同狀態的,單單隻須要實現跨組件、隔代組件通訊的話,使用eventbus,provide/inject等就能夠實現。前端
首先咱們來弄清楚Vuex中管理數據的一套基本流程:vue
修改state中數據的流程:ios
在組件內派發一個action即dispatch(或者直接調用)一個action => action再commit一個mutation => mutation修改statevuex
state中的數據都在action中請求,再經過commit一個mutation設置state中的數據axios
getter中存放着state的計算值,至關於組件中的計算屬性(computed);同時getter中的值都是響應的,就是隻要依賴的state一發生改變,getter中的值立刻就能檢測到,而後對應就會更新狀態了api
注意點:action中的請求是異步的,mutation是同步的數組
官方效果:bash
咱們能夠從上圖中看到購物車的功能,這裏我簡單總結一下,分爲如下十點:app
功能已經分析完畢,接下來思考一下該怎麼管理狀態,以及劃分模塊異步
由於是購物車,因此這裏我將這個購物車裏的狀態在Vuex中劃分爲了兩個模塊;products模塊和cart模塊,products模塊用來存放全部的商品數據列表信息,cart模塊放置了購物車內商品的列表信息;這裏須要提的一點是,由於cart模塊中的每條商品信息是不須要提供相似prodcuts中一條商品的全部字段的,只須要提供幾個關鍵的字段,而後到prodcuts模塊中去查詢該條商品的信息便可。可能描述不清,但在下面我會用代碼展現,你們就會清楚了。
個人store目錄以下:
我簡單介紹一下:
// cart模塊
export const CART_ADD_PRODUCT_TO_CART = 'CART_ADD_PRODUCT_TO_CART' // 添加購物車
export const CART_DEL_PRODUCT_TO_CART = 'CART_DEL_PRODUCT_TO_CART' // 刪除購物車
export const CART_CHANGE_LOGIN_STATUS = 'CART_CHANGE_LOGIN_STATUS' // 切換登錄狀態
export const CART_ADD_PRODUCT_QUANTITY = 'CART_ADD_PRODUCT_QUANTITY' // 添加商品數量
export const CART_DEL_PRODUCT_QUANTITY = 'CART_DEL_PRODUCT_QUANTITY' // 減小商品數量
export const CART_SET_CHECKOUT_STATUS_ALL = 'CART_SET_CHECKOUT_STATUS_ALL' // 一鍵改變全部商品購買狀態的方法
// products模塊
export const PRODUCTS_SET_PRODUCT = 'PRODUCTS_SET_PRODUCT' // 獲取全部商品的列表
複製代碼
這段代碼沒有什麼邏輯可言,就是把全部模塊中的mutations中的函數都用一個大寫的常量名,簡而言之就是按一個大寫的規範,把每一個模塊中mutations中的函數命名,就是一套命名規範。
Vue官方文檔的解釋:
使用常量替代 mutation 事件類型在各類 Flux 實現中是很常見的模式。這樣可使 linter 之類的工具發揮做用,同時把這些常量放在單獨的文件中可讓你的代碼合做者對整個 app 包含的 mutation 一目瞭然; 用不用常量取決於你——在須要多人協做的大型項目中,這會頗有幫助。但若是你不喜歡,你徹底能夠不這樣作。
import { fetchGet } from "@/api/index" // api文件夾下封裝的axios.get請求函數
import * as types from '../types' // types目錄下的mutations函數的常量名
const state = {
recommendList: [] // 存放全部商品的信息
}
const getters = {}
const mutations = {
[types.PRODUCTS_SET_PRODUCT](state, products) { // 第一個參數是state 能夠修改state 將請求回來的數據保存在state中
state.recommendList = products
}
}
const actions = {
getAllProducts({ commit }) { // 全部的api請求都放在actions中
fetchGet("/cart").then(res => {
let allProducts = res.data.list.list
commit(types.PRODUCTS_SET_PRODUCT, allProducts)
})
}
}
export default {
namespaced: true, // 添加命名空間
state,
getters,
mutations,
actions
}
複製代碼
這裏我放上recommendList中每一條數據的字段 例如其中一條爲:
/*
{
productid: "11137",
name: "小米CC9 Pro 6GB+128GB",
price: 2799,
image: "//i1.mifile.cn/a1/pms_1572941393.18077211.jpg",
comments: 0
}
*/
複製代碼
上面代碼的邏輯就是在actions中的getAllProducts方法中調用封裝在api目錄下index.js中的fetchGet()函數請求到數據,commit提交給mutations中的types.PRODUCTS_SET_PRODUCT函數(設置state),而後去設置全部的商品信息列表recommendList
這裏須要注意幾點:
products模塊中存放着state、getters(這個模塊暫時未用到)、mutations、actions,這是分模塊每一個模塊都存在的,最後導出這個模塊的四部分;
導出的時候使用了命名空間namespaced: true,命名空間是啥,就是可讓咱們模塊module分的更加仔細,每一個模塊中都存放着state、getters、mutations、actions;使用Vue devtools調試工具查看一下Vuex中的狀態就很清楚的,就是注意一點,這裏使用了命名空間,全部模塊都請使用,同時在組件中調用getters、actions等方法時都須要添加模塊名稱
1. this.$store.dispatch("products/getAllProducts");
2. methods: mapActions("cart", ["addProductToCart"])
複製代碼
computed: mapGetters("user", ["loginStatus"])
複製代碼
就是須要添加一個模塊的前綴名,才能正確執行操做
全部數據的請求都請放在actions中
我先放一下代碼吧,下面再來慢慢解釋
import * as types from '../types'
const state = { // 購物車須要本身的狀態 購物列表
items: [
{ productid: "11137", quantity: 1, checkoutStatus: false },
{ productid: "8750", quantity: 1, checkoutStatus: false }
]
}
const getters = {
// 返回購物車商品列表完整信息
cartProducts: (state, getters, rootState) => {
if (!state.items.length) return [] // map不會對空數組進行檢測 map不會改變原始數組
return state.items.map(({ productid, quantity, checkoutStatus }) => { // map()方法返回一個新數組,數組中的元素爲原始數組元素調用函數處理的後值。
const product = rootState.products.recommendList.find(product => product.productid === productid) // 拿到items中的數據去查閱products中的數據, rootState(根節點狀態)參數能夠拿到別的模塊的state狀態
if (!product) return {} // action請求異步,若是此時的數據尚未請求回來 就返回空對象
return {
src: product.image, // product的圖片地址
name: product.name, // product的名字
price: product.price, // product的單價
productid, // product的id
quantity, // product的數量,默認爲1
simpleTotal: quantity * product.price, // 單項product的總價價
checkoutStatus: checkoutStatus // product的選中狀態
}
})
},
// 返回選中商品的總價
cartTotalPrice: (state, getters) => {
return getters.cartProducts.reduce((total, product) => {
if (product.checkoutStatus) {
return total + product.simpleTotal
}
return total
}, 0)
},
// 返回全部商品總價,無論有沒有選中
allPrice: (state, getters) => {
return getters.cartProducts.reduce((total, product) => {
return total + product.simpleTotal
}, 0)
},
// 返回全部商品總數量,無論有沒有選中
allProducts: (state, getters) => {
return getters.cartProducts.reduce((total, product) => {
return total + product.quantity
}, 0)
},
// 返回全部選中的商品數量
allSelectProducts: (state, getters) => {
return getters.cartProducts.reduce((total, product) => {
if (product.checkoutStatus) {
return total + product.quantity
}
return total
}, 0)
},
// 返回全部商品條數
allProductsItem: (state) => {
return state.items.length
},
// 返回商品是否全選 是返回true 不然false
isSelectAll: (state) => {
if (!state.items.length) return false
return state.items.every(item => { // every() 不會對空數組進行檢測
return item.checkoutStatus === true
})
},
// 返回是否有選中的商品 是返回true 不然false
hasSelect: (state) => {
if (!state.items.length) return false
return state.items.some(item => { // some() 不會對空數組進行檢測
return item.checkoutStatus === true
})
}
}
const mutations = {
// 添加一條商品的方法
[types.CART_ADD_PRODUCT_TO_CART](state, { productid }) {
state.items.push({
productid,
quantity: 1,
checkoutStatus: false
})
},
// 刪除一條商品的方法
[types.CART_DEL_PRODUCT_TO_CART](state, productid) {
state.items.forEach((item, index) => {
if (item.productid === productid) {
state.items.splice(index, 1)
}
});
},
// 增長一條商品中商品數量的方法
[types.CART_ADD_PRODUCT_QUANTITY](state, productid) {
const cartItem = state.items.find(item => item.productid == productid)
cartItem.quantity++
},
// 減小一條商品中商品數量的方法
[types.CART_DEL_PRODUCT_QUANTITY](state, productid) {
const cartItem = state.items.find(item => item.productid == productid)
if (cartItem.quantity > 1) { // 商品數量大於1時才能減小
cartItem.quantity--
}
else cartItem.quantity = 1
},
// 改變單條商品的選中不選中狀態的方法(單選按鈕)
[types.CART_SET_CHECKOUT_STATUS](state, productid) {
const cartItem = state.items.find(item => item.productid == productid)
cartItem.checkoutStatus = !cartItem.checkoutStatus
},
// 改變全部商品的選中不選中狀態的方法(全選按鈕)
[types.CART_SET_CHECKOUT_STATUS_ALL](state, status) {
state.items.forEach(item => {
if (!item.checkoutStatus === status) {
item.checkoutStatus = status
}
})
}
}
const actions = {
// 添加購物車的方法,若是此時購物車內有該條商品,就添加商品數量,不然添加商品
addProductToCart({ state, commit }, product) {
const cartItem = state.items.find(item => item.productid === product.productid)
if (!cartItem) {
commit(types.CART_ADD_PRODUCT_TO_CART, { productid: product.productid })
} else {
commit(types.CART_ADD_PRODUCT_QUANTITY, cartItem.productid)
}
},
// 購物車內刪除一條商品的方法
delProductToCart({ commit }, productid) {
commit(types.CART_DEL_PRODUCT_TO_CART, productid)
},
// 添加商品數量的方法
addProductQuantity({ commit }, productid) {
commit(types.CART_ADD_PRODUCT_QUANTITY, productid)
},
// 減小商品數量的方法
delProductQuantity({ commit }, productid) {
commit(types.CART_DEL_PRODUCT_QUANTITY, productid)
},
// 切換一條商品的選中狀態的方法
setCheckoutStatus({ commit }, productid) {
commit(types.CART_SET_CHECKOUT_STATUS, productid)
},
// 切換全部商品選中狀態的方法
setCheckoutStatusAll({ commit }, status) {
commit(types.CART_SET_CHECKOUT_STATUS_ALL, status)
}
}
export default {
namespaced: true, // 添加命名空間
state,
getters,
mutations,
actions
}
複製代碼
上面一大堆的方法,其實最核心的仍是getters中的第一個方法cartProducts的返回值;其實這裏cartProducts的返回值就是拿到頁面上渲染的全部購物車中的商品數據;而購物車中的items中的每一條數據中只存在着三個字段,這裏我在items中放置了兩條默認的數據。
按照尤大大購物車的demo的思路:一個購物車中的每條數據中是不須要存儲到每條商品數據的全部字段的,只須要存在一些關鍵的字段便可,而後拿着這些字段去products中的查詢對應的商品數據就能夠了,而後返回這些數據。恰好Vuex中的getter就能夠完成這項任務,getter能夠維護好這些數據,而且自動更新響應你在購物車頁面上對商品數據的一些操做。
我在cartProducts中是使用map方法拿到items中每條商品信息的id,而後拿每一條商品的id到products模塊中的存放全部商品信息列表的recommendList中去查詢,查詢到一項,我就返回一個對象,對象格式以下:
{
src: product.image, // product的圖片地址
name: product.name, // product的名字
price: product.price, // product的單價
productid, // product的id
quantity, // product的數量,默認爲1
checkoutStatus: checkoutStatus, // product的選中狀態
simpleTotal: quantity * product.price, // 單項product的總價格
}
複製代碼
這上面一個對象就是頁面上購物車展現的整條商品的全部信息內容,前面三項都是拿到items中的id到products模塊中查詢到的字段,接着三項都是items中每條數據的字段,最後一項就是計算了一下當前這條商品的總價,就是拿這件商品的單價乘以這條商品內商品的數量。這個對象嚴格來講是合併了狀態的,由於你拿到的數據是不可能知足購物車中全部的要求的,因此仍是有些字段須要你本身定義添加,爲何一些公用的字段不在products中的recommendList每一項添加呢?好比checkoutStatus字段,由於個人商品數據是直接拿小米的部分返回的數據的,我直接放在了本地mock.js模塊中,我就沒有對那些官方的數據做修改。因此我就把checkoutStatus這個字段添加到items中了,效果也是不影響邏輯的。
這裏有必要講一下就是getters中每一個方法的參數,官方定義這些方法能夠有四個參數state·、 getters、rootState,rootGetters
state:表明當前模塊內的state
getters:表明當前這個模塊內的getters,即getters中的每一個方法的第二個參數能夠訪問到getters中其餘函數的返回值
rootState:開啓了namespace命名空間以後,一個模塊能夠訪問到另外模塊的state數據
rootGetters:開啓了namespace命名空間以後,一個模塊能夠訪問到另外模塊的Getters數據
既然講到了getters的參數,索性就把另外的actions,mutations的中方法的參數講一下吧
官方定義actions中每一個方法接收的參數:
**1. context (一個對象)包含着 { state, commit, rootState,rootGetters,getters ... }等 **
2. 調用時候傳進來的參數payload( 載荷 )
context.state
和 context.getters
來獲取 state 和 gettersmutations中每一個方法接收的參數,state, payload( 載荷 )
而後再重點講解一下添加購物車的方法和購物車內刪除一條商品的方法
添加購物車的方法: addProductToCart({ state, commit }, product)
在第二個參數中傳進來一個product是當前頁面上每條商品的信息,是一個對象,而後在addProductToCart中用當前這條商品product的id查詢一下items中有沒有該條商品,若是有該條商品我就commit一個添加商品數量的mutation,若是沒有,我就commit一個添加一條商品的方法;在添加該條商品進購物車的mutation中,我每次都是默認添加一條三個字段的對象,和items中每條數據同樣的格式,只要state中items一產生變化,getters中的cartProducts就會自動檢測到,而後從新計算,從新更新數據,就致使頁面上出現該條數據
**購物車內刪除一條商品的方法:**delProductToCart({ commit }, productid)
這個方法邏輯上沒有什麼特別之處,不過我這個方法調用的時候是在一個我自定義的彈窗內調用的,這個自定義彈窗相似於一個plugin,可是又沒有那麼優雅,我最後只是使用了Vue.extend()封裝了兩個全局的方法,掛載在Vue.prototype上,一個點擊彈窗,(往body中添加一段DOM),一個點擊關閉(移除該段DOM),在實現的時候也有一些小坑,可能會在下篇文章分享一下。
至於其餘的一些功能,點擊添加商品數量減小商品數量,點擊全選切換狀態,單選切換狀態,等都放在mutation中由對應的action觸發;每條商品的總價,全部商品數量,選中商品數,全部選中商品的總價,結算按鈕顯示等。我都放在了getters中,邏輯也不是很難,能夠看cart模塊中的代碼,都有比較詳細的註釋。
import Vue from 'vue'
import Vuex from 'vuex'
import cart from './module/cart'
import products from './module/products'
import user from './module/user'
Vue.use(Vuex)
export default new Vuex.Store({
// 設計數據中心 模塊
modules: { // 分模塊
user,
cart, // 購物車 cart
products // 商品 products
}
})
複製代碼
這裏就是整合了一下全部模塊,合併成一個store。最後在main.js裏面全局引入就能夠了。
最後再理一下總體的流程思路:首先應該分模塊,全部商品數據應該放在一個模塊,在action中請求回來;購物車中應該存放着本身的商品列表狀態,拿購物車中每條商品的id去商品的模塊中查詢到相應的信息,再結合實際的需求計算出相應的值,一塊兒合併成一個對象,這個對象就是一條商品基本上全部須要顯示在頁面上的東西了。在組件中取就行了。而後其餘對應的一些功能能夠分別經過getters和mutations來實現。實現以後就是在組件中去調用這些方法就行了。
一個相對功能還比較健全的購物車就此完成,其實沒有很難的代碼。可是對仍是小白的我來講,我以爲仍是不錯了,很開心,因此用心寫下了這篇文章。而後在寫這些方法的時候,用到了數組中的forEach、map、reduce、every、some等方法,我的感受仍是寫的比較優雅的。這是我寫的第一篇文章,因此寫的時候也一直是戰戰兢兢的,怕本身描述不清,講錯概念什麼的,總之也是比較艱辛吧。不過總算是寫出來了,也但願本身之後能堅持寫一些東西出來,讓本身更快的成長。
因爲項目還沒開發完,就不放項目地址了。
但願你們能給個贊,也但願大佬們指出不足的地方,虛心接受,很是感謝觀看。