react結合redux實現一個購物車功能
- 2019 年 10 月 10 日
- 筆記
使用react開發有一段時間了,今天給大家帶來一個案例,react結合redux實現購物車功能,頁面如下:

根據UI頁面我們將其拆分為組件:
header組件,cart組件,footer組件,car組件,由於car組件中渲染的是列表,所以我們把購物車物品的每一項拆分為item組件,這樣我們就得到了4個組件。
接著我們看一下功能,功能分析:
第一個功能,購物車的中物品數量的增加和減少功能
第二個功能,結算前需要勾選要結算的物品,實現單件物品的選中與未選中狀態,並且和全選複選框關聯。
第三個功能,可以實現所有物品的全選和取消全選,並且和所有物品的選中複選框狀態關聯。
第四個功能,被勾選要結算的物品的總件數和總價會根據勾選的物品實時計算並顯示。
分析出功能後,我們來模擬後端的數據,因為筆者在這個案例中沒有開發後端介面,所以用本地數據模擬後端數據,為了完全模擬後端數據我們在獲取數據的時候需要使用setTimout。
數據模擬的程式碼為:
class Localdata{ constructor(){ this.initdata = [ { id: "111", name: "【5本26.8元】經典兒童文學彩圖青少版八十天環遊地球中學生語文教學大綱", price: '12.60', img: 'upload/p1.jpg', count: 1 }, { id: "222", name: "【2000張貼紙】貼紙書 3-6歲 貼畫兒童 貼畫書全套12冊 貼畫 貼紙兒童 ", price: '22.60', img: 'upload/p2.jpg', count: 1 }, { id: "333", name: "唐詩三百首+成語故事全2冊 一年級課外書 精裝注音兒童版 小學生二三年級課外閱讀書籍", price: '32.60', img: 'upload/p3.jpg', count: 1 } ] } init(){ localStorage.setItem("initdata",JSON.stringify(this.initdata)); } getdata(){ // console.log(localStorage.getItem("initdata")) return JSON.parse(localStorage.getItem("initdata")||"{}") } savedata(data){ localStorage.setItem("initdata", JSON.stringify(data)); } updatecar(obj){ let data = this.getdata(); let index=-1; data.find((e,i)=>{ index=i return obj.id===e.id }); if(index>-1){ data[index] = { ...data[index],...obj} }else{ data.push(obj) } this.savedata(data); } } let local = new Localdata() // local.init(); export default local; ;
閱讀程式碼,前端獲取數據主要是調用以下兩個方法,getdata讀取數據,update根據傳遞的參數修改數據。
後端數據有了,頁面組件也有了,我們開始構造我們的store了,構造store需要先配置reducer,我們引用redux文檔中介紹reducer的語句:
Reducers 指定了應用狀態的變化如何響應 actions 並發送到 store 的,記住 actions 只是描述了有事情發生了這一事實,並沒有描述應用如何更新 state。 http://cn.redux.js.org/docs/basics/Reducers.html
讀完發現也沒說啥,這裡我簡單介紹一下reducer,首先我們將store理解成一個容器,這個容器中存放著我們將來要在頁面中使用(通常是渲染)的數據,對照本案例,數據就是購物車中的商品。
那麼這些數據如何變化呢,我們需要根據action中的type來規定如何變化,但是action中只有指令,數據如何變化就需要通過reducer根據指令來指定了。
那麼這個案例中的商品會發生哪些變化呢,這些變化需要對應哪些指令呢?這就需要我們來制定和預測了。
首先第一個變化是從無變成有,我們用init這個指令來指定這個變化,因為store中的數據是從遠程服務端獲取的(這裡我們用本地存儲模擬)。其次是物品的數量或者選中狀態會發生變化,也就是購物車物品屬性發生變化,還有就是所有商品全選與反選的狀態。
所以我們這裡的reducer需要完成三個指令的配置:
1、init指令指代獲取初始化數據
2、update指令根據傳遞的參數修改數據
3、selectall指令根據參數完成購物車物品全選與全不選操作,程式碼如下:
function car(state=[],{type,payload}){ switch(type){ case 'init': return [...payload]; case 'update': let newstate = state.map(e=>{ if(e.id===payload.id){ let obj = { ...e, ...payload } return obj }else{ return e } }) return [...newstate]; case 'selectall': let newdata = state.map(e=>{ e.checked=payload; return e; }) return [...newdata] default: return state; } } export default car;
reducer完成了,我們接著完成action的配置,action的配置如下:
import local from "../../utils/local"; function getdata() { return function (dispatch) { setTimeout(() => { let data = local.getdata(); data = data.map(e=>{ e.checked = false; return e; }) dispatch({ type: "init", payload: data }) }, 500); } } // 更改購物車中商品數量是需要同步到伺服器端的 function setdata(e) { return function (dispatch) { setTimeout(() => { local.updatecar(e); dispatch({ type: "update", payload: e }) }, 300); } } //選中結算商品不需要同步到伺服器端 function selectdata(e) { return function (dispatch) { dispatch({ type: "update", payload: e }) } } function selectAll(all){ console.log(all); return function (dispatch){ dispatch({ type:'selectall', payload:all }) } } export { getdata, setdata, selectdata, selectAll}
這裡我們用函數來生成action,並且我們使用redux-thunk中間件,這個中間件對action進行了擴展,使action不僅僅可以是一個對象,也可以是一個函數,這裡需要注意函數默認第一個參數是dispatch。這樣的話就可以在action函數的內部使用非同步函數了,如果這裡大家有疑惑可以參照redux-thunk的文檔。
getdata函數生成的action對應著獲取初始數據,我們將非同步獲取數據的過程放到這個action中,得到數據並對數據做處理。因為遠端獲取的數據並不包含數據的選中狀態,所以我們要對數據做處理,為每一條數據添加一個checked屬性,默認為false,這樣數據初始狀態就都是未選中狀態,並且刷新頁面,數據又都變為未選中狀態,這裡的功能類似手淘的購物車功能。
selectAll函數生成的action會根據參數來修改數據選中和未選中的狀態。
接下里看這兩個方法:setdata和selectdata,仔細觀察發現前者比後者多了一個非同步操作,這是為什麼呢?因為當修改購物車中物品數量的時候,我們需要同步到後端數據,所以這裡用setTimeout模擬非同步操作,但是selectdata修改數據選中狀態不需要同步到後端伺服器,所以程式碼刪除了非同步操作。
接下來我們看一下cart組件中對數據的處理,首先看程式碼:
import React, { Component } from 'react' import CarHeader from './components/carheader' import Carfooter from './components/carfooter' import Wrapheader from './components/wrapheader' import Item from './components/item' import { connect } from "react-redux"; import { getdata } from "../../store/actions/car"; function Cartitle(){ return ( <div className="cart-filter-bar"> <em>全部商品</em> </div> ) } class Cart extends Component { constructor(){ super() } render() { return (<div> <CarHeader /> <div className="c-container"> <div className="w"> <Cartitle/> <div className="cart-warp"> <Wrapheader/> <div className="cart-item-list"> {this.props.car.map(e=>{ return <Item e={e} key={e.id}/> })} </div> <Carfooter/> </div> </div> </div> </div>) } select(e){ console.log(e) } componentDidMount(){ this.props.dispatch(getdata()); } } let mapstatetoprops = ({car})=>{ return { car } } export default connect(mapstatetoprops)(Cart)
閱讀程式碼我們發現,在cart組件中我們用connect將car數據注入到了組件中,並且在組件生命周期函數componentDidMount中我們調用了this.props.dispatch(getdata())來初始化數據,然後在render函數中將car做渲染處理。
具體每條數據是如何渲染的的,這裡我們將每一條數據傳入item組件,在item中進行處理,這裡也可以使用es6的擴展運算符傳值,item組件程式碼如下:
import React, { Component } from 'react' import {connect} from 'react-redux'; import { setdata ,selectdata} from "../../../store/actions/car"; class Item extends Component { constructor(props){ super(props) } select(id,event){ // this.props.dispatch(setdata({ checked: event.target.checked, id })) this.props.dispatch(selectdata({ checked: event.target.checked, id })) } addone(id,count){ count++; this.props.dispatch(setdata({ id, count })) } reduceone(id,count) { count--; this.props.dispatch(setdata({ id, count })) } render() { let { id, count, price, name, img, checked} =this.props.e return ( <div className={checked ? "cart-item check-cart-item" :"cart-item"} key={id}> <div className="p-checkbox"> <input type="checkbox" checked={checked} className="j-checkbox" onChange={(event) => this.select(id,event)} /> </div> <div className="p-goods"> <div className="p-img"> <img src="upload/p1.jpg" alt="" /> </div> <div className="p-msg">{name}</div> </div> <div className="p-price">{price}</div> <div className="p-num"> <div className="quantity-form"> <a onClick={this.reduceone.bind(this,id,count)} className="decrement" >-</a> <input type="text" className="itxt" value={count} onChange={() => { }} /> <a onClick={this.addone.bind(this,id,count)} className="increment">+</a> </div> </div> <div className="p-sum">{(price*100)*count/100}</div> <div className="p-action"><a href={"/"}>刪除</a></div> </div> ) } } let mapstatetoprops = (state)=>{ return { car:state.car } } export default connect(mapstatetoprops)(Item)
在item組件內部通過props接受參數,並且在item組件中我們要處理三個事件,一個是標識物品是否需要結算的複選框,另外兩個是對商品數量進行增減的操作的點擊事件。

在操作物品是否被選中的複選框事件中,我們用dispatch調用selectdata這個action來更改本條物品的選中狀態,在增減數量的點擊事件上我們調用setdata這個action來完成數據的操作。
這裡需要注意的是,item組件通過props接收到父組件傳遞的值後,直接將其綁定到了dom上,當點擊選中複選框或者數量增減按鈕時,我們並沒有直接修改props,這是絕對不允許的,程式碼中是如何做的呢?
我們在render函數中通過es6的解構語法將props中的數據全部解構出來,程式碼如下:
let { id, count, price, name, img, checked} =this.props.e
這樣再去修改解構出來的數據的話,和props就沒有關係了。
還有一點需要注意:不論是點擊選中商品還是增減商品按鈕,都是修改商品的狀態,為什麼要調用不同的action呢?
這裡需要注意,當我們在修改商品數量的時候,其實是修改了兩份數據,一份是store中的數據,一份是遠端伺服器的數據,這裡有同學可能會問,為什麼我們不修改完遠端數據後,直接發送請求,然後發送非同步請求得到新的數據再去渲染呢?
這個案例如果採用這種方案的話,商品是否處於選中狀態就不好維護操作了,這是本案例的特殊之處。所以我們這裡在初始化的時候給每一個商品都添加一個屬性,即是否選中的屬性,然後後面根據每次操作,如果是修改是否選中狀態,那麼就觸發selectdata這個action,只修改store中的數據。
如果要修改除此之外的屬性,那麼必須要同步到伺服器端,就必須調用setdata了,例如商品的數量,或者我們沒有完成的刪除操作。
最後我們看全選操作的功能如何完成,這裡我們看footer這個組件,程式碼如下:
import React, { Component } from 'react' import {connect} from 'react-redux'; import { selectAll } from '../../../store/actions/car' class Carfooter extends Component { selectall(e) { let isselectall = e.target.checked; this.props.dispatch(selectAll(isselectall)) } all() { return this.props.car.every((e) => e.checked) } len() { return this.props.car.filter(e => e.checked).length } counts(){ let selectproducts = this.props.car.filter(e => e.checked); let count=0; selectproducts.forEach(e=>{ count+=Number(e.count); }) return count; } mount(){ let selectproducts = this.props.car.filter(e => e.checked); let count = 0; selectproducts.forEach(e => { count += Number(e.count)*(Number(e.price)*100)/100; }) return count; } render() { return ( <div className="cart-floatbar"> <div className="select-all"> <input checked={this.all()} onChange={(e)=>{this.selectall(e)}} type="checkbox" name="" id="" className="checkall" />全選 </div> <div className="operation"> <a href={"/"} className="remove-batch"> 刪除選中的商品</a> <a href={"/"} className="clear-all">清理購物車</a> </div> <div className="toolbar-right"> <div className="amount-sum">已經選<em>{this.counts()}</em>件商品</div> <div className="price-sum">總價: <em>¥{this.mount()}</em></div> <div className="btn-area">去結算</div> <h2></h2> </div> </div> ) } } let mapstatetoprops = (state)=>{ return { car:state.car } } export default connect(mapstatetoprops)(Carfooter)
閱讀源碼,當我們點擊全選複選框時會獲取複選框DOM的狀態,並調用dispatch觸發selectall這個action,將獲取的複選框狀態進行傳遞,reducer根據參數,修改商品是否選中。
頁面中渲染的數據是從store中得到的,觸發action修改了store,所有綁定store的dom都會自動更新。
我們定義一個all計算函數,這個函數返回結果計算商品是否被全部選中,我們將其和全選/反選複選框進行綁定,當store觸發action時,這個all函數會重新計算,這樣的話,當我們點擊單件商品的選中狀態,全部選中時,全選複選框也會實時發生變化。
商品的總件數,總價,也是參照上面思路完成的,我們用函數根據store中的數據來實時計算,並渲染到頁面中,這就完成了數據的實時計算。
有的朋友看完這個案例可能會想到redux完成的todolist案例,這個案例和todolist案例是有一些不同的,不同之處就主要在於商品選中的狀態是否隨著頁面的刷新需要重置。
以上就是react結合redux完成的購物車功能,源碼地址:https://github.com/clm1100/reactcar,或者點擊閱讀原文查看源碼。