一、创建项目
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 plugin 与 extend 的区别:
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要改为classNamestyle要写成 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 模板的重大区别
- 判断,Vue 模板用
v-if指令 cn.vuejs.org/guide/essen… - 循环,Vue 模板用
v-for指令 cn.vuejs.org/guide/essen… - Vue 模板中没有
{xxx}写法,全都是"xxx"
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> <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 是国内流行的第三方 Hooks 库
- 功能全面
- 使用简单
- 文档 demo 清晰易懂
后面会再次用到 ahooks ,到时候再代码演示
react-use
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 - 可用
clsx或classnames条件判断
代码演示
// 第一种
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
代码演示,参考 components/Button3.tsx
Styled-jsx
优点
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 组件库
- antDesign 国内最常用,现已发布到 v5 版本 ant.design/index-cn
- Material UI 国外非常流行 mui.com/core/
- Tailwind UI 国外流行,但收费 tailwindui.com/components
目前,国内 antD 是最好的选择,所以其他的了解一下即可,不用过多演示。
使用 antd
安装和使用
测试 Button 组件
antd 组件
根据官网 ant.design/components/… 浏览一遍 antd 所有的组件,有一个了解
实战
- 重构 QuestionCard 组件
- 重构几个 Layout
- 开发 Star Trash 页面,使用 Table 组件
Tailwind CSS
特点:把常用的 CSS 样式都细分,自己自己随意搭配。
例如
- 字体大小 www.tailwindcss.cn/docs/font-s…
- 间距 www.tailwindcss.cn/docs/paddin…
- 宽度 www.tailwindcss.cn/docs/width
示例:
<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
代码参考 pages/ReactHookFormDemo1.tsx
formik
React 官网推荐的f
代码参考 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
- 前端引入 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 配置
- 使用 craco github.com/dilanx/crac…
- 可参考 www.lingjie.tech/article/202…
在线 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
传统方式需要自己处理 loading error ,而 useRequest 解决了这个问题。
重构以上两个
问卷功能2
获取问卷列表 List Star Trash —— 抽离为 hooks/useLoadQuestionListData.ts
问卷操作
- Trash 恢复、彻底删除
- QuestionCard.tsx 星标、删除、复制
分页和搜索
回顾:ListSearch 和 url 参数
分页 url 参数 page 和 pageSize
- Antd 分页组件
- 修改 hooks/useLoadQuestionListData.ts
LoadMore
List 不用传统分页,用 LoadMore
- 初始化:加载第一页,监听滚动
- 滚动到底部,加载下一页
- 不能用 url 参数
page和pageSize,否则刷新会出问题的
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
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.js、nuxt.js、nest.js。
- 渲染页面
- 提交答卷
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.jsonscripts)
测试代码文件的位置
- 统一放在
__tests__目录下 - 和源文件放在一起,增加
.test.ts后缀 - 选择后者(1. 结合更密切,可读性好;2. 不容易忘,敦促及时写单元测试)
小结
- test it 构建测试用例
- expect 断言 —— 很多,后面一一学习
- 测试文件的位置
组件单元测试
注意,不是所有前端代码都适合单元测试,一般只对一些核心的、功能封装独立的组件进行单元测试。
- QuestionInfo
- QuestionTitle
- QuestionParagraph
- QuestionInput
- QuestionTextarea
- QuestionRadio
- QuestionCheckbox
自动化测试
操作
npm run test加入到 husky.husky/pre-commit- 为 package.json
scriptstest增加--watchAll=false—— 重要!否则无法正常执行 commit - 每次 commit 会执行测试
自动测试的价值
- 每次 commit 都自动执行,测试失败,无法提交代码(不污染现有的代码)
- 避免各种“不小心” “忘了” 的问题 —— 自动化,电脑忘不了
- 要及时完善组件单元测试,新组件也要添加单元测试
storybook
组件可视化测试
或者,它可以被理解为一种文档或组件使用介绍,通过 storybook 就可以看到组件的 UI 结构、属性配置等。
初始化
进入 react 目录,运行 npx storybook init ,添加 storybook
PS:期间会咨询是否使用 eslint 插件,选择否。因为我们已经自己定义好 eslint 规则了
先执行 npm run format 和 npm 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