前言

  • 个人笔记,记录个人过程,如有不对,敬请指出
  • React17+React Hook+TS4 最佳实践仿 Jira 企业级项目项目完成到第十章,剩下后面就没有看了,说的不是特别好
  • husky方便我们管理git hooks的工具

image-20221203204054233

REST-API风格

https://zhuanlan.zhihu.com/p/536437382

json-server

  • 安装
1
npm install -g json-server
  • 项目安装
1
npm install -D json-server 

项目开始

用jsx渲染开发工程列表

  • 初始化代码出现问题,表单收集的组件无法将请求结果发送给list组件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
import React, { useEffect, useState } from "react";

const List = () => {
const [params,setParams] = useState({
name:'',
personId:'',
});
const [selectOptions,setSelectOptions] = useState([])
const [listData,setListData] = useState([]) //请求列表数据
useEffect(async () => {
try{
const response = await fetch('/abc');
if(response.ok){
const result = await response.json();//获取数据结果
setListData(result ?? [])
}
}catch (e){
console.log('发生错误');
}
},[params])
return (
<form>
<input type='text' value={params.name} onChange={event => setParams({ ...params,name:event.target.value })}/>
<select value={params.personId} onChange={event => setParams({
...params,
personId: event.target.value,
})}>
{
selectOptions.map(item => {
return (
<option value={item.value}>{ item.label }</option>
)
})
}
</select>
</form>
);
};

export default List;

  • 解决办法(3-2 用状态提升分享组件状态,完成工程列表页面),也就是将请求放在父组件当中
  • 顺带一提,如果我们使用的是vite创建的react项目,就无法像老师一样直接使用process.env来读取设置的变量了

学习自定义hook

  • useHooks(不管是自带的hooks还是自己创建的hooks),不可以在普通函数中运行,只能在hooks当中使用或者其他hooks使用,所以在自定义hooks的时候,需要以useXxx开头

使用自定义的useMount和useDebounce

  • useMount
1
2
3
4
/*只在初次挂载执行*/
export const useMount = (callback) => {
useEffect(callback,[])
}
  • useDebounce(防抖)
1
2
3
4
5
6
7
8
9
10
11
12
13
/*自定义防抖hooks*/
export const useDebounce = (value,delay) => {
const [debounceValue,setDebounceValue] = useState(value)
useEffect(() => {
const timer = setTimeout(() => { setDebounceValue(value) },delay);
return () => {
/*下一次effect执行前的处理*/
clearTimeout(timer)
}
},[value,delay])
return debounceValue;
}

  • useDebounce的使用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/*搜索参数*/
const [params,setParams] = useState({
name:'',
personId:'',
})
const debounceValue = useDebounce(params,2000);

/*
* 当搜索条件发生变化的时候,就更新
* */
useEffect(async () => {
/*请求获取列表数据*/
try{
const response = await fetch(`${apiUrl}/projects?${qs.stringify(cleanEmptyObj(debounceValue))}`)
if(response.ok){
const result = await response.json();
setListData(result)
}
}catch (e){
console.log(e);
}
},[debounceValue])
  • useDebounce的理解

    • 传入: 传入需要节流的值和延迟

    • 返回: 返回节流后的新state数据

    • 原理:

      • 内部对传入的value进行重新构建一个state,当传入的value发生改变的时候的时候,会被内部debounce创建的节流函数所捕捉,捕捉到后,如果中途没有重新捕捉到新的值,则会在设置的时间之后更新内部debounce的值,否则的话就会中断更新,重新计时
    • 图示原理

js改造为ts

  • 文件名更改

    • js -> 改为 ts
    • jsx -> 改为tsx
  • 遇到qs模块types缺失的情况: Could not find a declaration file for module 'qs'.xxxx,npm i --save-dev @types/qs,安装对应的types即可

1
yarn add @types/qs -D 
  • 注意箭头函数和普通函数的泛型书写位置
1
2
3
4
5
6
7
8
9
// 箭头函数
const fn1 = <T,U>() => {

}

//普通函数
function fn2<T,U>() {

}

鸭子类型和json-server中间件

  • 鸭子类型
    • ts是只看是否实现了这个接口当中的成员,实现了就可以通过,没有实现就不通过,不管有没有定义
    • 说通俗点就是只要你符合这个规定里面的规则,就是他说的一个东西
    • 比如鸭子会嘎嘎叫,只要你会嘎嘎叫,ts就认为你是鸭子
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
interface Base {
id:number
}
const test = (param:Base) => {

}
/*定义一个对象,对象当中的实现了接口Base的成员*/
const a = {
id:100,
name:'李白'
}
test(a);//不会报错

/*定义一个对象,对象当中没有实现Base的成员*/
const b = {
name:'李白'
}
test(b);//警告 'id' is declared here.

  • json-server中间件
    • 注意POST为大写
    • package.json更改"json-server": "json-server __json_server_mock/db.json --watch --port 3033 --middlewares __json_server_mock/middleware.js"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
module.exports = (req,res,next) => {
if(req.method === 'POST' && req.path === '/login'){
if(req.body.username === 'qiuye' && req.body.password === '123456'){
return res.status(200).json({
user:{
token:'我是token'
}
})
}
/*密码错误*/
else{
return res.status(400).json({message:'用户名或密码错误'})
}
}
next();
}

  • 登录表单
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import React, { FormEvent } from "react";
const apiUrl = import.meta.env.VITE_REACT_APP_API_URL;
const Login = () => {
const login = (params:{username:string,password:string}) => {
fetch(`${apiUrl}/login`,{
method:'POST',
headers:{
'Content-Type':'application/json'
},
body:JSON.stringify(params),
}).then(async (response) =>{
if(response.ok){
//await response.json();
}
})
}
/*点击登录*/
const handleSubmit = (event:FormEvent) => {
event.preventDefault();//阻止默认行为
//@ts-ignore;
const username = event.target[0].value;
//@ts-ignore;
const password = event.target[1].value;
login({username,password})
}
return (
<form onSubmit={handleSubmit}>
<label htmlFor='username'>用户名</label>
<input type='text' id='username' />
<br/>
<label htmlFor='password'>密码</label>
<input type='password' id='password' />
<button type='submit'>登录</button>
</form>
);
};

export default Login;

安装jira-dev-tool

1
2
3
npx msw init public
//或者 指定到public目录下
npx msw init ./public

使用自定义useHttp处理登录状态

1
2
3
4
5
6
7
8
9
10
11
12
13
export const useHttp = () => {
const {userInfo} = useAuth();
return (...[url,config]:Parameters<typeof http>) => http(url,{...config,token:userInfo.token});
}

// 第一步:
parameters<typeof http>返回htpp的联合类型元组,也就是[url:string,{data,token,headers,...customConfig}:Config]
//所以你明白老师为什么要设置为一个数组了吧?因为这个是联合类型元组,描述的是数组的结构

