现代 React Web 开发实践 札记
写在前面
这是一个收费课程的学习札记,课程在极客时间。是宋一玮老师的课程。笔者并非时初学者,所记多有残断,如要系统的学习,还请参加宋一玮老师的课程:https://time.geekbang.org/column/intro/100119601?tab=catalog。
01 | React 如何学习以及前端在做什么
- 学习一个新框架,学习它的基本概念,上手调用 API 写代码,属于技术表面。理清概念联系,理解 API 内部的原理,则属于技术底层。如果学习实践只停留在表层,那一定会遇到瓶颈。
React 新版本 + 函数组件 + Hooks 优先 + 团队协作 = 高效进阶
-
React 开发者不仅是 React 框架本身,还包括对前端整体框架和 React 技术边界的把握。对新老前端技术差异的理解,与历史遗留代码的整合,与多个前端开发团队的协作,等等。
-
应对前端技术迭代过快
- 前端技术不只是技术,应该全面了解,有效总结,将其变为自己的知识点。
- 掌握技术的广度和深度一样重要。
-
GUI 设计的一般原则
- 可用性:自解释,无门槛
- 一致性:除非有真正出众的替代方案,否则还是遵循标准
- 遵循用户心智模型,避免实现模型。即颜色的输入最好是取色器,而不是 RGB 录入。
- 最小惊讶原则:用户也是系统的一部分,设计应该符合用户的经验,预期和心智。设计时应该面向一般用户,而不是面向对系统有深入理解的用户。
- 及时反馈,用户点击了提交按钮就要告诉他是否成功,需要时间处理,就告知用户正在处理。任何时候都要防止页面冻结无法交互。
-
前端开发的领域知识。
- 例如按钮图标。
- Web 浏览器,了解浏览器能做什么,不能做什么。不能轻易想当然。
-
前端领域的变与不变
- 模版,从 JSP 就已经存在的,通过模版生产 HTML。
- 模版的条件和循环,JSTL 和 vue 都支持类似的语法。实际还还有其他的语法支持,例如 handlebar。还有一些 ASP, C#, ejs 等的语法支持。
- 代码分层,Java 端有 Sevlet + Java Bean + JSP 的 MVC 实现。我还记得有 POJO 代替 Java Bean 的实现。Angular 有 MVVM 的实现。
- 软件分发,JSP 需要编译成 .war 包,部署到 Tomcat 内提供服务。虽然目标和实际渲染过程不同,但 React,Vue.js,Angular 一般而言也需要进行构建,现在比较流行的是 Webpack, Vite 等工具。更早之前,流行的是 jQuery + YUI Compressor.
- 项目依赖管理,Java 需要拷贝 .jar 文件完成依赖,后来引入了 Maven。JS 项目使用 npm 管理依赖
02 | 前端开发要点,React 的应对
前端开发的各高度视图
三万英尺视图, 一千英尺视图 这是商业战略中的概念,后被 <<97 Things Every Software Architect Should Know>> 这本合著中引入到软件架构设计领域。
前端应用分类
B/S , C/S 及 Hybrid (混杂)应用。React 善于 B/S 浏览器渲染。
前端逻辑框架
对业务进行分块,对可能用到的大块的功能进行区分,例如 CI/CD,打包变异,自动化测试,运维工具;UX 设计系统,响应式布局,可访问性等。
应用框架
解决各个模块内部的逻辑拆分,例如 SPA 中一般包含 MVC 框架,服务器端交互模块,前端路由,错误处理等。一个纯粹的 MVC 架构,视图会触发控制器,控制器修改模型,模型在触发视图更新。
软件架构
所谓软件架构,即是在软件开发之前进行计划,所谓软件架构师即是给出软件开发计划的人。
设计模式
将软件分隔为独立并能完成单独责任的模块,设计模式可以帮助创建可管理的,可测试的,可复用的并且优化的软件。MVC 是比较重要的设计模式。
MVC
将数据模型与数据展现相分隔。单独的修改可以不影响另一个。MVC 还有各种变体,MVVM, MVP, MVI, 其中细节值得进一步的研究。
image.png
08 | 组件生命周期
- 值得注意的是,更新之前,会执行一轮 Effect 的清除函数。
- useState 是基于 useReducer 实现的,尚未注意,值得再看看。
09 / 10 | React Hooks
副作用
: 当调用函数时,除了返回可能的函数值之外,还对主调用函数产生附加的影响。例如修改全局变量,修改参数,向主调用方的终端、管道改变外部存储信息等。
- useLayoutEffect 更接近
componentDidMount
,componentWillUnmount
,可以认为componentDidMount
,componentWillUnmount
,componentDidUpdate
执行时机与useLayoutEffect
相同。 -
useCallback
相当于useMemo
的马甲,值得看看。
11 | 合成事件
- SyntheticEvent 即是 合成事件,对 DOM 事件的包装,隐藏复杂性和浏览器兼容。
-
dom.onclick
c 是小写。 - React 也支持在捕获阶段监听事件,写法:
<div onClickCapture={handle}></div>
-
onChange
,在不会导致显示抖动的前提下,表单元素的改变会尽量可能及时地触发这一事件。合成 onChange 的事件触发频率多余原生时间次数。(<input />)原生触发时机是文本框被改变并失去焦点。
14 | 工程化
CRA (Create React Application) 包括
- 基于 Webpack 的开发服务器和生产环境构建;
- 用 Babel 做代码转译 ( Transpile );
- 基于 Jest 和 @testing-library/react 的自动化测试框架;
- ESLint 代码静态检查;
- 以 PostCSS 为首的 CSS 后处理器。
倾向性:工具或者框架具有倾向性,意味着它对你的使用场景做了假设和限定,为你提供了它认为是最有效或是最佳实践的默认配置。
可以使用 npm init @eslint/config -y
初始化 eslint
的配置。
15 | 不可变数据
React.memo 有另一个签名:const MyPureComponent = React.memo(MyComponent, compare)
,compare
可以自定义比较函数。compare
含有 oldProps
, newProps
16 | 应用状态管理
Redux 使用了 useSyncExternalStore
,参阅 React 18 useSyncExternalStore API 可知。
useSyncExternalStore
主要是解决撕裂(tearing)的问题
如果使用了类似于 startTransition
之类的调用,或者使用了外部的 store,由于并发渲染,可能会导致 React 18 渲染结果撕裂。
18 | 数据类型:活用 Typescript
type 与 interface 不同:
- type 可以作为联合 Union 类型的别名,但 interface 不可以;
type Pet = Cat | Dog; // 可以
interface IPet extends Cat | Dog {} // 不可以
- interface 可以重复声明 (Redeclaration),但 type 不可以;
interface ICat {
age: number;
}
interface ICat {
color: string;
} // 可以,会合并
const cat: ICat = { age: 4, color: 'silver shaded' };
type Cat = { age: number } ;
type Cat = { color: string }; // 不可以,会报错
越是希望组建封闭,越倾向于用 type,越是认为组件开放灵活,越倾向于 interface。开源组件库中用 interface 声明 props 的较多。就宋一玮老师而言,没有什么特别想法的时候,会使用 type。
19 | 代码复用:设计 Hooks 和组件
组件存在的问题。
- 承担了过多的职责
- 业务逻辑和交互逻辑揉杂
- 从其他组件中复制粘贴代码
具体表现为:
- 传递的 props 个数过多;
- 使用 useState 的个数过多;
- 单个 useEffect 的副作用毁掉函数行数过多。
抽象不仅是为了复用代码,更是为了开发出更有效,更易读,更好维护的代码
组件的抽象应以能被其他组件/页面组合为目的。
20 | 大型项目
node.js 提供了 subpath import
// ./node_modules/es-module-package/package.json
{
"exports": {
"./features/*.js": "./src/features/*.js"
},
"imports": {
"#internal/*.js": "./src/internal/*.js"
}
}
import internalZ from '#internal/z.js';
// Loads ./node_modules/es-module-package/src/internal/z.js
22 | 质量保证
目前比较流行的 E2E 工具有 Cypress, Selenium, Playwright.
- Cypress 是在 Electron 基础上运行的一个高度自定义的浏览器环境,在这个环境中加入了自动化测试的各种功能和 API。
- Selenium 则是基于各个浏览器各自的 WebDriver
- Playwright 基于 CDP 协议 (Chrome DevTools Protocal),标准较新,运行效率也更高一些。
测试金字塔之的是 E2E 测试 (10%), 整合测试 (20%), 单元测试 (70%)。
加餐 | 真自组件和 react/jsx-runtime
为了降低 props 的复杂度,使用 真子组件 或者 DSL,或者组件组合。
<Dialog>
<Dialog.Title>标题<Dialog.Title>
<Dialog.Content>内容<Dialog.Content>
<Dialog.Action type="confirm">确定<Dialog.Action>
<Dialog.Action type="cancel">取消<Dialog.Action>
</Dialog>
React 17 / 18 中的 react/jsx-runtime
React 17 开始使用全新的 JSX 运行时来替换 React.createElement
。在启用新的 JSX 运行时的状态下,用代码编译器编译 JSX:
- 在生产模式下被编译成了 react/jsx-runtime 下的 jsx 或 jsxs (目前同 jsx)
- 在开发模式下 jsx 被编译成了 react/jsx-dev-runtime 下的 jsxDEV
jsx-runtime 和 React.createElement 函数,他们返回的也同样是 React 元素。如果开发者手工调用 React 元素,依旧应该调用 React.createElement
。此 API 并不会移除。而 jsx-runtime 代码只应由编译器生成,开发者不应直接调用。
React 17 的 JSX 较 React.createElement 相比的变化包括:
- 自动导入;
- 在 props 之外传递 key 属性;
- 将 children 直接作为 props 的一部分;
- 分离生产模式和开发模式的 JSX 运行时。
加餐02 | Fiber 协调引擎
Fiber 协调(Reconciliation)引擎主要的工作包括并不限于:
- 创建各类 FiberNode 并组建 Fiber 树;
- 调度并执行各类工作 (Work),如渲染函数组件,挂载或是更新 Hooks,实例化或更新类组件等;
- 对比新旧 Fiber,触发 DOM 变更;
- 获取 context 数据;
- 错误处理;
- 性能监控。
type Fiber = {
// ---- Fiber类型 ----
/** 工作类型,枚举值包括:函数组件、类组件、HTML元素、Fragment等 */
tag: WorkTag,
/** 就是那个子元素列表用的key属性 */
key: null | string,
/** 对应React元素ReactElmement.type属性 */
elementType: any,
/** 函数组件对应的函数或类组件对应的类 */
type: any,
// ---- Fiber Tree树形结构 ----
/** 指向父FiberNode的指针 */
return: Fiber | null,
/** 指向子FiberNode的指针 */
child: Fiber | null,
/** 指向平级FiberNode的指针 */
sibling: Fiber | null,
// ---- Fiber数据 ----
/** 经本次渲染更新的props值 */
pendingProps: any,
/** 上一次渲染的props值 */
memoizedProps: any,
/** 上一次渲染的state值,或是本次更新中的state值 */
memoizedState: any,
/** 各种state更新、回调、副作用回调和DOM更新的队列 */
updateQueue: mixed,
/** 为类组件保存对实例对象的引用,或为HTML元素保存对真实DOM的引用 */
stateNode: any,
// ---- Effect副作用 ----
/** 副作用种类的位域,可同时标记多种副作用,如Placement、Update、Callback等 */
flags: Flags,
/** 指向下一个具有副作用的Fiber的引用,在React 18中貌似已被弃用 */
nextEffect: Fiber | null,
// ---- 异步性/并发性 ----
/** 当前Fiber与成对的进行中Fiber的双向引用 */
alternate: Fiber | null,
/** 标记Lane车道模型中车道的位域,表示调度的优先级 */
lanes: Lanes
};
workLoop
可以随时停,通过 shouldYield()
标记决定是否暂停工作,释放计算资源给更紧急的任务。
提交阶段分为3个县后同步执行的子阶段:
- 变更前(Before Mutation)子阶段。调用
getSnapshotBeforeUpdate
方法。 - 变更(Mutation)子阶段。此阶段更新真实 DOM 树。
- 递归提交与删除相关的副作用,包括移除 ref、移除真实 DOM、执行类组件的
componentWillUnmount
。 - 递归提交添加、重排真实 DOM 等副作用。
- 依次执行 FiberNode 上的 useLayoutEffect 的清除函数。
- 引擎用 FinishedWok 树替换 Current 树,供下次渲染阶段使用。
- 递归提交与删除相关的副作用,包括移除 ref、移除真实 DOM、执行类组件的
- 布局(Layout)阶段,这个子阶段真实 DOM 树已经完成了变更,会调用
useLayoutEffect
和componentDidMount
。
提交阶段会多轮执行 flushPassiveEffects()
- 第一轮,执行 updateQueue 里面的清除函数(如果有)。
- 第二轮,执行 updateQueue 里面的
useEffect
定义的副作用。
直播加餐1 | 前端为什么要工程化
软件开发生命周期
工程化,简单来说就是能让软件工程做成,让它做的快,做得好,再让做的过程可以被预期,可以被管理。最后质量可以被保证,让客户满意。
打包
- Chrome 对于单域下,只能并行 6 个 request。如果多了,需要排队
直播加餐2 | Freewheel 前端工程化的演进
- 早先使用 Browserify 进行构建,如今此工具已经退出了历史舞台
- FDD (Frontend Delivery Decoupling) 保证每个业务模块都会有它自己的构建,单独构建,单独上线。
- 使用了 Conventional commits,git commit message 里,加了 Breaking Change,就会自动发布 NPM 包,提升版本。
- Vite 较于 Webpack 有更高的性能提升。
结束语 | 对 React 和前端技术未来展望
学习一门技术,务必有大于一门技术的收获。
软件开发的从业者要具有终身学习的能力和决心。 不论是追逐理想还是直面功利。