shopping
用 redux+react 实现购物车案例
项目初始化
- react脚手架搭建项目
create-react-app shopping
- 删除多余文件,创建购物车与商品列表组件,并显示在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工作流
- 下载 redux react-redux redux-saga redux-actions
npm install redux react-redux redux-saga redux-actions --save
- 创建 store文件夹,创建actions、reducers、sagas、文件夹
mkdir -p src/store/{actions,reducers,sagas}
- 创建index.js 并使用createStore方法创建store
import { createStore } from 'redux'
export const store = createStore()
- 在创建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)
- 我们 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
})
- 把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')
);
- 在组件中去访问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中
- 创建 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')
- 让组件调用去调用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)
- 应为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))
- 创建合并 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()
])
}
- 调用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)
- 在模版中渲染列表数据
// 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
- 新建 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')
- 因为添加到购物车服务器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()
])
}
- 在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
})
- 这个时候我们的初步逻辑已经执行完成,现在在商品列表组件中去绑定事件执行添加到购物车中操作
// 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)
购物车列表数据展示
- 创建从服务器拉去购物车数据action和把数据保存到本地store中的action
// src/store/actions/cart.actions.js
// 发送求情获取购物车数据
export const loadCarts = createAction('loadCarts')
// 同步服务器返回的购物车数据
export const savaCarts = createAction('savaCarts')
- 因为从服务器拉去数据是异步操作,所以我们需要在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)
}
- 在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)
- 在购物车组件的钩子函数中触发获取购物车数据的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)
从购物车中删除商品
- 创建告诉服务器删除哪一个商品和删除本地购物车的aciton
// src/store/actions/cart.actions.js
// 告诉服务器删除哪一个商品
export const deleteProductFormCart = createAction('deleteProductFormCart')
// 删除本地购物车中的数据
export const deleteProductFormLocalCart = createAction('deleteProductFormLocalCart')
- 因为在告诉服务器删除哪一个商品的是异步操作的所以我们需要在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)
}
- 在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)
- 在视图中触发从服务器拉去购物车数据的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要求我们不要动原数据
更改购物车中商品数量
- 新增告诉服务器改变哪一个商品数量和改变本地商品数量action
// src/store/actions/cart.actions.js
// 发送请求 告诉将哪一个商品数量更改
......
export const changeServiceProductNumber = createAction('changeProductNumber')
export const changeLocalProductNumber = createAction('changeLocalProductNumber')
- 因为告诉服务器改变那个商品是异步操作所以需要在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)
}
- 在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)
- 在视图中绑定更改商品数量发送给服务器事件
// 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>