// 第二步:使用扩展运算符展开函数参数
//如果不适用展开运算符,我们调用函数必须要 Example(['请求地址',config对象])

//如果使用了,就可以 Example('请求地址',config对象)
  • 在使用函数的时候,如果是数组,想要将数组当中的数据依次传入数组,可以使用扩展运算符
1
2
3
4
5
6
7
8
9
10
const arr1 = [1, -1, 0, 5, 3];
const min = Math.min(...arr1);
console.log(min);// -1

Math.min用法是传入0个或多个数字,0 个或多个数字,将在其中选择,并返回最小值。

const arr1 = [1, -1, 0, 5, 3];
const max = Math.max(...arr1);
console.log(max);// 5
Math.max用法是传入0个或多个数字,0 个或多个数字,将在其中选择,并返回最大值。

更改为antd

1
2
3
4
5
6
7
8
9
10
11
12
string.localeCompare(targetString,locales,options);


该方法返回的是一个数字用来表示一个参考字符串和对比字符串是排序在前,在后或者相同

返回值:返回值是一个数字,目前的主流浏览器都返回的是1、0、-1三个值,但是也有其他情况,所以不可以用绝对的值等于1、-1这种去判断返回的结果

返回值大于0:说明当前字符串string大于对比字符串targetString

返回值小于0:说明当前字符串string小于对比字符串targetString

返回值等于0:说明当前字符串string等于对比字符串targetString

使用css-in-js-Emotion

  • App.css样式更改为如下(App.css为全局样式)
1
2
3
4
5
6
7
8
html{
/*使得1rem === 10px*/
font-size: 62.5%;/* 这样子使得font-size为16px * 62.5% = 10px */
}

html body #root .App {
min-height: 100vh;
}
  • 安装emotion
1
yarn add @emotion/react @emotion/styled
  • 安装编辑器插件

    • webstorm: Styled Components & Styled JSX,不过最新版本的好像都自动安装了
    • vscode: vscode-styled-components
  • 使用emotion

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// html自带标签的使用
const Container = styled.div
`
display: flex;
flex-direction: column;
align-items: center;
min-height: 100vh;
`

// 组件的使用
import {Card} from "antd";
const ShowCard = styled(Card)
`
box-sizing: border-box;
width: 40rem;
min-height: 56rem;
padding: 3.2rem 4rem;
border-radius: 0.3rem;
box-shadow: 0 0 1rem rgba(0,0,0,.1);
`

  • 注意,不管有没有css样式后面,必须要接一个模板字符串,否者会报错
1
2
3
4
5
//不报错
const HeaderLeft = styled(Row)``;

//报错
const HeaderLeft = styled(Row);
  • 设置多个背景(学到了,学到了,)
    • background-image 属性用于为一个元素设置一个或者多个背景图像。
    • background-position属性为每一个背景图片设置初位置
      • 一个值为x,y设置相同的位置
      • 二个值分别为x轴位置和y轴位置
    • background-size属性设置背景图片大小
      • 一个值: 指定图片的宽度,高度为auto
      • 二个值: 分别指定图片的高度 和 宽度
      • 逗号分割多个值,设置多重背景
1
2
3
4
5
6
/*设置背景图*/
background-repeat: no-repeat;
background-position: left bottom, right bottom;
background-size: calc(((100vw - 40rem)/2) - 3.2rem ) ,calc(((100vw - 40rem)/2) - 3.2rem ),cover;
background-attachment: fixed;
background-image: url(${LeftBg}),url(${RightBg});

grid和flex各自的应用场景

  • 一看空间
    • 一般来说,一维布局用flex,二维布局用grid
  • 二看内容和布局
    • 从内容出发: 有一组内容(数量一般不固定),然后希望他们均匀分布在容器在当中,并且由内容自己的大小决定占据的空间(用flex)
    • 从布局出发:先规划网格(数量一般比较固定),然后再把元素往里填充(用grid)

css-in-js:Row组件实现

  • emotion允许我们像react一样传递参数来达到自定义样式的效果
    • Row组件样式
    • 当然,可以不写ts在这里,不过不写会有警告在tsx当中~
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import styled from "@emotion/styled";

export default styled.div<{
gap?: number | boolean,//右侧间距设置
between?: boolean,//内容是否居中
marginBottom?:number,//距离底部距离
}>
`
display: flex;
align-items: center;
justify-content: ${ props => props.between ? 'space-between' : undefined };
margin-bottom: ${ props => props.marginBottom ? props.marginBottom : 0 };
> * {
margin-top: 0!important;
margin-bottom: 0!important;
margin-right: ${ props => typeof props.gap === 'number' ? props.gap + 'rem' : props.gap ? '2rem' : undefined}
}
`
  • 使用
1
2
3
4
5
6
7
8
9
10
import styled from "@emotion/styled";
import Row from "../src/component/lib";

const HeaderLeft = styled(Row)``;
// 传入对应的参数即可
<HeaderLeft gap = {true}>
<h3>Logo</h3>
<h3>Logo</h3>
<h3>Logo</h3>
</HeaderLeft>

完成项目列表页面样式

  • 使用emotion的css

    • 在react当中,我们可以直接对组件使用style设置样式,但是不支持一些子元素选择符,伪类等一些高级选择器的
    1
    <MyComponent style={{ marginBottom:'2rem' }}/>
    • 所以我们可以使用@emotion/react来代替我们
    1
    2
    3
    4
    5
    /** @jsx jsx */
    import { jsx } from "@emotion/react";
    <Form css={{marginBottom: '2rem'}} >

    </Form/>
    • 老师是这样子写的,但是我报错了pragma and pragmaFrag cannot be set when runtime is automatic.,不知道为什么,就这样子吧,后面遇到再说
  • 图片以svg形式渲染

    • 我们如果直接在React使用img去使用svg图片的时候,并不能去设置svg参数了

1
2
3
import Logo from "../src/assets/svg/software-logo.svg";
// 无法设置svg属性了
<img src={Logo}/>
  • 所以我们应该使用如下方式去使用svg图片(通过组件的形式),这样子我们就可以设置svg的一些参数了

1
2
import { ReactComponent as SoftwareLogo } from "../src/assets/svg/software-logo.svg";
<SoftwareLogo width={'18rem'} color={'rgb(38,132,255)'}/>

清除警告todo

  • The href attribute is required for an anchor to be keyboard accessible. Provide a valid, navigable address a....

    • 原因:a标签没有href导致的,必须要一个合法的跳转链接,不可以#,也不可以javascript:;
    • 解决:替换为button,或者使用组件库,设置button的属性为link
  • React Hook useEffect has a missing dependency: 'callback'. Either include it or remove the dependency array.

    • 原因:依赖项没有加入在依懒收集里面导致报错
    • 解决:
