shopping

用 redux+react 实现购物车案例

模板

购物车案例接口

项目初始化

  1. react脚手架搭建项目
create-react-app shopping
  1. 删除多余文件,创建购物车与商品列表组件,并显示在app。js组件中,复制样式文件并引入
// src/components/cart.js
import React, { Component } from 'react'

export default class Cart extends Component {
    render() {
        return <section className="container content-section">
            <h2 className="section-header">购物车</h2>
            <div className="cart-row">
                <span className="cart-item cart-header cart-column">商品</span>
                <span className="cart-price cart-header cart-column">价格</span>
                <span className="cart-quantity cart-header cart-column">数量</span>
            </div>
            <div className="cart-items">
                <div className="cart-row">
                    <div className="cart-item cart-column">
                        <img className="cart-item-image" src="images/01.webp" alt='123' width="100" height="100" />
                        <span className="cart-item-title">小户型简约现代网红双人三人客厅科技布免洗布艺</span>
                    </div>
                    <span className="cart-price cart-column">¥1020</span>
                    <div className="cart-quantity cart-column">
                        <input className="cart-quantity-input" type="number" />
                        <button className="btn btn-danger" type="button">删除</button>
                    </div>
                </div>
                <div className="cart-row">
                    <div className="cart-item cart-column">
                        <img className="cart-item-image" src="images/02.webp" alt='123'  width="100" height="100"/>
                        <span className="cart-item-title">11全网通4G手机官方iPhonexr</span>
                    </div>
                    <span className="cart-price cart-column">¥4758</span>
                    <div className="cart-quantity cart-column">
                        <input className="cart-quantity-input" type="number" />
                        <button className="btn btn-danger" type="button">删除</button>
                    </div>
                </div>
            </div>
            <div className="cart-total">
                <strong className="cart-total-title">总价</strong>
                <span className="cart-total-price">¥39.97</span>
            </div>
        </section>
    }
}
// src/components/product.js
import React, { Component } from 'react'

export default class Cart extends Component {
    render() {
        return  <section class="container content-section">
        <h2 class="section-header">商品列表</h2>
        <div class="shop-items">
            <div class="shop-item">
                <img class="shop-item-image" src="images/01.webp" />
                <span class="shop-item-title">小户型简约现代网红双人三人客厅科技布免洗布艺</span>
                <div class="shop-item-details">
                    <span class="shop-item-price">¥1020</span>
                    <button class="btn btn-primary shop-item-button" type="button">加入购物车</button>
                </div>
            </div>
            <div class="shop-item">
                <img class="shop-item-image" src="images/02.webp"/>
                <span class="shop-item-title">11全网通4G手机官方iPhonexr</span>
                <div class="shop-item-details">
                    <span class="shop-item-price">¥4758</span>
                    <button class="btn btn-primary shop-item-button"type="button">加入购物车</button>
                </div>
            </div>
            <div class="shop-item">
                <img class="shop-item-image" src="images/03.webp"/>
                <span class="shop-item-title">潮休闲网红小西服套装英伦风春装</span>
                <div class="shop-item-details">
                    <span class="shop-item-price">¥59</span>
                    <button class="btn btn-primary shop-item-button" type="button">加入购物车</button>
                </div>
            </div>
            <div class="shop-item">
                <img class="shop-item-image" src="images/04.webp" />
                <span class="shop-item-title">夏新27英寸超薄曲面高清电脑</span>
                <div class="shop-item-details">
                    <span class="shop-item-price">¥369</span>
                    <button class="btn btn-primary shop-item-button" type="button">加入购物车</button>
                </div>
            </div>
        </div>
    </section>
    }
}
// src/components/App.js

import Cart from './cart'
import Product from './product'
import './../style.css'

function App() {
  return (
    <div>
      <Cart />
      <Product />
    </div>
  );
}

export default App;

创建redux工作流

  1. 下载 redux react-redux redux-saga redux-actions
npm install redux react-redux redux-saga redux-actions --save
  1. 创建 store文件夹,创建actions、reducers、sagas、文件夹
mkdir -p src/store/{actions,reducers,sagas}
  1. 创建index.js 并使用createStore方法创建store
import { createStore } from 'redux'

