对于不复杂的情况,比如父组件传递状态给子组件,可以使用 props
进行传递。如果需要传递的状态过多,我们还可以使用组合组件的方法将子组件内的部分组件提升到父组件中,这样就将一层层传递多个状态转化为只传递一个组件
对于服务端状态的维护,也就是发送网络请求才能获取到的数据,如果这些数据需要在多个组件中共享,以前很多人会选择用 context
或 redux
来管理服务端的状态,但这样会导致它们所维护的状态树过重,不利于项目的维护。随着 react-query
和 swr
这些库的火爆,越来越多的人愿意使用它们来管理服务端的状态,除了管理状态以外,它们还可以对我们的项目做一些优化,比如 react-query
可以避免重复发送相同的请求以及方便实现乐观更新等操作
对于复杂的客户端状态来说,我们一般有几种方法来管理,一是通过网页的 url
,这种方式可以有效的管理较少的状态。二是通过传统的 redux
,但是大部分的项目其实并没有那么多的全局状态要管理,如果都使用 redux
进行管理的话,反而会让整个项目显得十分臃肿。不过随着 react hooks
的火爆,其大大简化了使用 context
管理状态的代码量,以至于 contex
结合 hooks
慢慢成为了项目中状态管理的主流
使用 React
官方提供的脚手架 create-react-app
来初始化 React + TS
项目
使用统一的代码格式化工具 prettier
(www.prettier.cn/docs/instal…),这样你的团队成员无论使用什么 IDE
和插件,格式化项目的时候效果都是一样的,不容易产生分歧
配置 commitlint
帮助我们检查每次 git commit
的信息是否符合规范,如果不符合就让本次提交失败,这回让团队协作的效率更高,项目的可维护性也会更强
直接在代码中写死 Mock
数据,或者请求本地的 JSON
文件
请求拦截向后端发送的请求,比如使用 Mock.js 来模拟数据
使用接口管理工具来 mock
数据,比如 apipost
、yapi
等等,但前提项目是文档先行而不是代码先行
自己用 node
开启一个本地服务器,也可以借助一些好用的库,比如 json-server
错误边界是一种 React 组件,这种组件可以捕获发生在其子组件树任何位置的 JavaScript 错误,并打印这些错误,同时展示降级 UI,而并不会渲染那些发生崩溃的子组件树。错误边界可以捕获发生在整个子组件树的渲染期间、生命周期方法以及构造函数中的错误
当参数是函数的时候,useState
帮我们保存的状态并不是该函数,而是其返回值,为了获取到该返回值,react
会在组件第一次渲染的时候执行该函数,后续组件的重复渲染中,该函数就不会再被执行了,所以这种初始化 state
的方法也叫作惰性初始化,其适用于初始 state
需要通过复杂计算才能获得的情况。所以如果想用 useState
保存函数,不能直接传入函数,而是需要多嵌套一层函数
客户端假设请求必然成功,因此不等待接口的返回,先行对视图进行更新,随后再根据请求返回的结果调整数据,如果请求是成功的,则不改变提前更新好的 UI
;如果请求失败了,则需要将数据和 UI
视图回滚至请求前的状态并用此时数据库中的准确数据代替
Profiler
测量一个 React 应用多久渲染一次以及渲染一次的“代价”。 它的目的是识别出应用中渲染较慢的部分,或是可以使用类似 memoization 优化的部分,并从相关优化中获益
性能追踪是我们在开发项目时经常会被忽略的一个问题,React
恰好为我们提供了用于性能追踪的 API
——Profiler,它的用法很简单,只需要嵌套在组件外部就可以知道该组件渲染所花费的时间,很方便帮助我们去定位哪些组件需要被优化
很多时候我们都需要对原先的项目添加新的功能,相信很多人都会遇到添加了新功能后,原先的功能却出现 bug
的情况,以至于我们每次加一个功能都还需要手动检查原先的功能有没有被影响
自动化测试就可以很方便的解决刚刚的问题,每当代码更改都会使用我们预先写好的代码测试原先的函数、hook
、组件、页面是否能够正常工作,返回我们想要的结果,这样我们开发完新功能之后就不用在手动的去测试了,不过由于这一块知识点难度较大,所以我也只是做了一个简单的了解~
在正常的项目中,我们可以先在本地保存用户的 token
和用户信息,每当用户初始化页面时,我们就判断用户的浏览器本地是否存有 token
,如果没有则直接跳转到登录页面;如果有则先展示本地存储的用户信息,然后根据 token
查询数据库中最新的用户信息并更新到本地
网上已经针对该问题给出了很多的解决方案,比如可以使用 react-helmet 这个库,不过我们也可以自己实现:使用原生的 doucument.title
来控制标题的变换。为了增强该功能的复用性,我们可以将其封装成一个自定义 hook
在不同的组件中使用
export default function useDocumentTitle(title: string, keepOnUnmount: boolean = true) {
// 保存当前路由对应的标题
const oldTitle = useRef(document.title).current
useEffect(() => {
// 当该组件挂载到页面中时,将开发者指定的文本替换成网页标题
document.title = title
// 根据传入该函数的参数来决定组件卸载时是否回滚到先前的网页标题
return () => {
if (!keepOnUnmount) document.title = oldTitle
}
}, [oldTitle, keepOnUnmount, title])
}
复制代码
我们尽量控制传递给 useEffect
的依赖项数组中的变量可以是组件中管理的状态 state
,也可以是 useRef
保存的值或者基本数据类型,但一定不能是对象类型的变量,因为不同的对象即使看起来是一样的但本质上它们的地址却是不同的,而 react
内部在比较新旧两个变量时用的是 ===
号,在这种情况下,每次组件重复渲染时 useEffect
所比对出的新旧依赖都不相同,从而导致组件无限渲染。所以如果要传递普通对象,则该对象一定需要用 useMemo
包裹处理以避免组件无限循环渲染
ComponentProps
是 React
内置的类型,用于获取组件的 props
类型,其需要传递一个组件(函数式或类)的类型
import { ComponentProps } from "react";
import { Select } from "antd";
type SelectProps = ComponentProps<typeof Select>;
// 其实上述的方法和下面的是等价的 // type SelectProps = Parameters[0]
// Parameters 可以获取函数的参数并放置到一个元组中,而 antd 的组件恰好是一个函数,所以 [0] 就表示取出 props 的类型
// 创建自己组件的 props 类型,用好 TS 的内置类型 Omit 来防止一些我们添加的属性影响到原先 antd 组件的属性
interface IdSelectProps
extends Omit<
SelectProps,
"value" | "setState" | "defaultOptionName" | "options"
> {
value?: Raw | null | undefined;
setState?(value?: number): void;
defaultOptionName?: string;
options?: { name: string; id: number }[];
}
// 在 antd 组件的基础上再封装一个组件,使得该组件不仅可以传递原先 antd 组件中的参数,还可以传递一些我们定义的属性
export default function IdSelect(props: IdSelectProps) {
const { value, setState, defaultOptionName, options, ...restProps } = props;
return (
<Select
value={value || 0}
onChange={(value) => setState?.(toNumber(value) || undefined)}
// 这个 {} 并不表示对象的意思,不要理解错了,而是 jsx 语法要求在变量外边需要套个 {}
{...restProps} >
{defaultOptionName ? (
<Select.Option value={0}>{defaultOptionName}</Select.Option>
) : null}
{options?.map((item) =>
item ? (
<Select.Option key={item.id} value={item.id}>
{item.name}
</Select.Option>
) : null
)}
</Select>
);
}
复制代码
当文件中包含组件的时候用 tsx
或 jsx
后缀命名,其它情况下用 ts
或 js
命名就可以了。正确的使用文件后缀名可以提高项目的可读性,当别人一看到该文件的后缀是以 tsx
结尾的,就能立马知道该文件中包含的是一个组件而不是普通的函数
rem
是相对于根元素 html
的font-size
来决定大小的,当浏览器窗口大小发生变化时,只需要改变 font-size
的值,所有用了 rem
为单位的 css
属性值也会发生相应的变化。 这样一来,我们只需要利用 js
或者媒体查询监听浏览器窗口的大小变化,根据设计稿的比例更改根元素的字体大小就可以实现页面的等比缩放效果
Context 主要应用场景在于很多不同层级的组件需要访问同样一些的数据。请谨慎使用,因为这会使得组件的复用性变差。
如果你只是想避免层层传递一些属性, 组件组合(component composition) 有时候是一个比 context 更好的解决方案。—— React 官网
听起来很高大上的名词,其实非常简单,就是在面对一些状态需要层层跨组件传递时,如果这些状态都是集中在某一个区域里面使用,那么可以把这一块区域抽离出一个组件放置到状态初始化的地方。这样原本需要层层传递多个状态,现在就只需要将组件传递过去即可。这种做法还有一个好处就是子组件不需要担心如何消费上层传入过来的状态,只需要将注意力放到渲染传入进来的组件上,下面是官网给出的示例:
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} />
// ... 渲染出 ...Page的子组件
<PageLayout userLink={...} />
// ... 渲染出 ...PageLayout的子组件
<NavigationBar userLink={...} />
// ... 渲染出 ...
{props.userLink}
复制代码
新建一个名为 你的github用户名.github.io
的仓库
在开发环境下安装 gh-pages
依赖 yarn add gh-pages -D
,该库是 github
专门为开发者提供用来部署项目的
在 package.json
文件夹中找到 scripts
字段,新加下列代码中的信息
// 如果你是用 npm 来启动项目的,也可以修改为 npm run build
"predeploy": "yarn build",
// 该字段表示的意思为将打包后的 build 文件夹推送到指定仓库的 main 分支
"deploy": "gh-pages -d build -r 创建的仓库地址 -b main"
复制代码
在命令行中执行 yarn deploy
命令,其会预先 predeploy
字段对应的命令 yarn build
,然后将打包后的内容 push
到指定的仓库中去,所有操作完成之后打开 github
指定的网页即可看到你的应用啦!如果后续需要更新项目,只需要更新代码后重新执行该命令即可
假设我们部署在 github
上的地址为 sindu12jun.github.io,由于我们的项目是单页面应用,里面用的都是前端路由,如果当前 url
变化为了 sindu12jun.github.io/projects,此时…
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HlxC287C-1666232975035)(https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/bb55c40e90664946ab9bd838e655242c~tplv-k3u1fbpfcp-zoom-in-crop-mark:4536:0:0:0.image)]
这样服务端接收到这个 url
对应的请求后会误认为是服务器路由,其并找不到该 url
匹配的接口,也不会像访问首页 url
一样将 index.html
响应给客户端,而是返回一个404
的页面,网上已经有很多成熟的解决方案了,可以参考 github.com/rafgraph/sp…
可能学习过 TS
的朋友都知道其真正要在浏览器或者 node
中被执行需要提前编译成 JS
文件,对于这个编译工具大家的第一反应可能是 tsc
其实不是, 目前大多数的 ts
项目都是 ts
类型检查 + babel
编译 这样的组合,这个项目也不例外 (可以去项目 node_modules
下面看一下,会发现有个 @babel
文件夹),用 babel
编译 ts
,就可以实现 babel
编译一切,从而降低开发/配置成本
在子组件没有经过特殊处理的情况下,父组件由于状态改变导致重复渲染时,子组件也会进行重复渲染,但有的时候我们并不想让子组件进行无用的渲染,这时就会想到在组件外用 React.memo
来包裹
React.memo
会比较函数式组件前后两次的 props
是否发生了变化,比较方法是浅层比较,如果判断为没有变化,则组件不会重新渲染,听起来是一个很不错的优化方案,那是不是可以在每一个组件外面都包裹一层 React.memo
呢?
其实是不需要的,React.memo
由于自身需要做前后两次 props
的浅层比较,是要消耗一定性能的。再者有 React diff
算法的加持下,其实很多 DOM
元素并不会被真正的渲染,所以很多组件就算没有做 memo
优化,仍然不会对项目的性能造成什么影响。所以我们在想使用 React.memo
之前最好先想想这个组件重新渲染和浅层比较 props
谁花费的性能较大再决定是否使用它,如果子组件本身比较复杂,那确实是可以在它外面套一层 memo
进行优化的
通过这个项目不仅巩固了自己的 React
和 TS
知识,同时也学到了很多 Mock
数据、性能优化、性能追踪等新知识