仿-问卷星

847 阅读32分钟

一、创建项目

yarn create react-app wenjuanxing --template typescript
yarn create vite wenjuanxing --template react-ts

代码规范

高质量代码的特点

  • 严格编码规范(靠工具、流程,而非自觉)
  • 合理、规范的注释
  • 代码合理拆分

两者区别

eslint prettier

  • eslint 编码规范,如变量未定义(语法语义)
  • prettier 编码风格,如末尾是否用 ;
  • eslint 也有编码风格的功能,两者可能会有冲突

eslint

安装插件

npm install eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin -save-dev

初始化配置文件 .eslint.js

npx eslint --init ## 然后根据引导一步一步走

解释:eslint pluginextend 的区别:

  • extend 提供的是 eslint 现有规则的一系列预设
  • plugin 则提供了除预设之外的自定义规则,当你在 eslint 的规则里找不到合适的的时候就可以借用插件来实现了

安装 vscode 插件 eslint ,此时就可以看到代码 App.txs 中的错误提示(如定义一个未使用的变量)

package.json 中增加 scripts "lint": " eslint 'src/**/*.+(js|ts|jsx|tsx)' "

控制台运行 npm run lint 也可以看到错误提示。如果要自动修复,可以加 --fix 参数

prettier

npm install prettier eslint-config-prettier eslint-plugin-prettier -save-dev
  • eslint-config-prettier 禁用所有和 Prettier 产生冲突的规则
  • eslint-plugin-prettier 把 Prettier 应用到 Eslint,配合 rules "prettier/prettier": "error" 实现 Eslint 提醒。

在 eslint 配置文件的 extends 最后 增加 'plugin:prettier/recommended'

安装 vscode 插件 prettier ,此时可以看到代码 App.txs 中的格式提示(如末尾是否使用 ; ,或单引号、双引号)

package.json 中增加 scripts "format": " prettier --write 'src/**/*.+(js|ts|jsx|tsx)' "

控制台运行 npm run format 可以修复所有的格式错误

设置 vscode .vscode/settings.json 自动保存格式,可以在文件保存时,自动保留格式

{
    "editor.codeActionsOnSave": {
        "source.fixAll.eslint": true
    }
}

增加配置文件 .prettierrc.js 规定自己的编码格式,运行 npm run format 可以看到效果,保存文件也可以看到效果。

【注意】如果此处没效果,可以重启 vscode 再重试。


一直搞不定,重启 vscode 就好了。

在 vscode 搜“prettier” 插件时,发现一个 “reload required” 的提示,于是就重启了

CRA 创建的项目,配置文件是 js 格式

vite 创建的项目,配置文件是 cjs 格式

提交到 git 仓库

PS:git 不会从 0 讲起

选择平台

  • 工作中,按照公司规定,可能有内网 git 仓库
  • 正式的开源项目,需要积累 star ,可以考虑 github (但有时访问不稳定,不同网络环境不一样)
  • 个人学习项目,尽量选择国内平台,速度快

我选择 coding.net

演示

git remote add origin xxx
git push -u origin master

husky

git-hook

安装依赖

npm install husky -save-dev

参考文档 github.com/typicode/hu… 增加三个 pre-commit 命令

npm run lint
npm run format
git add .

