美文网首页前端技术
现代 React Web 开发实践 札记

现代 React Web 开发实践 札记

作者: 吴摩西 | 来源:发表于2022-11-03 09:44 被阅读0次

写在前面

这是一个收费课程的学习札记,课程在极客时间。是宋一玮老师的课程。笔者并非时初学者,所记多有残断,如要系统的学习,还请参加宋一玮老师的课程:https://time.geekbang.org/column/intro/100119601?tab=catalog

01 | React 如何学习以及前端在做什么

  1. 学习一个新框架,学习它的基本概念,上手调用 API 写代码,属于技术表面。理清概念联系,理解 API 内部的原理,则属于技术底层。如果学习实践只停留在表层,那一定会遇到瓶颈。

React 新版本 + 函数组件 + Hooks 优先 + 团队协作 = 高效进阶

  1. React 开发者不仅是 React 框架本身,还包括对前端整体框架和 React 技术边界的把握。对新老前端技术差异的理解,与历史遗留代码的整合,与多个前端开发团队的协作,等等。

  2. 应对前端技术迭代过快

    1. 前端技术不只是技术,应该全面了解,有效总结,将其变为自己的知识点。
    2. 掌握技术的广度和深度一样重要。
  3. GUI 设计的一般原则

    1. 可用性:自解释,无门槛
    2. 一致性:除非有真正出众的替代方案,否则还是遵循标准
    3. 遵循用户心智模型,避免实现模型。即颜色的输入最好是取色器,而不是 RGB 录入。
    4. 最小惊讶原则:用户也是系统的一部分,设计应该符合用户的经验,预期和心智。设计时应该面向一般用户,而不是面向对系统有深入理解的用户。
    5. 及时反馈,用户点击了提交按钮就要告诉他是否成功,需要时间处理,就告知用户正在处理。任何时候都要防止页面冻结无法交互。
  4. 前端开发的领域知识。

    1. 例如按钮图标。
    2. Web 浏览器,了解浏览器能做什么,不能做什么。不能轻易想当然。
  5. 前端领域的变与不变

    1. 模版,从 JSP 就已经存在的,通过模版生产 HTML。
    2. 模版的条件和循环,JSTL 和 vue 都支持类似的语法。实际还还有其他的语法支持,例如 handlebar。还有一些 ASP, C#, ejs 等的语法支持。
    3. 代码分层,Java 端有 Sevlet + Java Bean + JSP 的 MVC 实现。我还记得有 POJO 代替 Java Bean 的实现。Angular 有 MVVM 的实现。
    4. 软件分发,JSP 需要编译成 .war 包,部署到 Tomcat 内提供服务。虽然目标和实际渲染过程不同,但 React,Vue.js,Angular 一般而言也需要进行构建,现在比较流行的是 Webpack, Vite 等工具。更早之前,流行的是 jQuery + YUI Compressor.
    5. 项目依赖管理,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 | 组件生命周期

  1. 值得注意的是,更新之前,会执行一轮 Effect 的清除函数。
  2. 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)的问题

image.png

如果使用了类似于 startTransition 之类的调用,或者使用了外部的 store,由于并发渲染,可能会导致 React 18 渲染结果撕裂。

18 | 数据类型:活用 Typescript

type 与 interface 不同:

  1. type 可以作为联合 Union 类型的别名,但 interface 不可以;
type Pet = Cat | Dog; // 可以
interface IPet extends Cat | Dog {} // 不可以
  1. 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 树,供下次渲染阶段使用。
  • 布局(Layout)阶段,这个子阶段真实 DOM 树已经完成了变更,会调用 useLayoutEffectcomponentDidMount

提交阶段会多轮执行 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 和前端技术未来展望

学习一门技术,务必有大于一门技术的收获。
软件开发的从业者要具有终身学习的能力和决心。 不论是追逐理想还是直面功利。

相关文章

网友评论

    本文标题:现代 React Web 开发实践 札记

    本文链接:https://www.haomeiwen.com/subject/sgrezrtx.html