export const store = createStore()
  1. 在创建store的时候需要我们传入reducer函数,所以我们在reducers文件夹中创建root.reducer.js文件创建reducer函数,这个reducer是合并后的大的reducer 所以我们需要引入combineReducers合并reducer,并在store/index.js里面去引入这个合并后的大的reducer
// store/reducers/root.reducer.js
import { combineReducers } from 'redux'

export default combineReducers({
  
})
// src/store/index.js
import { createStore } from 'redux'
import rootReducer from './reducers/root.reducer'

export const store = createStore(rootReducer)
  1. 我们 combineReducers 方法传入了一个空对象这个是不被允许的必须要有一个reducer,所以我们创建,porduct.reducer.js来存放商品相关的数据,这个文件要返回一个reducer,我们使用redux-action来简化操作并在root.reducer中引入并使用
// src/store/reducers/porduct.reducer.js
import { handleActions as createRrducer } from 'redux-actions'

// 商品列表数据
const initialState = []

export default createRrducer({
  
}, initialState)
// src/store/index.js
import { combineReducers } from 'redux'
import porductReducer from './porduct.reducer'

// 数据结构 { products: [] }
export default combineReducers({
    products: porductReducer
})
  1. 把store放在全局的地方让全局可以访问,全局访问需要使用 Provider 组件
import React from 'react';
import ReactDOM from 'react-dom';
import App from './components/App';
import './style.css'
import { Provider } from 'react-redux'
import { store } from './store';

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('root')
);
  1. 在组件中去访问store中的products数据,需要使用到connect方法,并在组件中获取products测试是可以获取到
// src/components/product.js

import React, { Component } from 'react'

class Product extends Component {
    render() {
          const { products } = this.props
        
        console.log(products)
      
        return <div></div>
    }
}

const mapStateToProps = state => {
    return {
        products: state.products
    }
}

export default connect(mapStateToProps)(Product)

展示商品列表数据

展示商品列表数据需要使用到两个actions,第一个actions是用来像服务端发送请求用来获取数据,第二个actions是把服务器返回的数据保存到本地的store中

  1. 创建 product.actions.js文件 创建两个action,我们使用redux-actions中的createAction方法来简化操作
// src/store/actions/product.action.js
import { createAction } from 'redux-actions'

export const loadProducts = createAction('load products')
export const saveProducts = createAction('save products')
  1. 让组件调用去调用actioncreater函数,我们使用bindActionCreators与connect把actioncreater函数映射到props中
// src/components/product.js
import { bindActionCreators } from 'redux'
import * as productActions from './../store/actions/product.action'

const mapDispatchToProps = dispatch => ({
    ...bindActionCreators(productActions, dispatch)
})

class Product extends Component {
    componentDidMount(){
        const { loadProducts } = this.props
        // 获取数据
        loadProducts()
    }
    render() {
        const { products } = this.props
        
        // ......
    }
}

export default connect(mapStateToProps, mapDispatchToProps)(Product)
  1. 应为loadProducts是异步操作所以我们需要使用saga来接收action,所以我们在store/index.js来注册saga中间件
import { applyMiddleware, createStore } from 'redux'
import rootReducer from './reducers/root.reducer'
import createSagaMiddleware from 'redux-saga'

const sagaMiddleware = createSagaMiddleware()

export const store = createStore(rootReducer, applyMiddleware(sagaMiddleware))
  1. 创建合并 src/store/sagas/root.saga.js 文件用来合并saga,还需要创建product.saga.js文件 用来处理获取商品列表的异步操作(注:这里的接口地址是上面的购物车案例接口)
// src/store/sagas/product.saga.js
import { takeEvery, put} from 'redux-saga/effects'
import { loadProducts, saveProducts } from '../actions/product.action'
import axios from 'axios'

function* handLoadProducts (){
   const { data } = yield axios.get('http://localhost:3005/goods')
   yield put(saveProducts(data))
}

export default function* productSage () {
    yield takeEvery(loadProducts, handLoadProducts)
}
// src/store/sagas/root.saga.js
import { all } from 'redux-saga/effects'
import productSage from './product.saga'

export default function* rootSaga (){
    yield all([
        productSage()
    ])
}
  1. 调用sagaMiddleware.run方法来启用saga
