一、Redux
1、什么是Redux
Redux是React最常用的集中状态管理工具,类似于Vue中的Pinia(Vuex),可以独立于框架运行。
作用:通过集中管理的方式管理应用的状态。
2、快速体验Redux
需求: 不和任何框架绑定,不适用任何构建工具,使用纯Redux实现计数器。
使用步骤:
① 定义一个reducer函数(根据当前想要做的修改返回一个新的状态)
② 使用createStore方法传入reducer函数,生成一个store实例对象
③ 使用store实例的subscribe方法订阅数据的变化(数据一旦变化,可以得到通知)
④ 使用store实例的dispatch方法提交action对象触发数据变化(告诉reducer你想怎么改数据)
⑤ 使用store实例的getState方法获取最新的状态数据更新到视图中。
代码:
<button id="decrement">-</button>
<span id="count">0</span>
<button id="increment">+</button>
<script src="https://unpkg.com/redux@latest/dist/redux.min.js"></script>
<script>
//1、定义reducer函数
//作用:根据不同的action对象,返回不同的state
//state:管理的数据初始对象
//action:对象type标记当前想要做什么样的修改
fucntion reducer (state = {count :0 } ,action) {
//数据不可变:基于原始状态生成一个新状态
if(action.type === 'INCREMENT') {
return { count:state.count + 1 }
}
if(action.type === 'DECREMENT'){
return { count:state.count - 1 }
}
return state
}
//2、使用reducer函数生成store实例
const store = Redux.createStore(reducer)
//3、通过store实例的subcribe订阅数据变化
//回调函数可以在每次state发送变化的时候自动执行
store.subscribe(() => {
console.log('state变化了',store.getState())
document.getElementById('count').innerText = store.getState().count;
} )
//4、通过store实例的dispatch函数提交action更改状态
const isBtn = document.getElementById('increment')
isBtn.addEventListener('click',()=>{
//增加
store.dispatch({
'type':'INCREMENT',
})
})
const isBtn = document.getElementById('crement')
isBtn.addEventListener('click',()=>{
//减少
store.dispatch({
'type':'INCREMENT',
})
})
//5、通过store实例的getState方法获取最新状态更新到视图中
</script>
3、Redux管理数据流程梳理
为了职责清晰,数据流向明确,Redux把整个数据修改的流程分为了三个核心概念,分别是:state、action和reducer。
①、state:一个对象,存放着我们管理的数据状态
②、action:一个对象,用来描述你想怎么改数据
③、reducer:一个函数,根据action的描述生成一个新的state
4、Redux与React环境准备
4.1 配套工具
在React中使用Redux,官方要去安装两个其他插件:Redux Toolkit 和 react-redux。
① Reux Toolkit(RTK):官方推荐编写Redux逻辑的方式,是一套工具的集合集,简化书写方式。
② react-redux:用了链接Redux和React组件的中间件。
4.2 配置基础环境
① 使用CRA快速创建react项目
npx create-react-app react-redux
② 安装redux
npm i --save redux
③ 安装配套工具
npm i @reduxjs/toolkit react-redux
④ 启动项目
npm run start
4.3 store目录结构设计
① 通常集中状态管理的部分都会单独创建一个单独的store目录。
② 应用通常会有很多个子store模块,所以创建一个modules目录,在内部编写业务分类的子store。
③ store中的入口文件index.js的作用是组合modules中所有的子模块,并导出store。
5、实现counter
5.1 使用React Toolkit创建counterStore
5.2 为React注入store
react-redux负责把Redux和React链接起来,内置Provider组件,通过store参数把创建好的store实例注入到应用中,链接正式建立。
5.3 React组件使用store中的数据
在React组件中使用store中的数据,需要用到一个钩子函数 useSelector,它的作用是把store中的数据映射到组件中,使用样例如下:
const { count } = useSelector(state => state.counter)
其中state.counter来自于创建根store组合子模块里的counter。
import { configureStore } from "@reduxjs/toolkit";
//导入子模块reducer
import counterStore from "../store/modules/counterStore";
//创建根store组合子模块
const store = configureStore({
reducer:{
counter:counterStore
}
})
export default store
5.4 React组件修改store中的数据
React组件中修改store中的数据需要借助另外一个hook函数: useDispatch,它的作用是生成提交action对象的dispatch函数,使用样例如下:
6、提交action传参
需求说明:
组件中有两个按钮:add to 10 和 add to 20 可以直接把count的值修改到对应的数字,目标count值是在组件中传递过去的,需要在提交action的时候传递参数。
实现步骤:
在reducers的同步修改方法中添加action对象参数,在调用actionCreater的时候传参,参数会传递到action对象payload属性上。
7、异步状态操作
① 创建store的写法保持不变,配置好同步修改状态的方法。
② 单独封装一个函数,在函数内部return一个新函数,在新函数中:
封装异步请求获取数据
调用同步actionCreater传入异步数据生成一个action对象,并使用dispatch提交
③ 组件中dispatch的写法保持不变。
8、Redux调试——devtools
Redux 官方提供了针对Redux的调试工具,支持实时state信息展示,action提交信息查看等。
调试工具下载链接:Redux DevTools_3.1.6_Chrome插件下载_极简插件 (zzzmh.cn)
9、美团案例
需求:
基本开发思路:
使用RTK(Redux Toolkit)来管理应用状态,组件负责数据渲染和dispatch action。
9.1 开发环境准备
① 克隆项目到本地(内置基础静态组件和模块)
git clone http://git.itcast.cn/heimaqianduan/redux-meituan.git
② 安装所有依赖
npm i
③ 启动mock服务(内置了json-server)
npm run serve
④ 启动前端服务
npm run start
9.2 分类和商品列表渲染
需求:
实现步骤:
① 启动项目(mock服务+前端服务)
② 使用RTK编写store(异步action)
③ 组件触发action并且渲染数据
9.3 点击分类激活和互助实现
需求:
实现步骤:
① 使用RTK编写管理activeIndex
② 组件中点击时触发action更改activeIndex
③ 动态控制激活类名显示
9.4 商品列表切换显示
需求:
9.5 添加购物车实现
需求:
实现步骤:
① 使用RTK管理状态cartList
② 如果添加过,只更新数量count,没有添加过,直接push进去
③ 组件中点击时收集数据提交action添加购物车
9.6 统计区域功能实现
需求:
实现步骤:
① 基于store中的cartList的length渲染数量
② 基于store中的cartList累加price*count
③ 购物车cartList的length不为0则高亮
9.7 购物车列表功能实现
需求:
实现步骤:
① 使用cartList遍历渲染列表
② RTK中增加增减reducer,组件中提交action
③ RTK中增加清除购物车reducer,组件中提交action
9.8 控制购物车显示和隐藏
需求:
实现步骤:
① 使用useState声明控制显隐的状态
② 点击统计区域设置状态为true
③ 点击蒙层区域设置状态为false
二、 ReactRouter
1、准备工作
1.1 什么是前端路由
一个路由path对应一个组件component,当我们浏览器中访问一个path的时候,path对应的组件会在页面中进行渲染:
const router = [
{
path: "/about",
component: About,
},
{
path: "/article",
component: Article,
},
];
1.2 创建路由开发环境
使用路由我们还是采用CRA创建项目的方式进行基础环境配置。
① 创建项目并安装所有依赖
npx create-react-app react-router-pro
② 安装最新的ReactRouter包
npm i react-router-dom
③ 启动项目
npm run start
2、抽象路由模块
实际开发中的router配置
3、路由导航跳转
路由系统中的多个路由之间需要进行路由跳转,并且在跳转的同时有可能需要传递参数进行通信。
3.1 声明式导航
声明式导航:通过在模板中通过‘<Link /> 组件描述出要跳转到哪里去’,比如后台管理系统的左侧菜单通常使用这种方式进行。
语法说明:通过给组件的to属性指定要跳转到路由path,组件会渲染为浏览器支持的a链接,如果需要传参直接 通过字符串拼接的方式拼接参数即可。
3.2 编程式导航
编程式导航:通过'useNavigate’钩子得到导航方法,然后通过调用方法以命令式的形式进行路由跳转,比如想在登录请求完毕之后跳转就可以选择这种方法,更加灵活。
语法说明:通过调用navigate方法传入地址path实现跳转。
4、导航跳转传参
5、嵌套路由配置
在一级路由中又嵌套了其他路由,这种关系就叫嵌套路由,嵌套至一级路由内的路由又称作二级路由。
实现步骤:
① 使用 children 属性配置路由嵌套关系
② 使用 '<Outlet />' 组件配置二级路由渲染位置
6、默认二级路由配置
当访问的是一级路由时,默认的二级路由组件可以得到渲染,只需要在二级路由的位置去掉path,设置index属性值为true。
7、404路由配置
场景:当浏览器输入url的路径在整个路由配置中都找不到对应的path,为了用户体验,可以使用404兜底组件进行渲染。
实现步骤:
① 准备一个NotFount组件
② 在路由表数组的末尾,以*号作为路由path配置路由
8、两种路由模式
各个主流框架的路由常用的路由模式有两种,history模式和hash模式,ReactRouter分别由createBrowerRouter和createHashRouter函数负责创建。
9、案例:记账本
9.1 环境搭建
使用CRA创建项目,并安装必要依赖,包括下列基础包:
① Redux状态管理: @reduxjs/tookit、react-redux
② 路由:react-router-dom
③ 时间处理:dayjs
④ class类名处理:classnames
⑤ 移动端组件库:antd-mobile
⑥ 请求插件:axios
9.2 配置别名路径
①、路径解析配置(webpack),把@/解析为src/
②、路径联想配置(VsCode),VsCode在输入@/
9.2.1 路径解析配置
CRA本身把webpack配置包装到了黑盒里无法直接修改,需要借助一个插件 -craco。
①、安装craco
npm i -D @craco/craco
②、项目根目录下创建配置未见
craco.config.js
③、配置文件中添加路径解析配置
④、包文件中配置启动和打包命令
9.2.2 联想路径配置
VsCode的联想配置,需要我们在项目目录下添加jsconfig.json文件,加入配置之后VsCode会自动读取配置帮助我们自动联想提示。
①、根目录下新增配置文件:jsconfig.json
②、添加路径提示配置
9.3 数据Mock实现
9.3.1 什么是数据Mock
在前后端分离的开发模式下,前端可以在没有实际后端接口的支持下先进行接口数据的模拟,进行正常的业务开发功能。
9.3.2 json-server实现数据Mock
json-server是一个node包,可以在不到30秒内获得零编码的完整的Mock服务。
① 项目中安装json-server
npm i -D json-server@0.17.3
② 准备一个json文件
③ 添加启动命令
④ 访问接口进行测试
9.4 整体路由设计
1、两个以及路由(Layout 、 new)
2、两个二级路由(Layout— month 、year)
9.5 antD主题色定制
9.6 Redux管理账目列表
9.7 TabBar功能实现
需求:使用antD的TabBar标签栏进行布局以及路由的切换。
实现方式:看文档(找到相似Demo—复制代码跑通—定制化修改)
Layout—index.js
import { Outlet, useNavigate } from "react-router-dom";
import { useEffect } from "react";
import { useDispatch } from "react-redux";
import { getBillList } from "@/store/modules/billStore";
import "./index.scss";
import {
BillOutline,
CalculatorOutline,
AddCircleOutline,
} from "antd-mobile-icons";
import { TabBar } from "antd-mobile";
const tabs = [
{
key: "/",
title: "月度账单",
icon: <BillOutline />,
},
{
key: "/new",
title: "记账",
icon: <AddCircleOutline />,
},
{
key: "/year",
title: "年度账单",
icon: <CalculatorOutline />,
},
];
const Layout = () => {
const dispatch = useDispatch();
useEffect(() => {
dispatch(getBillList());
}, [dispatch]);
//切换路由菜单
const navigate = useNavigate();
const switchRoute = (path) => {
navigate(path);
};
return (
<div className="layout">
<div className="container">
<Outlet />
</div>
<div className="footer">
<TabBar onChange={switchRoute}>
{tabs.map((item) => (
<TabBar.Item key={item.key} icon={item.icon} title={item.title} />
))}
</TabBar>
</div>
</div>
);
};
export default Layout;
9.8 统计区域
Month —— index.js
import { NavBar, DatePicker } from "antd-mobile";
import { useState, useMemo, useEffect } from "react";
import classNames from "classnames";
import "./index.scss";
import dayjs from "dayjs";
import { useSelector } from "react-redux";
import _ from "lodash";
const Month = () => {
//按月做数据分组
const billList = useSelector((state) => state.bill.billList);
const monthGroup = useMemo(() => {
//return出去计算之后的值
//根据年月进行数据分组
return _.groupBy(billList, (item) => dayjs(item.date).format("YYYY-MM"));
}, [billList]);
//控制弹框的打开和关闭
const [dateVisible, setDateVisible] = useState(false);
//控制时间显示
const [currentDate, setCurrentDate] = useState(() => {
return dayjs(new Date()).format("YYYY-MM");
});
const [currentMonthList, setMonthList] = useState([]);
const monthResult = useMemo(() => {
//支出 /收入 /结余
const pay = currentMonthList
.filter((item) => item.type === "pay")
.reduce((a, c) => a + c.money, 0);
const income = currentMonthList
.filter((item) => item.type === "income")
.reduce((a, c) => a + c.money, 0);
return { pay, income, total: pay + income };
}, [currentMonthList]);
//初始化渲染统计数据,把当前月的统计数据显示出来
useEffect(() => {
const nowDate = dayjs().format("YYYY-MM")
setMonthList(monthGroup[nowDate] ?? [])
}, [monthGroup]);
//点击确认回调
const onConfirm = (date) => {
setDateVisible(false);
//其他逻辑
const formatDate = dayjs(date).format("YYYY-MM");
setMonthList(monthGroup[formatDate] ?? []);
setCurrentDate(formatDate);
};
return (
<div className="monthlyBill">
<NavBar className="nav" backArrow={false}>
月度收支
</NavBar>
<div className="content">
<div className="header">
{/* 时间切换区域 */}
<div className="date" onClick={() => setDateVisible(true)}>
<span className="text">{currentDate + ""}月账单</span>
{/* 思路:根据当前弹框打开的状态控制expand类名是否存在 */}
<span
className={classNames("arrow", dateVisible && "expand")}
></span>
</div>
{/* 统计区域 */}
<div className="twoLineOverview">
<div className="item">
<span className="money">{monthResult.pay.toFixed(2)}</span>
<span className="type">支出</span>
</div>
<div className="item">
<span className="money">{monthResult.income.toFixed(2)}</span>
<span className="type">收入</span>
</div>
<div className="item">
<span className="money">{monthResult.total.toFixed(2)}</span>
<span className="type">结余</span>
</div>
</div>
{/* 时间选择器 */}
<DatePicker
className="kaDate"
title="记账日期"
precision="month"
visible={dateVisible}
onCancel={() => setDateVisible(false)}
onConfirm={onConfirm}
onClose={() => setDateVisible(false)}
max={new Date()}
/>
</div>
</div>
</div>
);
};
export default Month;
9.9 列表区域
src— pages— Month — index.js
import { NavBar, DatePicker } from "antd-mobile";
import { useState, useMemo, useEffect } from "react";
import classNames from "classnames";
import "./index.scss";
import dayjs from "dayjs";
import { useSelector } from "react-redux";
import _ from "lodash";
import DayBill from "./components/DayBill";
const Month = () => {
//按月做数据分组
const billList = useSelector((state) => state.bill.billList);
const monthGroup = useMemo(() => {
//return出去计算之后的值
//根据年月进行数据分组
return _.groupBy(billList, (item) => dayjs(item.date).format("YYYY-MM"));
}, [billList]);
//控制弹框的打开和关闭
const [dateVisible, setDateVisible] = useState(false);
//控制时间显示
const [currentDate, setCurrentDate] = useState(() => {
return dayjs(new Date()).format("YYYY-MM");
});
const [currentMonthList, setMonthList] = useState([]);
const monthResult = useMemo(() => {
//支出 /收入 /结余
const pay = currentMonthList
.filter((item) => item.type === "pay")
.reduce((a, c) => a + c.money, 0);
const income = currentMonthList
.filter((item) => item.type === "income")
.reduce((a, c) => a + c.money, 0);
return { pay, income, total: pay + income };
}, [currentMonthList]);
//初始化渲染统计数据,把当前月的统计数据显示出来
useEffect(() => {
const nowDate = dayjs().format("YYYY-MM");
setMonthList(monthGroup[nowDate] ?? []);
}, [monthGroup]);
//点击确认回调
const onConfirm = (date) => {
setDateVisible(false);
//其他逻辑
const formatDate = dayjs(date).format("YYYY-MM");
setMonthList(monthGroup[formatDate] ?? []);
setCurrentDate(formatDate);
};
//当前月按照日来做分组
const dayGroup = useMemo(() => {
const groupData = _.groupBy(currentMonthList, (item) =>
dayjs(item.date).format("YYYY-MM-DD")
);
const keys = Object.keys(groupData);
return {
groupData,
keys,
};
}, [currentMonthList]);
return (
<div className="monthlyBill">
<NavBar className="nav" backArrow={false}>
月度收支
</NavBar>
<div className="content">
<div className="header">
{/* 时间切换区域 */}
<div className="date" onClick={() => setDateVisible(true)}>
<span className="text">{currentDate + ""}月账单</span>
{/* 思路:根据当前弹框打开的状态控制expand类名是否存在 */}
<span
className={classNames("arrow", dateVisible && "expand")}
></span>
</div>
{/* 统计区域 */}
<div className="twoLineOverview">
<div className="item">
<span className="money">{monthResult.pay.toFixed(2)}</span>
<span className="type">支出</span>
</div>
<div className="item">
<span className="money">{monthResult.income.toFixed(2)}</span>
<span className="type">收入</span>
</div>
<div className="item">
<span className="money">{monthResult.total.toFixed(2)}</span>
<span className="type">结余</span>
</div>
</div>
{/* 时间选择器 */}
<DatePicker
className="kaDate"
title="记账日期"
precision="month"
visible={dateVisible}
onCancel={() => setDateVisible(false)}
onConfirm={onConfirm}
onClose={() => setDateVisible(false)}
max={new Date()}
/>
</div>
{/* 单日列表统计 */}
{dayGroup.keys.map((key) => {
return (
<DayBill key={key} date={key} billList={dayGroup.groupData[key]} />
);
})}
</div>
</div>
);
};
export default Month;
src— pages— Month — components— DayBill —— index.js
//月度账单详情
import Icon from "@/components/Icon";
import "./index.scss";
import classNames from "classnames";
import { useMemo } from "react";
import { useState } from "react";
import { billTypeToName } from '@/contants/index'
const DayBill = ({ date, billList }) => {
const dayResult = useMemo(() => {
//支出 /收入 /结余
const pay = billList
.filter((item) => item.type === "pay")
.reduce((a, c) => a + c.money, 0);
const income = billList
.filter((item) => item.type === "income")
.reduce((a, c) => a + c.money, 0);
return { pay, income, total: pay + income };
}, [billList]);
//控制展开收起
const [visible, setVisible] = useState(false);
return (
<div className={classNames("dailyBill")}>
<div className="header">
<div className="dateIcon">
<span className="date">{date}</span>
{/* expand 有个类名 站开点箭头朝上*/}
<span
className={classNames("arrow", visible && "expand")}
onClick={() => setVisible(!visible)}
></span>
</div>
<div className="oneLineOverview">
<div className="pay">
<span className="type">支出</span>
<span className="money">{dayResult.pay.toFixed(2)}</span>
</div>
<div className="icon">
<span className="type">收入</span>
<span className="money">{dayResult.income.toFixed(2)}</span>
</div>
<div className="balance">
<span className="money">{dayResult.total.toFixed(2)}</span>
<span className="type">结余</span>
</div>
</div>
</div>
{/*单日列表*/}
<div className="billList" style={{ display: visible ? "block" : "none" }}>
{billList.map((item) => {
return (
<div className="bill" key={item.id}>
{/* 图标 */}
<Icon type={item.useFor} />
<div className="detail">
<div className="billType">{billTypeToName[item.useFor]}</div>
</div>
<div className={classNames("money", item.type)}>
{item.money.toFixed(2)}
</div>
</div>
);
})}
</div>
</div>
);
};
export default DayBill;
9.10 新增账单
src — store — billStore.js
src — pages — New — indexjs
import { Button, Input, NavBar, DatePicker } from "antd-mobile";
import classNames from "classnames";
import { useNavigate } from "react-router-dom";
import { useDispatch } from "react-redux";
import { useState } from "react";
import dayjs from "dayjs";
import { billListData } from "@/contants";
import Icon from "@/components/Icon";
import "./index.scss";
import { addBillList } from "@/store/modules/billStore";
const New = () => {
const navigate = useNavigate();
// 1. 准备一个控制收入支出的状态
const [billType, setBillType] = useState("pay"); // pay-支出 income-收入
// 收集金额
const [money, setMoney] = useState(0);
const moneyChange = (value) => {
setMoney(value);
};
// 收集账单类型
const [useFor, setUseFor] = useState("");
const dispatch = useDispatch();
// 保存账单
const saveBill = () => {
// 收集表单数据
const data = {
type: billType,
money: billType === "pay" ? -money : +money,
date: date,
useFor: useFor,
};
console.log(data);
dispatch(addBillList(data));
};
// 存储选择的时间
const [date, setDate] = useState();
// 控制时间打开关闭
const [dateVisible, setDateVisible] = useState(false);
// 确认选择时间
const dateConfirm = (value) => {
console.log(value);
setDate(value);
setDateVisible(false);
};
return (
<div className="keepAccounts">
<NavBar className="nav" onBack={() => navigate(-1)}>
记一笔
</NavBar>
<div className="header">
<div className="kaType">
<Button
shape="rounded"
className={classNames(billType === "pay" ? "selected" : "")}
onClick={() => setBillType("pay")}
>
支出
</Button>
<Button
className={classNames(billType === "income" ? "selected" : "")}
shape="rounded"
onClick={() => setBillType("income")}
>
收入
</Button>
</div>
<div className="kaFormWrapper">
<div className="kaForm">
<div className="date">
<Icon type="calendar" className="icon" />
<span className="text" onClick={() => setDateVisible(true)}>
{dayjs(date).format("YYYY-MM-DD")}
</span>
{/* 时间选择器 */}
<DatePicker
className="kaDate"
title="记账日期"
max={new Date()}
visible={dateVisible}
onConfirm={dateConfirm}
onClose={() => setDateVisible(false)}
onCancel={() => setDateVisible(false)}
/>
</div>
<div className="kaInput">
<Input
className="input"
placeholder="0.00"
type="number"
value={money}
onChange={moneyChange}
/>
<span className="iconYuan">¥</span>
</div>
</div>
</div>
</div>
<div className="kaTypeList">
{/* 数据区域 */}
{billListData[billType].map((item) => {
return (
<div className="kaType" key={item.type}>
<div className="title">{item.name}</div>
<div className="list">
{item.list.map((item) => {
return (
// selected
<div
className={classNames(
"item",
useFor === item.type ? "selected" : ""
)}
key={item.type}
onClick={() => setUseFor(item.type)}
>
<div className="icon">
<Icon type={item.type} />
</div>
<div className="text">{item.name}</div>
</div>
);
})}
</div>
</div>
);
})}
</div>
<div className="btns">
<Button className="btn save" onClick={saveBill}>
保 存
</Button>
</div>
</div>
);
};
export default New;