第十二步:react
React
1、安装
1、脚手架
- npm i -g create-react-app:安装react官方脚手架
- create-react-app 项目名称:初始化项目
2、包简介
- react:react框架的核心
- react-dom:react视图渲染核心
- react-native:构建和渲染App的核心
- react-scripts:脚手架的webpack配置
- web-vitals:性能检测工具
3、运行
- npm run start:运行项目
- npm run build:打包项目
4、配置简介
- package.eslintConfig:代码规范配置
- package.browserslist:浏览器兼容配置
- package.proxy:配置服务代理
5、文件目录
根据项目需求创建即可,以下为参考建议
- project 项目总文件- public 资源文件- src- api 接口- assets 资源- components 组件- layout 主体- router 路由- store 状态- utils 工具- view 页面- test
- public:打包时不会进行处理,打包完成后会复制一份到包里
- src/assets:打包时会压缩,编译等处理。
6、理论
1、react
用于构建 Web 和原生交互界面的库
2、设计
- 操作DOM - 操作DOM比较消耗性能,可能会导致DOM重绘和回流
- 操作繁琐,容易出错,效率底,不利于维护
 
- 数据驱动: - 通过数据驱动视图,减少DOM操作
- 框架底层也是操作DOM - 构建 虚拟DOM 到 真实DOM 的渲染体系
- 有效避免DOM重绘和回流
 
- 开发效率高,易于维护
 
3、模式
react采用MVC模式, vue采用MVVM模式
- MVC模式:Model数据层 + View视图层 + Controller控制器
- MVVM模式:Model数据层 + View视图层 + ViewModel视图/数据监听层
4、根
- react通过创造更节点,开始渲染dom
- const root = ReactDom.createRoot(node)- node:获取的节点元素,通常是<div id="root"></div>
- root:创建的根节点 - root.render(组件),开始渲染组件
 
 
- node:获取的节点元素,通常是
- 每个组件必须只有一个根标签
7、渲染机制
- 把jsx转换为虚拟dom。
- 虚拟dom转换为真实dom。
- 数据变化,通知Controller,修改数据层。
- 数据变化,通过diff算法,计算出视图差异部分(补丁包)。
- 把补丁包进行渲染。
- 转译:通过babel-preset-react-app把jsx转译成React.createElement
- 虚拟dom:通过React.createElement创建出虚拟dom
- 虚拟dom对象: - $$typeof:是否为有效的react元素,- Symbol("react.element")
- ref:允许访问该dom实例
- key:列表元素的唯一标识
- type:标签名,或组件名
- props:接受到的样式、类名、事件、子节点、参数 - style:样式对象
- className:类名
- children:子节点
- onClick:绑定的点击事件
- …
 
 
- 真实DOM:通过ReactDom中的render方法,把虚拟dom转换为真实dom
2、样式
1、样式设置
function App() {return (<><div style={{ color: "red" }}>hello world</div><div className="box">react</div></>)
}
2、样式穿透
- react中,父组件样式文件,能够直接影响子组件
3、样式隔离
- 样式文件设置为index.module.[css|less|scss|sass]
- 引入样式import style from "./index.module.css"
- 使用样式<div className={style.box}>hello world</div>
3、渲染
1、基础渲染
组件渲染、条件渲染、列表渲染、事件响应、数据显示、动态数据
import { useState } from "react";function Box(props) {let [isShow, setIsShow] = useState(false);let arr = ["a", "b", "c"];let content;if (isShow) {content = <div>我是内容</div>;} else {content = null;}return (<dov><button onClick={() => setIsShow(!isShow)}>按钮 - {isShow}</button>{arr.map((item, index) => (<li key={index}>{item}</li>))}{isShow && <div>我是内容</div>}{content}</dov>);
}export default Box;
2、插槽
1、使用
-  通过 props.children能获取子节点。function Box(props) {return (<div>插槽-{props.children}</div>) }
2、具名
- 通过React.ChildrenApi能够遍历获取每个插槽子节点
- 通过children.props.slot属性,能够读取子节点自定义的slot
- 根据slot属性,能够判断每个子节点的渲染位置
3、传参一:
-  通过 props.children获取到的子节点,无法修改它的props
-  但是能通过 React.cloneElement复制一个子节点,然后重新赋值propsimport { cloneElement } from "react"; function Box() {let child = cloneElement(props.children, {data: "123",...props.children.props});return (<div>{child}</div>); }
4、传参二:
- 通过createContext和useContext进行传参
4、传参
1、父传子
- 父组件可以直接通过属性赋值的方式,把变量和函数传递给子组件
- 子组件可以通过参数props读取变量和方法,props只读无法修改
2、父读子
import { useRef, useImperativeHandle, forwardRef } from "react";// 父组件
function Fa() {let ref1 = useRef();let ref2 = useRef();function btnClick() {console.log(ref1.current.Tname);console.log(ref1.current.handleT());console.log(ref2.current.Tname);console.log(ref2.current.handleT());}return (<div><button onClick={btnClick}>父按钮</button><Son1 ref={ref1}></Son1><Son2 ref={ref2}></Son2></div>);
}// 子组件一
let Son1 = forwardRef(function (props, ref) {let Tname = "子1";let handleT = () => `我是${Tname}`;useImperativeHandle(ref, () => ({ Tname, handleT }));return <div>Son1</div>;
});// 子组件二
let Son2 = forwardRef(function (props, ref) {let Tname = "子2";let handleT = () => `我是${Tname}`;useImperativeHandle(ref, () => ({ Tname, handleT }));return <div>Son2</div>;
});export default Fa;
3、兄弟传参
- 借用父组件变量传参
- 使用 状态管理 传参,最优选
- 使用createContext和useContext进行传参,优选
5、生命周期
1、类组件
- constructor:组件加载前
- render:组件加载
- componentDidMount:组件加载完成
- shouldComponentUpdate:数据更新时
- componentDidUpdate:数据更新完成
- componentWillUnmount:数据卸载前
2、函数组件
1、执行
函数组件,每次加载前,数据更新时,渲染时,都会执行当前函数组件的函数体。
所以函数组件本身就能监听:加载前、加载、数据更新 三种状态
2、加载完成
-  使用 useEffect能监听组件加载完成function App() {useEffect(() => {console.log("组件加载");});return(<div>hello world</div>); }
3、卸载前
-  使用 useEffect能够监听组件卸载前function App() {useEffect(() => {return () => console.log("组件即将卸载");});return(<div>hello world</div>); }
4、数据更新后
-  使用 useEffect能监听数据的变化,包括props,state
-  但是加载完成时,也会触发数据监听 function App() {let [value, setValue] = useState("");let change = (e) => setValue(e.target.value);useEffect(() => {console.log("数据更新");}, [value]);return (<input type="text" onChange={change} value={value} />); }
5、出入动画
虽然能够监听组件即将卸载这个生命周期
但是由于react组件每次数据更新,都会重新执行当前函数,就会导致执行到隐藏条件判断时,不对动画节点进行虚拟dom构造,也就导致dom树节点没有动画的节点。
所以,进入动画能够很方便的完成,消失动画无法正常完成。
所以需要利用数据更新后,每次执行函数体的特征,指定3种状态。
0 隐藏,1 显示,-1 消失,消失动画完成后,再切换为0
- 初始时,状态为0,className为空字符
- 点击显示,状态变成1,className变显示动画类
- 点击消失,状态变成-1,className变消失动画类
- 消失动画完成后,状态变成0,className变空
function App() {let [show, setShow] = useState(0);// 使用useMemo,避免其他变量变化时,影响到cNamelet cName = useMemo(() => {return {1: "animate__backInDown",[-1]: "animate__backOutDown",}[show];}, [show]);return (<div><button onClick={() => setShow(show ? -1 : 1)}>按钮</button>{show ? (<divclassName={`animate__animated ${cName}`}onAnimationEnd={() => show === -1 && setShow(0)}/>) : null}</div>);
}
6、redux
1、安装
- redux:核心包
- @reduxjs/toolkit:新的创建方式
2、api
- import { createStore } from "redux"
- const store = createStore(reducer, init):初始化数据对象- store:数据对象 - getState():获取数据
- dispatch(action):修改数据 - action:传递的动作对象
 
