Jira 任务管理系统项目总结

1,486 阅读13分钟

跨组件状态管理

简单场景

对于不复杂的情况,比如父组件传递状态给子组件,可以使用 props 进行传递。如果需要传递的状态过多,我们还可以使用组合组件的方法将子组件内的部分组件提升到父组件中,这样就将一层层传递多个状态转化为只传递一个组件

服务端状态

对于服务端状态的维护,也就是发送网络请求才能获取到的数据,如果这些数据需要在多个组件中共享,以前很多人会选择用 contextredux 来管理服务端的状态,但这样会导致它们所维护的状态树过重,不利于项目的维护。随着 react-queryswr 这些库的火爆,越来越多的人愿意使用它们来管理服务端的状态,除了管理状态以外,它们还可以对我们的项目做一些优化,比如 react-query 可以避免重复发送相同的请求以及方便实现乐观更新等操作

客户端状态

对于复杂的客户端状态来说,我们一般有几种方法来管理,一是通过网页的 url,这种方式可以有效的管理较少的状态。二是通过传统的 redux,但是大部分的项目其实并没有那么多的全局状态要管理,如果都使用 redux 进行管理的话,反而会让整个项目显得十分臃肿。不过随着 react hooks 的火爆,其大大简化了使用 context管理状态的代码量,以至于 contex 结合 hooks 慢慢成为了项目中状态管理的主流

项目初始化

  1. 使用 React 官方提供的脚手架 create-react-app 来初始化 React + TS 项目
  1. 使用统一的代码格式化工具 prettierwww.prettier.cn/docs/instal…),这样你的团队成员无论使用什么 IDE 和插件,格式化项目的时候效果都是一样的,不容易产生分歧
  1. 配置 commitlint 帮助我们检查每次 git commit 的信息是否符合规范,如果不符合就让本次提交失败,这回让团队协作的效率更高,项目的可维护性也会更强

Mock 方案

  1. 直接在代码中写死 Mock 数据,或者请求本地的 JSON 文件
  1. 请求拦截向后端发送的请求,比如使用 Mock.js 来模拟数据
  1. 使用接口管理工具来 mock 数据,比如 apipostyapi 等等,但前提项目是文档先行而不是代码先行
  1. 自己用 node 开启一个本地服务器,也可以借助一些好用的库,比如 json-server

错误边界

错误边界是一种 React 组件,这种组件可以捕获发生在其子组件树任何位置的 JavaScript 错误,并打印这些错误,同时展示降级 UI,而并不会渲染那些发生崩溃的子组件树。错误边界可以捕获发生在整个子组件树的渲染期间、生命周期方法以及构造函数中的错误

useState 的惰性初始化

当参数是函数的时候,useState 帮我们保存的状态并不是该函数,而是其返回值,为了获取到该返回值,react 会在组件第一次渲染的时候执行该函数,后续组件的重复渲染中,该函数就不会再被执行了,所以这种初始化 state 的方法也叫作惰性初始化,其适用于初始 state 需要通过复杂计算才能获得的情况。所以如果想用 useState 保存函数,不能直接传入函数,而是需要多嵌套一层函数

乐观更新

客户端假设请求必然成功,因此不等待接口的返回,先行对视图进行更新,随后再根据请求返回的结果调整数据,如果请求是成功的,则不改变提前更新好的 UI;如果请求失败了,则需要将数据和 UI 视图回滚至请求前的状态并用此时数据库中的准确数据代替

性能追踪

Profiler 测量一个 React 应用多久渲染一次以及渲染一次的“代价”。 它的目的是识别出应用中渲染较慢的部分,或是可以使用类似 memoization 优化的部分,并从相关优化中获益

性能追踪是我们在开发项目时经常会被忽略的一个问题,React 恰好为我们提供了用于性能追踪的 API——Profiler,它的用法很简单,只需要嵌套在组件外部就可以知道该组件渲染所花费的时间,很方便帮助我们去定位哪些组件需要被优化

自动化测试

很多时候我们都需要对原先的项目添加新的功能,相信很多人都会遇到添加了新功能后,原先的功能却出现 bug 的情况,以至于我们每次加一个功能都还需要手动检查原先的功能有没有被影响

自动化测试就可以很方便的解决刚刚的问题,每当代码更改都会使用我们预先写好的代码测试原先的函数、hook、组件、页面是否能够正常工作,返回我们想要的结果,这样我们开发完新功能之后就不用在手动的去测试了,不过由于这一块知识点难度较大,所以我也只是做了一个简单的了解~

Q & A

  1. 如何实现页面刷新后持久化存储用户信息?

在正常的项目中,我们可以先在本地保存用户的 token 和用户信息,每当用户初始化页面时,我们就判断用户的浏览器本地是否存有 token,如果没有则直接跳转到登录页面;如果有则先展示本地存储的用户信息,然后根据 token 查询数据库中最新的用户信息并更新到本地

  1. 如何在不同路由组件中实现网页标题的切换?

网上已经针对该问题给出了很多的解决方案,比如可以使用 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])
}
  1. 如何避免无限渲染问题?