1
2
3
4
useEffect(() => {
callback();
// todo 依懒项里加上callback会造成无限循环,这个和useCallback以及useMemo有关系
},[])
  • 不要乱用object
    • 覆盖范围很广,比如一个箭头函数变量,ts都认为这个是object
1
2
3
4
5
6
7
8
9
10
11
12
13
export const isVoid = (value:unknown) => value === undefined || value == null || value === '';

/*清除空对象*/
export const cleanEmptyObj = (obj: {[key:string]:unknown}) => {
const temp = {...obj};
Object.keys(temp).forEach(key => {
const value = temp[key]
if(isVoid(value)){
delete temp[key]
}
})
return temp;
}
  • Detected multiple renderers concurrently rendering the same context provider. This is currently unsupported.(我这边失败了,这个具体的就不加了)

    • 原因:jira-dev-tool问题
    • 解决:安装jira-dev-tool@next
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    yarn add jira-dev-tool@next

    App.tsc

    - import { loadDevTools } from 'jira-dev-tool';
    + import { loadServer,DevTools } from 'jira-dev-tool';

    //原来写法
    loadDevTools(() => {
    ReactDOM.render(
    <React.StrictMode>
    <AppProvider>
    <App />
    </AppProvider>
    </React.StrictMode>,
    document.getElementById('root')
    );
    });


    //更改为
    loadServer(() => {
    ReactDOM.render(
    <React.StrictMode>
    <AppProvider>
    <DevTools/>
    <App />
    </AppProvider>
    </React.StrictMode>,
    document.getElementById('root')
    );
    });

登录注册页面loading和error状态处理

需要注意的是try-catch是同步的

为什么不能用useAsync的error,因为error更新是异步的,try-catch是同步的

1
2
3
4
5
6
7
8
9
10
11
  const {run,isLoading,error} = useAsync(undefined,{throwOnError: true});
/*点击登录*/
const handleSubmit = async ({username,password}: {username:string,password:string}) => {
try {
await run(login({ username, password }))
}catch (e) {
console.log(e)
}
};

try ... catch执行完成后,才开始执行error的更新函数,所以你会发现,第一次error输出没有值,第二次error就有了(第二次的error为上一次出错的值)

未捕获错误(Uncaught Errors)-错误边界

1
2
3
4
5
6
7
8
事件处理

异步代码(例如 setTimeout 或 requestAnimationFrame 回调函数)

服务端渲染

它自身抛出来的错误(并非它的子组件)

  • 老师创建的组件的要求

    • 1.可以自定义错误显示的组件
    • 2.可以展示子组件当没有错误的时候
  • 代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import React, {Component, ErrorInfo, ReactNode} from "react";

export type Props = {
children:ReactNode,//子组件
fallBackRender : (props: { error:Error | null }) => React.ReactElement,
}

export interface State {
error: Error | null;
}

export class Boundary extends Component<Props, State> {
state = {
error:null
}

static getDerivedStateFromError(error:Error){
return {
error
}
}

componentDidCatch(error:Error, errorInfo:ErrorInfo) {
// 你同样可以将错误日志上报给服务器
}

render() {
const { error } = this.state;
const { children, fallBackRender } = this.props;
if(error) return fallBackRender({error})
return children;
}
}

使用useRef实现useDocumentTitle

  • 需求

    • 每一个组件可以使用useDocumentTitle方法,第一个参数传入新标题,第二个标题传入卸载组件后是否复原
  • 疑问

    • 为什么useDocumentTitle当中的设置document.title要放置在useEffect
    1
    2
    3
    4
    5
    6
    7
    因为在函数组件主体内(这里指在 React 渲染阶段)改变 DOM、添加订阅、设置定时器、记录日志以及执行其他包含副作用的操作都是不被允许的,因为这可能会产生莫名其妙的 bug 并破坏 UI 的一致性。

    使用 useEffect 完成副作用操作。赋值给 useEffect 的函数会在组件渲染到屏幕之后执行。你可以把 effect 看作从 React 的纯函数式世界通往命令式世界的逃生通道。

    说通俗点就是useEffect可以放置一些副作用操作在里面,并设置为依赖.而我们的document.title就是一个副作用,所以需要放置在useEffect

    别忘记了`effect`单词本身的意思哦
    • useRef为什么要使用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
/* 网页标题更改 */
export const useDocumentTitle = (title:string,keepOnUnmount:boolean = true) => {
//获取旧标题
const oldTitle = document.title;
useEffect(() => {
document.title = title;
},[title])
//更改新标题(副作用,使用需要使用useEffect)
useEffect(() => {
return () => {
if(!keepOnUnmount) {
//卸载的时候执行
document.title = oldTitle
}
}
},[]);
}

//在上述代码中,因为闭包的问题,导致`oldTitle`可以保存最老的值

//但是控制台可能会有警告,所以我们加入依赖,但是这又导致了oldTitle永远都是最新的值(也就是每执行一次,当前组件的oldTime都会被更新为上一次的值,而不是我们所想的指向最初始的title)
export const useDocumentTitle = (title:string,keepOnUnmount:boolean = true) => {
//获取旧标题
const oldTitle = document.title;
useEffect(() => {
document.title = title;
},[title])
//更改新标题(副作用,使用需要使用useEffect)
useEffect(() => {
return () => {
if(!keepOnUnmount) {
//卸载的时候执行
document.title = oldTitle
}
}
},[keepOnUnmount,oldTitle]);
}


所以我们可以借助useRef
useRef 返回一个可变的 ref 对象,其 .current 属性被初始化为传入的参数(initialValue)。返回的 ref 对象在组件的整个生命周期内持续存在。
//代码改造如下
export const useDocumentTitle = (title:string,keepOnUnmount:boolean = true) => {
//获取旧标题
const oldTitle = useRef(document.title).current;
//更改新标题(副作用,使用需要使用useEffect)
useEffect(() => {
document.title = title;
return () => {
if(!keepOnUnmount) {
//卸载的时候执行
document.title = oldTitle
}
}
},[title,keepOnUnmount,oldTitle]);
}

动态改变网页标签显示的标题#todo

  • 第一种
    • react-helmet
  • 第二种