- subscribe(fn):变化订阅函数 - fn:数据变化时,执行
- 返回一个函数,执行后取消变化订阅,fn将不再执行
 
 
- reducer:操作函数 - 参数一(state):修改前的数据
- 参数二(action):dispatch传递的动作对象
 
- init:初始化的数据
 
- store:数据对象 
3、使用
-  外部定义变量 import { createStore } from "redux";const data = { count: 1 }; function reducer(state, action) {if (action.type in state) state[action.type] = action.value;return { ...state }; }// 教程视频都是用switch判断action的type,然后执行逻辑。 // 如果项目设计时,在redux写逻辑,就用switch // 如果项目设计时,就只是用redux进行状态管理,就直接修改值// 和useReducer完全一样 export default createStore(reducer, init);
-  组件内使用 import { useState, useEffect } from "react"; import store from "@/store/index";function App() {const data = store.getState();const [count, setCount] = useState(data.count);function changeCount() {store.dispatch({ type: "count", value: count + 1 });}// 订阅store变化const callback = store.subscribe(() => {setCount(store.getState().count);});// 组件卸载时,取消订阅useEffect(() => callback);return (<><div>Count: {count}</div><button onClick={changeCount}>按钮</button></>) }
4、模块化一
- 使用combineReducers合并
import { createStore, combineReducers } from "redux";const counter = { count: 1 };
const sumer = { sum: 10 };
function reducer(data) {return (state = data, action) => {if (action.type in state) state[action.type] = action.value;return { ...state };}
}const reducers = combineReducers({counter: reducer(counter),sumer: reducer(sumer),
})const store(reducers);// 读取值
const counterData = store.getState().counter;
const sumerData = store.getState().sumer;// 其他的没有变化
5、模块化二
- 首先:没有任何文档说明,store只能创建一个。
- 使用上下文,能够更好的管理模块数据,也方便使用
import { createStore } from "redux";
import { createContext, useContext } from "react";function reducer(data) {return (state = data, action) => {if (action.type in state) state[action.type] = action.value;return { ...state };}
}const counter = createStore(reducer, { count: 1 });
const sumer = createStore(reducer, { sum: 10 });
const store = createContext({ counter,sumer });export default function useStore() {return useContext(store);
}
6、toolkit-定义
- const model = createSlice(optons):创建一个store模块- model:store模块 - model.reducer:模块的reducer
- model.actions:模块的方法对象
 
- options:模块配置 - name:模块名称
- initialState:初始化值
- reducers:reducer函数对象 - reducer对象的方法名,就是model.actions对象的方法名
- reducer方法 - 参数一:修改前的state值
- 参数二:一个对象 - payload:对应的model.actions对象方法传递的参数
 
 
 
 
 
- model:store模块 
- const store = configureStore(options):创建一个store- store:创建的状态管理
- options:配置 - reducer: - 直接设为model,就只有一个store模块,没有名称
- 设对象时,name: model,多个模块
 
- middleware:中间件列表
 
- reducer: 
 
7、toolkit-使用
-  定义数据 import { createSlice, configureStore } from "@reduxjs/toolkit";const counter = createSlice({name: "counter",initialState: 0,reducers: {add(state, action) {return state + action.payload;},}, });export const { add } = counter.actions;const store = configureStore({reducer: counter.reducer,// reducer: { counter: counter.reducer } });export default store;
-  应用 import { useState, useEffect } from "react"; import store, { add } from "@/stores/index";function App() {const [count, setCount] = useState(store.getState());function countChange() {store.dispatch(add(1));}let callback = store.subscribe(() => {setCount(store.getState());});useEffect(() => callback);return (<><h1>Count: {count}</h1><button onClick={countChange}>按钮</button></>); }
8、tookit-模块化
- configureStore.reducer配置为对象
- 组件使用store.getState().model获取值
- 其他不变
9、持久化
import { createStore } from "redux";let counter = {count: sessionStorage.getItem("count") || 0,
};
function reducer(state, action) {if (action.type in state) {state[action.type] = action[action.type];sessionStorage.setItem(action.type, state[action.type]);}return { ...state };
}const store = createStore(reducer, counter);
export default store;
10、组件外使用
需求:部分业务逻辑可能会在组件外部对数据进行修改
如:接口拦截,统一获取登录状态
- 需要在外部使用story.dispatch方法修改数据
- 封装统一请求方法,然后请求拦截时,获取登录状态,修改登录状态
- 请求时有组件发起的 - 通过事件响应触发接口请求
- 通过useEffect监听组件挂载成功,触发接口请求
 
import { useEffect } from "react";
import store from "@/store/index";
function axios() {setTimeout(() => {sumStore.dispatch({ type: "isLogin", value: true });}, 3000);
}function App() {const { isLogin } = store.getState();useEffect(() => {// 模拟接口请求,修改登录状态axios();});return (<div>{ isLogin ? "登录中" : "未登录" }  </div>)
}
7、mobx
灵活,体积小,适合快速配置
1、安装
- 下载插件:npm i mobx
- 下载插件:npm i mobx-react-lite:体积小,支支持函数组件
- 下载插件:npm i mobx-react:体积大,支持函数组件,类组件
2、数据定义
import { makeObservable, observable, computed, action, flow } from "mobx";class Count {count = 0; // 定义静态属性constructor() {makeObservable(this, {count: observable,double: computed,add: action,api: flow,});}get double() {return this.count * 2;}add() {this.count = this.count + 1;}*api() {let res = yield Promise.resolve(1);this.count = res;}
}const count = new Count();
export default count;
- makeObservable:在构造函数中,定义哪些属性,方法是可观察的- 参数一:当前类的指向
- 参数二:指定属性,方法
 
- observable:定义哪些值为数据值
- computed:定义哪些值为计算值
- action:定义哪些值为方法
- action.bound:定义哪些值为方法,并且强制this执行为当前类
- flow:定义哪些值为迭代方法
- 只能绑定静态属性,动态属性无法绑定。
-  makeAutoObservable:自动把属性和方法进行绑定-  参数一:当前类的指向 
-  参数二:可选,排除哪些属性,或方法 如: { reset: flase },reset方法排除可观测
-  参数三:可选 - autoBind:是否自动把this指向绑定到当前类
 
 
-  
3、数据使用
import count from "./count";
import { observer } from "mobx-react-lite";function App() {return (<><div>{count.count}</div><div>{count.double}</div><button onClick={() => count.add()}>加一</button><button onClick={() => count.api()}>请求</button></>);
}// 通过高阶函数observer处理
export default observer(App);
4、生成器
mobx通过
flow定义哪些属性可以通过生成器进行处理
*api() {const res = yield Promise.resolve(2);const res2 = yield Promise.resolve(2 + res);this.count = res;
}
执行过程:
- 通过调用api,获取一个generator对象,这个对象是个可迭代对象(iterator)。
- 第一次next, - 会执行代码到第一个yield。然后把第一个yield后面的结果返回
- 同时会把next传递的参数传递给res
 
- 依次类推,每次next都是如此执行。
- 通过for-of,能够遍历执行
- 所以flow就是通过generator生成器,获取请求结果。
注意:flow可以通过yield获取其他值,但是推荐获取Promise对象
5、指针
- 通过action定义的方法,可以给外界使用,但是this并不一定会指向store
- 通过action.bound定义的方法,就会把this强制指向到当前store
6、数据监听
- autorun:监听数据变化- 回调函数: - 如果回调函数内没有任何属性数据,只会监听初始化
- 如果回调函数内有属性的数据,就会监听属性数据的变化
- 属性数据可以有多个,监听就会同时进行
 
 
- 回调函数: 
- reaction:只监听store内的某一个数据是否发生变化- 参数一:回调函数,需要返回观察属性
- 参数二:观察属性发送变化,才会执行
- 不会监听初始化
 
import { makeAutoObservable, autorun, reaction } from "mobx";class Count {count = 0;sum = 0;constructor() {makeAutoObservable(this, {}, {autoBind: true});}addCount() {this.count++;}addSum() {this.sum++;}
}const count = new Count();// 只监听初始化
// autorun(() => {
//   console.log("只监听初始化");
// });// 只监听sum
// autorun(() => {
//   console.log("监听sum变化", count.sum);
// });// 监听count, sum
// autorun(() => {
//   console.log("监听sum和count变化", count.sum, count.count);
// });reaction(() => count.count,(c) => console.log("count变化", c);
)
7、异步
-  action:定义的方法可以直接使用异步,但是会进行警告。
-  runInAction:让方法中可以异步修改属性import { makeAutoObservable, runInAction } from "mobx"; class Count {value=0;constructor() {makeAutoObservable(this, {}, {autoBind: true});}add() {setTimeout(() => {runInAction(() => {this.value++;});}, 1000);} }let count = new Count(); export default count;
8、模块化
通过useContext进行跨组件通信
import { createContext, useContext } from "react";
import count from "./count";
import sum from "./sum";const countext = createCountext({ count, sum });
export function useStore() {return useContext(countext);
}
组件内使用
import { useStore } from "./store/index";
import { observer } from "mobx-react-lite";function App() {let { count, sum } = useStore();return (<div><span>{ count.value }</span><span>{ sum.value }</span></div>)
}export default observe(App);
9、持久化
通过sessionStorage轻松完成数据持久化
import { makeAutoObservable, autorun } from "mobx";class Count {value = sessionStorage.getItem("count") || 0;constructor() {makeAutoObservable(this);}add() {this.value++;}
}let count = new Count();
autorun(() => {sessionStorage.setItem("count", count.value);
});export default count;
10、组件外使用
需求:部分业务逻辑可能会在组件外部对数据进行修改
如:接口拦截,统一获取登录状态
- 需要在外部使用mobx订阅的方法修改数据
- 封装统一请求方法,然后请求拦截时,获取登录状态,修改登录状态
- 请求时有组件发起的 - 通过事件响应触发接口请求
- 通过useEffect监听组件挂载成功,触发接口请求
 
import { useEffect } from "react";
import { observer } from "mobx-react-lite";
import store from "@/store/index.js";
function axios() {setTimeout(() => {store.setIsLogin(true);}, 3000);
}function App() {useEffect(() => {// 模拟接口请求,修改登录状态axios();});return (<div>{ store.isLogin ? "登录中" : "未登录" }  </div>)
}export default observer(App);
8、路由
什么是路由?请求接口的地址是路由,网页的地址也是路由。
所以,网页的路由就是通过不同的GET请求地址,获取不同的页面。
1、安装
- react-router-dom:路由插件,版本6+
2、标签导航
1、路由定义
- BrowserRouter:定义history路由模式
- HashRouter:定义hash理由模式
- Routes:定义路由页面
- Route:定义路由页面与匹配路径- index: 是否为默认路由,为路由索引头部,不能给索引添加子路由
- path:定义路由地址
- element:定义路由元素
- Component:定义路由组件元素
 
import { BrowserRouter, Routes, Route } from "react-router-dom";
import Mine from "./mine";function App() {return <BrowserRouter><h1>app</h1><Routes><Route index element={<div>首页</div>}></Route><Route path="mine" Component={<Mine />}></Route></Routes></BrowserRouter>
}
2、路由跳转
- Link:路由跳转标签- to:路由跳转地址
 
- NavLink:路由跳转标签- to:路由跳转地址
 
import { BrowserRouter, Link, NavLink } from "react-router-dom";function App() {return <BrowserRouter><h1>app</h1><Link to="home">首页</Link><NavLink to="mine">我的</NavLink></BrowserRouter>
}
3、路由重定向
-  修改默认路由指向路径 
-  重定向路由指向路径 
-  Navigate:路由重定向,普通组件,不是路由组件- to:目标路由
 import { BrowserRouter, Routes, Route, Navigate } from "react-router-dom"; import Mine from "./mine";function App() {return <BrowserRouter><h1>app</h1><Routes><Route path="home" element={<div>首页</div>}></Route><Route path="mine" element={<Mine />}></Route><Route index element={<Navigate to="home" />}></Route></Routes></BrowserRouter> }
4、子路由
-  Outlet:嵌套路由插槽
-  多级路由: -  <Route path="first/second" element={<div>second</div>} />
-  一个路由地址对应一个标签或组件 
 
-  
-  嵌套路由: - 一级路由对应页面A
- 二级路由对应页面B
- 页面B嵌套再页面A中,页面B是页面A的子页面
 import { BrowserRouter as Router, Routes, Route, NavLink,Outlet, Navigate } from "react-router-dom";function Box() {return (<div><h3>box</h3><Outlet /></div>); }function App() {return (<Router><h1>app</h1><NavLink to="home">首页</NavLink><NavLink to="home/about">关于</NavLink><Routes><Route path="home" element={<Box />}><Route path="info" element={<div>信息</div>}></Route><Route path="about" element={<div>关于</div>}></Route><Route index element={<Navigate to="info" />}></Route></Route><Route index element={<Navigate to="home" />}></Route></Routes></Router>); }
3、编程导航
1、路由定义
-  createBrowserRouter:定义history路由
-  createHashRouter:定义hash路由
-  RouterProvider:定义路由页面- router:使用创建的路由
 
-  routerListItem:路由配置- path:路由路径
- element:路由页面
- Component:路由组件,可以为函数,可以为组件标签
- index:是否为默认路由,为路由索引头部,不能给索引添加子路由
- meta:路由元信息,值为对象
- children:子路由列表
 import { createBrowserRouter, RouterProvider } from "react-router-dom";const routers = createBrowserRouter([{ path: "home", element: <div>home</div>, index: true },{path: "mine",element: <Mine />,meta: { icon: "mine" },children: [{ path: "info",element: <div>mine info</div>,index: true,}]} ]);function App() {return <div><h1>app</h1><RouterProvider router={routers} /></div>; }
2、路由跳转
- const navigate = useNavigate():返回路由跳转函数- navigate(参数):进行路由跳转 - 字符串参数:进行路由路径跳转
- -1:向后跳转
- 1:向前跳转
 
 
- navigate(参数):进行路由跳转 
- 注意:只能在存在Router上下文的组件中使用
3、路由重定向
-  createBrowserRouter的路由配置项中,并没有路由重定向配置
-  只能用 Navigate进行路由重定向const routers = createBrowserRouter([{ path: "/", index: true, element: <Navigate to="home" /> },{ path: "home", element: <div>首页</div> } ]);
4、子路由
- 多级路由:正常配置
- 嵌套路由: - 通过children进行路由配置
- 被嵌套页面通过Outlet组件进行接受
 
- 通过
5、最佳实践
- src/router/index.js:定义路由配置列表
- src/index.js:- 导入路由列表
- 使用createBrowserRouter创建路由
- 使用RouterProvider挂载路由
 
- src/view/app.jsx:使用- Outlet:挂载子路由
4、路由应用
1、路由传值
- 注意:只能在存在Router上下文的组件中使用
- 路由传值,也就是get请求传参。 - params:url/:id,需要对路由路径进行修改
- query:url?a=1&b=2
- hash:url#123
 
- params:
- 读取传参: - const params = useParams();读取params传值
- const [querys] = useSearchParams();读取query传值- query.get(key):读取key的值
- query.append(key, value):新增
- query.delete(key):删除
- query.set(key, value):修改
- query.has(key):判断是否存在
- query.keys()
- query.values()
- query.forEach(fn)
- query.toString():输出字符串
- query.size:个数
 
- const location = useLocation():读取当前路由对象- location.hash:读取hash传值
- location.meta:读取路由元信息
- location.pathname:读取路由地址
- location.search:读取query传值
 
 
2、路由懒加载
-  react-router-dom:没有路由懒加载功能
-  使用 React.lazy高阶函数,能够实现路由组件懒加载const routers = createBrowserRouter([{ path: "/", index: true, element: <Navigate to="home" /> },{path: "/home",Component: React.lazy(() => import("@/view/home"))} ]);
3、跳转前拦截
react-router-dom没有路由跳转前拦截- 只能在
navigate调用前,手动执行拦截逻辑
4、跳转后通知
react-router-dom没有没有路由跳转后拦截- 只能通过监听
useLocation获取的loaction变化,判断是否跳转完成- 使用
useEffect监听
5、路由封装
-  路由设置封装 - 设计思想:文件驱动路由
- 通过动态读取view文件目录,生成路由配置
 // src/route/index.js import { lazy } from "react"; import { createBrowserRouter, Navigate } from "react-router-dom";const baseUrl = "view"; // 配置读取目标 const root = "app.jsx"; // 配置layout根节点 const indexUrl = "home"; // 配置默认路由 const error = "error.jsx"; // 配置404const routes = [{ path: "/", Component: lazy(() => import(`@/${baseUrl}/${root}`)) },{ path: "*", Component: lazy(() => import(`@/${baseUrl}/${error}`)) }, ]; const children = [{ index: true, element: <Navigate to={indexUrl} /> }]; const files = require.context("@/view", true, /index\.jsx$/); files.keys().forEach((file) => {const model = lazy(() => import(`@/${baseUrl}${file.slice(1)}`));const segments = file.split("/");let current = {};for (let i = 1; i < segments.length; i++) {const segment = segments[i];if (segment === "index.jsx") {current.Component = model;} else {let list = children;if (i !== 1) {if (!current.children) current.children = [];list = current.children;}const child = list.find((child) => child.path === segment);if (child) current = child;else {current = { path: segment };list.push(current);}}} }); routes[0].children = children;export default createBrowserRouter(routes);
-  路由使用封装 - 根据跳转前拦截,和跳转后通知逻辑进行封装
- 封装自定义hook:useRoute
- 返回:{navigate,loaction,beforeRouter,afterRouter,Outlet}- navigate(path,params) - path:跳转路径
- params:可选,query传参
 
- location:路由信息
- beforeRouter(callback):跳转前拦截,订阅函数 - callback:回调函数 - 参数一:to,路由跳转目标
- 参数二:from,路由原地址
- 参数三:next([path]),通过函数,可修改跳转路径
 
 
- callback:回调函数 
- afterRouter(callback):跳转后通知,订阅函数 - callback:回调函数 - 参数一:to,路由跳转目标
- 参数二:from,路由原地址
 
 
- callback:回调函数 
- Outlet:子路由挂载组件
 
- navigate(path,params) 
 // src/router/hook.js import { useEffect, useRef } from "react"; import { useNavigate, Outlet, useLocation } from "react-router-dom";function useRoute() {const befores = new Set();const afters = new Set();const navigateTo = useNavigate();const location = useLocation();const from = useRef(location.pathname);useEffect(() => {afters.forEach((callback) => callback(location.pathname, from.current));from.current = location.pathname;return () => {befores.clear();afters.clear();};// eslint-disable-next-line react-hooks/exhaustive-deps}, [location]);function navigate(path, params) {let query = "";if (typeof to === "string" && params) query = switchParams(params);if (!befores.size) navigateGo(path + query);else {let pass = [];let url = path + query;befores.forEach((callback) => {let promise = new Promise((resolve) => {callback(path.split("?")[0], from.current, (r) => {if(r) url = r;resolve()});});pass.push(promise);});Promise.all(pass).then(() => {navigateGo(url);});}}function switchParams(params) {return new URLSearchParams(params).toString();}function navigateGo(path) {navigateTo(path);}function beforeRouter(callback) {if (typeof callback === "function") {befores.add(callback);return () => {befores.delete(callback);};}}function afterRouter(callback) {if (typeof callback === "function") {afters.add(callback);return () => {afters.delete(callback);};}}return { navigate, location, beforeRouter, afterRouter, Outlet }; }export default useRoute;
6、路由进度条
- 路由进度条,就是在body上面添加一个定位元素,然后控制宽度的变化。
- 路由跳转前,创建元素,并使其宽度变化定值,模拟路由进度
- 路由跳转后
- 如果没有元素,创建元素
- 如果有元素,执行元素动画
- 元素动画宽度变化为100%,填充body,然后删除元素
-  使用 gsap完成进度动画
-  封装 useRouteProgress,配合useRoute进行使用
-  const progress = useRouteProgress(time)-  time:动画时间,默认0.2秒 
-  progress.start(n):开启进度条,默认动画宽度30%
-  progress.end():完成进度条,并删除进度条
 
-  
// src/router/progress.js
import gsap from "gsap";
import { useRef } from "react";function useRouteProgress(time=0.2) {let ref = useRef();let timer = useRef();function createDom() {cleanDom();ref.current = document.createElement("div");ref.current.className = "progress";document.body.appendChild(ref.current);timer.current = gsap.timeline({ duration: time });timer.current.set(ref.current, {position: "absolute",top: 0,left: 0,width: 0,height: 2,background:"linear-gradient(90deg,#FFFF00 0%,#DE7474 49.26%,#EE82EE 100%)",zIndex: 9999,});}function cleanDom() {if (ref.current) {document.body.removeChild(ref.current);ref.current.remove();timer.current.kill();ref.current = null;timer.current = null;}}function start(n = 30) {createDom();timer.current.to(ref.current, { width: `${n}%` });}function end() {if (!ref.current) createDom();timer.current.to(ref.current, { width: "100%" }).then(() => cleanDom());}return { start, end };
}export default useRouteProgress;7、进出动画
- 出入动画:通过路由跳转前后拦截,进行layout层的动画
- 路由跳转前:
- 开启消失动画,控制layout元素在显示器消失
- 动画完成后,才执行路由跳转功能
- 路由跳转后:开启进入动画,控制layout元素在显示器出现
- 使用gsap完成进出动画
- 封装useRouteAnimate,配合useRoute进行使用
- const routeAnimate = useAnimate(ref, time);- ref:通过ref获取到的layout元素,建议获取h5纯标签元素
- time:动画时间,默认0.2秒
- routeAnimate:动画控制器- onEnter():进入动画
- onLeave(callback):消失动画- callback消失动画完成回调
 
 
 
// src/router/animate.js
import gsap from "gsap";
import { useRef } from "react";function useRouteAnimate(ref, time = 0.5) {const timer = useRef();function createTimer() {clearTimer();timer.current = gsap.timeline({ duration: time });}function clearTimer() {if (timer.current) {timer.current.kill();timer.current = null;}}function onEnter() {createTimer();timer.current.fromTo(ref.current,{ x: -20, opacity: 0 },{ x: 0, opacity: 1 });}function onLeave(callback) {if (timer.current && ref.current) {timer.current.to(ref.current, { x: 20, opacity: 0 }).then(() => callback());}}return { onEnter, onLeave };
}export default useRouteAnimate;8、使用案例
入口组件使用
// src/index.js
import React from "react";
import ReactDOM from "react-dom/client";
import { RouterProvider } from "react-router-dom";
import "animate.css";
import "./index.scss";
import routers from "./router";const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<><RouterProvider router={routers} /></>
);layout组件使用
// src/view/app.js
import "./app.scss";
import { useRef } from "react";
import { useRoute, useRouteProgress, useRouteAnimate } from "@/router/hook";function App() {const ref = useRef();const { navigate, Outlet, beforeRouter, afterRouter } = useRoute();const progress = useRouteProgress();const routeAnimate = useRouteAnimate(ref);beforeRouter((to, from, next) => {routeAnimate.onLeave(() => {progress.start();next();});});afterRouter((to, from) => {progress.end();routeAnimate.onEnter();});function handleClick(path) {navigate(path);}return (<div className="wrap"><div className="nav"><button onClick={() => handleClick("home")}>首页</button><button onClick={() => handleClick("mine?a=10")}>我的</button><button onClick={() => handleClick("config/list")}>配置列表</button><button onClick={() => handleClick("config/detail")}>配置详情</button></div><div className="box" ref={ref}><Outlet /></div></div>);
}export default App;9、登录判断
- 通过useRoute,可知:beforeRouter可订阅多个路由跳转前拦截
- 通过路由跳转前,获取redux或者mobx管理的是否登录状态,判断是否重定向到登录
- 通过接口请求拦截时,可以修改redux或者mobx存储的登录状态
- 由于目前,前后端大部分都用token无状态登录验证
- 所以后端会传给前端token, - 可以后端存储在浏览器的cookie中
- 也可以前端获取后,存储在浏览器的cookie或者locationStorage、sessionStorage
 
- 但是否登录,单靠前端无法进行有效判断。
- 需要后端通过接口请求返回登录状态,前端才能进行判断。
- 返回的token,后端存储在cookie中,并设置响应头HttpOnly,前端无法通过JavaScript访问。
9、高阶组件
- 高阶组件:通过对组件进行预处理的函数,函数返回一个组件
- 定义高阶组件:withApp
- 参数:函数组件
- 返回:返回一个函数callback - callback是一个函数组件
 
function withApp(Component) {return (props) => {useEffect(() => {console.log("App 加载完成");});return <Component />;}
}function App() {return <div>App</div>;
}export default withApp(App);
10、请求
1、fetch封装
class Api {constructor(baseurl, timeOut) {this.baseurl = baseurl;this.timeOut = timeOut || 10000;}async #request(url, method = "GET", data, json = false, fileName) {const path = this.baseurl + url;const controller = new AbortController();const config = { method, signal: controller.signal };const timeoutPromise = new Promise((_, reject) =>setTimeout(() => {controller.abort();reject(new Error("请求超时"));}, this.timeOut));if (data) {config.body = json ? JSON.stringify(data) : data;if (json) config.headers = { "Content-Type": "application/json" };}try {const res = await Promise.race([fetch(path, config), timeoutPromise]);if (!res.ok) throw new Error(res.statusText);// 进行接口响应后拦截逻辑 - 可通过响应头获取登录状态等const contentType = res.headers.get("content-type").split(";")[0].trim();if (!contentType) throw new Error("Unknown content type");// 处理文件下载if (fileName) {const resData = await res.arrayBuffer();this.#downloadFile(resData, contentType, fileName);return { success: true };}// 返回请求结果return contentType === "application/json"? await res.json(): await res.text();} catch (error) {throw new Error(`请求失败: ${error.message}`);}}#downloadFile(res, contentType, fileName) {const blob = new Blob([res], { type: contentType });const url = URL.createObjectURL(blob);const a = document.createElement("a");a.href = url;a.download = fileName;a.click();URL.revokeObjectURL(url);a.remove();}get(url, query, param) {let path = url;if (query) path += "?" + new URLSearchParams(query).toString();if (param) path += "/" + param;return this.#request(path);}post(url, data) {return this.#request(url, "POST", data, true);}postByFormData(url, data) {let formData = new FormData();for (const key in data) {formData.append(key, data[key]);}return this.#request(url, "POST", formData);}download(url, fileName = "file") {this.#request(url, "GET", null, false, fileName);}upload(url, file, key = "file") {const formData = new FormData();formData.append(key, file);return this.#request(url, "POST", formData);}uploads(url, files, key = "files") {const formData = new FormData();for (const file of files) {formData.append(key, file);}return this.#request(url, "POST", formData);}
}let baseurl = process.env.NODE_ENV === "production" ? "" : "/api/v1";
let api = new Api(baseurl);
export default api;
2、定义接口
-  在 src/api文件夹下,创建js文件,并定义接口import api from "../utils/api";export const getApi = (params) => api.get("/getApi", params);export const postApi = (params) => api.post("/postApi", params);
11、服务代理
脚手架
create-react-app的代理配置
-  修改端口:通过在 .env文件内修改POST,修改端口号
-  服务代理: -  方式一: - 通过直接在package.json中,添加proxy字段,进行代理
- "proxy": "http://localhost:8080"- 方便快捷,直接设置
- 只能配置字符串,只能代理一个服务,无法修改前缀
 
 
- 通过直接在
-  方式二: -  下载插件: npm i -D http-proxy-middleware
-  通过在 src下创建setupProxy.js配置代理
-  脚手架会自动寻找 src/setupProxy.js,然后执行代理配置
-  注意: setupProxy.js不能使用ESM导入导出const { createProxyMiddleware } = require("http-proxy-middleware");module.exports = function (app) {app.use("/api",createProxyMiddleware({target: "http://localhost:8080",changeOrigin: true,pathRewrite: { "^/api": "" },})); };- 配置灵活,能够代理多个服务,可以修改前缀
 
 
-  
 
-  
12、环境变量
react最新的脚手架,也能使用.env环境变量文件
-  .env:通用环境变量文件
-  .env.development:开发读取
-  .env.test:测试读取
-  .env.production:生成读取
-  由于脚手架的设置,环境变量名必须以 REACT_APP_开头如: REACT_APP_MYCODE = abcdef
-  环境变量文件修改,不会触发热更新 
- 代码中,通过process.env.*读取环境变量
- process:nodejs中进程模块
- process.env.NODE_ENV:脚手架自动设置的环境变量,值为:- development:开发环境
- production:生成环境
- test:测试环境
 
13、配置别名
-  下载插件 npm i -D @craco/craco
-  修改启动项 "scripts": {"dev": "craco start","build": "craco build","test": "craco test","eject": "react-scripts eject" },
-  新增 craco.config.js文件const path = require("path");module.exports = {webpack: {alias: {"@": path.resolve(__dirname, "src"),},}, };
-  新增 jsconfig.json文件{"compilerOptions": {"baseUrl": ".","paths": {"@/*": ["src/*"]}},"include": ["src"] }
- 可以发现,使用@craco/craco插件,会修改react-scripts的默认配置
- 所有:完全可以不需要使用npm run eject,抛出默认配置
- 只需要使用@craco/craco插件,对webpack配置进行微调
14、静态资源
- public文件夹:不会被编译,压缩。打包时会复制内容到dist包目录
- src/assets文件夹:打包时会被编译,压缩
- src连接使用 - public:- <img src="/imgs/bg.png">
- 直接使用public为根路径,然后使用文件地址
 
- src/assets:- import bg from "@/assets/imgs/bg.png"
- <img src={bg} />
- 需要使用ESM引入图片,然后复制给图片src
 
 
- css使用:正常使用路径引入 - public:- background: url("../../public/imgs/bg.png");
 
- src/assets:- background: url("../assets/imgs/bg.png");
 
 
15、Hooks
1、useState
 
- 创建参与渲染有关的变量
- let [data, setData] = useState(0)- 参数:初始化的值
- 返回数组 - item1:参与渲染的变量
- item2:修改变量的函数
 
 
- 每次修改变量,都会刷新组件。
2、useEffect
 
- 监听函数组件挂载,卸载
- 监听函数组件内,动态数据的变化
3、useRef
 
1、记忆
希望能够像useState一样能够记录一个值,但又不想参与渲染,如定时器
const ref = useRef(null)
2、获取dom
- 通过定义ref,获取一个不参与渲染的变量。
- 通过props赋值,把ref赋值给子节点
import { useRef, forwardRef, useImperativeHandle } from "react";function App() {let ref1 = useRef(null);let ref2 = useRef(null);let ref3 = useRef(null);function handleClick() {console.log(ref1.current);console.log(ref2.current);console.log(ref3.current);}return (<><h1 ref={ref3}>根</h1><button onClick={handleClick}>按钮</button><Soned ref={ref1} /><Soned ref={ref2} /></>);
}function Son(props, ref) {useImperativeHandle(ref, () => ({name: "son",}));return (<><h3>Son</h3>  </>)
}const Soned = forwardRef(Son);
- 普通标签,可以直接通过ref获取到元素。
- 自定义组件 - 首先需要forwardRef把ref注入到函数组件的第二个参数中
- 然后需要使用useImperativeHandle定义哪些属性暴漏给ref
 
- 首先需要
4、useImperativeHandle
 
- 定义哪些属性暴漏给ref
- 参数一:接受到的ref
- 参数二:回调函数,返回暴漏的值
5、useCoutext
 
-  const MyContext = createContext(defaultValue):创建一个上下文对象- defaultValue:设置默认值
 
-  通过 MyContext.Provider包裹子组件,通过value设置值<MyContext.Provider value={{ data }}>{children}</MyContext.Provider>
-  通过 useContext(MyContext):读取上下文对象const { data } = useContext(MyContext);
6、useReduce
 
- const [state, dispatch] = useReduce(reducer, init)- state:显示的数据
- dispatch:修改函数
- init:初始化的默认值
- reducer:修改函数 - state:原有的state值
- action:dispatch传递的参数
- 必须进行返回,返回的值会覆盖原有的state
 
 
import { useReducer } from "react";const init = { title: "abc" };const reducer = (state, action) {if ( action.type in state ) {state[action.type] = action.value;}return { ...state };
}function App() {let [state, dispatch] = useReducer(reducer, init);function handleClick() {dispatch({ type: "title", value: "xdw" });}return (<><h3>{state.title}</h3><button onClick={handleClick}>按钮</button></>)
}
7、useMemo
 
- 如果组件内,有动态时间显示,那么这个组件就会每秒就进行刷新
- 如果这个组件内同时存在一个依赖与另一个值的大量计算,那么每次刷新都会重新大量计算
- 所以就出现需求:某个计算值,不会受其他的state变化印象的需求
- let data = useMemo(work, [dependencies])- data:work函数执行后返回的值
- work:执行函数,必须有返回值
- dependencies:监听的states
 
- 首次渲染时会执行
- 只有在监听的states变化时,才会执行work,data才会变化
- 可以充当计算属性使用,拥有缓存的效果
- 可以避免大量重复性计算,提高性能
如果不使用useMemo、如何解决?
- 进行状态降级,就是把功能细分后,变成更小的组件。
- 让两个组件不会相会影响
8、useCallback
 
useMemo保证了值的不变性,useCallback就保证了函数的不变性
- 通过useMemo和memo可知:
- 如果传递的props是引用类型数据,子节点还是会被刷新。
- 所以需要useMemo处理传递的引用类型数据。
- 如果传递是函数,就可以使用useCallback
- const fn = useCallback(work, [dependencies])- fn:就是work函数
- work:需要处理的函数
- dependencies:监听的states
 
- 可以理解为useCallback为useMemo的降级处理。
- useMemo会调用work,然后获得返回值
- useCallback不会调用函数,而是把调用职权弹出
9、useLayoutEffect
 
- useEffect:是监听组件渲染完成后执行
- uesLayoutEffect:是监听组件渲染前执行 - 使用 方式 和useEffect完全一样。
- 例如:动态设置元素的高度,让元素高度超过父元素时隐藏。 - 此时就可以通过useLayoutEffect在浏览器渲染组件前获取到高度
- 然后执行判断逻辑,动态设置高度。
- 然后再进行渲染
 
- 所以useLayoutEffect会阻塞组件渲染,非必要不要使用
- 会造成页面卡顿
 
10、useDeferredValue
 
用于延迟state的变化。
在处理大量数据时,或者优先显示时很有用。
import { useState, useDeferredValue } from "react";function App() {const [query, setQuery] = useState('');const deferredQuery = useDeferredValue(query);return (<div><inputtype="text"value={query}onChange={(e) => setQuery(e.target.value)}/><div>延迟显示:{deferredQuery}</div></div>);
}
11、useTransition
 
让setState变成非紧急处理,让其他的setState优先变化,渲染。
如果state变化时间过长,希望监听state是否变化完成。
- 可以通过useEffect监听数据的变化
- 可以通过useTransition监听事件是否变化完成
function App() {const [query, setQuery] = useState("");const [isPadding, startTransition] = useTransition();function handleChange(e) {startTransition(() => {setQuery(e.target.value);});}return (<div><input type="text" value={query} onChange={handleChange} /><div>即时显示:{query}</div><div>{isPadding ? "延迟中..." : ""}</div></div>);
}
12、useSyncExternalStore
 
连接外部变量
- const state = useSyncExternalStore(subscribe, getSnapshot)- state:通过getSnapshot函数返回的值
- getSnapshost:返回外部的变量值
- subscribe:订阅函数 - 参数:回调函数。当state发送变化后,只有调用回调函数,才能触发组件刷新。
- 返回值:回调函数, - 当组件卸载时,会调用该回调函数,用于取消订阅。
- 当subscribe的this被修改时,每次修改数据,都会执行取消订阅
 
 
 
- 注意: - 只能处理基础类型的数据,对象,或数组的修改,无法处理。
- 对象或数组,可以使用JSON.stringify格式化处理
 
let count = 0;
const subScribers = new Set();
const countStore = {get() {return count;},sub(callback) {subScribers.add(callback);return () => {console.log("组件卸载,取消订阅");subScribers.delete(callback);};},// 数据发送变化,通知所有订阅者add() {count++;subScribers.forEach((callback) => callback());},
};function App() {let state = useSyncExternalStore(countStore.sub, countStore.get);return (<div><button onClick={countStore.add}>按钮</button><div>{state}</div></div>);
}
13、自定义
- 定义以use前缀开头的函数
- 函数内可以使用react自带的hook
- 返回处理好的数据或方法
- 如封装的useStore、useRoute、useRouteProgress、useRouteAnimate
16、组件
1、Fragment
 
react提供React.Fragment空文档标记,既保证只有一个根节点,又不会增加层级
const App = () => <>hello world</>
2、Suspense
 
占位异步加载组件
-  判断依据: Suspense组件加载的子组件,如果子组件抛出Promise.resolve或Promise.reject,都会使suspense组件判定为加载状态。function Box() {throw Promise.resolve(); }function App() {return <><h1>app</h1><Suspense fallback={<p>loading...</p>}><Box /></Suspense> </> }
-  用法一:配合lazy实现组件懒加载 const Box = lazy(() => import("@/view/box"));function App() {return <><h1>app</h1> <Suspense fallback={<p>loading...</p>}><Box /></Suspense> </>; }
-  用法二:阻塞Box渲染 function Box() {// throw Promise.resolve(); 会阻塞渲染,显示loading// throw Promise.reject(); 会阻塞渲染,显示loadingconst data = Promise.resolve("box");console.log("padding");return <box>{data}</box>; }function App() {return <><h1>app</h1> <Suspense fallback={<p>loading...</p>}><Box /></Suspense> </>; }
-  通过上面的案例,可以知道Box会执行两次 - 第一次: - 获取到Promise异步执行
- Suspense组件判断显示loading组件
- 监听Promise的状态
 
- 第二次: - 监听到Promise执行完成,获取到结果
- 结束loading状态
- 显示Box组件
 
- 并不一定会只执行两次,而是通过对Promise的监听,判断是否数据加载完成
 
- 第一次: 
-  模拟接口 // 模拟接口1 function api1() {return new Promise((resolve) => {setTimeout(() => {resolve("hello");}, 3000);}); }// 模拟接口2 function api2() {return new Promise((resolve) => {setTimeout(() => {resolve("box");}, 5000);}); }// 接口防抖处理 function axios(fn) {let res = null;const promise = fn;promise.then((data) => {res = data;});return function () {if (res) return res;return promise;}; }const resPromise1 = axios(api1()); const resPromise2 = axios(api2());function Box() {const data1 = resPromise1();const data2 = resPromise2();return (<div><p>{data1}</p><p>{data2}</p></div>); }function App() {return <><h1>app</h1><Suspense fallback={<p>loading...</p>}><Box /></Suspense></>; }- 对接口进行防抖处理
- 此时会调用三次Box: - 第一次调用,会创建一个promise,此时promise还没有任何状态
- 第二次调用,promise进行padding状态,触发第二次调用
- 第三次调用,promise进入resolve状态,触发第三次调用,获取结果
 
- 注意,如果有多个接口调用,会监听最长的响应
 
17、API
1、createElement
 
- 已过时,执行完成后,返回虚拟dom对象
- 引入:import { createElement } from "react";
- const Dom = createElement(ele, props, ...children):创建虚拟dom- Dom:创建的Dom组件元素
- ele:dom标签,或者react提供的组件。比如React.Fragment
- props:属性,事件 - className:定义类名
- style:定义样式
- onClick:绑定点击事件
- data:props.data其他属性赋值,都是props
 
- children:子节点的虚拟dom对象
 
2、Children
 
- 已过时,用于处理插槽props.children
- 引入:import { Children } from "react";
- Children.count(props.children):获取props.children的数量
- Children.forEach(children, (child, index) => {}):遍历
- Children.map(children, (child, index) => ele):map
- Children.toArray(children):返回children数组
3、forwardRef
 
- 将ref注入到函数组件的第二个参数中
- 配合useRef、useImperativeHandle完成自定义组件的读取
4、createContext
 
-  创建上下文对象,并设置默认值 const MyContext = createContext(defaultValue)
-  上下文对象设置值 <MyContext.Provider value={{ data }}>{children}</MyContext.Provider>
5、lazy
 
- 高阶组件,实现组件懒加载
- const AppLazy = React.lazy(App)
- const AppLazu = React.lazy(() => import("@/view/app.jsx"))
6、memo
 
- 和useMemo的情况差不多
- 父组件内有动态时间显示,就会不停的刷新子组件。
- 子组件如果有大量计算,就会因为刷新而不断的执行
- 需求:子组件变成纯组件,只会由与父组件绑定的state变化影响,其他的变量不会刷新子组件
- const PureComponent = memo(Component)- PureComponent:纯组件
- Component:组件
 
- 通过memo高阶组件处理,就能到的一个纯组件。输入不变,输出就不会变化
- 避免大量的计算,提高性能
特殊情况:父组件给子组件的props包含数组时
- 输入不变,输出就不会变
- 如果输入的是一个数组这样的引用数据时,也就是给纯组件传递的props数据是引用类型。此时还是会被影响
- 原因:每次刷新时,引用数据会重新生成,虽然值相同,但引用地址会发送变化,所以就导致输入其实是变化的。
- 解决:在父组件中使用useMemo处理
7、startTransition
 
就是
useTransition的第二个参数和useTransition一样,把包裹的setState操作,放入非紧急处理。
18、TS开发
- 使用create-react-app myApp --template typescript,创建使用ts开发的项目
- npm i -s typescript @types/node @types/react @types/react-dom @types/jest- 添加ts到已有的项目
 
19、规范配置
- create-react-app脚手架默认的- eslint配置为- react-app、- react-app/jest
对项目
eslint默认配置进行微调
-  方式一: -  创建 .eslintrc.js文件module.exports = {extends: ["react-app", "react-app/jest"],rules: {"no-console": "warn", // 如果出现打印,就报错}, };
-  然后重启项目,完成规范微调 
-  规范参考:https://eslint.nodejs.cn/docs/latest/rules/ 
 
-  
-  方式二: - 在package.json的eslinConfig配置项修改
- 添加rules配置
- 在rules内微调
 
- 在
20、GIT拦截
1、格式化代码
-  根据 create-react-app脚手架官网文档
-  下载插件: npm i -D husky lint-staged prettier
-  package.json添加配置 {// ..."husky": {"pre-commit": "lint-staged"} }
2、commit规范
- 使用插件@commitlint/cli能进行规范校验
- 配置很繁琐,很少项目进行配置
- 不建议配置commit规范,建议参考一下提交模板
[任务/bug号] 1024
[修改内容] 完成create-react-app脚手架解析