可以故意制造一个错误:定义一个未使用变量(eslint 配置文件 rules 增加 'no-unused-vars': 'error',

然后执行 git commit 试试

commit-lint

参考文档 github.com/conventiona… 安装设置即可

commit 规则查看 node_modules/@commitlint/config-conventional (在 commitlint.config.js 中有配置)

尝试 git commit -m "test" 会失败,再尝试 git commit -m "chore: commit lint" 会成功

加餐:vite 和 webpack

PS:加餐课程,选学

CRA 使用 webpack 作为打包工具,和 vite 是竞品。

区别

  • vite 启动速度更快,使用 ES Module 语法,开发环境节省打包过程
  • CRA 或 webpack 使用者更多,时间更久,插件更丰富

我们选择 CRA ,先考虑稳定、开发成本,再说效率

代码演示

ES Module 代码演示,参考 es-module-demo

CRA demo 和 vite demo 启动之后,看浏览器控制台 source

二、JSX

JSX - JS 语法扩展,可以在 JS 中写模板(类似 HTML 语法)
JSX 已经成为 ES 语法标准,也可用于其他框架,如 Vue3

  • 标签
  • 属性
  • 事件
  • JS 表达式
  • 判断
  • 循环

标签

  • 首字母小写 - HTML tag
  • 首字母大写 - 自定义组件
  • <input/><Input/> 就不一样 可以像 HTML 一样嵌套

JSX 里的标签必须是闭合的,<input> <br> 这样写在 JSX 会报错(在 HTML 中不会报错),必须闭合 <input/>

每一段 JSX 只能有一个根节点,或者使用 <></> ( Fragment )

属性

和 HTML 属性基本一样,但有些和 JS 关键字冲突了

  • class 要改为 className
  • style 要写成 JS 对象(不能是 string),key 采用驼峰写法
  • for 要改为 htmlFor

事件

onXxx 的形式

注意 TS 的写法

function clickHandler(event: React.MouseEvent<HTMLParagraphElement>) {
    event.preventDefault()
    console.log('clicked')
}
return <p onClick={clickHandler}>hello world</p>

如果要想传递参数,可以通过如下方式

function clickHandler(event: React.MouseEvent<HTMLParagraphElement>, x: string) {
    event.preventDefault()
    console.log('clicked', x)
}
return (
    <p onClick={(e: React.MouseEvent<HTMLParagraphElement>) => clickHandler(e, 'hello')}>
    hello world
    </p>
)

PS:Event handlers must be passed, not called! onClick={handleClick}, not onClick={handleClick()}.

JS 表达式

{xxx} 格式表示一个 JS 变量或表达式,可用于

  • 普通文本内容,或判断、循环
  • 属性值
  • 用于注释

判断

JS 一般使用 if...else 做判断,但不能用于 JSX 的 {xxx} 中。 所以,可以选择其他方式做判断

  • 运算符 &&
  • 三元表达式 a ? b : c
  • 用函数封装
const flag = true
return <div>
        {flag && <p>hello</p>}
        {flag ? <p>你好</p> : <p>再见</p>}
       </div>

或者用函数封装

function Hello() {
    if (flag) return <p>你好</p>
    else return <p>再见</p>
}
return <Hello></Hello>

循环

使用 map 做循环

const list = [
    { username: 'zhangsan', name: '张三' },
    { username: 'lisi', name: '李四' },
    { username: 'shuangyue', name: '双越' },
]

const ul = <ul>
{list.map(user => {
    return <li key={user.username}>{user.name}</li>
})}
</ul>

JSX 循环必须有 key - 帮助 React 识别哪些元素改变了,比如被添加或删除。

  • 同级别 key 必须唯一
  • key 是不可改变的 —— 尽量不用 index ,要用业务 ID (也不要用随机数)
  • key 用于优化 VDOM diff 算法(后面再说)

显示 HTML 代码

JSX 防止注入攻击,否则用 dangerouslySetInnerHTML={{ __html: 'xxx' }}

三、组件和 props

React 一切皆组件

React apps are made out of components. A component is a piece of the UI (user interface) that has its own logic and appearance. A component can be as small as a button, or as large as an entire page.

组件可嵌套

  • React 通过组件来构建 UI
  • 组件拆分也有利于代码组织和维护,尤其对于大型软件
  • JSX 中,组件 tag 首字母要大写

代码演示:从 index.tsx 开始,到 <App> 全都是组件。

组件就是一个函数

  • React 之前是 class 组件
  • 现已被函数组件 FC 全面取代
  • 输入 props ,返回一段 JSX

实战:List 页面抽离组件

代码参考 react-ts-demo 中 components/QuestionCard1.tsx

  • props 类型
  • TS 泛型

进阶:type 还是 interface

都可以实现类型定义的功能 (具体代码演示),用哪个都可以


PS:组件之间的数据传递不仅仅只有 props ,课程后面还会继续讲解其他形式。

TS 语法如果一开始不熟练,就先记住当前的。随着课程深入,用多了也就熟练了。

PS:函数也可以当做属性来传递

JSX 对比 Vue 模板

列几个 JSX 和 Vue 模板的重大区别

PS:Vue3 也能很友好的支持 JSX —— 从一开始抨击 JSX 到最后接纳

通过对比,可以看出 React 和 Vue 最初设计理念的区别

  • React - JS 能实现的都交给 JS ,不重复定义 —— 要求使用者 JS 熟练
  • Vue - 自定义很多指令和写法,初学者好理解好记忆,好推广 —— 这么多写法需要记忆、查文档,麻烦

PS:Vue 最初模仿的是 Angular ,Angular 是几个 Java 程序员开发的。他们对 JS 不熟练,才搞了这么多指令,方便自己使用。

四、Hooks

useState

让页面“动”起来

例如实现一个 click 计数功能,普通变量无法实现。即:修改普通变量无法触发组件的更新 rerender

通过 useState 即可实现。

state 是什么

State, A component's memory —— 这个比喻非常好!

  • props 父组件传递过来的信息
  • state 组件自己内部的状态,不对外

每次 state 变化,都会触发组件更新,从新渲染页面。

代码演示,参考 react-ts-demo 中 pages/StateDemo1.tsx

import React, { FC, useState } from 'react'

const Demo: FC = () => {
    // let count = 0 // 普通的 js 变量,无法触发组件的更新
    const [count, setCount] = useState(0) // useState 可以触发组件的更新,

    // const [name, setName] = useState('双越')

    function add() {
        // count++
        // setCount(count + 1)
        setCount(count => count + 1) // 使用函数,state 更新不会被合并
        setCount(count => count + 1)
        setCount(count => count + 1)
        setCount(count => count + 1)
        setCount(count => count + 1)

        // setCount(count => count + 1)
        console.log('cur count ', count) // 异步更新,无法直接拿到最新的 state 值
        
        // setName('x')
        // console.log(name) // 如果说一个变量,不用于JSX中显示,那就不要用useState来管理,用useRef。 
    }
    return (
        <div>
            <button onClick={add}>add {count}</button>
        </div>
    )
}
export default Demo

state 的特点

异步更新

代码演示

PS:setState 传入函数,可同步更新

可能会被合并

代码演示

不可变数据

state 可以是任意 JS 类型,不仅仅是值类型。

不可直接修改 state ,而要 setState 新值。

代码演示

import React, { FC, useState } from 'react'
const Demo: FC = () => {
// const [userInfo, setUserInfo] = useState({ name: '双越', age: 20 })
// function changeAge() {
// // **不可变数据** - 不去修改 state 的值,而是要传入一个新的值 —— 重要!
// setUserInfo({
// ...userInfo,
// age: 21,
// })
// }

const [list, setList] = useState(['x', 'y'])
function addItem() {
    // **不可变数据** - 不去修改 state 的值,而是要传入一个新的值 —— 重要!
    setList(list.concat('z'))
    // setList([...list, 'z'])
}
return (
    <div>
        <h2>state 不可变数据</h2>
        {/* <div>{JSON.stringify(userInfo)}</div>
        <button onClick={changeAge}>change age</button> */}
    
        <div>{JSON.stringify(list)}</div>
        <button onClick={addItem}>add item</button>
    </div>
    )
}

PS:函数组件,每个更新函数从新执行,state 被重置,而不是被修改。state 可以理解为 readOnly

immer

Immer 简化了不可变数据结构的处理。特别是对于 JS 语法没那么熟悉的人。

代码演示,参考 react-ts-demo 中 pages/ImmerDemo1.tsx

实战:List 页面使用 state

  • 使用 state
  • 使用 immer
  • push
  • 修改 isPublish

代码参考 pages/List2.tsx

import React, { FC, useState, useEffect } from 'react'
import produce from 'immer'
import QuestionCard from './components/QuestionCard'

// 组件是一个函数(执行返回 JSX 片段),组件初次渲染执行这个函数
// 任何 state 更新,都会触发组件的更新(重新执行函数)

const List2: FC = () => {
    useEffect(() => {
        console.log('加载 ajax 网络请求')
        return () => {
            console.log('销毁')
        }
    }, []) // 无依赖,组件初次渲染时执行

    // const [count, setCount] = useState(0)
    const [questionList, setQuestionList] = useState([
        { id: 'q1', title: '问卷1', isPublished: false },
        { id: 'q2', title: '问卷2', isPublished: true },
        { id: 'q3', title: '问卷3', isPublished: false },
        { id: 'q4', title: '问卷4', isPublished: true },
    ])

    // useEffect(() => {
    // console.log('question list changed')
    // }, [questionList])

    // useEffect(() => {
    // console.log('count changed')
    // }, [count, questionList])

    function add() {
    // setCount(count + 1)

    const r = Math.random().toString().slice(-3)

    // setQuestionList(
    // // 新增 concat
    // questionList.concat({
    // id: 'q' + r,
    // title: '问卷' + r,
    // isPublished: false,
    // })
    // )

    // immer 的方式
    setQuestionList(
        produce(draft => {
            draft.push({
            id: 'q' + r,
            title: '问卷' + r,
            isPublished: false,
            })
        })
    )
}

function deleteQuestion(id: string) {
// // 不可变数据
// setQuestionList(
// // 删除 filter
// questionList.filter(q => {
// if (q.id === id) return false
// else return true
// })
// )

// immer 的方式
    setQuestionList(
        produce(draft => {
            const index = draft.findIndex(q => q.id === id)
            draft.splice(index, 1)
        })
    )
}

    function publishQuestion(id: string) {
    // setQuestionList(
    // // 修改 map
    // questionList.map(q => {
    // if (q.id !== id) return q
    // return {
    // ...q,
    // isPublished: true,
    // }
    // })
    // )

    // immer 的方式
    setQuestionList(
        produce(draft => {
            const q = draft.find(item => item.id === id)
            if (q) q.isPublished = true
        })
    )
}
    return (
        <div>
            <h1>问卷列表页2</h1>
            <div>
                {questionList.map(question => {
                    const { id, title, isPublished } = question
                    return (
                        <QuestionCard
                        key={id}
                        id={id}
                        title={title}
                        isPublished={isPublished}
                        deleteQuestion={deleteQuestion}
                        publishQuestion={publishQuestion}
                        />
                    )
                })}
                </div>
                <div>
                <button onClick={add}>新增问卷</button>
            </div>
        </div>
    )
}
export default List2

最重要的就是:不可变数据 —— 这是 React state 的核心

useEffect

副作用

干了自己不该干的事情,函数本来执行完就行。

函数组件:执行函数,返回 JSX

  • 初次渲染时
  • state 更新时

但有些场景需要如下功能

  • 当渲染完成时,做某些事情
  • 当某个 state 变化时,做某些事情
  • 如 ajax 加载数据(state 变化重新加载)

如果只有 执行函数,返回 JSX 这个逻辑,无法满足上面的场景。

所以需要 useEffect

组件渲染完成时

代码演示:List2.tsx 使用 useEffect 模拟 ajax 请求

useEffect(() => {
    console.log('加载 ajax 网络请求')
    return () => {
        console.log('销毁')
    }
}, []) // 无依赖,组件初次渲染时执行

某些 state 更新时

代码演示:QuestionCard 监听 isPublished 的更新

useEffect(() => {
    console.log('question list changed')
}, [questionList])

// 依赖两个state
useEffect(() => {
    console.log('count changed')
}, [count, questionList])

组件销毁时

有创建就有销毁,有生就有死

代码演示:增加 isDeleted 属性和 delete 事件,看 QuestionCard 组件的销毁

【重要】如果有定时任务,或者 DOM 事件,组件销毁时一定要解绑

执行两次(销毁一次)

useEffect(() => {
    console.log('question card mounted')
    return () => {
        console.log('question card unmounted', id) // 销毁
    }
    // 生命周期:创建,更新(state 变化),销毁
}, [])

React18 开始,useEffect 在开发环境下执行两次。
模拟组件挂载、销毁、重新挂载的完整流程,及早发现后续的问题。如果只挂载一次,有可能卸载组件时有问题。

而且,实际项目中某些组件真的有可能会被挂载很多次(如重置 state),要及早模拟这种情况,避免出现重复挂载的问题(如弹窗重复、bindEvent 重复)

生产环境下,不会再执行两次

其他 Hooks

useRef

  • 一般用于操作 DOM 元素,代码演示:src/pages/UseRefDemo1.tsx
  • useRef 也可以传入 JS 值,但更新时不会触发 rerender ,需替换为 useState
  • 要和vue3 ref区分开(vue3 ref 用于操作DOM,用于响应式监听)
const Demo: FC = () => {
    const inputRef = useRef<HTMLInputElement>(null)
    function selectInput() {
    const inputElem = inputRef.current    // 当前指向的什么
        if (inputElem) inputElem.select() // DOM 节点,DOM 操作 API
    }
    return (
        <div>
            <input ref={inputRef} defaultValue="hello world" />
            <button onClick={selectInput}>选中 input</button>
        </div>
    )
}
const Demo: FC = () => {
    const nameRef = useRef('双越') // 不是 DOM 节点,普通的 JS 变量

    function changeName() {
        nameRef.current = '双越老师' // 修改 ref 值,不会触发 rerender ( state 修改会触发组件 rerender )
        // console.log(nameRef.current)
    }

    return (
        <>
            <p>name {nameRef.current}</p>
            <div>
                <button onClick={changeName}>change name</button>
            </div>
        </>
    )
}

useMemo

  • 函数组件,默认,每次 state 变化都会重新执行
  • useMemo 可以缓存某个数据,不用每次都重新生成
  • 可用于计算量比较大的数据场景

代码参考 pages/UseMemoAndCallback/UseMemoDemo1.tsx

import React, { FC, useMemo, useState } from 'react'
const Demo: FC = () => {
    console.log('demo...')

    const [num1, setNum1] = useState(10)
    const [num2, setNum2] = useState(20)
    const [text, setText] = useState('hello') // 更新,导致组件 rerender

    const sum = useMemo(() => {
        console.log('gen sum...') // 缓存
        return num1 + num2
    }, [num1, num2])

    return (
        <>
            <p>{sum}</p>
            <p>
                {num1} <button onClick={() => setNum1(num1 + 1)}>add num1</button>
            </p>
            <p>
                {num2} <button onClick={() => setNum2(num2 + 1)}>add num2</button>
            </p>
            <div>

                {/* form 组件,受控组件 */}
                <input onChange={e => setText(e.target.value)} value={text}></input>
            </div>
        </>
    )
}

export default Demo

注意文档中的这段话 “你可以把 useMemo 作为性能优化的手段,但不要把它当成语义上的保证。” zh-hans.reactjs.org/docs/hooks-…

即,useMemo 的控制权在 React ,不一定保证每个都会缓存,但都是为了全局的性能最佳。

useCallback

useCallback 就是 useMemo 的语法糖,和 useMemo 一样。用于缓存函数。

代码参考 pages/UseMemoAndCallback/UseCallbackDemo1.tsx

import React, { FC, useState, useCallback } from 'react'
const Demo: FC = () => {
    const [text, setText] = useState('hello')
    const fn1 = () => console.log('fn1 text: ', text)
    const fn2 = useCallback(() => {
        console.log('fn2 text: ', text)
    }, [text])

    return (
        <>
            <div>
                <button onClick={fn1}>fn1</button> &nbsp; <button onClick={fn2}>fn2</button>
            </div>
            <div>
                {/* form 组件,受控组件 */}
                <input onChange={e => setText(e.target.value)} value={text}></input>
            </div>
        </>
    )
}
export default Demo

自定义 Hooks

已学习了几个常用的内置 Hooks ,可以用于很多业务功能。

修改网页标题

  • 第一步,直接在组件内部写
  • 第二步,可以抽离一个函数
  • 第三步,可以直接抽离一个文件 src/hooks/useTitle.ts ,引用使用

抽离自定义 Hook ,可用于很多组件,复用代码

import { useEffect } from 'react'
function useTitle(title: string) {
    useEffect(() => {
        document.title = title
    }, [])
}
export default useTitle

获取鼠标位置

(刚才的没有返回值,这次有返回值)

代码演示 src/hooks/useMousePosition.ts (直接在 App.tsx 中使用)

import { useState, useEffect, useCallback } from 'react'
// 获取鼠标位置(自定义 Hook)
function useMouse() {
    const [x, setX] = useState(0)
    const [y, setY] = useState(0)

    const mouseMoveHandler = useCallback((event: MouseEvent) => {
        setX(event.clientX)
        setY(event.clientY)
    }, [])

    useEffect(() => {
        // 监听鼠标事件
        window.addEventListener('mousemove', mouseMoveHandler)

        // 组件销毁时,一定要解绑 DOM 事件!!! (可能会出现组件内存泄漏问题)
        return () => {
            window.removeEventListener('mousemove', mouseMoveHandler)
        }
    }, [])
    return { x, y }
}
export default useMouse

异步获取信息

(再来个异步的)

代码演示 src/hooks/useGetInfo.ts (直接在 App.tsx 中使用)

import { useState, useEffect } from 'react'
// 异步获取信息
function getInfo(): Promise<string> {
    return new Promise(resolve => {
        setTimeout(() => {
            resolve(Date.now().toString())
        }, 1500)
    })
}

const useGetInfo = () => {
    const [loading, setLoading] = useState(true)
    const [info, setInfo] = useState('')

    useEffect(() => {
        getInfo().then(info => {
            setLoading(false)
            setInfo(info)
        })
    }, [])
    
    return { loading, info }
}
export default useGetInfo

小节

自定义 Hooks 可以抽离公共逻辑,复用到多个组件中 —— 这是 Hooks 设计的初衷

在 Hooks 和函数组件之前,class 组件也有一些方法:mixin HOC render-prop 等,但都没有 Hooks 来的简单。

第三方 Hooks

回顾上一节的“三步”,其实还有第四步:抽离为单独的模块,发布到 npm ,供所有开发者使用。

例如,之前开发的 useTitle useMousePosition ,就现成的

ahooks

ahooks.js.org/zh-CN/

ahooks 是国内流行的第三方 Hooks 库

  • 功能全面
  • 使用简单
  • 文档 demo 清晰易懂

后面会再次用到 ahooks ,到时候再代码演示

react-use

github.com/streamich/r…

react-use 是国外比较流行的,功能也很前面,但是英文文档。

先做了解吧,项目中不会使用。

Hooks 使用规则

命名规则

Hook 必须 useXxx 格式来命名。

PS:这种命名规则也很易读,简单粗暴

调用位置

Hook 或自定义 Hook ,只能在两个地方被调用

  • 组件内部
  • 其他 Hook 内部

组件外部,或一个普通函数中,不能调用 Hook

顺序一致

Hook 在每次渲染时都按照相同的顺序被调用。

  • Hook 必须是组件“第一层代码”
  • Hook 不可放在 if 等条件语句中 ( 或者前面有 return ,也算是条件 )
  • Hook 不可放在 for 等循环语句中

代码演示

闭包陷阱

异步函数中获取 state 时,可能不是最新的 state 值。

解决方案:替换为 useRef —— 但 ref 变化不会触发 rerender ,所以得结合 state 一起

代码参考 src/pages/ClosureTrap.tsx

import React, { FC, useState, useRef, useEffect } from 'react'
const Demo: FC = () => {
    const [count, setCount] = useState(0)
    const countRef = useRef(0)
    useEffect(() => {
        countRef.current = count
    }, [count])

    function add() {
        setCount(count + 1)
    }

    function alertFn() {
        setTimeout(() => {
            // alert(count) // count 值类型
            alert(countRef.current) // ref 引用类型
        }, 3000)
    }

    return (
        <>
            <p>闭包陷阱</p>
            <div>
                <span>{count}</span>
                <button onClick={add}>add</button>
                <button onClick={alertFn}>alert</button>
            </div>
        </>
    )
}
export default Demo

五、使用CSS

普通 CSS

内联 style

  • 和 HTML style 一样,元素的内联样式
  • 必须是 JS 对象形式,不可以是字符串
  • 样式名称用驼峰式写法,如 fontSize

代码演示

className

  • 和 HTML class 一样,设置 CSS 样式名
  • 和 JS class 重复,所以改名 className
  • 可用 clsxclassnames 条件判断

代码演示

// 第一种
let itemClassName = 'list-item'
if (isPublished) itemClassName += ' published'
// 逻辑稍微复杂

// 第二种
const itemClassName = classnames('list-item', { published: isPublished })
const itemClassName = classnames({
    'list-item': true,
    published: isPublished,
})

链接

尽量不用内联 style

  • 内联 style 代码量多,性能差
  • 外链样式(用 className)代码复用,性能好
  • 这和 React 无关,在学 HTML CSS 时就知道

CSS Module

普通 CSS 的问题

  • React 使用组件化
  • 多个组件,对应多个 CSS
  • 多个 CSS 就会造成命名重复,不好管理

CSS Module

  • 每个 CSS 都是一个独立的模块,命名 xxx.module.css
  • 每个模块中的 className 都不一样
  • CRA 原生支持 CSS Module

代码演示,参考 components/Button2.tsx

// 第三种
import styles from './QuestionCard.module.scss'

const listItemClass = styles['list-item']
const publishedClass = styles.published
const itemClassName = classnames({
    [listItemClass]: true,
    [publishedClass]: isPublished,
})

使用 Sass

  • CSS 写法比较原始
  • 一般使用 Sass less 等预处理语言
  • CRA 支持 Sass Module ,把后缀改为 .scss 即可

代码演示

CSS-in-js

  • 在 JS 中(组件代码中)写 CSS
  • 不用担心 CSS class 重名的问题
  • CSS-in-js 是一个解决方案,并不是一个工具的名称

PS:CSS-in-js 并不是内联 style (重要!!!),它会经过工具的编译处理,生成 CSS class 的形式。

Styled-components

styled-components.com/

代码演示,参考 components/Button3.tsx

Styled-jsx

github.com/vercel/styl…

优点

CSS-in-js 能更灵活的支持动态样式,直接在 JS 中完成计算和样式切换。这比 css-module 更好。

实战 - List 页增加样式

选择 CSS Module

  • 使用简单,学习成本低
  • 性能更好,css-in-js 需要一定的编译时间
  • 不需要那么多动态样式(css-in-js 的优点)

重构 List 页面 - 增加样式

UI 设计

代码演示


渐变背景色 color.oulu.me/

六、路由设计

页面对应的路由

  • 首页 /
  • 登录 /login
  • 注册 /register
  • 问卷管理
  • 我的问卷 /manage/list
  • 星标问卷 /manage/star
  • 回收站 /manage/trash
  • 问卷详情
  • 编辑问卷 /question/edit/:id (动态路由)
  • 问卷统计 /question/stat/:id
  • 404

Layout 模板

  • MainLayout
  • ManageLayout
  • QuestionLayout

开发

增加页面

  • pages/Home.tsx

  • pages/Login.tsx

  • pages/Register.tsx

  • pages/NotFoundPage.tsx

  • pages/manage/List.tsx

  • pages/manage/Star.tsx

  • pages/manage/Delete.tsx

  • pages/question/Edit.tsx

  • pages/question/Stat.tsx

增加 Layout

layouts 目录下,所有文件。

PS:先不要管 antd 组件和样式,先把 JSX 写出再说。

增加 React-router 配置

安装 react-router-dom ,配置参考 router/index.ts

还有 App.ts 也有改动

路由功能

  • 跳转 - Home 页面使用 useNavigate<Link>
  • 获取动态路由参数 - Edit 页面使用 useParams
  • 获取 query - Home 页面使用 useSearchParams

七、UI组件库

认识 UI 组件库

  • UI 组件库,封装了常用的 UI 组件,例如标题、导航、按钮、输入框等。
  • 每个成熟的技术栈都有 UI 组件库,如 Vue elementUI ,React antd
  • 一般是第三方开发的

React 常用 UI 组件库

目前,国内 antD 是最好的选择,所以其他的了解一下即可,不用过多演示。

使用 antd

安装和使用

参考文档 ant.design/docs/react/…

测试 Button 组件

antd 组件

根据官网 ant.design/components/… 浏览一遍 antd 所有的组件,有一个了解

实战

  • 重构 QuestionCard 组件
  • 重构几个 Layout
  • 开发 Star Trash 页面,使用 Table 组件

Tailwind CSS

特点:把常用的 CSS 样式都细分,自己自己随意搭配。

例如

示例:

<h1 className="text-3xl font-bold underline">
Use tailwind CSS
</h1>

安装参考文档 www.tailwindcss.cn/docs/guides…

八、表单组件

普通表单组件

input 组件

import React, { FC, useState, ChangeEvent } from 'react'
const Demo: FC = () => {
    const [text, setText] = useState('')
    function handleChange(event: ChangeEvent<HTMLInputElement>) {
        setText(event.target.value)
    }
    return <input onChange={handleChange} value={text} />
}

受控组件 vs 非受控组件

受控组件

  • 元素的值同步到 state
  • 使用 value 属性

非受控组件

  • 元素的值,不同步到 state
  • 使用 defaultValue 属性

React 推荐使用受控组件。看似麻烦,其实让设计更简单。

其他表单组件

textarea radio checkbox select

代码参考 react-ts-demo pages/FormElemDemo1.tsx

form

参考 react-ts-demo pages/homePageDemo/Demo3.tsx

antd form 组件

参考 antd 官网 ant.design/components/…

表单组件

  • Input
  • Radio
  • Checkbox
  • Select

实战:搜索

ListSearch 组件

实战 - 登录注册

注册页

  • Form 组件 onFinish
  • Form.Item 组件 name 和 values 的关系
  • rules

登录页

  • useForm 和设置 Form values

  • “记住我”

表单校验

antd Form rules

参考登录、注册页

PS:选择使用 antd ,顺带了解一下两个其他的 Form 验证工具

react-hook-form

react-hook-form.com/

代码参考 pages/ReactHookFormDemo1.tsx

formik

React 官网推荐的f

formik.org/docs/overvi…

代码参考 pages/FormikDemo1.tsx

九、ajax

基础知识

  • HTTP 请求,前后端通讯 —— 不再具体讲解了
  • API - XMLHttpRequest 和 fetch
  • 常用工具:axios

代码演示

  • XMLHttpRequest 和 fetch
  • axios - 在项目中安装演示

(用已有的 mock 服务来演示)

var xhr = new XMLHttpRequest();
xhr.open('get', '/api/test', true);
xhr.onreadystatechange = function () {
    if (xhr.readyState === 4 && xhr.status === 200) {
        var result = JSON.parse(xhr.responseText);
    }
}
xhr.send();
fetch('/api/test')
.then(res => res.json())
.then(data => console.log(data))

搭建 mock 服务

Mock.js

mockjs.com/

  • 前端引入 Mock.js
  • 定义路由
  • Mock.js 劫持 ajax 请求

代码演示

  • 安装 Mock.js 和 axios
  • 定义路由,参考 _mock/index.ts
  • App.js 测试 ajax
import './_mock/index'
import axios from 'axios'

useEffect(() => {
    // fetch('/api/test') // 不能用 fetch
    // .then(res => res.json())
    // .then(data => console.log(data))
    axios.get('/api/test').then(res => console.log('res', res.data))
}, [])

Mock.js 的强大之处:

  • 前端劫持 Ajax
  • Random 的模拟能力

注意

  • Mock.js 只能劫持 XHR ,不能劫持 fetch ,所以不要用 fetch 请求。
  • Mock.js 要在生产环境下去掉,否则上线会有问题 —— Mock.js 体积也很大
  • 结论:不建议直接在前端使用 Mock.js

nodejs 搭建 Mock 服务

代码参考 question-mock

  • 刻意延迟 1s ,模拟真实效果
  • 使用 Mock.js 的 Random 功能
  • 定义写 Mock 的格式,考虑扩展性

前端修改 devServer ,参考 craco.config.js

扩展 webpack 配置

在线 Mock 平台

不稳定,可能不维护了。有数据泄漏风险(多人使用,难免会写敏感数据)

  • fast-mock
  • y-api
  • swagger - 尽量不推荐用国外平台(可以用做工具,但别用作服务)

API 设计

用户功能

获取用户信息

  • method get
  • path /api/user/info
  • response { errno: 0, data: {...} }{ errno: 10001, msg: 'xxx' }

注册

  • method post
  • path /api/user/register
  • request body { username, password, nickname }
  • response { errno: 0 }

登录

  • method post
  • path /api/user/login
  • request body { username, password }
  • response { errno: 0, data: { token } } —— JWT 使用 token

问卷功能

创建问卷

  • method post
  • path /api/question
  • request body - 无 (点击一个按钮即可创建,title 自动生成)
  • response { errno: 0, data: { id } }

获取单个问卷

  • method get
  • path /api/question/:id
  • response { errno: 0, data: { id, title ... } }

获取问卷列表

  • method get
  • path /api/question
  • response: { errno: 0, data: { list: [ ... ], total } }

更新问卷信息

  • method patch
  • path /api/question/:id
  • request body { title, isStar ... } (之前忘记了,现补上)
  • response: { errno: 0 }

PS:删除是假删除,实际是更新 isDeleted 属性

批量彻底删除问卷

  • method delete
  • path /api/question
  • request body { ids: [ ... ] }
  • response: { errno: 0 }

复制问卷

  • method post
  • path /api/question/duplicate/:id
  • response: { errno: 0, data: { id } }

小结

  • 使用 Restful API
  • 用户验证使用 JWT (后面再讲)
  • 统一返回格式 { errno, data, msg }

实战 问卷

配置 axios

创建 src/services/ajax.ts ,先不写 request 拦截器

创建 src/services/question.ts

  • 即用即添加 service
  • 即用即增加 mock

问卷功能1

创建问卷 ManageLayout.tsx

获取单个问卷 Edit Stat( 虽先不做,但可先获取数据 ) —— 抽离为 hooks/useLoadQuestionData.ts

使用 useRequest

ahooks.js.org/zh-CN

传统方式需要自己处理 loading error ,而 useRequest 解决了这个问题。

重构以上两个

问卷功能2

获取问卷列表 List Star Trash —— 抽离为 hooks/useLoadQuestionListData.ts

问卷操作

  • Trash 恢复、彻底删除
  • QuestionCard.tsx 星标、删除、复制

分页和搜索

回顾:ListSearch 和 url 参数

分页 url 参数 pagepageSize

  • Antd 分页组件
  • 修改 hooks/useLoadQuestionListData.ts

LoadMore

List 不用传统分页,用 LoadMore

  • 初始化:加载第一页,监听滚动
  • 滚动到底部,加载下一页
  • 不能用 url 参数 pagepageSize ,否则刷新会出问题的

PS:搜索依然可以用 url 参数

实战 用户

JWT

  • JWT - JSON Web Token
  • 登录成功,服务端派发一个 token
  • 后续 HTTP 请求都带着这个 token ,以表明自己的身份

(画图表示)

services

创建 src/services/user.ts

  • 即用即添加 service
  • 即用即增加 mock

用户功能

注册

登录

  • 保存 token
  • 给 src/services/ajax.ts 增加 request 拦截器

获取用户信息 MainLayout QuestionLayout(两者平等) —— 抽离 hooks/userLoadUserData.ts

退出( UserInfo 组件)—— 删除 token

十、状态管理

为何使用状态管理

状态提升

  • 一个复杂页面,要拆分 UI 组件
  • 但数据保存在顶级组件
  • 通过 props 传递到下级组件

代码演示,参考 react-ts-demo 中 pages/homePageDemo/demo3.tsx —— 需要抽离 <form> 为单独的组件

(画图表示)

状态管理

但如果情况再复杂,例如问卷编辑器,光通过状态提升无法满足。需要状态管理 —— 数据放在一个集中的第三方。

(画图表示)

React 状态管理的方式

  • 自带的 Context useReducer
  • Redux
  • Mobx

Context

介绍

  • 向下级组件,跨组件传递信息
  • 不用像 props 层层传递
  • 例如:切换语言、切换主题等

(画图解释)

代码演示 react-ts-demo 中 pages/ContextDemo1

Context 只适合统一设置、下发某些全局变量(语言,主题等),应用场景比较单一

useReducer

背景

第一,是 useState 的代替方案。

当数据简单时用 useState ,当数据结构较为复杂时,可以考虑用 useReducer

第二,参考了 redux (马上要学)的设计,一个简化了的 redux

代码演示

简单 demo pages/CountReducer.tsx

todo list demo pages/TodoReducer/index.tsx

概念

  • state 或 store - 存储数据
  • action - 动作,格式如 { type: 'xxx', ... }
  • reducer - 根据 action 生成新 state —— 不可变数据
  • dispatch - 触发 action

PS:在 React 环境下,永远不能忘记 不可变数据

问题

需结合 useContext 跨组件通讯

另,state 和 dispatch 没有模块化,数据混在一起,也不适合复杂项目。

但简单项目还是可以用的。

Redux

Redux 是 React 最出名的状态管理工具

概念

redux 和 useReducer 的概念一样

  • state 或 store - 存储数据
  • action - 动作,格式如 { type: 'xxx', ... }
  • reducer - 根据 action 生成新 state —— 不可变数据
  • dispatch - 触发 action

但 redux 和 useReducer 有很多区别

  • store 可拆分模块
  • 可通过 Hook 获取 state 和 dispatch
  • 开发者工具

代码演示

代码参考 undo-redo-demo (忽略 undo redo 功能)。分两步:

  • 不使用 immer
  • 使用 immer

开发者工具

Chrome redux DevTools

chrome.google.com/webstore/de…

可以看到每一步 dispatch 的 state 变化,开发调试很方便

redux 单项数据流模型

参考这里的动图 cn.redux.js.org/tutorials/f…

Mobx

Mobx 可以通过声明式的方式来修改数据。像 Vue 。

不像 React 和 redux 那样,需要用纯函数和不可变值。

基本概念

主旨 zh.mobx.js.org/the-gist-of…

  • state 数据
  • action 动作
  • derivation 派生
  • computed
  • observer 监听变化,包裹的 React 组件
  • autorun 监听变化,像 watch

代码演示

简单 demo - mobx-demo/src/BasicDemo

todo-list demo - mobx-demo/src/TodoDemo2

小结

Mobx 使用单项数据流 zh.mobx.js.org/the-gist-of…

其他

v6 已经默认去掉了装饰器语法,为了大部分的兼容性 mobx.js.org/installatio…

尽量使用 computed

mobx.js.org/the-gist-of…

When starting with MobX, people tend to overuse reactions. The golden rule is, always use computed if you want to create a value based on the current state.

computed 必须是纯函数。而 action 可以修改 state (如 arr.push)

computed 采用惰性求值,会缓存其输出,并且只有当其依赖的可观察对象被改变时才会重新计算。 它们在不被任何值观察时会被暂时停用。

Redux 管理用户信息

store

创建 src/store/store.ts 和 src/store/userReducer.ts

开发

Logo 组件,根据 username 判断链接地址

新建 hooks/useGetUserInfo.ts

hooks/useLoadUserData.ts

  • 使用 loginReducer
  • useSelect - 根据 username 判断是否已经登录
  • UserInfo 组件,去掉 service ,改用 useGetUserInfo

UserInfo 组件

  • 使用 logoutReducer
  • useSelect - 显示用户信息或“登录”

新建 hooks/useNavPage 执行跳转逻辑,用于 MainLayout QuestionLayout

十一、编辑器

编辑器

背景

首页,登录注册,列表页,都已经开发完了。接下来开发问卷页:编辑器 + 统计

目标

  • 完成编辑器页面的设计和开发
  • 体验复杂系统的 UI 组件拆分
  • 体验复杂系统的数据结构设计

内容

  • 需求分析
  • 技术方案设计
  • 开发
  • 拖拽排序
  • 撤销重做

注意事项

  • 需求指导设计,设计指导开发。需求、设计这两步很重要。

一般是“需求 - 设计 - 开发” ,但页面太复杂,会脱节,所以边设计边开发

十二、统计页面

统计页面

背景

问卷发布,用户提交了答卷以后,发布者要能及时看到统计信息,业务闭环

目标

开发问卷统计功能,让发布者看到所有答卷信息。

内容

  • 需求分析
  • 技术方案设计
  • chart lib 选型
  • 开发

注意事项

  • 需求指导设计,设计指导开发。需求、设计这两步很重要。

十三、C端

C 端

PS:如果服务端没做,可以使用 mock 数据

背景

开发完编辑器和服务端,就剩下 C 端了。

C 端适合用 SSR

  • 页面简单,操作不复杂
  • 用于移动端,网络环境不稳定,要求性能 ( SSR 性能很好 )

React 的 SSR 框架 - Next.js

目标

  • 开发问卷 C 端,并和编辑器的发布功能打通

内容

  • SSR 介绍 和 系统设计

SSR Server side render 服务端渲染
CSR 和 SSR 的区别 (画图解释)
SSR 的好处:性能 + SEO
SSR 并不是新东西,早在 JSP ASP 时代就是 SSR 。现在是前端框架和 SSR 的结合。

  • Next.js 介绍

Vercel 创建,最流行的 React SSR 框架
初学者区分三个框架 next.jsnuxt.jsnest.js

  • 渲染页面
  • 提交答卷

www.nextjs.cn/docs/gettin…

npx create-next-app@latest --typescript
## 或 yarn create next-app --typescript

运行 npm run dev 添加新功能

  • 页面 src/pages , 可创建一个 pages/about.tsx 组件
  • API src/pages/api
  • 静态资源,如 public/data/about.json ,GET 访问 /data/about.json

两种形式 pre-render

  • Static Generation 项目构建时生成 HTML 文件,以后访问不会重复生成
  • Server-side rendering 每次请求都重新生成 HTML 文件

Static Generation

代码参考 about.tsx

构建项目

  • npm run build 构建,可以看到 getStaticProps 被执行
  • npm run start 运行构建的项目 ( 不是运行源码,可修改源码试试 )

PS:如果就一个普通的 React 组件,也会默认使用这种方式,如 Success.tsx

Server-side rendering

代码参考 blog.tsx

构建项目,运行看 getServerSideProps 每次都执行

动态路由

想访问 /blog/1 这种动态路由,还需要继续调整。

  • 新建 pages/question/[id].tsx
  • getServerSideProps 获取动态参数 id

注意事项

  • SSR 虽然有利于性能,但不可随便用,要看常用 ( 如编辑器,即不适合用 SSR )

十四、性能优化

缓存数据 减少计算

PS:React18 开发环境下,组件会渲染两次。生产环境则不会。

useState 传入函数

  • useState 传入初始化数据
  • 如传入函数,则只在组件渲染执行一次
  • 如果数据结构较复杂,可使用函数

代码演示,参考 react-ts-demo 中 pages/UseStateFnDemo.tsx

useMemo 缓存数据

(之前讲过,再回顾一遍,不用代码演示了)

  • 函数组件,默认,每次 state 变化都会重新执行
  • useMemo 可以缓存某个数据,不用每次都重新生成
  • 可用于计算量比较大的数据场景

代码参考 pages/UseMemoAndCallback/UseMemoDemo1.tsx

注意文档中的这段话 “你可以把 useMemo 作为性能优化的手段,但不要把它当成语义上的保证。” zh-hans.reactjs.org/docs/hooks-…

即,useMemo 的控制权在 React ,不一定保证每个都会缓存,但都是为了全局的性能最佳。

项目示例:统计页,链接和二维码的 Elem

代码体积和拆分

代码体积分析

create-react-app.dev/docs/analyz…

# build 以后,可以直接运行结果 (看控制台提示)
yarn global add serve
# npm install serve -g
serve -s build

发现 main.js 体积有 1.5M —— 首页加载就需要 1.5M ,有点大,需要拆分。

分析内部发现比较大的体积来自于 antd recharts react-dom dnd-kit 等。

首先想到的:拆分页面,路由懒加载,把编辑页、统计页拆分开

路由懒加载

代码参考 src/router/index.ts

再进行代码体积分析,发现 main.js 减小到 1.0M 还是很大。

但至少编辑页面、统计页面的代码都移除了。

分析结果中,发现一个不符合预期的现象:@dnd-kit 是拖拽排序的,应该在编辑页,不应该在 main.js

查代码发现,在 src/store/componentsReducer/index.ts 中用到了 @dnd-kit/sortable ,而后者用依赖于 @dnd-kit/core

这个是否要优化掉?(把相关代码移动到编辑页面的引用) —— 不值得!!!

  • 这部分代码只占用 50kb ,最后GZip 压缩以后大约 16kb ,体积不算大
  • 如果移动代码,将导致代码修改较多,而且可能破坏语义、可读性
  • 综合考虑成本和收益,这里保持不变

继续:分析结果中,占比最大的是 antd 和 react-dom ,可以抽离公共代码。

抽离公共代码

PS:生成环境需要抽离。开发环境不需要抽离,否则影响打包速度。

代码参考 craco.config.js ,注意两点

  • 必须是生成环境。开发环境不需要抽离,否则影响打包速度。
  • 设置 chunks: 'all'

重新 build 以后,发现 main.js 只有 35kb ,react-dom antd vendors 都被拆分出去了。

合理使用缓存

运行 build 结果,发现首页依然要加载好几个 JS 文件: main.js react-dom antd vendors

它们体积的总和依然是 1M 左右,那和优化之前一样吗? —— 不一样

  • 优化之前是一个文件,一旦有代码改动,文件变化,缓存失效
  • 优化之后拆分多个文件,代码改动只会导致 main.js 变化,其他文件都会缓存
  • 如果不频繁升级 npm 插件,其他 js 文件不会频繁变动

CSS

不用做优化,css 已经被分离为 main.css antd-chunk.css edit-page.css stat-page.css

小结

从一开始的 1.6M 到最后的 33KB ,效果明显。

PS:浏览器和服务端一般都默认支持 Gzip 压缩,体积能压缩 1/3 左右

useCallback 缓存函数

(之前讲过,再回顾一遍,不用代码演示了)

useCallback 就是 useMemo 的语法糖,和 useMemo 一样。用于缓存函数。

代码参考 pages/UseMemoAndCallback/UseCallbackDemo1.tsx

项目示例:新增组件

React.memo 缓存组件

当 state 变化时,React 会默认渲染所有子组件,无论其 props 是否变化

但如果想要控制子组件根据 props 变化来渲染,可以使用 React.memo

代码演示,可以在 pages/homePageDemo/Demo3.tsx

List 组件增加 React.memo

PS:注意和 useMemo 的区别,一开始容易搞混了

十五、测试

单元测试

对比

  • 单元测试 - 某个功能模块、函数、组件的测试 —— 开发人员
  • 系统测试 - 整个功能流程的测试 —— 专业测试人员

jest 入门

jest 是最流行的前端单元测试工具

可参考官网首页的 demo www.jestjs.cn/docs/gettin…

react-ts-demo 代码演示

  • CRA 直接安装了 jest ,可直接写
  • utils/math.ts 和 utils/math.test.ts
  • 运行 npm run test (看 package.json scripts

测试代码文件的位置

  • 统一放在 __tests__ 目录下
  • 和源文件放在一起,增加 .test.ts 后缀
  • 选择后者(1. 结合更密切,可读性好;2. 不容易忘,敦促及时写单元测试)

小结

  • test it 构建测试用例
  • expect 断言 —— 很多,后面一一学习
  • 测试文件的位置

组件单元测试

注意,不是所有前端代码都适合单元测试,一般只对一些核心的、功能封装独立的组件进行单元测试。

  • QuestionInfo
  • QuestionTitle
  • QuestionParagraph
  • QuestionInput
  • QuestionTextarea
  • QuestionRadio
  • QuestionCheckbox

自动化测试

操作

  • npm run test 加入到 husky .husky/pre-commit
  • 为 package.json scripts test 增加 --watchAll=false —— 重要!否则无法正常执行 commit
  • 每次 commit 会执行测试

自动测试的价值

  • 每次 commit 都自动执行,测试失败,无法提交代码(不污染现有的代码)
  • 避免各种“不小心” “忘了” 的问题 —— 自动化,电脑忘不了
  • 要及时完善组件单元测试,新组件也要添加单元测试

storybook

组件可视化测试

或者,它可以被理解为一种文档或组件使用介绍,通过 storybook 就可以看到组件的 UI 结构、属性配置等。

初始化

storybook.js.org/docs/react/…

进入 react 目录,运行 npx storybook init ,添加 storybook

PS:期间会咨询是否使用 eslint 插件,选择否。因为我们已经自己定义好 eslint 规则了

先执行 npm run formatnpm run lint 统一代码格式(storybook 新增的组件,代码风格可能不一样)

执行 npm run storybook 启动,可以看到现成的 demo

最后,新建 stories/examples 文件夹,把示例都拖进来。否则太多太乱。

代码演示

新建 stories/question 目录,在这里新建组件

  • QuestionInfo.stories.tsx
  • QuestionTitle.stories.tsx
  • QuestionParagraph.stories.tsx
  • QuestionInput.stories.tsx

十六、总结

知识点

创建项目环境

  • Create-React-App
  • Vite
  • Next.js
  • ESLint
  • Prettier
  • Husky
  • Commit-lint
  • Craco.js 扩展 webpack 配置

React 基础

  • JSX 语法
  • 函数组件
  • Props
  • Typescript 类型
  • 开发者工具

Hooks

  • useState
  • useEffect
  • useRef
  • 自定义 Hook
  • Hooks 规则
  • ahooks
  • react-use

CSS 样式

  • 普通 CSS
  • CSS-Module Sass
  • CSS-in-JS
  • Styled-components
  • Styled-jsx
  • Emotion
  • classnames

路由

  • React-Router
  • 路由嵌套和 Outlet
  • 动态路由
  • 页面跳转
  • 获取参数

UI 组件库

  • AntDesign
  • AntDesign-icons
  • Layout 和布局
  • MaterialUI
  • Tailwind

表单组件

  • 受控组件
  • Form.List 动态表单
  • AntDesign-Form-rules
  • React-hook-form
  • Formik

Ajax 网络请求

  • XMLHttpRequest
  • Fetch
  • Axios
  • useRequest ahooks
  • Mock.js
  • Postman
  • Restful API
  • JWT 用户校验

状态管理

  • 状态提升
  • useContext
  • useReducer
  • Redux
  • Immer 不可变数据
  • Redux-undo 撤销重做
  • MobX

拖拽排序

  • React-dnd
  • React-beautiful-dnd
  • Sortable.js
  • React-sortable-hoc
  • dnd-kit

可视化图表

  • React-chartjs-2
  • Recharts
  • ECharts-for-react

性能优化

  • useMemo

  • useCallback

  • React.memo

  • analyzing-the-bundle-size

  • 路由懒加载

  • 抽离公共代码 缓存

测试

  • jest
  • React-Testing-Library
  • pre-commit 自动化测试
  • storybook