添加项目列表和项目详情

  • react-router-dom和react-router的关系

    • react-router管理路由关系,计算结果由react-router-dom来消费使用
  • 为什么Link是从react-router-dom引入的

    • 因为Link会被渲染为一个a标签,并且需要处理点击事件,和浏览器是相关联的
  • 路由表和路由的书写技巧

    • 不可以写斜杆,写斜杆代表不管你从哪里开始,我都是从/开始的,
      所以我们需要在不破坏当前路由的基础上把路由加进去,所以不能有/
    • /带了斜杆就是根了(也就url的路径从我开始算起)
    • ./在不破坏当前路径下载后面添加内容,也可以不写./
    • 于是乎下面三个效果都是一样的,最终匹配的都是/home/message
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    //路由表
    {
    path:'/home',
    element:<Home/>
    children:[
    path:"./message",
    path:'/home/message',
    path:'message',
    ]
    }
    //路由链接的to的书写也和这个相同,
    //下面二个均是在对应url后面添加相应的内容
    //比如之前url为/home,那么点击`message组件显示`,
    //那么url就会变为/home/message
    <NavLink to="message">Message组件显示</NavLink>
    <NavLink to="news">News组件显示</NavLink>
  • 一个很奇怪的问题

    • 必须要转化为字符串才可以
1
return <Link to={String(project.id)}>{ project.name } </Link>
  • 注意Navigate写法
1
2
3
4
5
6
7
8
9
10
<h1>ProjectScreen</h1>
<Link to={'kanban'}>看板</Link>
<Link to={'epic'}>任务组</Link>
<Routes>
<Route path={'kanban'} element={<KanbanScreen/>}></Route>
<Route path={'epic'} element={<EpicScreen/>}></Route>
{/*/!*<Navigate to={window.location.pathname + '/kanban'}/>*!/ react5写法*/}
<Route path={''} element={<Navigate to={'kanban'}/>}/>
</Routes>
</div>

useSearchParams初步完成

  • 可以用来获取search参数(也就是url后面的这种参数/a?id=4&age=14)
  • 返回一个URLSearchParams,
  • 所以要读取某一个值就需要使用get方法
1
2
3
4
//url
/a?id=4&age=14
const [a] = useSearchParams();
a.get(age);//输出14
  • 问题

    • 为什么没有as const会出现下图问题
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    import {useSearchParams} from "react-router-dom";
    export const useUrlQueryParams = (keys:string[]) => {
    const [searchParams] = useSearchParams();
    return [
    keys.reduce((pre,key) => {
    return {...pre,[key]:searchParams.get(key) ?? ''}
    },{} ),
    searchParams,
    ]
    }

    查看返回值类型

    • 先来看一个小例子
      • 想一想a的类型是什么
    1
    const a = ['jack',12,{gender:'男'}]

    a的类型

    • 原因很简单,因为ts认为数组都是相同的,使用为了确保数组当中都有相同的,所以就使用了多个|
    • 所以我们return后面添加一个as const即可
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    export const useUrlQueryParams = (keys:string[]) => {
    const [searchParams] = useSearchParams();
    return [
    keys.reduce((pre,key) => {
    return {...pre,[key]:searchParams.get(key) ?? ''}
    },{} ),
    searchParams,
    ] as const
    }

  • 不过这还不够,我们点入reduce的ts声明可以看到,reduce当中previousValue返回值依赖于初始化时候传入的泛型,所以我们可以指明initialValue在reduce当中

1
reduce(callbackfn: (previousValue: T, currentValue: T, currentIndex: number, array: T[]) => T, initialValue: T): T;
  • 最终初步完成如下
1
2
3
4
5
6
7
8
9
export const useUrlQueryParams = (keys:string[]) => {
const [searchParams,setSearchParams] = useSearchParams();
return [
keys.reduce((pre,key) => {
return {...pre,[key]:searchParams.get(key) ?? ''}
},{} as {[key in string]:string}),
setSearchParams,
] as const
}

useSearchParams完成(使用useMemo解决)

  • 上一次完成的方法我们使用发现会无限渲染我们可以借助why-did-you-render来检测是什么造成了页面渲染

  • 通过排查,useDebounce当中的useEffect发现依赖项目变化了,进而去重新渲染页面,但是params在每次渲染的时候都是一个新的,导致useEffect又认为发生了变化,进而重复无限渲染

    • 所以我们使用useEffect的时候,我们应该将基本类型和组件状态(使用useState)放置到依赖里面,非组件状态的对象,绝对不可以放在依赖里面!!
    • 所以检查我们可以使用why-did-you-render
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const [params] = useUrlQueryParams(['name','age'])

const debounceValue = useDebounce(params,100);

/*自定义防抖hooks*/
export const useDebounce = <T>(value:T,delay?:number):T => {
const [debounceValue,setDebounceValue] = useState(value)
useEffect(() => {
const timer = setTimeout(() => { setDebounceValue(value) },delay);
return () => {
/*下一次effect执行前的处理*/
clearTimeout(timer)
}
},[value,delay])
return debounceValue;
}
  • 所以我们循环遍历search参数的时候,返回的是一个对象,又因为react只对对象进行地址比较,所以就导致每次重新渲染返回的对象不同,所以就造成了重复渲染,解决后代码如下(使用useMemo)
1
2
3
4
5
6
7
8
9
10
11
12
export const useUrlQueryParams = <K extends string>(keys:K[]) => {
const [searchParams,setSearchParams] = useSearchParams();
return [
useMemo(() => {
return keys.reduce((pre,key) => {
return {...pre,[key]:searchParams.get(key) ?? ''}
},{} as {[key in K]:string})
},[searchParams]),
setSearchParams,
] as const
}

完成的URL状态管理代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
export const useUrlQueryParams = <K extends string>(keys:K[]) => {
const [searchParams,setSearchParams] = useSearchParams();
return [
useMemo(() => {
return keys.reduce((pre,key) => {
return {...pre,[key]:searchParams.get(key) ?? ''}
},{} as {[key in K]:string})
},[searchParams]),
(params:Partial<{[key in K] : unknown}>) => {
const o = cleanEmptyObj({...Object.fromEntries(searchParams),...params}) as URLSearchParamsInit
return setSearchParams(o);
}
] as const
}

实现Id-Select解决Id难题

  • 获取AntDesign组件当中属性的二种方式

    • 方法1:通过React.ComponentProps;
    1
    2
    3
    4
    import React from "react";
    import {Select} from "antd";

    type SelectProps = React.ComponentProps<typeof Select>;
    • 方法2:Ctrl按照进入组件,然后找到后复制粘贴就可以引入
    1
    import {SelectProps} from "antd/es/select";

用useEditProject编辑项目

柯里化

  • 因为有一个参数实现已经知道了,后一个参数需要等待才可以知道,就可以采用这种方式
1
2
3
4
5
6
7
8
9
const { mutate } = useEditProject();
const pinProject = (id:number) => (checked:boolean) => mutate({id,pin:checked})
<Pin checked={project.pin} onCheckedChange={pinProject(project.id)}/>

等同于
const pinProject = (id:number,checked:boolean) => {
mutate({id,pin:checked})
}
<Pin checked={project.pin} onCheckedChange={(checked:boolean) => pinProject(project.id,checked) }/>

惰性初始化和使用useRef保存函数和useState保存函数的方法

