第十二步: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.Children
Api能够遍历获取每个插槽子节点 - 通过
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)
:mapChildren.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脚手架解析