笔记来源:拉勾教育 - 大前端就业集训营
文章内容:学习过程中的笔记、感悟、和经验
提示:项目实战类文章资源无法上传,仅供参考
Redux案例
课程提供:
- shoppingCart - 静态资源
- shoppingCartService - 服务端项目,本地启动即可
项目搭建
- 命令行创建项目:
create-react-app 项目名 - 安装费服务端项目依赖:
npm i - 启动服务端:
npm start - 初始化项目:清理我们不需要的文件、修改代码、创建我们需要的文件结构
- 创建组件并引用:商品列表组件、购物车列表组件(使用类组件)
- 使用结构:使用课程提供的HTML文件中的文件结构复制给两个组件
- 引入CSS文件:引入课程提供的CSS文件
- 引入图片:使用课程提供的资源文件,添加到项目中
// src/components/list.js 商品列表组件,直接使用提供的HTML,后期更改
import React, { Component } from 'react'
// 商品列表
export class List 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' alt='' />
<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' alt='' />
<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' alt='' />
<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' alt='' />
<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>
</>
)
}
}
export default List
// src/components/cart.js 购物车组件,直接使用提供的HTML,后期更改
import React, { Component } from 'react'
// 购物车
export class Cart extends Component {
render() {
return (
<>
<section class='container content-section'>
<h2 class='section-header'>购物车</h2>
<div class='cart-row'>
<span class='cart-item cart-header cart-column'>商品</span>
<span class='cart-price cart-header cart-column'>价格</span>
<span class='cart-quantity cart-header cart-column'>数量</span>
</div>
<div class='cart-items'>
<div class='cart-row'>
<div class='cart-item cart-column'>
<img
class='cart-item-image'
src='images/01.webp'
width='100'
height='100'
alt=''
/>
<span class='cart-item-title'>
小户型简约现代网红双人三人客厅科技布免洗布艺
</span>
</div>
<span class='cart-price cart-column'>¥1020</span>
<div class='cart-quantity cart-column'>
<input class='cart-quantity-input' type='number' />
<button class='btn btn-danger' type='button'>
删除
</button>
</div>
</div>
<div class='cart-row'>
<div class='cart-item cart-column'>
<img
class='cart-item-image'
src='images/02.webp'
width='100'
height='100'
alt=''
/>
<span class='cart-item-title'>11全网通4G手机官方iPhonexr</span>
</div>
<span class='cart-price cart-column'>¥4758</span>
<div class='cart-quantity cart-column'>
<input class='cart-quantity-input' type='number' />
<button class='btn btn-danger' type='button'>
删除
</button>
</div>
</div>
</div>
<div class='cart-total'>
<strong class='cart-total-title'>总价</strong>
<span class='cart-total-price'>¥39.97</span>
</div>
</section>
</>
)
}
}
export default Cart
// src/components/App.js 根组件,引入两个子组件
import Cart from './cart'
import List from './list'
// app根组件
function App() {
return (
<div>
{/* 商品列表 */}
<List />
{/* 购物车 */}
<Cart />
</div>
)
}
export default App
搭建redux工作流
- 安装redux、react-redux、redux-saga、redux-actions依赖:
npm i redux react-redux redux-saga redux-actions -D
- 新建store目录,存放redux相关文件
- 创建action(指令)、reducer、saga目录和index.js(store)文件
- 创建store仓库,并创建reducer(合并 - 不允许为空,先创建一个)
- 将store传递给App组件
- 组件接收reducer
// src/store/index.js 创建store
// import { createStore } from 'redux'
import rootReducer from './reducer/rootReducer'
// 创建 store 仓库
const store = createStore(rootReducer)
export default store
// src/store/reducer/rootReducer.js 创建文件合并 reducer
import { combineReducers } from 'redux'
import listReducer from './listReducer'
// reducer 合并
const rootReducer = combineReducers({
// 商品列表
list: listReducer,
})
export default rootReducer
// src/store/reducer/listReducer.js 创建商品列表 reducer
import { handleActions } from 'redux-actions'
// 商品列表初始数据
const list = []
// 商品列表 reducer
const listReducer = handleActions({}, list)
export default listReducer
// src/index.js 入口文件获取 store 并向下传递
import React from 'react'
import ReactDOM from 'react-dom'
import App from './components/App'
import { Provider } from 'react-redux'
import './style.css'
import store from './store'
ReactDOM.render(
// 传递 store
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
)
// src/components/list.js 组件尝试获取状态
import React, { Component } from 'react'
import { connect } from 'react-redux'
// 商品列表
export class List extends Component {
render() {
// 打印状态看一下
console.log(this.props)
return (
<>
<section className='container content-section'>
<h2 className='section-header'>商品列表</h2>
<div className='shop-items'>
<div className='shop-item'>
<img className='shop-item-image' src='images/01.webp' alt='' />
<span className='shop-item-title'>
小户型简约现代网红双人三人客厅科技布免洗布艺
</span>
<div className='shop-item-details'>
<span className='shop-item-price'>¥1020</span>
<button
className='btn btn-primary shop-item-button'
type='button'>
加入购物车
</button>
</div>
</div>
<div className='shop-item'>
<img className='shop-item-image' src='images/02.webp' alt='' />
<span className='shop-item-title'>
11全网通4G手机官方iPhonexr
</span>
<div className='shop-item-details'>
<span className='shop-item-price'>¥4758</span>
<button
className='btn btn-primary shop-item-button'
type='button'>
加入购物车
</button>
</div>
</div>
<div className='shop-item'>
<img className='shop-item-image' src='images/03.webp' alt='' />
<span className='shop-item-title'>
潮休闲网红小西服套装英伦风春装
</span>
<div className='shop-item-details'>
<span className='shop-item-price'>¥59</span>
<button
className='btn btn-primary shop-item-button'
type='button'>
加入购物车
</button>
</div>
</div>
<div className='shop-item'>
<img className='shop-item-image' src='images/04.webp' alt='' />
<span className='shop-item-title'>
夏新27英寸超薄曲面高清电脑
</span>
<div className='shop-item-details'>
<span className='shop-item-price'>¥369</span>
<button
className='btn btn-primary shop-item-button'
type='button'>
加入购物车
</button>
</div>
</div>
</div>
</section>
</>
)
}
}
// 获取状态
const data = state => ({
list: state.list,
})
export default connect(data)(List)
展示商品列表数据
需要安装axios
创建指令 - 创建reducer - 合并reducer(已实现)- 创建saga中间件 - 创建saga - 合并saga - 组件触发指令
- 需要准备两个action指令,并且书写saga中间件拦截指令进行数据请求
- 组件需要获取指令和名称
- 组件挂载完成后立即自动获取数据
- 引入并创建saga中间件,合并需要使用的saga,并让它运行起来
- 使用接口文档查看接口信息
- 组件获取数据后进行遍历绑定数据
// src/store/action/listAction.js 新建文件 - 创建指令
import { createAction } from 'redux-actions'
// 获取商品列表
export const loadList = createAction('loadList')
// 保存商品列表
export const loadList_save = createAction('loadList_save')
// src/store/reducer/listReducer.js 书写保存商品列表指令逻辑
import { handleActions } from 'redux-actions'
import { loadList_save } from '../action/listAction'
// 商品列表初始数据
const list = []
// 商品列表 reducer
const listReducer = handleActions(
{
// 保存商品列表
[loadList_save]: (state, action) => action.payload,
},
list
)
export default listReducer
// src/store/saga/rootSage.js 新建文件 - saga合并
import { all } from 'redux-saga/effects'
import listSaga from './listSaga'
// 合并saga
export default function* rootSage() {
yield all([listSaga()])
}
// src/store/saga/listSaga.js 新建文件 - 创建list相关saga
import { takeEvery, put } from 'redux-saga/effects'
import { loadList_save } from '../action/listAction'
import axios from 'axios'
// 拦截获取列表指令后回调函数
function* handelLoadList() {
const { data } = yield axios.get('http://localhost:3005/goods')
yield put(loadList_save(data))
}
// 拦截指令
export default function* listSaga() {
// 拦截获取商品列表指令
yield takeEvery('loadList', handelLoadList)
}
// src/store/index.js store添加中间件
import { createStore, applyMiddleware } from 'redux'
import rootReducer from './reducer/rootReducer'
import createSaga from 'redux-saga'
import rootSage from './saga/rootSage'
// 创建saga
const saga = createSaga()
// 创建 store 仓库,使用 saga 中间件
const store = createStore(rootReducer, applyMiddleware(saga))
// 执行saga中间件
saga.run(rootSage)
export default store
// src/components/list.js list组件挂载后触发指令获取数据,并遍历数据
import React, { Component } from 'react'
import { connect } from 'react-redux'
import { bindActionCreators } from 'redux'
import * as listAction from '../store/action/listAction'
// 商品列表
export class List extends Component {
// 钩子 - 挂载完成后获取商品列表
componentDidMount() {
this.props.loadList()
}
render() {
return (
<>
<section className='container content-section'>
<h2 className='section-header'>商品列表</h2>
<div className='shop-items'>
{/* 遍历商品列表 */}
{this.props.list.map(item => (
<div key={item.id} className='shop-item'>
<img
className='shop-item-image'
// 商品图片通过接口获得
src={`http://localhost:3005${item.thumbnail}`}
alt=''
/>
<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>
</>
)
}
}
// 获取状态
const data = state => ({
list: state.list,
})
// 获取指令
const action = dispatch => ({
...bindActionCreators(listAction, dispatch),
})
export default connect(data, action)(List)
将商品加入购物车
创建指令 - 创建reducer - 合并reducer - 创建saga中间件 - 创建saga - 合并saga - 组件触发指令
- 接口信息查看文档
- list组件触发添加购物车指令,购物车组件获取购物车列表
- 添加时需要进行判断,当前购物车数据是否已经存在这个商品,没有则添加,有则让数量+1
// src/store/action/cartAction.js 新建文件 - 创建指令(购物车相关)
import { createAction } from 'redux-actions'
// 购物车添加商品
export const addCart = createAction('addCart')
// 添加商品后保存到本地
export const addCart_save = createAction('addCart_save')
// src/store/reducer/cartReducer.js 新建文件 - 创建购物车reducer
import { handleActions } from 'redux-actions'
import { addCart_save } from '../action/cartAction'
// 购物车初始状态
const cart = []
// 向本地购物车添加数据
const addCart_save_handle = (state, action) => {
const newState = JSON.parse(JSON.stringify(state))
const my = newState.find(item => item.id === action.payload.id)
if (my) {
my.count += 1
} else {
newState.push(action.payload)
}
return newState
}
const cartReducer = handleActions(
{
// 向本地购物车添加数据
[addCart_save]: addCart_save_handle,
},
cart
)
export default cartReducer
// src/store/reducer/rootReducer.js 合并reducer
import { combineReducers } from 'redux'
import cartReducer from './cartReducer'
import listReducer from './listReducer'
// reducer 合并
const rootReducer = combineReducers({
// 商品列表
list: listReducer,
cart: cartReducer,
})
export default rootReducer
// src/store/saga/cartSaga.js 新建文件 - 常见购物车相关saga
import axios from 'axios'
import { takeEvery, put } from 'redux-saga/effects'
import { addCart_save } from '../action/cartAction'
// 添加购物车指令拦截后回调函数
function* handleaddCart(action) {
const { data } = yield axios.post('http://localhost:3005/cart/add', {
gid: action.payload,
})
yield put(addCart_save(data))
}
// 拦截指令
export default function* cartSaga() {
// 拦截添加购物车指令
yield takeEvery('addCart', handleaddCart)
}
// src/store/saga/rootSage.js 合并saga
import { all } from 'redux-saga/effects'
import cartSaga from './cartSaga'
import listSaga from './listSaga'
// 合并saga
export default function* rootSage() {
yield all([listSaga(), cartSaga()])
}
// src/components/list.js list组件获取购物车相关指令,并在点击添加购物车按钮时触发指令
import React, { Component } from 'react'
import { connect } from 'react-redux'
import { bindActionCreators } from 'redux'
import * as listAction from '../store/action/listAction'
import * as cartAction from '../store/action/cartAction'
// 商品列表
export class List extends Component {
// 钩子 - 挂载完成后获取商品列表
componentDidMount() {
this.props.loadList()
}
render() {
return (
<>
<section className='container content-section'>
<h2 className='section-header'>商品列表</h2>
<div className='shop-items'>
{/* 遍历商品列表 */}
{this.props.list.map(item => (
<div key={item.id} className='shop-item'>
<img
className='shop-item-image'
// 商品图片通过接口获得
src={`http://localhost:3005${item.thumbnail}`}
alt=''
/>
<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={() => this.props.addCart(item.id)}>
加入购物车
</button>
</div>
</div>
))}
</div>
</section>
</>
)
}
}
// 获取状态
const data = state => ({
list: state.list,
})
// 获取指令
const action = dispatch => ({
...bindActionCreators(listAction, dispatch),
// 将购物车相关指令也添加进来
...bindActionCreators(cartAction, dispatch),
})
export default connect(data, action)(List)
// src/components/cart.js 购物车组件获取数据和指令
import React, { Component } from 'react'
import { bindActionCreators } from 'redux'
import { connect } from 'react-redux'
import * as cartAction from '../store/action/cartAction'
// 购物车
export class Cart extends Component {
render() {
return (
<>
{/* 打印到页面看一下 */}
<p>{JSON.stringify(this.props.cart)}</p>
<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'
width='100'
height='100'
alt=''
/>
<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'
width='100'
height='100'
alt=''
/>
<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>
</>
)
}
}
// 获取数据
const data = store => ({
cart: store.cart,
})
// 获取指令
const action = dispatch => ({
...bindActionCreators(cartAction, dispatch),
})
export default connect(data, action)(Cart)
购物车列表展示
直接在上面创建文件中书写即可
两个指令:获取购物车列表数据,将数据同步到本地购物车列表中
reducer书写初始化reducer逻辑
saga添加一条拦截逻辑
购物车组件挂载后立即出发获取购物车列表数据
获取数据后遍历数据创建结构并绑定数据
从购物车删除商品
依旧直接在上面创建文件中书写即可
两个指令:删除商品,删除本地购物车中商品
点击删除按钮触发删除指令
saga添加拦截删除指令逻辑
服务器端删除商品后会把索引返回,我们直接使用这个索引删除本地购物车即可
更改购物车商品数量
逻辑同上
购物车商品数量发生变化触发修改指令
saga拦截指令后请求接口修改
修改后接口会给我们返回修改后的商品信息
同步到本地购物车中
更正视图图片显示问题
修改图片地址为服务器地址
计算商品总价
直接使用购物车商品数据,计算商品总价格
以上四个写在一块
// src/store/action/cartAction.js 添加指令
import { createAction } from 'redux-actions'
// 购物车添加商品
export const addCart = createAction('addCart')
// 添加商品后保存到本地
export const addCart_save = createAction('addCart_save')
// 获取购物车数据
export const loadcart = createAction('loadcart')
export const loadcart_save = createAction('loadcart_save')
// 删除商品
export const deletecart = createAction('deletecart')
export const deletecart_save = createAction('deletecart_save')
// 修改商品数量
export const alterCountCart = createAction('alterCountCart')
export const alterCountCart_save = createAction('alterCountCart_save')
// src/store/reducer/cartReducer.js 添加指令相关逻辑
import { handleActions } from 'redux-actions'
import {
addCart_save,
loadcart_save,
deletecart_save,
alterCountCart_save,
} from '../action/cartAction'
// 购物车初始状态
const cart = []
// 向本地购物车添加数据
const addCart_save_handle = (state, action) => {
const newState = JSON.parse(JSON.stringify(state))
const my = newState.find(item => item.id === action.payload.id)
if (my) {
my.count += 1
} else {
newState.push(action.payload)
}
return newState
}
// 删除商品
const deleteCart_save_handle = (state, action) => {
const newState = JSON.parse(JSON.stringify(state))
newState.splice(action.payload, 1)
return newState
}
// 修改商品数量
const alterCountCart_save_handle = (state, action) => {
const newState = JSON.parse(JSON.stringify(state))
const my = newState.find(item => item.id === action.payload.id)
my.count = action.payload.count
return newState
}
const cartReducer = handleActions(
{
// 向本地购物车添加数据
[addCart_save]: addCart_save_handle,
[loadcart_save]: (state, action) => action.payload,
// 删除商品
[deletecart_save]: deleteCart_save_handle,
// 修改商品数量
[alterCountCart_save]: alterCountCart_save_handle,
},
cart
)
export default cartReducer
// src/store/saga/cartSaga.js 添加其他指令拦截saga逻辑
import axios from 'axios'
import { takeEvery, put } from 'redux-saga/effects'
import {
addCart_save,
loadcart_save,
deletecart_save,
alterCountCart_save,
} from '../action/cartAction'
// 添加购物车指令拦截后回调函数
function* handleaddCart(action) {
const { data } = yield axios.post('http://localhost:3005/cart/add', {
gid: action.payload,
})
yield put(addCart_save(data))
}
//拦截货物购物车商品指令后的回调函数
function* handleloadcart() {
const { data } = yield axios.get('http://localhost:3005/cart')
yield put(loadcart_save(data))
}
// 拦截删除购物车商品后的回调函数
function* handledeletecart(action) {
const { data } = yield axios.delete('http://localhost:3005/cart/delete', {
params: { cid: action.payload },
})
yield put(deletecart_save(data.index))
}
// 拦截修改商品数量指令的回调函数
function* handlealterCountCart(action) {
const { data } = yield axios.put('http://localhost:3005/cart', {
...action.payload,
})
yield put(alterCountCart_save(data))
}
// 拦截指令
export default function* cartSaga() {
// 拦截添加购物车指令
yield takeEvery('addCart', handleaddCart)
// 拦截获取购物车商品指令
yield takeEvery('loadcart', handleloadcart)
// 拦截删除商品指令
yield takeEvery('deletecart', handledeletecart)
// 拦截修改商品数量指令
yield takeEvery('alterCountCart', handlealterCountCart)
}
// src/components/cart.js 购物车添加指令触发方法、计算总价等逻辑
import React, { Component } from 'react'
import { bindActionCreators } from 'redux'
import { connect } from 'react-redux'
import * as cartAction from '../store/action/cartAction'
// 购物车
export class Cart extends Component {
// 钩子 - 挂载完成后获取购物车商品
componentDidMount() {
this.props.loadcart()
}
render() {
// 商品数量修改事件
const countChange = (id, e) => {
this.props.alterCountCart({ cid: id, count: e.target.value })
}
// 计算商品总价
const Totalprice = () => {
return this.props.cart.reduce((total, item) => {
return (total += item.price * item.count)
}, 0)
}
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'>
{this.props.cart.map(item => (
<div key={item.id} className='cart-row'>
<div className='cart-item cart-column'>
<img
className='cart-item-image'
// 使用图片地址
src={`http://localhost:3005${item.thumbnail}`}
width='100'
height='100'
alt=''
/>
<span className='cart-item-title'>{item.title}</span>
</div>
<span className='cart-price cart-column'>¥{item.price}</span>
<div className='cart-quantity cart-column'>
<input
className='cart-quantity-input'
type='number'
value={item.count}
// 商品数量修改事件
onChange={e => countChange(item.id, e)}
/>
<button
className='btn btn-danger'
type='button'
// 删除按钮点击事件
onClick={() => this.props.deletecart(item.id)}>
删除
</button>
</div>
</div>
))}
</div>
<div className='cart-total'>
<strong className='cart-total-title'>总价</strong>
{/* 直接调用方法计算总价 */}
<span className='cart-total-price'>¥{Totalprice()}</span>
</div>
</section>
</>
)
}
}
// 获取数据
const data = store => ({
cart: store.cart,
})
// 获取指令
const action = dispatch => ({
...bindActionCreators(cartAction, dispatch),
})
export default connect(data, action)(Cart)