什么是惰性初始化

  • 惰性初始化的时候(也就是传入一个函数就是惰性初始),此函数会被立即执行,并将返回的值作为返回数组的第一个参数,其他和普通state是相同的(调用setState也会触发页面重新渲染)
  • 惰性初始代码
1
2
3
4
const [state,setState] = useState(() => {
const initialState = someExpensiveComputation(props);
return initialState;
})
  • 非惰性初始(也就是普通的state,传入的不是一个函数,)
1
2
3
4
5
const [state,setState] = useState(someExpensiveComputation(props));
const [state,setState] = useState({
name:'李白',
sex:'男'
})
  • 所以当我们想通过useState保存函数的时候,就不可以了,我们可以使用useRef来保存函数

useRef

  • 我觉得我快忘记了,再看看React官网的描述吧
    • 比较重点就是ref对象发生变化,并不会引发组件的重新渲染,useRef的值是保存在一个.current属性当中

  • 老师在最后说的一个问题,将代码改为下面样子,我们点击设置callBack后,再点击执行callBack-设置后不正常输出'init'按钮,发现输出的却是init
    • 这是因为组件在编译渲染完成后,执行callBack-设置后不正常输出'init'按钮始终指向初次时候的函数地址,又因为useRef即使被更新也不会被重新渲染,导致此按钮指向的依旧是初始化时候的函数
    • 而为什么执行callBack-设置后正常输出'updated'按钮却是正常输出,因为传入的是高阶函数,在执行的时候才会寻找current所指向的函数去执行,所以执行就没有问题
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import * as React from 'react'
import './style.css';

export default function App() {
const callBackRef = React.useRef(() => {
return alert('init');
});
const callBackCurrent = callBackRef.current;
console.log('输出查看callBack的current(是否重新渲染)', callBackCurrent);
// 设置callBack
const setCallBack = () => {
callBackRef.current = () => {
return alert('updated');
};
};

//执行callBack - 正常
const fnCallBack = () => {
callBackRef.current();
};

return (
<div>
<button onClick={setCallBack}>设置callBack</button>
<button onClick={fnCallBack}>执行callBack-设置后正常输出'updated'</button>
<button onClick={callBackRef.current}>
执行callBack-设置后不正常输出'init'
</button>
<h1>Hello StackBlitz!</h1>
<p>Start editing to see some magic happen :)</p>
</div>
);
}

useState保存函数的方法

  • 既然存在惰性初始化,我们就让他执行就可以,我们返回一个函数就可以啦
1
2
3
const [retry,setRetry] = useState(() => () => {
//返回一个函数
})

优化异步请求

  • 出现的情况

    • 在外面等待数据返回的时候,突然退出登录,然后未中断请求,导致setData等操作会出现异常(我练习的时候好像没有,不过也学习下)
  • 解决异步的时候退出的问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
//1.建立useMountedRef,并设置状态
/*
* 返回组件的状态,如果没有挂载或者已经卸载,则返回false,
* */
export const useMountedRef = () => {
const mountedRef = useRef(false);
useEffect(() => {
mountedRef.current = true;
return () => {
mountedRef.current = false;//组件卸载
}
})
return mountedRef
}

// 伪代码,当调用then的时候判断是否组件卸载了
return promiseGive
.then((res:D) => {
if(mountedRef.current){
setData(res);
}
return Promise.resolve(res);//实现链式调用
})
.catch(error => {
setError(error);
if(config.throwOnError) return Promise.reject(error);
return error;//实现链式调用
})
  • 当使用useCallback,或者useMemo的时候,可以里面会有依赖state,然后我们会将依赖加入进去,但是加入后,会发生无限循环的问题,这个时候我们就可以使用setData的第二种形式了,然后结合useCallback或者useMemo
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//第一种(造成无限循环)
//当data改变,触发重新渲染,因为当调用setData的时候,就代表data改变
//然后触发useCallback的重新渲染,然后又调用setData,又代表data改变
//又重新触发setData,,,,,,这样子无限循环下去
const handleChange = useCallback(() => {
const [data,setData] = useState({name:'李白',loading:false})
setData({...state,loading:true})
},[data,setData])


//第二种(解决无限循环)
const handleChange = useCallback(() => {
const [data,setData] = useState({name:'李白',loading:false})
setData((preState) => {...preState,loading:true})
},[setData])

什么时候使用useMemo,useCallback

  • 非基本类型想做依赖,就需要使用这二个
  • 比如在想自定义hooks的时候,返回了函数,或者非基本数据类型,或者并没有被useState包裹的数据的时候,就需要使用这二个了

状态提升

  • const其实变量提升(依照var的变量提升,我们可以想一想组件的状态提升)

  • 目前我们为了使用一个能被多个组件调用的方法或者是属性(可以称其为”全局方法或属性“),我们将其提升到共同的父组件当中,但是当子组件需要使用全局方法或属性的时候,父组件和要使用的子组件只有一层还好说,当有多层的时候,就会出现父传给A组件,A组件传递给B组件,B组件在传给C组件,C组件再来使用,来看看下面集中方法

    • 第一种: 放在全局状态,通过一层一层传递,

    • 第二种: 还有一种Context的方法

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      class App extends React.Component {
      render() {
      return <Toolbar theme="dark" />;
      }
      }

      function Toolbar(props) {
      // Toolbar 组件接受一个额外的“theme”属性,然后传递给 ThemedButton 组件。
      // 如果应用中每一个单独的按钮都需要知道 theme 的值,这会是件很麻烦的事,
      // 因为必须将这个值层层传递所有组件。
      return (
      <div>
      <ThemedButton theme={props.theme} />
      </div>
      );
      }

      class ThemedButton extends React.Component {
      render() {
      return <Button theme={this.props.theme} />;
      }
      }
      • 使用createContext传递
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      // Context 可以让我们无须明确地传遍每一个组件,就能将值深入传递进组件树。
      // 为当前的 theme 创建一个 context(“light”为默认值)。
      const ThemeContext = React.createContext('light');
      class App extends React.Component {
      render() {
      // 使用一个 Provider 来将当前的 theme 传递给以下的组件树。
      // 无论多深,任何组件都能读取这个值。
      // 在这个例子中,我们将 “dark” 作为当前的值传递下去。
      return (
      <ThemeContext.Provider value="dark">
      <Toolbar />
      </ThemeContext.Provider>
      );
      }
      }

      // 中间的组件再也不必指明往下传递 theme 了。
      function Toolbar() {
      return (
      <div>
      <ThemedButton />
      </div>
      );
      }

      class ThemedButton extends React.Component {
      // 指定 contextType 读取当前的 theme context。
      // React 会往上找到最近的 theme Provider,然后使用它的值。
      // 在这个例子中,当前的 theme 值为 “dark”。
      static contextType = ThemeContext;
      render() {
      return <Button theme={this.context} />;
      }
      }
    • 第三种: 组合组件(component composition)

      • 其实就是将组件传递(传递jsx) - 缺点是没有解决向下钻的问题,但是方法不需要传递了,(定义和调用很近),示例如下
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      // 使用component composition
      function Page(props) {
      const user = props.user;
      const userLink = (
      <Link href={user.permalink}>
      <Avatar user={user} size={props.avatarSize} />
      </Link>
      );
      return <PageLayout userLink={userLink} />;
      }

      // 现在,我们有这样的组件:
      <Page user={user} avatarSize={avatarSize} />
      // ... 渲染出 ...
      <PageLayout userLink={...} />
      // ... 渲染出 ...
      <NavigationBar userLink={...} />
      // ... 渲染出 ...
      {props.userLink}

      //未使用component composition之前
      <Page user={user} avatarSize={avatarSize} />
      // ... 渲染出 ...
      <PageLayout user={user} avatarSize={avatarSize} />
      // ... 渲染出 ...
      <NavigationBar user={user} avatarSize={avatarSize} />
      // ... 渲染出 ...
      <Link href={user.permalink}>
      <Avatar user={user} size={avatarSize} />
      </Link>

