当前位置: 首页 > news >正文

React17+React Hook+TS4 最佳实践仿 Jira 企业级项目笔记

前言

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

image-20221203204054233

REST-API风格

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

json-server

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

项目开始

用jsx渲染开发工程列表

  • 初始化代码出现问题,表单收集的组件无法将请求结果发送给list组件
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
/*只在初次挂载执行*/
export const useMount = (callback) => {useEffect(callback,[])
}
  • useDebounce(防抖)
/*自定义防抖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的使用
/*搜索参数*/
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即可

yarn add @types/qs -D 
  • 注意箭头函数和普通函数的泛型书写位置
// 箭头函数
const fn1 = <T,U>() => {}//普通函数
function fn2<T,U>() {}

鸭子类型和json-server中间件

  • 鸭子类型
    • ts是只看是否实现了这个接口当中的成员,实现了就可以通过,没有实现就不通过,不管有没有定义
    • 说通俗点就是只要你符合这个规定里面的规则,就是他说的一个东西
    • 比如鸭子会嘎嘎叫,只要你会嘎嘎叫,ts就认为你是鸭子
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"
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 (<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

  • npm地址:https://www.npmjs.com/package/jira-dev-tool

  • 如果安装后项目启动数据库连接失败,可以试试运行这个命令

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

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

  • 关于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 个或多个数字,将在其中选择,并返回最大值。

更改为antd

  • 表格排序的知识
    • localeCompare:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/String/localeCompare
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为全局样式)
html{/*使得1rem === 10px*/font-size: 62.5%;/* 这样子使得font-size为16px * 62.5% = 10px */
}html body #root .App {min-height: 100vh;
}
  • 安装emotion