// src/store/index.js
import { applyMiddleware, createStore } from 'redux'
import rootReducer from './reducers/root.reducer'
import createSagaMiddleware from 'redux-saga'
import rootSaga from './sagas/root.saga'

const sagaMiddleware = createSagaMiddleware()

export const store = createStore(rootReducer, applyMiddleware(sagaMiddleware))

// 启用saga
sagaMiddleware.run(rootSaga)
  1. 在模版中渲染列表数据
// src/components/product.js
render() {
        const { products } = this.props
        return <section className="container content-section">
            <h2 className="section-header">商品列表</h2>
            <div className="shop-items">
                {
                    products.map( (item) => (
                        <div className="shop-item" key={item.id}>
                            <img className="shop-item-image" src={item.thumbnail} alt='123' />
                            <span className="shop-item-title">{item.title}</span>
                            <div className="shop-item-details">
                                <span className="shop-item-price">¥{item.price}</span>
                                <button className="btn btn-primary shop-item-button" type="button">加入购物车</button>
                            </div>
                        </div>
                    ))
                }
            </div>
        </section>
}

将商品加入到购物车中

添加商品有两种情况,第一种是购物车中没有商品数据这种情况直接添加到购物车中,第二种情况就是购物车中已经又了商品数据所以需要在商品数据上+1

  1. 新建 cart.actions.js 创建新增actions,一个action是接收服务器返回数据,一个aciton是添加到本地store中
//  src/store/actions/cart.actions.js
import { createAction } from 'redux-actions'

export const addProductToCart = createAction('addProductToCart')
export const addProductToLocalCart = createAction('addProductToLocalCart')
  1. 因为添加到购物车服务器action是异步操作所以我们用saga来接收action,并合并saga,接收到数据后传递给添触发操作本地store的action来吧数据添加到store中
// src/store/sagas/cart.saga.js
import { takeEvery, put } from 'redux-saga/effects'
import axios from 'axios'
import { addProductToCart, addProductToLocalCart } from './../actions/cart.actions'


function* handleAddProductToCart(action){
   const { data } = yield axios.post('http://localhost:3005/cart/add', {gid: action.payload})
   yield put(addProductToLocalCart(data))
}

export default function* cartSata () {
    yield takeEvery(addProductToCart, handleAddProductToCart)
}
// src/store/sagas/root.saga.js
import { all } from 'redux-saga/effects'
import productSage from './product.saga'
import cartSage from './cart.saga'

export default function* rootSaga (){
    yield all([
        productSage(),
        cartSage()
    ])
}
  1. 在reducer中接收添加到本地store的action,并合并reducer
// src/store/reducers/cart.reducer.js
import {  handleActions as crateReducer } from 'redux-actions'
import { addProductToLocalCart } from './../actions/cart.actions'

const initialSate = []

const handleAddProductToLocalCart = (state, action) => {
    console.log(state, action)
    // 两种情况 添加的商品没有在购物车中 直接添加
    // 如果已经有了 购物车该商品数量加一
    const newState = JSON.parse(JSON.stringify(state))

    const product = newState.find(product => product.id === action.payload.id)

    if(product){
        // 已经有了的情况
        product.count = product.count + 1
    }else{
        // 没有的情况
        newState.push(action.payload)
    }
  
    return newState
}

export default crateReducer({
    [addProductToLocalCart]: handleAddProductToLocalCart
}, initialSate)
// src/store/reducers/root.reducer.js

import { combineReducers } from 'redux'
import porductReducer from './porduct.reducer'
import cartReducer from './cart.reducer'

// { products: [] }
export default combineReducers({
    products: porductReducer,
    carts: cartReducer
})
  1. 这个时候我们的初步逻辑已经执行完成,现在在商品列表组件中去绑定事件执行添加到购物车中操作
// src/components/product.js
import React, { Component } from 'react'
import { connect } from 'react-redux'
import { bindActionCreators } from 'redux'
import * as productActions from './../store/actions/product.action'
import * as cartActions from './../store/actions/cart.actions'