useUndo

  • 未使用useReducer的写法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
import {useCallback, useState} from "react";

const UseUndo = <T>(initData:T) => {
const [state,setState] = useState<{
backList:T[],//过去的记录
present:T,//现在的值,
goList:T[],//前面的记录
}>({
backList:[],
goList:[],
present:initData,
})
const [canBack,setCanBack] = useState(() => state.backList.length > 0);//是否可以后退
const [canGo,setCanGo] = useState(() => state.goList.length > 0);//是否可以前进

/* 执行返回 */
const execBack = useCallback(() => {
setState((currentState) => {
if(!canBack) return currentState;
const { goList:oldGoList,backList:oldBackList } = currentState;
const present = oldBackList[oldBackList.length-1];
const backList = oldBackList.slice(0,oldGoList.length - 1);
const goList = [...oldGoList,present];
return {
goList,
backList,
present,
}
})
},[])

/* 执行前进 */
const execGo = useCallback(() => {
setState((currentState) => {
if(!canGo) return currentState;
const { goList:oldGoList,backList:oldBackList } = currentState;
const present = oldGoList[0];
const goList = oldGoList.slice(1);
const backList = [...oldBackList,present];
return {
goList,
backList,
present,
}
})
},[])

/* 设置值 */
const set = useCallback((newData:T) => {
setState((currentState) => {
const { goList:oldGoList,backList:oldBackList,present:oldPresent } = currentState;
if(newData === oldPresent) return currentState;
const backList = [...oldBackList,oldPresent];
return {
goList:[],
backList,
present:newData,
}
})
},[])


/* 重置 */
const reset = useCallback(() => {
setState((currentState) => {
const { goList,backList } = currentState;
return {
goList:[],
backList:[],
present:initData,
}
})
},[])

return [
state,
execBack,
execGo,
set,
reset,
canBack,
canGo,
] as const
}

export default UseUndo

  • 使用useReducer的写法,仅供参考
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
import {useCallback, useReducer, useState} from "react";

export enum TypeOperation {
go='go',
back='back',
set='set',
reset='reset',
}

export interface State<T> {
backList:T[],//过去的记录
present:T,//现在的值,
goList:T[],//前面的记录
}

export interface Action<T> {
newPresent?:T,
type: TypeOperation,
}
const unDoReducer = <T>(state:State<T>,action:Action<T>) => {
const {type,newPresent} = action;
const { goList:oldGoList,backList:oldBackList,present:oldPresent } = state;
switch (type){
case "back": {
if(oldBackList.length ===0 ) return state;
const present = oldBackList[oldBackList.length-1];
const backList = oldBackList.slice(0,oldGoList.length - 1);
const goList = [...oldGoList,present];
return {
goList,
backList,
present,
}
}
case "go": {
if(oldGoList.length === 0) return state;
const present = oldGoList[0];
const goList = oldGoList.slice(1);
const backList = [...oldBackList,present];
return {
goList,
backList,
present,
}
}
case "reset":{
return {
goList:[],
backList:[],
present:newPresent,
}
}
case "set": {
if(newPresent === oldPresent) return state;
const backList = [...oldBackList,oldPresent];
return {
goList:[],
backList,
present:newPresent,
}
}
default: return state;
}
}


const UseUndo = <T>(initData:T) => {
const [state,dispatch] = useReducer(unDoReducer,{
backList:[],
goList:[],
present:initData,
})
const [canBack,setCanBack] = useState(() => state.backList.length > 0);//是否可以后退
const [canGo,setCanGo] = useState(() => state.goList.length > 0);//是否可以前进
/* 执行返回 */
const execBack = useCallback(() => dispatch({type:TypeOperation.back}),[dispatch])

/* 执行前进 */
const execGo = useCallback(() => dispatch({type:TypeOperation.go}),[dispatch])

/* 设置值 */
const set = useCallback((newData:T) => dispatch({type:TypeOperation.set,newPresent:newData}),[dispatch])

/* 重置 */
const reset = useCallback((newData:T) => dispatch({type:TypeOperation.reset,newPresent:newData}),[dispatch])

return [
state,
execBack,
execGo,
set,
reset,
canBack,
canGo,
] as const
}

export default UseUndo

  • 顺带一提,其实useReudcer里面的action,其实什么都可以,只是我们用的多的都是带有type的而已

redux

  • redux用在哪里都可以,react-redux连接redux.如果redux不使用在react,就可以不适用react-redux

  • redux作用就是用现在的state,产生下一个state

  • 当redux发生什么事情的时候,就会戳一下

    • redux发生dispatch,触发subscribe
    • dispatch -> counter -> store
  • redux保持一个同步之前学习的,为什么呢?保持纯洁性,因为如果是异步请求了,就不是可预测了的

    • 副作用,对现实世界产生影响
      • 比如发送请求,修改全局变量
  • redux怎么知道要更新数据呢?怎么知道要执行订阅的内容呢?

    • 判断前一次的数据是否和后一次的数据相同,相同就不更新,不相同就不更新
    • 这样子比较 变量 a === 变量b
  • redux-thunk在redux里面处理异步流行的一个库(注意,redux可以进行异步操作,但是redux-thunk可以帮助我们隐藏异步实现的细节)

  • 可以看到,组件内部并不想知道怎么请求的,(具体异步细节忽略)

reduxjs/tooltik和react-redux

  • 安装依赖
1
yarn add react-redux @reduxjs/tooltik

有关reduxjs/toolkit部分

  • 书写片段
    • project-list.slice.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import {createSlice} from "@reduxjs/toolkit";

