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,或者点击阅读原文查看源码。