使用vuex較爲優雅的實現一個購物車功能

前言

最近使用Vue全家桶手擼了一個pc版小米商城的前端項目,對於組件通訊和狀態管理有了一個更加深入的認識。由於組件劃分的比較細,開始我使用的是基本的props和emit傳值,後來發現一旦嵌套過深就變得很繁瑣,同時考慮到有多個組件存在須要共同管理的狀態,基本的傳值已經沒有辦法知足需求了,因此使用到了vuex來劃分模塊管理狀態。這裏須要提一點就是,若是不存在多組件共同管理的狀態,最好是不用vuex管理,vuex是用來管理多組件共同狀態的,單單隻須要實現跨組件、隔代組件通訊的話,使用eventbus,provide/inject等就能夠實現。前端

Vuex修改數據的一套基本流程

首先咱們來弄清楚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

  1. 全選功能按鈕:當全選按鈕亮時,表明下面全部單選按鈕所有爲選中狀態;點擊一下全選,再點擊一下,所有取消;同時下面單選按鈕所有選中時,上面全選按鈕會自動更新狀態爲全選,此時再點擊全選按鈕就會所有取消;
  2. 單選按鈕:點擊一下選中當前這條商品,點擊兩下取消選中這條商品,當全部單選按鈕選中時,上面全選按鈕會自動亮(全選狀態),只要當前購物車商品一條未選擇,上面全選按鈕就不會亮;
  3. 減小商品數量按鈕:點擊加號減小商品的數量;
  4. 增長商品數量按鈕:點擊加號增長商品的數量;
  5. 每條商品的總價:計算當前這一條商品的總價;
  6. 刪除商品按鈕:點擊刪除按鈕,將這條商品刪除購物車;
  7. 全部商品數量:顯示當前購物車內全部商品的數量;
  8. 選中商品數量:顯示當前購物車內選中了商品的數量;
  9. 全部選中商品的總價:計算當前購物車內全部選中的商品總價,不包括未選擇的商品;
  10. 結算按鈕:有選中商品時顯示,未選擇商品不顯示;

功能已經分析完畢,接下來思考一下該怎麼管理狀態,以及劃分模塊異步

Vuex模塊思路

由於是購物車,因此這裏我將這個購物車裏的狀態在Vuex中劃分爲了兩個模塊;products模塊和cart模塊,products模塊用來存放全部的商品數據列表信息,cart模塊放置了購物車內商品的列表信息;這裏須要提的一點是,由於cart模塊中的每條商品信息是不須要提供相似prodcuts中一條商品的全部字段的,只須要提供幾個關鍵的字段,而後到prodcuts模塊中去查詢該條商品的信息便可。可能描述不清,但在下面我會用代碼展現,你們就會清楚了。

Vuex模塊結構設計

個人store目錄以下:

我簡單介紹一下:

  1. module文件夾放置着全部的模塊,我這裏暫時放置三個模塊cart.js、products.js、user.js(能夠不用看,和購物車的功能實現沒有太大關係)
  2. index.js文件整合全部模塊的內容,每一個模塊中都存放各自模塊的state、mutations、actions、getters
  3. types.js存放着全部模塊的mutations常量名,這裏沒有強制,就是Vuex也和Redux、Flux 中的狀態管理同樣,修改數據遵循一套流程。每次commit都是一個常量的函數。

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'  // 切換登錄狀態
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 一目瞭然; 用不用常量取決於你——在須要多人協做的大型項目中,這會頗有幫助。但若是你不喜歡,你徹底能夠不這樣作。

products模塊代碼

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

這裏須要注意幾點:

  1. products模塊中存放着state、getters(這個模塊暫時未用到)、mutations、actions,這是分模塊每一個模塊都存在的,最後導出這個模塊的四部分;

  2. 導出的時候使用了命名空間namespaced: true,命名空間是啥,就是可讓咱們模塊module分的更加仔細,每一個模塊中都存放着state、getters、mutations、actions;使用Vue devtools調試工具查看一下Vuex中的狀態就很清楚的,就是注意一點,這裏使用了命名空間,全部模塊都請使用,同時在組件中調用getters、actions等方法時都須要添加模塊名稱

    • 好比調用actions的時候:
    1. this.$store.dispatch("products/getAllProducts"); 
    2. methods: mapActions("cart", ["addProductToCart"])
    複製代碼
    • 調用getters的時候:
    computed: mapGetters("user", ["loginStatus"])
    複製代碼

    就是須要添加一個模塊的前綴名,才能正確執行操做

  3. 全部數據的請求都請放在actions中

cart模塊代碼(核心模塊)

我先放一下代碼吧,下面再來慢慢解釋

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: action 函數接受一個與 store 實例具備相同方法和屬性的 context 對象 ,能夠經過調用context .commit提交一個mutation; context.statecontext.getters 來獲取 state 和 getters
  • payload:就是調用action時傳進來的參數,多數狀況下傳進來的參數是一個對象,官方叫這個參數爲載荷。

mutations中每一個方法接收的參數,state, payload( 載荷 )

  • state:表明當前模塊內的state
  • payload( 載荷 ): 其實就是commit時傳進來的參數,只官方文檔上說 在大多數狀況下,載荷應該是一個對象,這樣能夠包含多個字段而且記錄的 mutation 會更易讀。意思就是說大多數狀況下,提交的參數是一個對象更好一些,也沒有強制要求啥的。

而後再重點講解一下添加購物車的方法和購物車內刪除一條商品的方法

添加購物車的方法: 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模塊中的代碼,都有比較詳細的註釋。

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({
  // 設計數據中心 模塊
  modules: {  // 分模塊
    user, 
    cart, 	  // 購物車 cart 
    products // 商品 products
  }
})
複製代碼

這裏就是整合了一下全部模塊,合併成一個store。最後在main.js裏面全局引入就能夠了。

最後實現的效果

總結

最後再理一下總體的流程思路:首先應該分模塊,全部商品數據應該放在一個模塊,在action中請求回來;購物車中應該存放着本身的商品列表狀態,拿購物車中每條商品的id去商品的模塊中查詢到相應的信息,再結合實際的需求計算出相應的值,一塊兒合併成一個對象,這個對象就是一條商品基本上全部須要顯示在頁面上的東西了。在組件中取就行了。而後其餘對應的一些功能能夠分別經過getters和mutations來實現。實現以後就是在組件中去調用這些方法就行了。

一個相對功能還比較健全的購物車就此完成,其實沒有很難的代碼。可是對仍是小白的我來講,我以爲仍是不錯了,很開心,因此用心寫下了這篇文章。而後在寫這些方法的時候,用到了數組中的forEach、map、reduce、every、some等方法,我的感受仍是寫的比較優雅的。這是我寫的第一篇文章,因此寫的時候也一直是戰戰兢兢的,怕本身描述不清,講錯概念什麼的,總之也是比較艱辛吧。不過總算是寫出來了,也但願本身之後能堅持寫一些東西出來,讓本身更快的成長。

因爲項目還沒開發完,就不放項目地址了。

但願你們能給個贊,也但願大佬們指出不足的地方,虛心接受,很是感謝觀看。

相關文章
相關標籤/搜索