export const projectListSlice = createSlice({
name:'projectListSlice',
initialState:{
projectModalOpen:false,
},
reducers:{
/* 开启对话框 */
openProjectModal(state,action){
//immer帮我们处理了,所以我们可以直接在返回的state书写
state.projectModalOpen = true;
},
/* 关闭对话框 */
closeProjectModal(state,action){
state.projectModalOpen = false;
}
}
})

export const projectListSliceReducer = projectListSlice.reducer;

  • 主入口
    • index.tsx
1
2
3
4
5
6
7
8
9
10
import {configureStore} from "@reduxjs/toolkit";
import {projectListSliceReducer} from "../pages/projectList/projectList.slice";

export const store = configureStore({
/* 设置状态管理 */
reducer:{
projectList:projectListSliceReducer
},
})

有关react-redux

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import {AuthProvider} from "./authContext";
import React, {ReactNode} from "react"
import {store} from "../store";//新增
import {Provider} from "react-redux";//新增
export const AppProvider = ({children}:{children:ReactNode}) => {
return (
<Provider store={store}>
<AuthProvider>
{ children }
</AuthProvider>
</Provider>
)
}
export default AppProvider;

使用

  • 获取设置的state参数const { useSelector } from "react-redux"
1
2
3
4
5
6
import {useSelector} from "react-redux"
const selectProjectModalOpen = state => state.projectList.projectModalOpen;
const showModal = useSelector(selectProjectModalOpen);


//等同于 const showModal = useSelector((state) => state.projectList.projectModalOpen)
  • 调用设置的方法const { useDispatch } from "react-redux"
1
2
3
4
const { useDispatch } from "react-redux";
import {projectListSliceActions} from "../projectList/projectList.slice";
const dispatch = useDispatch();//不需要传入任何参数,react-redux会自动去处理store
<button onClick={() => dispatch(projectListSliceActions.closeProjectModal())}>点击我关闭</button>
  • 这里面的projectListSliceActions对应下面暴露出来的projectListSliceActions
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import {createSlice} from "@reduxjs/toolkit";

export const projectListSlice = createSlice({
name:'projectListSlice',
initialState:{
projectModalOpen:false,
},
reducers:{
/* 开启对话框 */
openProjectModal(state){
//immer帮我们处理了,所以我们可以直接在返回的state书写
state.projectModalOpen = true;
},
/* 关闭对话框 */
closeProjectModal(state){
state.projectModalOpen = false;
}
}
})

export const projectListSliceReducer = projectListSlice.reducer;
export const projectListSliceActions = projectListSlice.actions;
//获取state,从store当中的reducer获取值
export const selectProjectModalOpen = (state) => state.projectList.projectModalOpen;

执行异步

方法1:

  • 在home.js中, 通过createAsyncThunk函数创建一个异步的action

    再在extraReducers中监听这个异步的action的状态, 当他处于fulfilled状态时, 获取到网络请求的数据, 并修改原来state中的数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import { createSlice, createAsyncThunk } from "@reduxjs/toolkit"
import axios from "axios"

// 创建一个异步的action
export const fetchHomeMultidataAction = createAsyncThunk("fetch/homemultidata", async () => {
const res = await axios.get("http://123.207.32.32:8000/home/multidata")
// 返回结果会传递到监听函数的actions中
return res.data
})

const homeSlice = createSlice({
name: "home",
initialState: {
banners: [],
recommends: []
},
// extraReducers中针对异步action, 监听它的状态
extraReducers: {
[fetchHomeMultidataAction.fulfilled](state, { payload }) {
// 在fulfilled状态下, 将state中的banners和recommends修改为网络请求后的数据
state.banners = payload.data.banner.list
state.recommends = payload.data.recommend.list
}
}
})

export default homeSlice.reducer

  • 其他地方引入执行异步
1
2
3
import { useDispatch } from "react-redux";
const dispatch = useDispatch();
dispatch(fetchHomeMultidataAction())

方法2

1
2
3
4
5
6
7
如果我们不想通过在extraReducers在监听状态, 再修改state这种方法的话, 还有另外的一种做法

我们创建的fetchHomeMultidataAction这个异步action是接受两个参数的

参数一, extraInfo: 在派发这个异步action时, 如果有传递参数, 会放在extraInfo里面
参数二, store: 第二个参数将store传递过来
这样我们获取到结果后, 通过dispatch修改store中的state, 无需再监听异步action的状态
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import { createSlice, createAsyncThunk } from "@reduxjs/toolkit"
import axios from "axios"

// 创建一个异步的action
export const fetchHomeMultidataAction = createAsyncThunk(
"fetch/homemultidata",
// 有传递过来两个个参数, 从store里面解构拿到dispatch
async (extraInfo, { dispatch }) => {
// 1.发送网络请求获取数据
const res = await axios.get("http://123.207.32.32:8000/home/multidata")
// 2.从网络请求结果中取出数据
const banners = res.data.data.banner.list
const recommends = res.data.data.recommend.list
// 3.执行dispatch, 派发action
dispatch(changeBanners(banners))
dispatch(changeRecommends(recommends))
}
)

const homeSlice = createSlice({
name: "home",
initialState: {
banners: [],
recommends: []
},
reducers: {
changeBanners(state, { payload }) {
state.banners = payload
},
changeRecommends(state, { payload }) {
state.recommends = payload
}
}
})

export const { changeBanners, changeRecommends } = homeSlice.actions

export default homeSlice.reducer

  • 其他地方引入执行异步
1
2
3
import { useDispatch } from "react-redux";
const dispatch = useDispatch();
dispatch(fetchHomeMultidataAction())

总结

  • 先看看片段
    • projectList.slice.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import {createSlice} from "@reduxjs/toolkit";

export const projectListSlice = createSlice({
name:'projectListSlice',
initialState:{
projectModalOpen:false,
},
reducers:{
/* 开启对话框 */
openProjectModal(state){
//immer帮我们处理了,所以我们可以直接在返回的state书写
state.projectModalOpen = true;
},
/* 关闭对话框 */
closeProjectModal(state){
state.projectModalOpen = false;
}
}
})

export const projectListSliceReducer = projectListSlice.reducer;
export const projectListSliceActions = projectListSlice.actions;
//获取state,从store当中的reducer获取值
export const selectProjectModalOpen = (state) => state.projectList.projectModalOpen;
  • store/index.ts
1
2
3
4
5
6
7
8
9
10
import {configureStore} from "@reduxjs/toolkit";
import {projectListSliceReducer} from "../pages/projectList/projectList.slice";

