使用vuex較為優(yōu)雅的實現(xiàn)一個購物車功能的示例代碼
前言
最近使用Vue全家桶手擼了一個pc版小米商城的前端項目,對于組件通信和狀態(tài)管理有了一個更加深刻的認識。因為組件劃分的比較細,開始我使用的是基本的props和emit傳值,后來發(fā)現(xiàn)一旦嵌套過深就變得很繁瑣,同時考慮到有多個組件存在需要共同管理的狀態(tài),基本的傳值已經(jīng)沒有辦法滿足需求了,所以使用到了vuex來劃分模塊管理狀態(tài)。這里需要提一點就是,如果不存在多組件共同管理的狀態(tài),最好是不用vuex管理,vuex是用來管理多組件共同狀態(tài)的,單單只需要實現(xiàn)跨組件、隔代組件通信的話,使用eventbus,provide/inject等就可以實現(xiàn)。
Vuex修改數(shù)據(jù)的一套基本流程
首先我們來弄清楚Vuex中管理數(shù)據(jù)的一套基本流程:
- 修改state中數(shù)據(jù)的流程:
在組件內(nèi)派發(fā)一個action即dispatch(或者直接調用)一個action => action再commit一個mutation => mutation修改state
- state中的數(shù)據(jù)都在action中請求,再通過commit一個mutation設置state中的數(shù)據(jù)
- getter中存放著state的計算值,相當于組件中的計算屬性(computed);同時getter中的值都是響應的,就是只要依賴的state一發(fā)生改變,getter中的值馬上就能檢測到,然后對應就會更新狀態(tài)了
- 注意點:action中的請求是異步的,mutation是同步的
小米官網(wǎng)購物車功能分析
官方效果:

我們可以從上圖中看到購物車的功能,這里我簡單總結一下,分為以下十點:
- 全選功能按鈕:當全選按鈕亮時,代表下面所有單選按鈕全部為選中狀態(tài);點擊一下全選,再點擊一下,全部取消;同時下面單選按鈕全部選中時,上面全選按鈕會自動更新狀態(tài)為全選,此時再點擊全選按鈕就會全部取消;
- 單選按鈕:點擊一下選中當前這條商品,點擊兩下取消選中這條商品,當所有單選按鈕選中時,上面全選按鈕會自動亮(全選狀態(tài)),只要當前購物車商品一條未選擇,上面全選按鈕就不會亮;
- 減少商品數(shù)量按鈕:點擊加號減少商品的數(shù)量;
- 增加商品數(shù)量按鈕:點擊加號增加商品的數(shù)量;
- 每條商品的總價:計算當前這一條商品的總價;
- 刪除商品按鈕:點擊刪除按鈕,將這條商品刪除購物車;
- 所有商品數(shù)量:顯示當前購物車內(nèi)所有商品的數(shù)量;
- 選中商品數(shù)量:顯示當前購物車內(nèi)選中了商品的數(shù)量;
- 所有選中商品的總價:計算當前購物車內(nèi)所有選中的商品總價,不包括未選擇的商品;
- 結算按鈕:有選中商品時顯示,未選擇商品不顯示;
功能已經(jīng)分析完畢,接下來思考一下該怎么管理狀態(tài),以及劃分模塊
Vuex模塊思路
因為是購物車,所以這里我將這個購物車里的狀態(tài)在Vuex中劃分為了兩個模塊;products模塊和cart模塊,products模塊用來存放所有的商品數(shù)據(jù)列表信息,cart模塊放置了購物車內(nèi)商品的列表信息;這里需要提的一點是,因為cart模塊中的每條商品信息是不需要提供類似prodcuts中一條商品的所有字段的,只需要提供幾個關鍵的字段,然后到prodcuts模塊中去查詢該條商品的信息即可??赡苊枋霾磺澹谙旅嫖視么a展示,大家就會清楚了。
Vuex模塊結構設計
我的store目錄如下:

我簡單介紹一下:
- module文件夾放置著所有的模塊,我這里暫時放置三個模塊cart.js、products.js、user.js(可以不用看,和購物車的功能實現(xiàn)沒有太大關系)
- index.js文件整合所有模塊的內(nèi)容,每個模塊中都存放各自模塊的state、mutations、actions、getters
- types.js存放著所有模塊的mutations常量名,這里沒有強制,就是Vuex也和Redux、Flux 中的狀態(tài)管理一樣,修改數(shù)據(jù)遵循一套流程。每次commit都是一個常量的函數(shù)。
types文件代碼
// 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' // 切換登陸狀態(tài) export const CART_ADD_PRODUCT_QUANTITY = 'CART_ADD_PRODUCT_QUANTITY' // 添加商品數(shù)量 export const CART_DEL_PRODUCT_QUANTITY = 'CART_DEL_PRODUCT_QUANTITY' // 減少商品數(shù)量 export const CART_SET_CHECKOUT_STATUS_ALL = 'CART_SET_CHECKOUT_STATUS_ALL' // 一鍵改變所有商品購買狀態(tài)的方法 // products模塊 export const PRODUCTS_SET_PRODUCT = 'PRODUCTS_SET_PRODUCT' // 獲取所有商品的列表
這段代碼沒有什么邏輯可言,就是把所有模塊中的mutations中的函數(shù)都用一個大寫的常量名,簡而言之就是按一個大寫的規(guī)范,把每個模塊中mutations中的函數(shù)命名,就是一套命名規(guī)范。
Vue官方文檔的解釋:
使用常量替代 mutation 事件類型在各種 Flux 實現(xiàn)中是很常見的模式。這樣可以使 linter 之類的工具發(fā)揮作用,同時把這些常量放在單獨的文件中可以讓你的代碼合作者對整個 app 包含的 mutation 一目了然; 用不用常量取決于你——在需要多人協(xié)作的大型項目中,這會很有幫助。但如果你不喜歡,你完全可以不這樣做。
products模塊代碼
import { fetchGet } from "@/api/index" // api文件夾下封裝的axios.get請求函數(shù)
import * as types from '../types' // types目錄下的mutations函數(shù)的常量名
const state = {
recommendList: [] // 存放所有商品的信息
}
const getters = {}
const mutations = {
[types.PRODUCTS_SET_PRODUCT](state, products) { // 第一個參數(shù)是state 可以修改state 將請求回來的數(shù)據(jù)保存在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中每一條數(shù)據(jù)的字段 例如其中一條為:
/*
{
productid: "11137",
name: "小米CC9 Pro 6GB+128GB",
price: 2799,
image: "http://i1.mifile.cn/a1/pms_1572941393.18077211.jpg",
comments: 0
}
*/
上面代碼的邏輯就是在actions中的getAllProducts方法中調用封裝在api目錄下index.js中的fetchGet()函數(shù)請求到數(shù)據(jù),commit提交給mutations中的types.PRODUCTS_SET_PRODUCT函數(shù)(設置state),然后去設置所有的商品信息列表recommendList
這里需要注意幾點:
- products模塊中存放著state、getters(這個模塊暫時未用到)、mutations、actions,這是分模塊每個模塊都存在的,最后導出這個模塊的四部分;
- 導出的時候使用了命名空間namespaced: true,命名空間是啥,就是可以讓我們模塊module分的更加仔細,每個模塊中都存放著state、getters、mutations、actions;使用Vue devtools調試工具查看一下Vuex中的狀態(tài)就很清楚的,就是注意一點,這里使用了命名空間,所有模塊都請使用,同時在組件中調用getters、actions等方法時都需要添加模塊名稱
比如調用actions的時候:
this.$store.dispatch("products/getAllProducts");
methods: mapActions("cart", ["addProductToCart"])
調用getters的時候:
computed: mapGetters("user", ["loginStatus"])
就是需要添加一個模塊的前綴名,才能正確執(zhí)行操作
所有數(shù)據(jù)的請求都請放在actions中
cart模塊代碼(核心模塊)
我先放一下代碼吧,下面再來慢慢解釋
import * as types from '../types'
const state = { // 購物車需要自己的狀態(tài) 購物列表
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不會對空數(shù)組進行檢測 map不會改變原始數(shù)組
return state.items.map(({ productid, quantity, checkoutStatus }) => { // map()方法返回一個新數(shù)組,數(shù)組中的元素為原始數(shù)組元素調用函數(shù)處理的后值。
const product = rootState.products.recommendList.find(product => product.productid === productid) // 拿到items中的數(shù)據(jù)去查閱products中的數(shù)據(jù), rootState(根節(jié)點狀態(tài))參數(shù)可以拿到別的模塊的state狀態(tài)
if (!product) return {} // action請求異步,如果此時的數(shù)據(jù)還沒有請求回來 就返回空對象
return {
src: product.image, // product的圖片地址
name: product.name, // product的名字
price: product.price, // product的單價
productid, // product的id
quantity, // product的數(shù)量,默認為1
simpleTotal: quantity * product.price, // 單項product的總價價
checkoutStatus: checkoutStatus // product的選中狀態(tài)
}
})
},
// 返回選中商品的總價
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)
},
// 返回所有商品總數(shù)量,不管有沒有選中
allProducts: (state, getters) => {
return getters.cartProducts.reduce((total, product) => {
return total + product.quantity
}, 0)
},
// 返回所有選中的商品數(shù)量
allSelectProducts: (state, getters) => {
return getters.cartProducts.reduce((total, product) => {
if (product.checkoutStatus) {
return total + product.quantity
}
return total
}, 0)
},
// 返回所有商品條數(shù)
allProductsItem: (state) => {
return state.items.length
},
// 返回商品是否全選 是返回true 否則false
isSelectAll: (state) => {
if (!state.items.length) return false
return state.items.every(item => { // every() 不會對空數(shù)組進行檢測
return item.checkoutStatus === true
})
},
// 返回是否有選中的商品 是返回true 否則false
hasSelect: (state) => {
if (!state.items.length) return false
return state.items.some(item => { // some() 不會對空數(shù)組進行檢測
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)
}
});
},
// 增加一條商品中商品數(shù)量的方法
[types.CART_ADD_PRODUCT_QUANTITY](state, productid) {
const cartItem = state.items.find(item => item.productid == productid)
cartItem.quantity++
},
// 減少一條商品中商品數(shù)量的方法
[types.CART_DEL_PRODUCT_QUANTITY](state, productid) {
const cartItem = state.items.find(item => item.productid == productid)
if (cartItem.quantity > 1) { // 商品數(shù)量大于1時才能減少
cartItem.quantity--
}
else cartItem.quantity = 1
},
// 改變單條商品的選中不選中狀態(tài)的方法(單選按鈕)
[types.CART_SET_CHECKOUT_STATUS](state, productid) {
const cartItem = state.items.find(item => item.productid == productid)
cartItem.checkoutStatus = !cartItem.checkoutStatus
},
// 改變所有商品的選中不選中狀態(tài)的方法(全選按鈕)
[types.CART_SET_CHECKOUT_STATUS_ALL](state, status) {
state.items.forEach(item => {
if (!item.checkoutStatus === status) {
item.checkoutStatus = status
}
})
}
}
const actions = {
// 添加購物車的方法,如果此時購物車內(nèi)有該條商品,就添加商品數(shù)量,否則添加商品
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)
}
},
// 購物車內(nèi)刪除一條商品的方法
delProductToCart({ commit }, productid) {
commit(types.CART_DEL_PRODUCT_TO_CART, productid)
},
// 添加商品數(shù)量的方法
addProductQuantity({ commit }, productid) {
commit(types.CART_ADD_PRODUCT_QUANTITY, productid)
},
// 減少商品數(shù)量的方法
delProductQuantity({ commit }, productid) {
commit(types.CART_DEL_PRODUCT_QUANTITY, productid)
},
// 切換一條商品的選中狀態(tài)的方法
setCheckoutStatus({ commit }, productid) {
commit(types.CART_SET_CHECKOUT_STATUS, productid)
},
// 切換所有商品選中狀態(tài)的方法
setCheckoutStatusAll({ commit }, status) {
commit(types.CART_SET_CHECKOUT_STATUS_ALL, status)
}
}
export default {
namespaced: true, // 添加命名空間
state,
getters,
mutations,
actions
}
上面一大堆的方法,其實最核心的還是getters中的第一個方法cartProducts的返回值;其實這里cartProducts的返回值就是拿到頁面上渲染的所有購物車中的商品數(shù)據(jù);而購物車中的items中的每一條數(shù)據(jù)中只存在著三個字段,這里我在items中放置了兩條默認的數(shù)據(jù)。
按照尤大大購物車的demo的思路:一個購物車中的每條數(shù)據(jù)中是不需要存儲到每條商品數(shù)據(jù)的所有字段的,只需要存在一些關鍵的字段即可,然后拿著這些字段去products中的查詢對應的商品數(shù)據(jù)就可以了,然后返回這些數(shù)據(jù)。剛好Vuex中的getter就可以完成這項任務,getter可以維護好這些數(shù)據(jù),并且自動更新響應你在購物車頁面上對商品數(shù)據(jù)的一些操作。
我在cartProducts中是使用map方法拿到items中每條商品信息的id,然后拿每一條商品的id到products模塊中的存放所有商品信息列表的recommendList中去查詢,查詢到一項,我就返回一個對象,對象格式如下:
{
src: product.image, // product的圖片地址
name: product.name, // product的名字
price: product.price, // product的單價
productid, // product的id
quantity, // product的數(shù)量,默認為1
checkoutStatus: checkoutStatus, // product的選中狀態(tài)
simpleTotal: quantity * product.price, // 單項product的總價格
}
這上面一個對象就是頁面上購物車展示的整條商品的所有信息內(nèi)容,前面三項都是拿到items中的id到products模塊中查詢到的字段,接著三項都是items中每條數(shù)據(jù)的字段,最后一項就是計算了一下當前這條商品的總價,就是拿這件商品的單價乘以這條商品內(nèi)商品的數(shù)量。這個對象嚴格來說是合并了狀態(tài)的,因為你拿到的數(shù)據(jù)是不可能滿足購物車中所有的要求的,所以還是有些字段需要你自己定義添加,為什么一些公用的字段不在products中的recommendList每一項添加呢?比如checkoutStatus字段,因為我的商品數(shù)據(jù)是直接拿小米的部分返回的數(shù)據(jù)的,我直接放在了本地mock.js模塊中,我就沒有對那些官方的數(shù)據(jù)作修改。所以我就把checkoutStatus這個字段添加到items中了,效果也是不影響邏輯的。
這里有必要講一下就是getters中每個方法的參數(shù),官方定義這些方法可以有四個參數(shù)state·、 getters、rootState,rootGetters
- state:代表當前模塊內(nèi)的state
- getters:代表當前這個模塊內(nèi)的getters,即getters中的每個方法的第二個參數(shù)可以訪問到getters中其他函數(shù)的返回值
- rootState:開啟了namespace命名空間之后,一個模塊可以訪問到另外模塊的state數(shù)據(jù)
- rootGetters:開啟了namespace命名空間之后,一個模塊可以訪問到另外模塊的Getters數(shù)據(jù)
既然講到了getters的參數(shù),索性就把另外的actions,mutations的中方法的參數(shù)講一下吧
官方定義actions中每個方法接收的參數(shù):
**1. context (一個對象)包含著 { state, commit, rootState,rootGetters,getters ... }等 **
2. 調用時候傳進來的參數(shù)payload( 載荷 )
- context: action 函數(shù)接受一個與 store 實例具有相同方法和屬性的 context 對象 ,可以通過調用context .commit提交一個mutation;
context.state和context.getters來獲取 state 和 getters - payload:就是調用action時傳進來的參數(shù),多數(shù)情況下傳進來的參數(shù)是一個對象,官方叫這個參數(shù)為載荷。
mutations中每個方法接收的參數(shù),state, payload( 載荷 )
state:代表當前模塊內(nèi)的state payload( 載荷 ): 其實就是commit時傳進來的參數(shù),只官方文檔上說 在大多數(shù)情況下,載荷應該是一個對象,這樣可以包含多個字段并且記錄的 mutation 會更易讀。意思就是說大多數(shù)情況下,提交的參數(shù)是一個對象更好一些,也沒有強制要求啥的。
然后再重點講解一下添加購物車的方法和購物車內(nèi)刪除一條商品的方法
添加購物車的方法:addProductToCart({ state, commit }, product)
在第二個參數(shù)中傳進來一個product是當前頁面上每條商品的信息,是一個對象,然后在addProductToCart中用當前這條商品product的id查詢一下items中有沒有該條商品,如果有該條商品我就commit一個添加商品數(shù)量的mutation,如果沒有,我就commit一個添加一條商品的方法;在添加該條商品進購物車的mutation中,我每次都是默認添加一條三個字段的對象,和items中每條數(shù)據(jù)一樣的格式,只要state中items一產(chǎn)生變化,getters中的cartProducts就會自動檢測到,然后重新計算,重新更新數(shù)據(jù),就導致頁面上出現(xiàn)該條數(shù)據(jù)
**購物車內(nèi)刪除一條商品的方法:**delProductToCart({ commit }, productid)
這個方法邏輯上沒有什么特別之處,不過我這個方法調用的時候是在一個我自定義的彈窗內(nèi)調用的,這個自定義彈窗類似于一個plugin,但是又沒有那么優(yōu)雅,我最后只是使用了Vue.extend()封裝了兩個全局的方法,掛載在Vue.prototype上,一個點擊彈窗,(往body中添加一段DOM),一個點擊關閉(移除該段DOM),在實現(xiàn)的時候也有一些小坑,可能會在下篇文章分享一下。
至于其他的一些功能,點擊添加商品數(shù)量減少商品數(shù)量,點擊全選切換狀態(tài),單選切換狀態(tài),等都放在mutation中由對應的action觸發(fā);每條商品的總價,所有商品數(shù)量,選中商品數(shù),所有選中商品的總價,結算按鈕顯示等。我都放在了getters中,邏輯也不是很難,可以看cart模塊中的代碼,都有比較詳細的注釋。
index.js代碼
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({
// 設計數(shù)據(jù)中心 模塊
modules: { // 分模塊
user,
cart, // 購物車 cart
products // 商品 products
}
})
這里就是整合了一下所有模塊,合并成一個store。最后在main.js里面全局引入就可以了。
最后實現(xiàn)的效果
總結
最后再理一下整體的流程思路:首先應該分模塊,所有商品數(shù)據(jù)應該放在一個模塊,在action中請求回來;購物車中應該存放著自己的商品列表狀態(tài),拿購物車中每條商品的id去商品的模塊中查詢到相應的信息,再結合實際的需求計算出相應的值,一起合并成一個對象,這個對象就是一條商品基本上所有需要顯示在頁面上的東西了。在組件中取就好了。然后其他對應的一些功能可以分別通過getters和mutations來實現(xiàn)。實現(xiàn)之后就是在組件中去調用這些方法就好了。
一個相對功能還比較健全的購物車就此完成,其實沒有很難的代碼。但是對還是小白的我來說,我覺得還是不錯了,很開心,所以用心寫下了這篇文章。然后在寫這些方法的時候,用到了數(shù)組中的forEach、map、reduce、every、some等方法,個人感覺還是寫的比較優(yōu)雅的。這是我寫的第一篇文章,所以寫的時候也一直是戰(zhàn)戰(zhàn)兢兢的,怕自己描述不清,講錯概念什么的,總之也是比較艱辛吧。不過總算是寫出來了,也希望自己以后能堅持寫一些東西出來,讓自己更快的成長。
由于項目還沒開發(fā)完,就不放項目地址了。
以上就是本文的全部內(nèi)容,希望對大家的學習有所幫助,也希望大家多多支持腳本之家。
相關文章
基于Vue 實現(xiàn)一個中規(guī)中矩loading組件
這篇文章主要介紹了基于Vue 實現(xiàn)一個中規(guī)中矩loading組件,本文通過實例代碼給大家介紹的非常詳細,具有一定的參考借鑒價值,需要的朋友可以參考下2019-04-04
基于vue2.0+vuex的日期選擇組件功能實現(xiàn)
這篇文章主要介紹了 基于vue2.0+vuex的日期選擇組件功能實現(xiàn),詳細介紹了使用vue編寫的日期組件,,非常具有實用價值,需要的朋友可以參考下。2017-03-03
Vue實現(xiàn)添加數(shù)據(jù)到二維數(shù)組并顯示
這篇文章主要介紹了Vue實現(xiàn)添加數(shù)據(jù)到二維數(shù)組并顯示方式,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-04-04
vue項目本地開發(fā)使用Nginx配置代理后端接口問題
這篇文章主要介紹了vue項目本地開發(fā)使用Nginx配置代理后端接口問題,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-12-12
vue使用iview的modal彈窗嵌套modal出現(xiàn)格式錯誤的解決
這篇文章主要介紹了vue使用iview的modal彈窗嵌套modal出現(xiàn)格式錯誤的解決方案,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-09-09
vue實現(xiàn)一個矩形標記區(qū)域(rectangle marker)的方法
這篇文章主要介紹了vue實現(xiàn)一個矩形標記區(qū)域 rectangle marker的方法,幫助大家實現(xiàn)區(qū)域標記功能,感興趣的朋友可以了解下2020-10-10