class Product extends Component {
    componentDidMount(){
        const { loadProducts } = this.props
        // 获取数据
        loadProducts()

    }
    render() {
        const { products, addProductToCart } = this.props
        return <section className="container content-section">
            <h2 className="section-header">商品列表</h2>
            <div className="shop-items">
                {
                    products.map((item) => (
                        <div key={item.id} className="shop-item">
                            <img className="shop-item-image" src={item.thumbnail} alt='123' />
                            <span className="shop-item-title">{item.title}</span>
                            <div className="shop-item-details">
                                <span className="shop-item-price">¥{item.price}</span>
                                <button className="btn btn-primary shop-item-button" type="button" onClick={() => addProductToCart(item.id)}>加入购物车</button>
                            </div>
                        </div>
                    ))
                }
            </div>
        </section>
    }
}


const mapStateToProps = state => {
    return {
        products: state.products
    }
}

const mapDispatchToProps = dispatch => ({
    ...bindActionCreators(productActions, dispatch),
    ...bindActionCreators(cartActions, dispatch)
})

export default connect(mapStateToProps, mapDispatchToProps)(Product)

购物车列表数据展示

  1. 创建从服务器拉去购物车数据action和把数据保存到本地store中的action
// src/store/actions/cart.actions.js
// 发送求情获取购物车数据
export const loadCarts = createAction('loadCarts')
// 同步服务器返回的购物车数据
export const savaCarts = createAction('savaCarts')
  1. 因为从服务器拉去数据是异步操作,所以我们需要在saga中去处理action,获取到数据后触发保存到本地的action
// src/store/sagas/cart.saga.js
import { takeEvery, put } from 'redux-saga/effects'
import axios from 'axios'
import { addProductToCart, addProductToLocalCart, loadCarts, savaCarts } from './../actions/cart.actions'


function* handleAddProductToCart(action){
   const { data } = yield axios.post('http://localhost:3005/cart/add', {gid: action.payload})
   yield put(addProductToLocalCart(data))
}

function* handleLoadCarts (){
    const { data } = yield axios.get('http://localhost:3005/cart')
    yield put(savaCarts(data))
}

export default function* cartSata () {
    yield takeEvery(addProductToCart, handleAddProductToCart)
    yield takeEvery(loadCarts, handleLoadCarts)
}
  1. 在cart.reducer.js 中处理保存到本地的action
// src/store/reducers/cart.reducer.js
import {  handleActions as crateReducer } from 'redux-actions'
import { addProductToLocalCart, savaCarts } from './../actions/cart.actions'

const initialSate = []

const handleAddProductToLocalCart = (state, action) => {......}}

const handleSaveCarts = (state, action) => {
    return action.payload
}

export default crateReducer({
    [addProductToLocalCart]: handleAddProductToLocalCart,
    [savaCarts]: handleSaveCarts
}, initialSate)
  1. 在购物车组件的钩子函数中触发获取购物车数据的action,并绑定数据到视图上
import React, { Component } from 'react'
import { connect } from 'react-redux'
import { bindActionCreators } from 'redux'
import * as cartActions from './../store/actions/cart.actions'

class Cart extends Component {
    componentDidMount() {
        const { loadCarts } = this.props
        // 拉取服务器购物车数据
        loadCarts()
    }
    render() {
        const { carts } = this.props
        return (
          ......
           {
                    carts.map(product => (
                        <div className="cart-row" key={product.id}>
                            <div className="cart-item cart-column">
                                <img className="cart-item-image" src={product.thumbnail} alt='123' width="100" height="100" />
                                <span className="cart-item-title">{product.title}</span>
                            </div>
                            <span className="cart-price cart-column">¥{product.price}</span>
                            <div className="cart-quantity cart-column">
                                <input className="cart-quantity-input" onChange={()=>{}} type="number" value={product.count} />
                                <button className="btn btn-danger" type="button">删除</button>
                            </div>
                        </div>
                    ))
                }


          ......
        )
    }
}


const mapStateToProps = state => {
    return {
        carts: state.carts
    }
}

const mapDispatchToProps = dispatch => ({
    ...bindActionCreators(cartActions, dispatch)
})

export default connect(mapStateToProps, mapDispatchToProps)(Cart)

从购物车中删除商品

  1. 创建告诉服务器删除哪一个商品和删除本地购物车的aciton