export const store = configureStore({
/* 设置状态管理 */
reducer:{
projectList:projectListSliceReducer
},
})

  • 获取数据使用useSelector (import {useSelector} from "react-redux")

    1
    2
    3
    4
    5
    import { useSelector } from "react-redux"
    const selectProjectModalOpen = state => state.projectList.projectModalOpen;
    const showModal = useSelector(selectProjectModalOpen);

    //等同于 const showModal = useSelector((state) => state.projectList.projectModalOpen)
  • 触发方法使用useDispatch ( import { useDispatch } from "react-redux" )

    1
    2
    3
    4
    5
    6
    import { useDispatch } from "react-redux"

    import {projectListSliceActions} = "./projectList.slice";

    <button onClick={() => dispatch(projectListSliceActions.closeProjectModal())}>点击我关闭</button>

可以自己加ts

用代码分割优化性能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import React,{lazy,Suspense} from 'react';
import {useAuth} from "./context/authContext";
import './App.css';
import {Boundary} from "./component/errorBoundary";
import {FullErrorFallBack, FullPageLoading} from "./component/lib";
//import UnAuthenticated from "./pages/unAuthenticated";
//import Authenticated from "./pages/authenticated";

const UnAuthenticated = lazy(() => import("./pages/unAuthenticated"))
const Authenticated = lazy(() => import("./pages/authenticated"))

function App() {
const {userInfo} = useAuth();
/*测试错误*/
return (
<div className='App'>
<Boundary fallBackRender={FullErrorFallBack}>
<Suspense fallback={FullPageLoading}>
{
userInfo && Object.keys(userInfo).length ? <Authenticated/> : <UnAuthenticated/>
}
</Suspense>
</Boundary>
</div>
);
}

export default App;

使用React.memo

  • 使用后,只有当组件的props或者全局状态,比如redux发生变化的时候,才会执行重新渲染

Profiler

  • 生产环境禁止使用

    • 如果需要,在编译的时候添加(如果是create react app)创建的话
    1
    2
    yarn build --profile
    npm run build --profile

单元测试(React)

  • 我们setupWorker以前曾为开发创建了一个假服务器。现在我们使用不同的函数,setupServer因为测试将在 Node.js 环境中运行,而不是在实际的浏览器环境中。只有浏览器环境具有 Service Worker 功能,因此我们在测试中使用 MSW 的方式实际上并不涉及 Service Worker。

    • 也就是说setupServer在node环境下使用的,不涉及到浏览器,setupWorker在浏览器中运行的
  • 最基础的单元测试

    • 随便哪里建立一个sum函数
    1
    2
    3
    export function sum(a, b) {
    return a + b;
    }
    • src/tests/sun.ts文件(其实你取名叫sun.test.ts也可以,其实都会识别)
    1
    2
    3
    4
    5
    import {sum} from "../utils";

    test('测试结果是否为100',() => {
    expect(sum(50,50)).toBe(100)
    })
  • 然后运行yarn test

    • 可以看到自动去寻找了__tests__当中的文件进行测试

先看看msw拦截异步请求的代码怎么写

  • 总的来说就是: 创建handler,使用handler,开始拦截

  • 1.创建handler

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import {rest} from "msw";

export const handlers = [
// 用于登录
rest.post('/login',(req,res,context) => {
sessionStorage.setItem('is-authenticated','true');//设置登录状态为真

return res(
context.status(200)
)
}),

// 用于获取用户信息
rest.get('/user',(req, res, context) => {
const isAuthenticated = sessionStorage.getItem('is-authenticated');
//未认证的用户
if(!isAuthenticated){
return res(context.status(403),context.json({
errorMessage:'未进行认证'
}))
}
// 已经认证的用户
return res(context.status(200),context.json({
username:'admin',
address:'dreamlove.top'
}))
}),
]

  • 2.使用handler
1
2
3
4
5
import { setupWorker,SetupWorker } from "msw";
import { handlers } from "./handler";

export const worker:SetupWorker = setupWorker(...handlers)

  • 3.开始拦截(只要匹配到了handler当中的列表,就进行拦截)
    • 我访问/login或者/list就会被拦截,从而返回假数据
1
2
3
4
5
6
import { worker } from "./mocks/browser"

if(import.meta.env.DEV){
/* 开发环境下就启动 */
worker.start();
}

setupServer和setupWorker

  • 也就是说setupServer在node环境下使用的,不涉及到浏览器,setupWorker在浏览器中运行的
  • 老师的代码没有过多解释,我们就以下面代码做个简单说明
    • 其实老师大概步骤也和上面msw拦截异步请求代码一样
    • 先监听,然后拦截,之后才是创建
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import {setupServer} from "msw/node";
import { rest } from "msw";//用于发送假数据

const server = setupServer();

//文件内所有测试开始前执行的钩子函数
beforeAll(() => {
server.listen();
})

//文件内每个测试完成后执行的钩子函数(执行完test之后执行的)
afterEach(() => {
server.resetHandlers();
})

//文件内所有测试完成后执行的钩子函数
afterEach(() => {
server.close();
})

test('发送异步请求',async () => {
/* 其实这一步就是向handler添加数据 */
server.use(rest.get('/list',(req,res,ctx) => {
return res(ctx.status(200),ctx.json([
{name:'李白1',age:18},
{name:'李白2',age:19},
{name:'李白3',age:20},
]))
}));
const response = await fetch('/list')
const result = await response.json();
expect(result).toEqual([
{name:'李白1',age:18},
{name:'李白2',age:19},
{name:'李白3',age:20},
])
})

  • 其实很简单,server.use理解为向监视器里面添加东西,添加的东西会被拦截并做处理,只不过我们每次测试完毕,都将里面的handler进行了清空

额外小知识

  • encodeURIComponent(转义URL当中部分字符的)

  • encodeURI(转移整个URL内容的)

  • .env,.env.development

    • 当为npm start的时候.webpack会去读取.env.development文件
    • 当为npm run build编译之后,webpack会去读取.env.development文件
    • 并将读取的文件作为一个对象存储在process.env当中
    • 比如.env.development有如下内容
    1
    abc = 'www.baidu.com'
    • 那么我们就可以读取
    1
    process.env.abc
    • 如果是vite读取,则需要通过如下来读取,并且设置的变量必须要以VITE_开头
    1
    const apiUrl = import.meta.env
  • 遇到这种不明确的情况(比如name=),到底是搜索名字为空的,还是要忽略这种情况呢?所以我们需要在前端做处理

  • TS7016: Could not find a declaration file for module './screens/projectList'. 'D:/develop/phpstudy_pro/WWW/React-cli/react_17_projectJira/src/screens/projectList/index.jsx' implicitly has an 'any' type.这种报错

    • 就是缺少声明文件,要么自己添加对应的xxx.d.ts文件或者使用//@ts-ignore进行忽略
  • useHooks(不管是自带的hooks还是自己创建的hooks),不可以在普通函数中运行,只能在hooks当中使用或者其他hooks使用,所以在自定义hooks的时候,需要以useXxx开头