yarn add @emotion/react @emotion/styled
  • 安装编辑器插件

    • webstorm: Styled Components & Styled JSX,不过最新版本的好像都自动安装了
    • vscode: 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);`
  • 注意,不管有没有css样式后面,必须要接一个模板字符串,否者会报错
//不报错
const HeaderLeft = styled(Row)``;//报错
const HeaderLeft = styled(Row);
  • 设置多个背景(学到了,学到了,)
    • background-image 属性用于为一个元素设置一个或者多个背景图像。
    • background-position属性为每一个背景图片设置初位置
      • 一个值为x,y设置相同的位置
      • 二个值分别为x轴位置和y轴位置
    • 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});

grid和flex各自的应用场景

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

css-in-js:Row组件实现

  • emotion允许我们像react一样传递参数来达到自定义样式的效果
    • Row组件样式
    • 当然,可以不写ts在这里,不过不写会有警告在tsx当中~
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

    • 在react当中,我们可以直接对组件使用style设置样式,但是不支持一些子元素选择符,伪类等一些高级选择器的
    <MyComponent style={{ marginBottom:'2rem' }}/>
    
    • 所以我们可以使用@emotion/react来代替我们
    /** @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参数了

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

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.

    • 原因:依赖项没有加入在依懒收集里面导致报错
    • 解决:
  useEffect(() => {callback();// todo 依懒项里加上callback会造成无限循环,这个和useCallback以及useMemo有关系},[])
  • 不要乱用object
    • 覆盖范围很广,比如一个箭头函数变量,ts都认为这个是object
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
    yarn add jira-dev-tool@nextApp.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是同步的

  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)-错误边界

  • react官网对于错误边界的说明

    • https://react.docschina.org/docs/error-boundaries.html
  • 只有 class 组件才可以成为错误边界组件

  • react-error-boundary

    • https://github.com/bvaughn/react-error-boundaryx
  • 错误边界无法捕获以下场景中产生的错误:

事件处理异步代码(例如 setTimeout 或 requestAnimationFrame 回调函数)服务端渲染它自身抛出来的错误(并非它的子组件)
  • 老师创建的组件的要求

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

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
    因为在函数组件主体内(这里指在 React 渲染阶段)改变 DOM、添加订阅、设置定时器、记录日志以及执行其他包含副作用的操作都是不被允许的,因为这可能会产生莫名其妙的 bug 并破坏 UI 的一致性。使用 useEffect 完成副作用操作。赋值给 useEffect 的函数会在组件渲染到屏幕之后执行。你可以把 effect 看作从 React 的纯函数式世界通往命令式世界的逃生通道。说通俗点就是useEffect可以放置一些副作用操作在里面,并设置为依赖.而我们的document.title就是一个副作用,所以需要放置在useEffect别忘记了`effect`单词本身的意思哦
    
    • useRef为什么要使用
/* 网页标题更改 */
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
    //路由表
    {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 <Link to={String(project.id)}>{ project.name } </Link>
  • 注意Navigate写法
   <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方法
//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,]
    }
    

    查看返回值类型

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

    a的类型

    • 原因很简单,因为ts认为数组都是相同的,使用为了确保数组当中都有相同的,所以就使用了多个|
    • 所以我们return后面添加一个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 
}

useSearchParams完成(使用useMemo解决)

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

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

    • 所以我们使用useEffect的时候,我们应该将基本类型和组件状态(使用useState)放置到依赖里面,非组件状态的对象,绝对不可以放在依赖里面!!
    • 所以检查我们可以使用why-did-you-render
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)
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状态管理代码

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 URLSearchParamsInitreturn setSearchParams(o);}] as const
}

实现Id-Select解决Id难题

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

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

用useEditProject编辑项目

柯里化

  • 因为有一个参数实现已经知道了,后一个参数需要等待才可以知道,就可以采用这种方式
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也会触发页面重新渲染)
  • 惰性初始代码
    • 示例:https://stackblitz.com/edit/react-ts-2meygh?file=App.tsx
const [state,setState] = useState(() => {const initialState = someExpensiveComputation(props);return initialState;
})
  • 非惰性初始(也就是普通的state,传入的不是一个函数,)
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所指向的函数去执行,所以执行就没有问题
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);// 设置callBackconst 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保存函数的方法

  • 既然存在惰性初始化,我们就让他执行就可以,我们返回一个函数就可以啦
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])

什么时候使用useMemo,useCallback

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

状态提升

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

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

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

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

      • @react官网说明

      • 不适用content传递

      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传递
      // 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) - 缺点是没有解决向下钻的问题,但是方法不需要传递了,(定义和调用很近),示例如下
      // 使用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的写法
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

  • 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

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

有关reduxjs/toolkit部分

  • 书写片段
    • project-list.slice.ts
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
import {configureStore} from "@reduxjs/toolkit";
import {projectListSliceReducer} from "../pages/projectList/projectList.slice";export const store = configureStore({/* 设置状态管理 */reducer:{projectList:projectListSliceReducer},
})

有关react-redux

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"
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;

执行异步

  • 具体可看这篇文章,这里做个记录~
    • https://blog.csdn.net/m0_71485750/article/details/126764667

方法1:

  • 在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.liststate.recommends = payload.data.recommend.list}}
})export default homeSlice.reducer
  • 其他地方引入执行异步
import { useDispatch } from "react-redux";
const dispatch = useDispatch();
dispatch(fetchHomeMultidataAction())

方法2

如果我们不想通过在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里面解构拿到dispatchasync (extraInfo, { dispatch }) => {// 1.发送网络请求获取数据const res = await axios.get("http://123.207.32.32:8000/home/multidata")// 2.从网络请求结果中取出数据const banners = res.data.data.banner.listconst recommends = res.data.data.recommend.list// 3.执行dispatch, 派发actiondispatch(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.actionsexport default homeSlice.reducer
  • 其他地方引入执行异步
import { useDispatch } from "react-redux";
const dispatch = useDispatch();
dispatch(fetchHomeMultidataAction())

总结

  • 先看看片段
    • projectList.slice.ts
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
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>

可以自己加ts

用代码分割优化性能

  • react官网说明
    • https://zh-hans.reactjs.org/docs/code-splitting.html
  • 使用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 (<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)创建的话
    yarn build --profile
    npm run build --profile
    

单元测试(React)

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

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

    • 随便哪里建立一个sum函数
    export function sum(a, b) {return a + b;
    }
    
    • src/tests/sun.ts文件(其实你取名叫sun.test.ts也可以,其实都会识别)
    import {sum} from "../utils";test('测试结果是否为100',() => {expect(sum(50,50)).toBe(100)
    })
    
  • 然后运行yarn test

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

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

  • 总的来说就是: 创建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'}))}),
]
  • 2.使用handler
import { setupWorker,SetupWorker } from "msw";
import { handlers } from "./handler";export const worker:SetupWorker = setupWorker(...handlers)
  • 3.开始拦截(只要匹配到了handler当中的列表,就进行拦截)
    • 我访问/login或者/list就会被拦截,从而返回假数据
import { worker } from "./mocks/browser"if(import.meta.env.DEV){/* 开发环境下就启动 */worker.start();
}
  • 演示地址
    • github:https://github.com/superBiuBiuMan/mswSimpleStudy
    • codesandbox:https://githubbox.com/superBiuBiuMan/mswSimpleStudy

setupServer和setupWorker

  • 也就是说setupServer在node环境下使用的,不涉及到浏览器,setupWorker在浏览器中运行的
  • 老师的代码没有过多解释,我们就以下面代码做个简单说明
    • 其实老师大概步骤也和上面msw拦截异步请求代码一样
    • 先监听,然后拦截,之后才是创建
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读取,则需要通过如下来读取,并且设置的变量必须要以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开头

相关文章:

React17+React Hook+TS4 最佳实践仿 Jira 企业级项目笔记

前言 个人笔记,记录个人过程,如有不对,敬请指出React17React HookTS4 最佳实践仿 Jira 企业级项目项目完成到第十章,剩下后面就没有看了,说的不是特别好 github地址:https://github.com/superBiuBiuMan/React-jira husky方便我们管理git hooks的工具 REST-API风格 https://zh…...

35- tensorboard的使用 (PyTorch系列) (深度学习)

知识要点 FashionMNIST数据集: 十种产品的分类. # T-shirt/top, Trouser, Pullover, Dress, Coat,Sandal, Shirt, Sneaker, Bag, Ankle Boot.writer SummaryWriter(run/fashion_mnist_experiment_1) # 网站显示一 tensorboard的使用 在网站显示pytorch的架构:1.1 …...

ChatGPT在工业领域的用法

在工业数字化时代&#xff0c;我们需要怎么样的ChatGPT&#xff1f; 近日&#xff0c;ChatGPT热度高居不下&#xff0c;强大的人机交互能力令人咋舌&#xff0c;在国内更是掀起一股讨论热潮。一时间&#xff0c;这场由ChatGPT引起的科技飓风&#xff0c;使得全球最顶尖科技力量…...

使用Chakra-UI封装简书的登录页面组件(React)

要求&#xff1a;使用chakra ui和react 框架将简书的登录页面的表单封装成独立的可重用的组件使用到的API&#xff1a;注册API请求方式&#xff1a;POST 请求地址&#xff1a;https://conduit.productionready.io/api/users请求数据: {"user":{ "username&quo…...

Three.js初试——基础概念(二)

前言 姊妹篇&#xff1a;Three.js初试——基础概念 介绍了 Three.js 的一些核心要素概念&#xff0c;这篇文章会讲一下它的关键要素概念。 之前我们了解到展示一个3D图像&#xff0c;必须要有场景、相机、渲染器这些核心要素&#xff0c;仅仅这些还不够&#xff0c;我们还需要…...

Qt音视频开发21-mpv内核万能属性机制

一、前言 搞过vlc内核后又顺带搞了搞mpv内核&#xff0c;mpv相比vlc&#xff0c;在文件数量、sdk开发便捷性方面绝对占优势的&#xff0c;单文件&#xff08;可能是静态编译&#xff09;&#xff0c;不像vlc带了一堆插件&#xff0c;通过各种属性来set和get值&#xff0c;后面…...

C语言学生随机抽号演讲计分系统

6.学生随机抽号演讲计分系统&#xff08;★★★★) 设计一款用于课程大作业检查或比赛计分的软件&#xff0c;基本功能: (1)设置本课程的学生总数 (2)根据本次参与的学生总数&#xff0c;随机抽取一个还未汇报演讲的学生的学号。 (3)每个学生汇报演讲完毕&#xff0c;输入该学生…...

Spring Boot 3.0系列【12】核心特性篇之任务调度

有道无术,术尚可求,有术无道,止于术。 本系列Spring Boot版本3.0.3 源码地址:https://gitee.com/pearl-organization/study-spring-boot3 文章目录 前言Spring Scheduler1. 单线程任务2. 自动配置3. 多线程异步任务Quartz1. 简介2. 核心组件2.1 Job(任务)2.2 Trigger(…...

Java操作XML

Java操作XML XML语法 一个XML文件分为文档声明、元素、属性、注释、CDATA区、特殊字符、处理指令。 转义字符 对于一些单个字符&#xff0c;若想显示其原始样式&#xff0c;也可以使用转义的形式予以处理。 & > &amp; < > < > > > " &g…...

女神节灯笼祝福【HTML+CSS】

✅作者简介&#xff1a;2022年博客新星 第八。热爱国学的Java后端开发者&#xff0c;修心和技术同步精进。 &#x1f34e;个人主页&#xff1a;Java Fans的博客 &#x1f34a;个人信条&#xff1a;不迁怒&#xff0c;不贰过。小知识&#xff0c;大智慧。 &#x1f49e;当前专栏…...

CUDA并行计算基础知识

1、相关缩写术语 显卡:GPU 显卡驱动:驱动软件 GPU架构: 硬件的设计方式,例如是否有L1 or L2缓存 CUDA: 一种编程语言像C++, Python等,只不过它是专门用来操控GPU的 cudnn: 一个专门为深度学习计算设计的软件库,里面提供了很多专门的计算函数 CUDAToolkit:所谓的装cuda首先…...

88. 合并两个有序数组

给你两个按 非递减顺序 排列的整数数组 nums1 和 nums2&#xff0c;另有两个整数 m 和 n &#xff0c;分别表示 nums1 和 nums2 中的元素数目。请你 合并 nums2 到 nums1 中&#xff0c;使合并后的数组同样按 非递减顺序 排列。注意&#xff1a;最终&#xff0c;合并后数组不应…...

卢益贵(码客):软件开发团队的管理要素

卢益贵&#xff08;码客&#xff09;&#xff1a;软件开发团队的管理要素 最好的范例是领导 无论个人素养、技术水平和代码风格&#xff0c;管理者应该起到典范的作用。 最高的权力是威望 管理者的威望比手中权力更有信服力。在处处倚仗权力施压的团队中&#xff0c;高压必有…...

中小企业的TO B蓝海,如何「掘金」?

中国中小企业的数字化转型土壤&#xff0c;如今究竟成长到了哪一步&#xff1f;对一众数字服务厂商而言&#xff0c;在另一个付费群体出现的当下&#xff0c;产品形态是否应该进行微调&#xff1f; 作者|皮爷 出品|产业家 中国市场存在一个黄金定律&#xff1a;二八法则。 这…...

C++ 算法主题系列之集结0-1背包问题的所有求解方案

1. 前言 背包问题是类型问题&#xff0c;通过对这一类型问题的理解和掌握&#xff0c;从而可以归纳出求解此类问题的思路和模板。 背包问题的分类有&#xff1a; 0-1背包问题&#xff0c;也称为不可分割背包问题。无限背包问题。判定性背包问题.带附属关系的背包问题。双背包…...

【Vue】Vue常见的6种指令

Vue的6种指令-前言指令&#xff08;Directives&#xff09;是vue 为开发者提供的模板语法&#xff0c;用于辅助开发者渲染页面的基本结构。vue 中的指令按照不同的用途可以分为如下6 大类① 内容渲染指令 ② 属性绑定指令 ③ 事件绑定指令 ④ 双向绑定指令 ⑤ 条件渲染指令 ⑥ …...

计算机科学与技术(嵌入式)四年学习资料_文件目录树

说明&#xff1a; 资料内容主要包括&#xff1a;计嵌专业2019级大学四年主要科目的各种电子资料&#xff0c;有电子实验报告、课程设计报告、课程设计项目、整理复习笔记、电子书、ppt、练习题、期末试卷、部分课程软件资源、科创项目&#xff0c;职业生涯规划书&#xff0c;大…...

【java】Java 继承

文章目录继承的概念生活中的继承&#xff1a;类的继承格式为什么需要继承公共父类&#xff1a;继承类型继承的特性继承关键字extends关键字implements关键字super 与 this 关键字final 关键字构造器继承的概念 继承是java面向对象编程技术的一块基石&#xff0c;因为它允许创建…...

自媒体账号数据分析从何入手?

账号的数据可以直接反应这个账号的好坏&#xff0c;数据越高收益就会越好&#xff0c;数据越差收益自然高不了。 新手要从哪些方面入手见效更快呢&#xff1f;今天大周就来把自己的经验分享给粉丝们&#xff01; 1、账号定位 &#xff08;1&#xff09;账号所创作的领域 &a…...

Clickhouse新版本JSON字段数据写入方式

Clickhouse新版本JSON字段数据写入方式 在Clickhouse版本22.3.1版本以上&#xff0c;提供了针对JSON格式数据的新的数据类型&#xff1a;JSON&#xff0c;从而实现了存储此类数据由原先的结构化表结构&#xff0c;更新为现在的半结构化表存储。对于新增字段&#xff0c;某些同…...

HNU-电路与电子学-实验2

实验二 模型机组合部件的实现&#xff08;一&#xff09; 班级 计XXXXX 姓名 wolf 学号 2021080XXXXX 一、实验目的 1&#xff0e;了解简易模型机的内部结构和工作原理。 2&#xff0e;熟悉译码器、运算器的工作原理。 3&#xff0e;分析模型机的功…...

从0开始学python -49

Python MySQL - mysql-connector 驱动 -2 插入数据 插入数据使用 “INSERT INTO” 语句&#xff1a; demo_mysql_test.py: 向 sites 表插入一条记录。 import mysql.connectormydb mysql.connector.connect(host"localhost",user"root",passwd"…...

Spring MVC 详解(连接、获取参数、返回数据)

在之前我们先简单那谈谈Spring、SpringBoot以及Spring MVC框架之间有什么关系&#xff1f;首先Spring是一个框架&#xff0c;SpringBoot脚手架是为了快速开发Spring框架而创造的技术。可以理解为SpringBoot又在Spring上面包了一层壳子&#xff0c;是基于Spring的&#xff0c;是…...

IT女神节(致敬中国IT界永远的女神严蔚敏-数据结构)

我们都知道程序数据结构算法。相信很多人都学过严蔚敏的数据结构的课程。作为一个码农&#xff0c;在这不管是3.7女神节&#xff0c;还是3.8妇女节。我觉得都有必要向这些教育界的老前辈致敬。今天我就梳理梳理&#xff0c;最经典的数据结构教材。 严蔚敏介绍&#xff08;来自…...

Java 集合分页

一、前言 在Java开发中&#xff0c;若单次展示的数据量太大&#xff0c;会造成程序响应缓慢&#xff0c;就需要用到 分页 功能&#xff0c;每一页展示一定量的数据&#xff0c;分多次展示 ... 那么在List集合中&#xff0c;如何实现 分页 功能呢&#xff1f; 本文将以3种方式&a…...

代码随想录之哈希表(力扣题号)

242. 有效的字母异位词 直接用数组模拟哈希表 只有小写字母&#xff0c;开26的数组就可以了 class Solution {public boolean isAnagram(String s, String t) {//24-28int[] hash new int[26];Arrays.fill(hash,0);for(int i0;i<s.length();i){hash[s.charAt(i)-a];}for(i…...

如何在知行之桥EDI系统中定时自动更换交易伙伴AS2证书?

为了保证客户与交易伙伴之间数据传输的安全性&#xff0c;AS2传输协议中&#xff0c;通常会通过一对数字证书对传输数据进行签名和加密。但是证书是有有效期的&#xff0c;在证书到期之前&#xff0c;需要贸易双方及时更换新的证书。 在更新证书时&#xff0c;由于客户通常是和…...

辽宁千圣文化:抖音店铺怎么做二次优化?

抖音商品卡订单是指永华在抖音、抖音极速版&#xff0c;通过直播的方式出现短视频页面商品卡之后&#xff0c;直接成交商品详情页直接成交后的订单&#xff0c;那么跟着辽宁千圣文化小编来一起看看吧&#xff01;一.与政策有关1.什么是「商品卡订单」&#xff1f;用户通过抖音、…...

检测js代码中可能导致内存泄漏的工具

JavaScript 中闭包等问题可能导致内存泄漏&#xff0c;因为闭包中引用的变量不会被垃圾回收器自动释放。以下是一些可以用来检测 JavaScript 代码中可能导致内存泄漏的工具&#xff1a; 1、Chrome 开发者工具 Chrome 开发者工具中有一个 Heap Profiler 工具&#xff0c;可以帮…...

linux和centos读写日期到文件并对日期进行比较

#!/bin/bash adate -d "${a}" %s #必须用数字 %s是取时间戳秒数 ddate -d "${c}" %s echo m$(($a - $d)) #必须2个小括号 a1date %s echo $a1 sleep 2 b1date %s echo $(($a1 - $b1)) #必须2个小括号 if [ $a1 -eq $b1 ];then #必须有空格 echo "…...

我想建网站/百度账号登录入口

&#xff08;重发下我这篇原发于 2014-03-18 的网易博客&#xff0c;原博客被网易莫名禁掉了。。被迫手动搬家&#xff0c;忧伤&#xff09;现在好像各种题目出树已经出烦了&#xff0c;开始出仙人掌了。什么时候咱们不出动态树了&#xff0c;搞个Link-Cut Cactus&#xff01;最…...

吉林做网站哪家好/电商怎么做

文章目录1. 问题描述2. 解决办法1. 问题描述 win10更新至21H1后任务栏中多了一个天气的图标和文本 2. 解决办法 鼠标右键单击任务栏的空白处&#xff0c;鼠标滑到咨询和兴趣那里&#xff0c;然后使用鼠标左键单击关闭...

宜昌营销型网站建设/软文关键词排名推广

gitlab 搭建 安装CentOS 7 用U盘安装 CentOS 7 CentOS 7 跟以前的版本有些不同&#xff0c;使用老毛桃&#xff0c;软碟通或者其他的启动U盘制作工具制作的镜像无法启动&#xff0c;需要使用Fedora Media Writer 制作安装镜像 https://wiki.centos.org/zh/Ho... 安装gitlab 按照…...

北京做微信网站哪家好/网站网页设计

Android File Transfer for Mac是一款强大的安卓文件传输工具&#xff0c;由Google公司开发。它可以帮助Mac用户快速、方便地将文件从安卓设备传输到电脑上&#xff0c;并支持多种文件类型和格式。Android File Transfer for Mac提供了一个简单易用的界面&#xff0c;用户只需要…...

wordpress编辑小工具栏/山东关键词网络推广

github常见查询操作小结...

海尔网站建设的优势/免费卖货平台

1肥胖 长期坐在电脑前的人&#xff0c;因缺乏锻炼&#xff0c;会出现重力性脂肪组织分布异常。脂肪堆积在下腹部和腰背部&#xff0c;易导致向心性肥胖。 2下肢静脉曲张 因缺乏活动&#xff0c;依赖骨骼肌收缩回流的下肢静脉的压力将增高&#xff0c;长时间静脉管腔扩张会引起静…...