// src/store/actions/cart.actions.js
// 告诉服务器删除哪一个商品
export const deleteProductFormCart = createAction('deleteProductFormCart')
// 删除本地购物车中的数据
export const deleteProductFormLocalCart = createAction('deleteProductFormLocalCart')
  1. 因为在告诉服务器删除哪一个商品的是异步操作的所以我们需要在saga中处理aciton,处理完成后触发更改本地购物车的action
// src/store/sagas/cart.saga.js
......
function* handleDeleteProductFormCart(action){
   const { data } = yield axios.delete('http://localhost:3005/cart/delete', {params: {cid: action.payload}})
   yield put(deleteProductFormLocalCart(data))
}

export default function* cartSata () {
    yield takeEvery(addProductToCart, handleAddProductToCart)
    yield takeEvery(loadCarts, handleLoadCarts)
    yield takeEvery(deleteProductFormCart, handleDeleteProductFormCart)
}
  1. 在reducer中处理更改本地购物车的action
// src/store/reducers/cart.reducer.js
......
const handleDeleteProductFormLocalCart =  (state, action) => {
    const newState = JSON.parse(JSON.stringify(state))
    newState.splice(action.payload, 1)
    return newState
}

export default crateReducer({
    [addProductToLocalCart]: handleAddProductToLocalCart,
    [savaCarts]: handleSaveCarts,
    [deleteProductFormLocalCart]: handleDeleteProductFormLocalCart
}, initialSate)
  1. 在视图中触发从服务器拉去购物车数据的action并绑定删除事件触发action
// src/components/cart.js
...... 
componentDidMount() {
  const { loadCarts } = this.props
  // 拉取服务器购物车数据
  loadCarts()
}
......
 render() {
   const { carts, deleteProductFormCart } = this.props
   return    (
           ......
          <button className="btn btn-danger" type="button" onClick={()=>{deleteProductFormCart(product.id)}}>删除</button>
         ......
   ) 
}

为什么我们每次在reducer都拷贝一次state呢 因为这是redux要求我们不要动原数据

更改购物车中商品数量

  1. 新增告诉服务器改变哪一个商品数量和改变本地商品数量action
// src/store/actions/cart.actions.js
// 发送请求 告诉将哪一个商品数量更改
......
export const changeServiceProductNumber = createAction('changeProductNumber')
export const changeLocalProductNumber = createAction('changeLocalProductNumber')
  1. 因为告诉服务器改变那个商品是异步操作所以需要在saga中处理aciton
// src/store/sagas/cart.saga.js
.....
function* handleChangeServiceProductNumber(action){
    const { data } = yield axios.put('http://localhost:3005/cart', action.payload)  
    yield put(changeLocalProductNumber(data))
}

export default function* cartSata () {
    yield takeEvery(addProductToCart, handleAddProductToCart)
    yield takeEvery(loadCarts, handleLoadCarts)
    yield takeEvery(deleteProductFormCart, handleDeleteProductFormCart)
    yield takeEvery(changeServiceProductNumber, handleChangeServiceProductNumber)
}
  1. 在reduce中处理更改本地商品数据
// src/store/reducers/cart.reducer.js
......
const handleChangeLocalProductNumber = (state, action) => {
    const newState = JSON.parse(JSON.stringify(state))
    const product = newState.find( product =>  product.id === action.payload.id)

    product.count = action.payload.count

    return newState
}

export default crateReducer({
    [addProductToLocalCart]: handleAddProductToLocalCart,
    [savaCarts]: handleSaveCarts,
    [deleteProductFormLocalCart]: handleDeleteProductFormLocalCart,
    [changeLocalProductNumber]: handleChangeLocalProductNumber
}, initialSate)
  1. 在视图中绑定更改商品数量发送给服务器事件
// src/components/cart.js
......
changeProductNumber(cid, event){
    const { changeServiceProductNumber } = this.props
    // 获取最新的商品数量
    const count = event.target.value

    changeServiceProductNumber({ cid, count})
}
render () {
  return (
    ......
      <input className="cart-quantity-input" onChange={e => this.changeProductNumber(product.id, e)} type="number" value={product.count} />
    ......
}

计算商品总价

<div className="cart-total">
  <strong className="cart-total-title">总价</strong>
  <span className="cart-total-price">¥{
    carts.reduce((total, product) => {
      return total+= product.count * product.price
    }, 0)
  }</span>
</div>