https://zhuanlan.zhihu.com/p/536437382
npm install -g json-server
npm install -D json-server
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 (
);
};
export default List;
vite
创建的react项目,就无法像老师一样直接使用process.env
来读取设置的变量了/*只在初次挂载执行*/
export const useMount = (callback) => {
useEffect(callback,[])
}
/*自定义防抖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;
}
/*搜索参数*/
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数据
原理:
state
,当传入的value发生改变的时候的时候,会被内部debounce创建的节流函数所捕捉,捕捉到后,如果中途没有重新捕捉到新的值,则会在设置的时间之后更新内部debounce的值,否则的话就会中断更新,重新计时图示原理
文件名更改
遇到qs模块types缺失的情况: Could not find a declaration file for module 'qs'.xxxx,npm i --save-dev @types/qs
,安装对应的types即可
yarn add @types/qs -D
// 箭头函数
const fn1 = () => {
}
//普通函数
function fn2() {
}
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": "json-server __json_server_mock/db.json --watch --port 3033 --middlewares __json_server_mock/middleware.js"
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();
}
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 (
);
};
export default Login;
npm地址:https://www.npmjs.com/package/jira-dev-tool
如果安装后项目启动数据库连接失败,可以试试运行这个命令
npx msw init public
//或者 指定到public目录下
npx msw init ./public
关于Parameters
的使用,可以看看ts官方示例https://www.typescriptlang.org/docs/handbook/utility-types.html
所以老师当中的这个是什么意思呢?
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对象)
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 个或多个数字,将在其中选择,并返回最大值。
localeCompare
:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/String/localeComparestring.localeCompare(targetString,locales,options);
该方法返回的是一个数字用来表示一个参考字符串和对比字符串是排序在前,在后或者相同
返回值:返回值是一个数字,目前的主流浏览器都返回的是1、0、-1三个值,但是也有其他情况,所以不可以用绝对的值等于1、-1这种去判断返回的结果
返回值大于0:说明当前字符串string大于对比字符串targetString
返回值小于0:说明当前字符串string小于对比字符串targetString
返回值等于0:说明当前字符串string等于对比字符串targetString
html{
/*使得1rem === 10px*/
font-size: 62.5%;/* 这样子使得font-size为16px * 62.5% = 10px */
}
html body #root .App {
min-height: 100vh;
}
yarn add @emotion/react @emotion/styled
安装编辑器插件
Styled Components & Styled JSX
,不过最新版本的好像都自动安装了vscode-styled-components
使用emotion
// 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);
`
//不报错
const HeaderLeft = styled(Row)``;
//报错
const HeaderLeft = styled(Row);
background-image
属性用于为一个元素设置一个或者多个背景图像。background-position
属性为每一个背景图片设置初位置
background-size
属性设置背景图片大小
auto
/*设置背景图*/
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});
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}
}
`
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
style
设置样式,但是不支持一些子元素选择符,伪类等一些高级选择器的<MyComponent style={{ marginBottom:'2rem' }}/>
@emotion/react
来代替我们/** @jsx jsx */
import { jsx } from "@emotion/react";
pragma and pragmaFrag cannot be set when runtime is automatic.
,不知道为什么,就这样子吧,后面遇到再说图片以svg形式渲染
import Logo from "../src/assets/svg/software-logo.svg";
// 无法设置svg属性了
<img src={Logo}/>
import { ReactComponent as SoftwareLogo } from "../src/assets/svg/software-logo.svg";
<SoftwareLogo width={'18rem'} color={'rgb(38,132,255)'}/>
The href attribute is required for an anchor to be keyboard accessible. Provide a valid, navigable address a....
#
,也不可以javascript:;
link
React Hook useEffect has a missing dependency: 'callback'. Either include it or remove the dependency array.
useEffect(() => {
callback();
// todo 依懒项里加上callback会造成无限循环,这个和useCallback以及useMemo有关系
},[])
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.
(我这边失败了,这个具体的就不加了)
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')
);
});
为什么不能用useAsync的error,因为error更新是异步的,try-catch是同步的
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为上一次出错的值)
react官网对于错误边界的说明
只有 class 组件才可以成为错误边界组件
react-error-boundary
错误边界无法捕获以下场景中产生的错误:
事件处理
异步代码(例如 setTimeout 或 requestAnimationFrame 回调函数)
服务端渲染
它自身抛出来的错误(并非它的子组件)
老师创建的组件的要求
代码
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 {
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;
}
}
需求
useDocumentTitle
方法,第一个参数传入新标题,第二个标题传入卸载组件后是否复原疑问
document.title
要放置在useEffect
因为在函数组件主体内(这里指在 React 渲染阶段)改变 DOM、添加订阅、设置定时器、记录日志以及执行其他包含副作用的操作都是不被允许的,因为这可能会产生莫名其妙的 bug 并破坏 UI 的一致性。
使用 useEffect 完成副作用操作。赋值给 useEffect 的函数会在组件渲染到屏幕之后执行。你可以把 effect 看作从 React 的纯函数式世界通往命令式世界的逃生通道。
说通俗点就是useEffect可以放置一些副作用操作在里面,并设置为依赖.而我们的document.title就是一个副作用,所以需要放置在useEffect
别忘记了`effect`单词本身的意思哦
/* 网页标题更改 */
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]);
}
react-router-dom和react-router的关系
为什么Link是从react-router-dom引入的
路由表和路由的书写技巧
/
开始的,/
/
带了斜杆就是根了(也就url的路径从我开始算起)./
在不破坏当前路径下载后面添加内容,也可以不写./
//路由表
{
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>
一个很奇怪的问题
return { project.name }
ProjectScreen
看板
任务组
}>
}>
{/*/!* *!/ react5写法*/}
}/>
/a?id=4&age=14
)get
方法//url
/a?id=4&age=14
const [a] = useSearchParams();
a.get(age);//输出14
问题
as const
会出现下图问题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,
]
}
const a = ['jack',12,{gender:'男'}]
|
as const
即可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当中
reduce(callbackfn: (previousValue: T, currentValue: T, currentIndex: number, array: T[]) => T, initialValue: T): T;
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
}
上一次完成的方法我们使用发现会无限渲染我们可以借助why-did-you-render
来检测是什么造成了页面渲染
通过排查,useDebounce当中的useEffect发现依赖项目变化了,进而去重新渲染页面,但是params在每次渲染的时候都是一个新的,导致useEffect又认为发生了变化,进而重复无限渲染
useEffect
的时候,我们应该将基本类型和组件状态(使用useState)放置到依赖里面,非组件状态的对象,绝对不可以放在依赖里面!!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;
}
useMemo
)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
}
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
}
获取AntDesign组件当中属性的二种方式
React.ComponentProps
;import React from "react";
import {Select} from "antd";
type SelectProps = React.ComponentProps<typeof Select>;
import {SelectProps} from "antd/es/select";
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) }/>
const [state,setState] = useState(() => {
const initialState = someExpensiveComputation(props);
return initialState;
})
const [state,setState] = useState(someExpensiveComputation(props));
const [state,setState] = useState({
name:'李白',
sex:'男'
})
useRef
来保存函数.current
属性当中设置callBack
后,再点击执行callBack-设置后不正常输出'init'
按钮,发现输出的却是init
执行callBack-设置后不正常输出'init'
按钮始终指向初次时候的函数地址,又因为useRef即使被更新也不会被重新渲染,导致此按钮指向的依旧是初始化时候的函数执行callBack-设置后正常输出'updated'
按钮却是正常输出,因为传入的是高阶函数,在执行的时候才会寻找current
所指向的函数去执行,所以执行就没有问题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 (
Hello StackBlitz!
Start editing to see some magic happen :)
);
}
const [retry,setRetry] = useState(() => () => {
//返回一个函数
})
出现的情况
setData
等操作会出现异常(我练习的时候好像没有,不过也学习下)解决异步的时候退出的问题
//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
//第一种(造成无限循环)
//当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])
目前我们为了使用一个能被多个组件调用的方法或者是属性(可以称其为"全局方法或属性"),我们将其提升到共同的父组件当中,但是当子组件需要使用全局方法或属性的时候,父组件和要使用的子组件只有一层还好说,当有多层的时候,就会出现父传给A组件,A组件传递给B组件,B组件在传给C组件,C组件再来使用,来看看下面集中方法
第一种: 放在全局状态,通过一层一层传递,
第二种: 还有一种Context的方法
@react官网说明
不适用content传递
class App extends React.Component {
render() {
return ;
}
}
function Toolbar(props) {
// Toolbar 组件接受一个额外的“theme”属性,然后传递给 ThemedButton 组件。
// 如果应用中每一个单独的按钮都需要知道 theme 的值,这会是件很麻烦的事,
// 因为必须将这个值层层传递所有组件。
return (
);
}
class ThemedButton extends React.Component {
render() {
return ;
}
}
createContext
传递// Context 可以让我们无须明确地传遍每一个组件,就能将值深入传递进组件树。
// 为当前的 theme 创建一个 context(“light”为默认值)。
const ThemeContext = React.createContext('light');
class App extends React.Component {
render() {
// 使用一个 Provider 来将当前的 theme 传递给以下的组件树。
// 无论多深,任何组件都能读取这个值。
// 在这个例子中,我们将 “dark” 作为当前的值传递下去。
return (
);
}
}
// 中间的组件再也不必指明往下传递 theme 了。
function Toolbar() {
return (
);
}
class ThemedButton extends React.Component {
// 指定 contextType 读取当前的 theme context。
// React 会往上找到最近的 theme Provider,然后使用它的值。
// 在这个例子中,当前的 theme 值为 “dark”。
static contextType = ThemeContext;
render() {
return ;
}
}
第三种: 组合组件(component composition)
// 使用component composition
function Page(props) {
const user = props.user;
const userLink = (
);
return ;
}
// 现在,我们有这样的组件:
// ... 渲染出 ...
// ... 渲染出 ...
// ... 渲染出 ...
{props.userLink}
//未使用component composition之前
// ... 渲染出 ...
// ... 渲染出 ...
// ... 渲染出 ...
useReducer
的写法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
的写法,仅供参考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用在哪里都可以,react-redux连接redux.如果redux不使用在react,就可以不适用react-redux
redux作用就是用现在的state
,产生下一个state
当redux发生什么事情的时候,就会戳一下
dispatch
,触发subscribe
redux保持一个同步之前学习的,为什么呢?保持纯洁性,因为如果是异步请求了,就不是可预测了的
redux怎么知道要更新数据呢?怎么知道要执行订阅
的内容呢?
redux-thunk在redux里面处理异步流行的一个库(注意,redux可以进行异步操作,但是redux-thunk可以帮助我们隐藏异步实现的细节)
可以看到,组件内部并不想知道怎么请求的,(具体异步细节忽略)
yarn add react-redux @reduxjs/tooltik
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;
import {configureStore} from "@reduxjs/toolkit";
import {projectListSliceReducer} from "../pages/projectList/projectList.slice";
export const store = configureStore({
/* 设置状态管理 */
reducer:{
projectList:projectListSliceReducer
},
})
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;
const { useSelector } from "react-redux"
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"
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
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;
在home.js中, 通过createAsyncThunk函数创建一个异步的action
再在extraReducers中监听这个异步的action的状态, 当他处于fulfilled状态时, 获取到网络请求的数据, 并修改原来state中的数据
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
import { useDispatch } from "react-redux";
const dispatch = useDispatch();
dispatch(fetchHomeMultidataAction())
如果我们不想通过在extraReducers在监听状态, 再修改state这种方法的话, 还有另外的一种做法
我们创建的fetchHomeMultidataAction这个异步action是接受两个参数的
参数一, extraInfo: 在派发这个异步action时, 如果有传递参数, 会放在extraInfo里面
参数二, store: 第二个参数将store传递过来
这样我们获取到结果后, 通过dispatch修改store中的state, 无需再监听异步action的状态
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
import { useDispatch } from "react-redux";
const dispatch = useDispatch();
dispatch(fetchHomeMultidataAction())
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;
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")
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" )
import { useDispatch } from "react-redux"
import {projectListSliceActions} = "./projectList.slice";
<button onClick={() => dispatch(projectListSliceActions.closeProjectModal())}>点击我关闭</button>
React.lazy
Suspense
组件使用,传入fallback
参数作为加载时候的画面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 (
{
userInfo && Object.keys(userInfo).length ? :
}
);
}
export default App;
生产环境禁止使用
yarn build --profile
npm run build --profile
我们setupWorker
以前曾为开发创建了一个假服务器。现在我们使用不同的函数,setupServer
因为测试将在 Node.js 环境中运行,而不是在实际的浏览器环境中。只有浏览器环境具有 Service Worker 功能,因此我们在测试中使用 MSW 的方式实际上并不涉及 Service Worker。
setupServer
在node环境下使用的,不涉及到浏览器,setupWorker
在浏览器中运行的最基础的单元测试
export function sum(a, b) {
return a + b;
}
sun.test.ts
也可以,其实都会识别)import {sum} from "../utils";
test('测试结果是否为100',() => {
expect(sum(50,50)).toBe(100)
})
然后运行yarn test
__tests__
当中的文件进行测试总的来说就是: 创建handler,使用handler,开始拦截
1.创建handler
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'
}))
}),
]
import { setupWorker,SetupWorker } from "msw";
import { handlers } from "./handler";
export const worker:SetupWorker = setupWorker(...handlers)
/login
或者/list
就会被拦截,从而返回假数据import { worker } from "./mocks/browser"
if(import.meta.env.DEV){
/* 开发环境下就启动 */
worker.start();
}
setupServer
在node环境下使用的,不涉及到浏览器,setupWorker
在浏览器中运行的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
有如下内容abc = 'www.baidu.com'
process.env.abc
VITE_
开头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开头