携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第1天,点击查看活动详情
前言
逮嘎猴!我是薛定谔的咖啡猫!一晃而过已入盛夏,凡人修React|新手练气筑基必备! 抖音商城[商品信息卡组件]开发也是一月前的。这段时间本人在修React之路上又喜获Redux它是应用的状态容器,提供可预测的状态管理,以及ReduxToolKit(RTK),可以简化配置store、创建reducer等,是编写Redux应用更加简便。本文重点也在Redux的使用,就稍微介绍一下Redux的重点。
Redux的基本术语
-
Actions
action是一个具有type
字段的普通 JavaScript 对象。你可以将 action 视为描述应用程序中发生了什么的事件。const addTodoAction = { type: 'todos/todoAdded', payload: 'Buy milk' }
-
Reducers reducer是一个函数,接收当前的
state
和一个action
对象,必要时决定如何更新状态,并返回新状态。不妨将 reducer 视为一个事件监听器,它根据接收到的 action(事件)类型处理事件。 -
Store Store 就是存储着应用的 state tree 的对象。
-
Dispatch Redux store 有一个方法叫
dispatch
。更新 state 的唯一方法是调用store.dispatch()
并传入一个 action 对象。 store 将执行所有 reducer 函数并计算出更新后的 state
Reudx的三大原则
- 全局应用状态保存在单个 store 中
- store 中的 state 是只读的
- Reducer 函数用于更新状态以响应 actions
Redux的单向数据流
- State 描述了应用程序在某个时间点的状态,UI 基于该状态渲染
- 当应用程序中发生某些事情时:
- UI dispatch 一个 action
- store 调用 reducer,随后根据发生的事情来更新 state
- store 通知 UI state 发生了变化
- UI 基于新 state 重新渲染
动画的方式来表达数据流更新:
含有异步逻辑时:
技术栈
- React:声明式、组件化的构建用户界面
- React Router:保持 UI 与 URL 同步
- Redux:应用的状态容器,提供可预测的状态管理。使用React-Redux与其绑定
- Reudx ToolKit:包含了Redux核心,官方推荐的编写Redux逻辑的方法,简化了大多数Redux任务
- Axios:是一个基于promise的网络请求库
- react-lazyload:React的懒加载库
- antd-mobile:蚂蚁金融团队推出开源组件库
项目构建
路由配置
路由封装进行统一管理,并搭配Lzay
和Suspense
实现页面懒加载,使得页面按需加载。
在/src/routes/index.js
中:
import React,{lazy} from 'react'
import {Routes,Route,Navigate} from 'react-router-dom'
import Home from '@/pages/Home'
const Orders =lazy(()=>import('@/pages/Orders'))
const Purchase =lazy(()=>import('@/pages/Purchase'))
const Cart=lazy(()=>import('@/pages/Cart'))
export default function RoutesConfig() {
return (
<Routes>
<Route path='/' element={<Navigate to={"/home"} replace />} />
<Route path='/home' element={<Home/>} />
<Route path='/orders' element={<Orders />} />
<Route path='/purchase/:id' element={<Purchase/>}></Route>
<Route path='/cart' element={<Cart/>}></Route>
</Routes>
)
}
在APP.jsx中加上Suspense
import { Suspense} from 'react'
import RoutesConfig from "./routes"
import Loading from './components/Loading'
function App() {
return (
<div className="App">
<Suspense fallback={<Loading/>}>
<RoutesConfig />
</Suspense>
</div>
)
}
export default App
接口配置
网络请求数量多,这时 axios 使用单例模式。每个请求域名+端口部分,没必要重复,可以配置到baseUr中,并设置拦截器。 在/api/config.js中:
import axios from "axios"
export const baseUrl = 'https://www.fastmock.site/mock/efb99b7bee0fe1e8fc2887e9a7d16759/douyin_mall';
const axiosInstance = axios.create({
baseURL: baseUrl
})
axiosInstance.interceptors.response.use(
res => res.data,
err => {
console.log(err, '网络错误')
}
)
export { axiosInstance }
在/api/request.js中:
import { axiosInstance } from "./config";
export const getGoodsInfoRequest = () =>
axiosInstance.get('/goodsInfo')
export const getOrdersInfoRequest = () =>
axiosInstance.get('/ordersInfo')
export const getCartInfoRequest = () =>
axiosInstance.get('/cartInfo')
Redux 配置
本文使用了ReduxToolKit,其包含了Redux的核心,仍然是Redux的思想与逻辑。只是有了简化操作的API。
-
在/store/index.js中创建store
// 使用configureStore创建Store import { configureStore } from "@reduxjs/toolkit"; import goodsInfoSlice from "./features/goodsInfoSlice"; import cartInfoSlice from "./features/cartInfoSlice"; import ordersInfoslice from "./features/ordersInfoslice"; // 调用configureStore创建一个store,这个函数是对Redux的 // createStore函数的一个封装,它接受一个对象,对象里包括: // reducer:接收单个reducer函数或者包含若干reducer函数的对象 // middleware?:接收Redux中间件函数数组,默认使用了react-thunk // devTools?:决定是否开启对Redux DevTools浏览器插件的支持 // preloadedState?:传给Redux的createStore函数中同名 // enhancers?:接收Redux store enhancer数组 const store =configureStore({ reducer:{ goodsInfo:goodsInfoSlice, cartInfo:cartInfoSlice, ordersInfo:ordersInfoslice } }) // 默认导出store export default store
Slice的创建以goodsInfoSlice为例
-
在/store/features/goodsInfoSlice.js 创建Slice
并使用createAsyncThunk创建异步函数,在Slice中的extraReducers中执行// 通过createSlice 创建Slice import { createSlice ,createAsyncThunk} from "@reduxjs/toolkit"; import { getGoodsInfoRequest } from "@/api/request"; // 使用createAsyncThunk创建异步action // 该方法触发时会有三种状态 // pending(进行中) fulfilled(成功),rejected(失败) export const getGoodsInfo=createAsyncThunk('goodsInfo/getGoodsInfo', //type // data async ()=>{ // console.log(await getGoodsInfoRequest()) return await getGoodsInfoRequest() // return data; } ) // createSlice为某一个类型的action创建了一个切片, // 将这个action相关的信息都归集到了一起,更便于维护 export const goodsInfoSlice=createSlice({ // slice的名称 name:'goodsInfo', // state的初始值 initialState, // 一个包含了action的对象,每一个key都会生成一个actions //(相当于原生redux的Switch Case写法) reducers:{ // toolkit内部调用了immer包,我们可以 //直接对state对象做修改,不用解构旧的state }, // extraReducers 字段让 slice 处理在别处定义的 actions, // 包括由 createAsyncThunk 或其他slice生成的actions。 extraReducers(builder){ builder .addCase(getGoodsInfo.pending,(state)=>{ console.log('请求数据中。。。'); // state.loading=false; }) .addCase(getGoodsInfo.fulfilled,(state,{payload})=>{ console.log('请求数据完成',payload); state=Object.assign(state,payload) state.loading=true; }) .addCase(getGoodsInfo.rejected,(state,err)=>{ console.log('请求数据失败',err) }) } } ) // 导出action ->reducers里方法 // export const {}=goodsInfoSlice.actions // 默认导出reducer export default goodsInfoSlice.reducer
三个主要页面
购物车页面
Checked的状态实现
购物车的选中有单个选中
,店铺内全选
,购物车全选
,其中全选状态的改变都由单个选中,即购物车中被选中的个数来决定。
-
在CartInfoSlice中添加相应的action
// 改变单件商品的checked状态 changeChecked:(state,{payload})=>{ state.buyGoods.byId[payload.id].checked=!state.buyGoods.byId[payload.id].checked; }, // 改变某个店铺的checked状态 实际是改变单件商品的checked状态 changeShopAllChecked:(state,{payload})=>{ // 获取目前是否全选 const isShopAllChecked= (payload.shop.buyGoods.length == payload.shop.buyGoods.filter((item)=> payload.buyGoods.byId[item].checked).length); // console.log(isShopAllChecked); payload.shop.buyGoods.map((item)=> // 将checked设置为目前全选状态的非 state.buyGoods.byId[item].checked=!isShopAllChecked); }, // 改变所有的Checked状态 实际是改变单件商品的checked状态 changeAllChecked:(state,{payload})=>{ // 获取目前是否全选 const isAllChecked= (payload.buyGoods.allIds.length == payload.buyGoods.allIds.filter((item)=> payload.buyGoods.byId[item].checked).length); payload.buyGoods.allIds.map((item)=> // 将checked设置为目前全选状态的非 state.buyGoods.byId[item].checked=!isAllChecked) },
-
在Cart/CartListItem/index.jsx中控制单个Checked和店铺Checked
// 使用useDispatch来派发action import { useDispatch } from 'react-redux' // 引入cart的action import { increcount , decrecount, changeChecked, changeShopAllChecked } from '@/store/features/cartInfoSlice' ... <div className="checkbox_wrapper" onClick={()=> {dispatch(changeShopAllChecked({shop,buyGoods}))}} > {/* 通过buyGoods中的商品的checked的长度 与 总物品的长度 来驱动 该店铺的全选状态 */} <Checkbox checked={shop.buyGoods.length == shop.buyGoods.filter((item)=> buyGoods.byId[item].checked).length}/> </div> ... <div className="checkbox_wrapper" onClick={()=>{dispatch(changeChecked({id}))}} > <Checkbox checked={checked} /> </div> ...
-
在CarFooter中控制所有全选状态
... <div className="checkbox_wrapper" onClick={()=> {dispatch(changeAllChecked({shops,buyGoods}))}} > <Checkbox checked={buyGoods.allIds.length == buyGoods.allIds.filter((item)=> buyGoods.byId[item].checked).length && shops.allIds.length }> 全选 </Checkbox> </div> ...
删除选中的Item
-
在cartInfoSlice中添加删除的action
// 删除CartList中的内容 deleteCartList:(state,{payload})=>{ const checkedItemId=payload.buyGoods.allIds.filter((item)=> payload.buyGoods.byId[item].checked); // 过滤掉被删除的ItemId state.buyGoods.allIds=state.buyGoods.allIds.filter((item)=>!checkedItemId.includes(item)); // 再通过checkedItemId修改 buyGoods中Item 内容 checkedItemId.forEach((item)=>{ Reflect.deleteProperty(state.buyGoods.byId,item); }); // 修改shops中的buyGoods state.shops.allIds.forEach((item)=>{ // 过滤掉shops中buyGoods中被删除的ItemId state.shops.byId[item].buyGoods=state.shops.byId[item].buyGoods.filter((item)=>!checkedItemId.includes(item)); }); // 检查某个店铺购买的goods是否全被删除 state.shops.allIds.forEach((item)=>{ if(state.shops.byId[item].buyGoods.length===0){ state.shops.allIds=state.shops.allIds.filter((id)=>!(id===item)) // 删除该店铺的cartInfo Reflect.deleteProperty(state.shops.byId,item) } }) },
-
点击管理切换到删除按钮进行删除
<button className="button_settle" onClick={()=>{ dispatch(deleteCartList({shops,buyGoods})); Toast.show({ content: '删除成功' }) }} >删除</button>}
价格总计及结算
-
count数量的修改,在CartSlice 添加相应的action
... // 购买商品数量加1 increcount:(state,{payload})=>{ state.buyGoods.byId[payload.id].count+=1; }, //购买商品数量减1 decrecount:(state,{payload})=>{ state.buyGoods.byId[payload.id].count-=1; if(state.buyGoods.byId[payload.id].count<1){ // count不能小于1 state.buyGoods.byId[payload.id].count=1; } }, ...
-
总价根据count和Checked状态确定结算时会将要购买的商品加入到订单页面中,并删除购物车对应的数据
...
<div className="buy_total">
<span>合计:</span><span className="total">¥{
// 通过商品的checked状态来计算总价total 驱动UI状态
buyGoods.allIds.reduce((total,item)=>
total+(buyGoods.byId[item].checked ? parseFloat(
buyGoods.byId[item].price * buyGoods.byId[item].count):0),0)
}</span>
</div>}
...
onClick:()=>{
//来自orders的action
dispatch(addOrder({shops,buyGoods}))
dispatch(deleteCartList({shops,buyGoods}));
}
...
订单页面
删除订单
-
OrdersSlice中的action
... deleteOrder:(state,{payload})=>{ Reflect.deleteProperty(state.orders.byId,payload.order.id) state.orders.allIds=state.orders.allIds.filter((item)=>{return !(item === payload.order.id)}) }, addOrder:(state,{payload})=>{ const checkedItemId=payload.buyGoods.allIds.filter((item)=> payload.buyGoods.byId[item].checked); // 通过checkedItemId找出结算的goods和shop const checkedItem=checkedItemId.map((item)=>{return payload.buyGoods.byId[item]}) // 增加结算的Item checkedItem.map((item)=>{ state.buyGoods.allIds.push(item.id); state.buyGoods.byId[item.id]=item; }); checkedItem.map((item)=>{ if(!state.shops.allIds.includes(item.shopId)){ state.shops.allIds.push(item.shopId) // 增加新的shop信息 state.shops.byId[item.shopId]={ id:payload.shops.byId[item.shopId].id, name:payload.shops.byId[item.shopId].name, imgUrl:payload.shops.byId[item.shopId].imgUrl, } } }); const tempbuyGoods={}; checkedItem.forEach((item)=>{ if(!(item.shopId in tempbuyGoods)){ tempbuyGoods[item.shopId]=[item.id] }else{ tempbuyGoods[item.shopId]=[...tempbuyGoods[item.shopId],item.id] } }); for (let key in tempbuyGoods){ state.orders.byId['order'+key]={ id:'order'+key, shopId:key, buyGoods:tempbuyGoods[key], state:"待支付" } state.orders.allIds.push('order'+key); } } ...
-
订单页面删除订单
... onClick:()=>{ dispatch(deleteOrder({order})); Toast.show({ content: '删除成功' }) } ...
购买页面/商品详情页
- 购买页面显示选中不同型号的商品并购买加入购物车
- CartSlice中添加相应的action
...
addToCart:(state,{payload})=>{
console.log(payload.targetShopInfo,payload.buyGoodsInfo)
// 店铺信息处理
// 新增商品的所属店铺是否已有
// 如果没有 加上
if(!state.shops.allIds.includes(payload.targetShopInfo.id)){
state.shops.allIds.push(payload.targetShopInfo.id);
state.shops.byId[payload.targetShopInfo.id]=payload.targetShopInfo;
// 增加buygoods的key
state.shops.byId[payload.targetShopInfo.id]={
...state.shops.byId[payload.targetShopInfo.id],...{buyGoods:[]}};
}
// 商品信息处理
// 已有该商品信息
if(state.buyGoods.allIds.includes(payload.buyGoodsInfo.id)){
// count 叠加
state.buyGoods.byId[payload.buyGoodsInfo.id].count+=payload.buyGoodsInfo.count
}else{
// 没有 新增商品信息
state.buyGoods.allIds.push(payload.buyGoodsInfo.id);
state.buyGoods.byId[payload.buyGoodsInfo.id]=payload.buyGoodsInfo
// 将新增商品信息添加到该店铺下
state.shops.byId[payload.targetShopInfo.id].buyGoods.push(payload.buyGoodsInfo.id)
}
// 商品信息处理完毕
}
...
State的范式化处理
本文的State(数据)有一个嵌套情况的存在,如店铺信息中包含商品信息,商品信息又需要知道隶属于哪一个店铺,及商品信息包含店铺信息,订单信息中亦是如此。这就带来了一些问题:
- 当数据在多处冗余后,需要更新时,很难保证所有的数据都进行更新。
- 嵌套的数据意味着 reducer 逻辑嵌套更多、复杂度更高。尤其是在打算更新深层嵌套数据时。
- 不可变的数据在更新时需要状态树的祖先数据进行复制和更新,并且新的对象引用会导致与之 connect 的所有 UI 组件都重复 render。尽管要显示的数据没有发生任何改变,对深层嵌套的数据对象进行更新也会强制完全无关的 UI 组件重复 render
那么不妨将这部分视为数据库,作如下State的范式管理,以goodsInfo为例
...
{
goods:{
byId:{
1:{
id:1,
shopId:"mi111",
name:"RedmiNote11 4G手机",
imgUrl:"https://img14.360buyimg.com/n7/jfs/t1/189071/26/24872/250013/62870273E0be46c79/2be13b0a9f01f66f.jpg",
price:999,
types:{
"颜色":['黑色','蓝色','紫色'],
"存储":['8+128G','8+256G','12+256G'],
allTypes:["颜色","存储"]
}
},
2:{
id:2,
shopId:"mi111",
name:"ipad mini 平板电脑",
imgUrl:"https://img10.360buyimg.com/n7/jfs/t1/86940/3/26618/122396/6246970cE40f70e5f/fbfef3df69cf8217.jpg",
price:3599,
types:{
"颜色":['黑色','蓝色','紫色'],
"存储":['8+128G','8+256G','12+256G'],
allTypes:["颜色","存储"]
}
},
3:{
id:3,
shopId:"mi222",
name:"RedmiNote11 4G手机",
imgUrl:"https://img14.360buyimg.com/n7/jfs/t1/189071/26/24872/250013/62870273E0be46c79/2be13b0a9f01f66f.jpg",
price:999,
types:{
"颜色":['黑色','蓝色','紫色'],
"存储":['8+128G','8+256G','12+256G'],
allTypes:["颜色","存储"]
}
},
4:{
id:4,
shopId:"mi222",
name:"ipad mini 平板电脑",
imgUrl:"https://img10.360buyimg.com/n7/jfs/t1/86940/3/26618/122396/6246970cE40f70e5f/fbfef3df69cf8217.jpg",
price:3599,
types:{
"颜色":['黑色','蓝色','紫色'],
"存储":['8+128G','8+256G','12+256G'],
allTypes:["颜色","存储"]
}
},
},
allIds:[1,2,3,4]
},
shops:{
byId:{
"mi111":{
id:"mi111",
name:"小米官方旗舰店111",
imgUrl:"https://dss0.bdstatic.com/-0U0bnSm1A5BphGlnYG/tam-ogel/1122879666_1184688902_88_88.png",
goods:[1,2]
},
"mi222":{
id:"mi222",
name:"小米官方旗舰店222",
imgUrl:"https://dss0.bdstatic.com/-0U0bnSm1A5BphGlnYG/tam-ogel/1122879666_1184688902_88_88.png",
goods:[3,4]
},
},
allIds:["mi111","mi222"]
},
}
...
其他配置及优化
配置vite alias
写代码时引入文件路径通常会跳很多级,使用“@”作为src的别称
...
var init = function () {
var clientWidth = document.documentElement.clientWidth || document.body.clientWidth;
if (clientWidth >= 640) {
clientWidth = 640;
}
var fontSize = 20 / 375 * clientWidth;
document.documentElement.style.fontSize = fontSize + "px";
}
init();
window.addEventListener("resize", init);
...
全局风格样式文件
项目中通常会用一个全局风格的样式文件global-style.js 设置属于该项目的颜色,字体大小等。
...
// 全局样式定义
export default {
"theme-color": "#FF2C57",
"theme-color-light":"#EB89A0"
}
...
react-lazyload实现图片懒加载
在placholder的属性中使用一张图片占位
...
<LazyLoad className="item_img_wrapper"
placeholder={
<img width="100%" height="100%" src={temp}/>}
>
<img src={imgUrl} alt="" className="item_img" />
</LazyLoad>
...
React.memo()组件性能优化
memo用来优化函数组件的重复渲染行为,如果函数组件在相同的props的情况渲染相同的结果,那么使用memo会跳过这次渲染,使用最近一次相同结果的渲染。
最后
项目写完仍有很多不足,继续加油!下面是Redux官网和本次项目地址