我们尽量控制传递给 useEffect 的依赖项数组中的变量可以是组件中管理的状态 state,也可以是 useRef 保存的值或者基本数据类型,但一定不能是对象类型的变量,因为不同的对象即使看起来是一样的但本质上它们的地址却是不同的,而 react 内部在比较新旧两个变量时用的是 === 号,在这种情况下,每次组件重复渲染时 useEffect 所比对出的新旧依赖都不相同,从而导致组件无限渲染。所以如果要传递普通对象,则该对象一定需要用 useMemo 包裹处理以避免组件无限循环渲染

  1. 如何在自己封装的组件以 antd 组件为基础的情况下,让 TS 允许扩展组件的 props?

ComponentPropsReact 内置的类型,用于获取组件的 props 类型,其需要传递一个组件(函数式或类)的类型

import { ComponentProps } from "react";
import { Select } from "antd";

type SelectProps = ComponentProps<typeof Select>;
// 其实上述的方法和下面的是等价的  // type SelectProps = Parameters<typeof Select>[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>
  );
}
  1. 什么时候将文件命名为 tsx 和 jsx,什么时候又命名为 ts 与 js?

当文件中包含组件的时候用 tsxjsx 后缀命名,其它情况下用 tsjs 命名就可以了。正确的使用文件后缀名可以提高项目的可读性,当别人一看到该文件的后缀是以 tsx 结尾的,就能立马知道该文件中包含的是一个组件而不是普通的函数

  1. 用 rem 作为单位布局有什么好处?

rem是相对于根元素 htmlfont-size来决定大小的,当浏览器窗口大小发生变化时,只需要改变 font-size 的值,所有用了 rem 为单位的 css 属性值也会发生相应的变化。 这样一来,我们只需要利用 js 或者媒体查询监听浏览器窗口的大小变化,根据设计稿的比例更改根元素的字体大小就可以实现页面的等比缩放效果

  1. 什么时候使用组件组合?

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}
  1. 如何将自己的项目部署到 github 上?

  1. 新建一个名为 你的github用户名.github.io 的仓库
  1. 在开发环境下安装 gh-pages 依赖 yarn add gh-pages -D,该库是 github 专门为开发者提供用来部署项目的
  1. package.json 文件夹中找到 scripts 字段,新加下列代码中的信息
// 如果你是用 npm 来启动项目的,也可以修改为 npm run build
"predeploy": "yarn build", 
// 该字段表示的意思为将打包后的 build 文件夹推送到指定仓库的 main 分支
"deploy": "gh-pages -d build -r 创建的仓库地址 -b main"
  1. 在命令行中执行 yarn deploy 命令,其会预先 predeploy 字段对应的命令 yarn build,然后将打包后的内容 push 到指定的仓库中去,所有操作完成之后打开 github 指定的网页即可看到你的应用啦!如果后续需要更新项目,只需要更新代码后重新执行该命令即可
  1. 部署到 github 上的项目为啥有时刷新页面会报 404 的错误?

假设我们部署在 github 上的地址为 sindu12jun.github.io,由于我们的项目是单页面应用,里面用的都是前端路由,如果当前 url 变化为了 sindu12jun.github.io/projects,此时…

这样服务端接收到这个 url 对应的请求后会误认为是服务器路由,其并找不到该 url 匹配的接口,也不会像访问首页 url 一样将 index.html 响应给客户端,而是返回一个404的页面,网上已经有很多成熟的解决方案了,可以参考 github.com/rafgraph/sp…

  1. 我们的项目是用什么工具把 TS 编译成 JS 文件的?

可能学习过 TS 的朋友都知道其真正要在浏览器或者 node 中被执行需要提前编译成 JS 文件,对于这个编译工具大家的第一反应可能是 tsc

其实不是, 目前大多数的 ts 项目都是 ts 类型检查 + babel 编译 这样的组合,这个项目也不例外 (可以去项目 node_modules 下面看一下,会发现有个 @babel 文件夹),用 babel 编译 ts,就可以实现 babel 编译一切,从而降低开发/配置成本

  1. 所有的函数式组件都应该被 React.memo 所包裹吗?

在子组件没有经过特殊处理的情况下,父组件由于状态改变导致重复渲染时,子组件也会进行重复渲染,但有的时候我们并不想让子组件进行无用的渲染,这时就会想到在组件外用 React.memo 来包裹

React.memo 会比较函数式组件前后两次的 props 是否发生了变化,比较方法是浅层比较,如果判断为没有变化,则组件不会重新渲染,听起来是一个很不错的优化方案,那是不是可以在每一个组件外面都包裹一层 React.memo 呢?

其实是不需要的,React.memo 由于自身需要做前后两次 props 的浅层比较,是要消耗一定性能的。再者有 React diff 算法的加持下,其实很多 DOM 元素并不会被真正的渲染,所以很多组件就算没有做 memo 优化,仍然不会对项目的性能造成什么影响。所以我们在想使用 React.memo 之前最好先想想这个组件重新渲染和浅层比较 props 谁花费的性能较大再决定是否使用它,如果子组件本身比较复杂,那确实是可以在它外面套一层 memo 进行优化的

总结

通过这个项目不仅巩固了自己的 ReactTS 知识,同时也学到了很多 Mock 数据、性能优化、性能追踪等新知识,希望自己可以凭借新的项目和自己的理解慢慢在前端领域有自己的见解~