002 《React Web 开发权威指南》


作者LouXiao, gemini创建时间2025-04-08 19:06:19更新时间2025-04-08 19:06:19

备注:由Gemini 2.0 Flash Thinking 创作的内容,用来辅助学习。

书籍大纲

▮▮▮▮ chapter 1: 前言
▮▮▮▮▮▮▮ 1.1 本书的目标读者
▮▮▮▮▮▮▮ 1.2 为什么选择 React?
▮▮▮▮▮▮▮ 1.3 如何使用本书
▮▮▮▮▮▮▮ 1.4 预备知识
▮▮▮▮ chapter 2: 起步准备:React 开发环境搭建
▮▮▮▮▮▮▮ 2.1 Node.js 和 npm(或 yarn / pnpm)的安装与配置
▮▮▮▮▮▮▮ 2.2 代码编辑器选择与推荐(VSCode,Sublime Text,WebStorm)
▮▮▮▮▮▮▮ 2.3 Create React App (CRA) 快速搭建 React 项目
▮▮▮▮▮▮▮ 2.4 Vite 构建工具介绍与使用
▮▮▮▮ chapter 3: React 核心概念:组件、JSX 与 Props
▮▮▮▮▮▮▮ 3.1 组件 (Component) 的本质:UI 构建的基石
▮▮▮▮▮▮▮ 3.2 JSX 语法详解:在 JavaScript 中编写 HTML
▮▮▮▮▮▮▮ 3.3 Props (属性):组件间数据传递的桥梁
▮▮▮▮ chapter 4: State(状态)与事件处理
▮▮▮▮▮▮▮ 4.1 State 的概念与作用:管理组件内部数据
▮▮▮▮▮▮▮ 4.2 useState Hook:函数式组件的状态管理
▮▮▮▮▮▮▮ 4.3 事件处理:响应用户交互
▮▮▮▮ chapter 5: 深入理解 Hooks
▮▮▮▮▮▮▮ 5.1 useEffect Hook:处理副作用操作
▮▮▮▮▮▮▮ 5.2 useContext Hook:跨组件共享状态
▮▮▮▮▮▮▮ 5.3 useRef Hook:访问 DOM 元素与存储变量
▮▮▮▮▮▮▮ 5.4 自定义 Hook (Custom Hook):逻辑复用的利器
▮▮▮▮ chapter 6: 条件渲染与列表渲染
▮▮▮▮▮▮▮ 6.1 条件渲染:根据条件展示不同内容
▮▮▮▮▮▮▮ 6.2 列表渲染:高效渲染动态列表
▮▮▮▮▮▮▮ 6.3 Keys 的重要性:优化列表渲染性能
▮▮▮▮ chapter 7: 表单 (Form) 处理
▮▮▮▮▮▮▮ 7.1 受控组件与非受控组件
▮▮▮▮▮▮▮ 7.2 表单验证与提交
▮▮▮▮ chapter 8: 路由 (Routing) 管理
▮▮▮▮▮▮▮ 8.1 React Router 介绍与安装
▮▮▮▮▮▮▮ 8.2 配置路由:定义页面路径与组件的映射
▮▮▮▮▮▮▮ 8.3 导航与路由参数
▮▮▮▮ chapter 9: 状态管理方案
▮▮▮▮▮▮▮ 9.1 Context API 深入:复杂状态管理的替代方案
▮▮▮▮▮▮▮ 9.2 Redux 核心概念:Store, Reducer, Action
▮▮▮▮▮▮▮ 9.3 Redux Toolkit:简化 Redux 开发
▮▮▮▮▮▮▮ 9.4 Zustand / Recoil 等轻量级状态管理库
▮▮▮▮ chapter 10: 样式 (Styling) 处理
▮▮▮▮▮▮▮ 10.1 行内样式 (Inline Styles) 与 CSS Modules
▮▮▮▮▮▮▮ 10.2 Styled Components:CSS-in-JS 的方案
▮▮▮▮▮▮▮ 10.3 Tailwind CSS:原子化 CSS 框架
▮▮▮▮▮▮▮ 10.4 CSS 预处理器 (Sass/Less)
▮▮▮▮ chapter 11: React 组件进阶
▮▮▮▮▮▮▮ 11.1 组件组合与复用模式
▮▮▮▮▮▮▮ 11.2 高阶组件 (HOC)
▮▮▮▮▮▮▮ 11.3 Render Props
▮▮▮▮▮▮▮ 11.4 Forwarding Refs
▮▮▮▮▮▮▮ 11.5 Portals
▮▮▮▮ chapter 12: 性能优化
▮▮▮▮▮▮▮ 12.1 代码分割 (Code Splitting)
▮▮▮▮▮▮▮ 12.2 懒加载 (Lazy Loading) 组件与资源
▮▮▮▮▮▮▮ 12.3 Memoization:useMemo, useCallback, React.memo
▮▮▮▮▮▮▮ 12.4 虚拟化 (Virtualization) 列表
▮▮▮▮ chapter 13: 服务端渲染 (SSR) 与 Next.js
▮▮▮▮▮▮▮ 13.1 服务端渲染的优势与应用场景
▮▮▮▮▮▮▮ 13.2 Next.js 快速上手:创建 SSR 应用
▮▮▮▮▮▮▮ 13.3 Next.js 核心特性:页面路由、数据获取
▮▮▮▮▮▮▮ 13.4 部署 Next.js 应用
▮▮▮▮ chapter 14: React 测试 (Testing)
▮▮▮▮▮▮▮ 14.1 单元测试 (Unit Test):Jest 与 React Testing Library
▮▮▮▮▮▮▮ 14.2 集成测试 (Integration Test)
▮▮▮▮▮▮▮ 14.3 端到端测试 (E2E Test):Cypress
▮▮▮▮ chapter 15: TypeScript 与 React
▮▮▮▮▮▮▮ 15.1 TypeScript 基础回顾:类型系统入门
▮▮▮▮▮▮▮ 15.2 在 React 项目中配置 TypeScript
▮▮▮▮▮▮▮ 15.3 TypeScript 与 React Hooks 的结合
▮▮▮▮▮▮▮ 15.4 使用 TypeScript 提升组件的可维护性
▮▮▮▮ chapter 16: React 性能监控与调试
▮▮▮▮▮▮▮ 16.1 React DevTools:开发者工具详解
▮▮▮▮▮▮▮ 16.2 性能分析工具:Profiler 的使用
▮▮▮▮▮▮▮ 16.3 错误监控与日志记录
▮▮▮▮ chapter 17: React 最佳实践
▮▮▮▮▮▮▮ 17.1 组件设计原则:单一职责与关注点分离
▮▮▮▮▮▮▮ 17.2 代码组织与项目结构
▮▮▮▮▮▮▮ 17.3 代码风格指南 (ESLint, Prettier)
▮▮▮▮ chapter 18: 无障碍 (Accessibility) 与 React
▮▮▮▮▮▮▮ 18.1 Web 无障碍的重要性与标准 (WCAG)
▮▮▮▮▮▮▮ 18.2 React 中的无障碍实践:ARIA 属性、语义化 HTML
▮▮▮▮▮▮▮ 18.3 无障碍测试工具
▮▮▮▮ chapter 19: 国际化 (i18n) 与本地化 (l10n)
▮▮▮▮▮▮▮ 19.1 国际化的概念与流程
▮▮▮▮▮▮▮ 19.2 React 国际化方案:react-intl, i18next
▮▮▮▮ chapter 20: 安全 (Security) 最佳实践
▮▮▮▮▮▮▮ 20.1 常见的 Web 安全漏洞 (XSS, CSRF)
▮▮▮▮▮▮▮ 20.2 React 应用安全防范
▮▮▮▮ chapter 21: React 生态系统与未来展望
▮▮▮▮▮▮▮ 21.1 React 社区与资源
▮▮▮▮▮▮▮ 21.2 React 前沿技术趋势:Server Components, 渐进式增强
▮▮▮▮ chapter 22: 实战项目案例分析
▮▮▮▮▮▮▮ 22.1 案例一:电商网站前端开发
▮▮▮▮▮▮▮ 22.2 案例二:仪表盘应用开发
▮▮▮▮ chapter 23: 总结与进阶学习
▮▮▮▮▮▮▮ 23.1 React 学习路线图
▮▮▮▮▮▮▮ 23.2 持续学习资源推荐

1. chapter 1: 前言

在浩瀚的前端技术海洋中,React 以其卓越的组件化思想、高效的性能和强大的生态系统,成为了构建现代 Web 应用的基石。无论你是初涉前端开发的 विद्यार्थी (xuéshēng, student),还是经验丰富的工程师,掌握 React 都是提升技能、应对挑战的关键一步。《React Web 开发权威指南》旨在成为你学习 React 的灯塔,照亮前行的道路,助你从入门到精通,最终成为 React 开发领域的专家。

1.1 本书的目标读者

本书 ориентирован (duìxiàng, target) 于所有对 React Web 开发感兴趣的读者,无论你的技术背景如何:

前端初学者:如果你是刚刚踏入前端领域的新手,本书将从零开始,系统地介绍 React 的基础概念和核心技术。通过清晰的解释、丰富的示例和实践项目,你将快速入门 React,并构建你的第一个 React 应用。
中级前端工程师:如果你已经具备一定的前端开发经验,希望深入学习 React,提升技能水平,本书将帮助你系统地梳理 React 的知识体系,掌握高级特性和最佳实践,解决实际开发中遇到的复杂问题。
高级前端工程师:如果你是经验丰富的 React 专家,本书亦能为你提供有价值的参考。书中深入探讨了 React 的高级主题、性能优化、架构设计以及最新的技术趋势,助你保持技术领先,应对更具挑战性的项目。

无论你是哪种类型的读者,本书都将根据你的需求,提供量身定制的学习路径和深入的知识解析。

1.2 为什么选择 React?

在众多的前端框架和库中,React 凭借其独特的优势,成为了众多开发者的首选。选择 React,你将获得:

组件化 (Component-Based) 开发模式:React 提倡将 UI 拆分成独立、可复用的组件。这种组件化的思想极大地提高了代码的可维护性、可测试性和复用性,降低了项目的复杂度,提升了开发效率。
声明式 (Declarative) 编程范式:React 采用声明式编程范式,你只需要描述 UI 的状态,React 负责高效地更新和渲染视图。这种方式使得代码更加简洁易懂,降低了心智负担,提升了开发效率。
高效的 Virtual DOM (虚拟文档对象模型):React 使用 Virtual DOM 技术,通过 diff 算法高效地更新页面,最大限度地减少了对真实 DOM 的操作,从而显著提升了应用的性能。
强大的生态系统 (Ecosystem):React 拥有庞大而活跃的社区,提供了丰富的第三方库和工具,涵盖了状态管理、路由、测试、UI 组件等各个方面,极大地丰富了 React 的功能,加速了开发进程。例如:
▮▮▮▮⚝ 状态管理:Redux, Zustand, Recoil, Context API 等
▮▮▮▮⚝ 路由管理:React Router, Reach Router
▮▮▮▮⚝ UI 组件库:Ant Design, Material UI, Chakra UI
▮▮▮▮⚝ 测试框架:Jest, React Testing Library, Cypress
广泛的应用场景 (Application Scenarios):无论是构建复杂的单页应用 (SPA),还是开发移动应用 (React Native),甚至是桌面应用 (Electron),React 都能胜任。众多知名公司和网站,如 Facebook、Instagram、Netflix、Airbnb 等,都采用了 React 技术,足以证明其成熟度和可靠性。
持续的技术创新 (Technological Innovation):React 团队不断进行技术创新,例如 Hooks 的引入、Concurrent Mode 的实验性特性、Server Components 的提出,都 نشان می‌دهد (zhǎnshì, demonstrate) React 持续进化的活力,确保其在前端技术领域保持领先地位。

选择 React,不仅是选择了一个优秀的技术框架,更是选择了一个充满活力和创新精神的生态系统。

1.3 如何使用本书

为了帮助你更好地利用本书进行学习,我们建议你采用以下方法:

系统学习,循序渐进:本书按照由浅入深、循序渐进的逻辑组织内容。建议你按照章节顺序进行学习,先掌握基础知识,再逐步深入高级主题。不要跳跃章节,以免遗漏重要的知识点。
理论结合实践,动手练习:学习编程最有效的方法是实践。本书在讲解理论知识的同时,提供了大量的代码示例和实践项目。请务必跟随书中的示例进行练习,亲手编写代码,加深理解,巩固知识。
关注重点,深入理解:本书重点讲解 React 的核心概念和常用技术。对于重要的知识点,例如组件、JSX、Props、State、Hooks、路由、状态管理等,要深入理解其原理和应用场景。
善用工具,提高效率:学习 React 的过程中,要善用各种开发工具,例如:
▮▮▮▮⚝ 代码编辑器 (Code Editor):VSCode, WebStorm 等,利用其代码提示、自动补全、代码格式化等功能,提高编码效率。
▮▮▮▮⚝ 浏览器开发者工具 (Browser Developer Tools):Chrome DevTools, Firefox Developer Tools 等,用于调试代码、查看元素、监控网络请求等。
▮▮▮▮⚝ React DevTools:React 官方提供的开发者工具扩展,用于查看 React 组件树、Props、State、Hooks 等信息,帮助你更好地理解 React 应用的运行机制。
积极探索,勇于提问:学习过程中遇到问题是正常的。遇到困难时,不要轻易放弃,要积极查阅资料、搜索答案、尝试解决。如果实在无法解决,可以向老师、同学或社区寻求帮助。提问是学习的重要环节,勇敢地提出你的问题,才能更快地进步。
持续学习,保持热情:前端技术发展日新月异,React 也在不断更新和演进。学习 React 不是一蹴而就的事情,需要持续学习、不断实践、保持对新技术的敏感性和热情。关注 React 社区的动态,学习最新的技术和最佳实践,才能在 React 开发领域保持竞争力。

1.4 预备知识

在开始学习本书之前,我们假设你已经具备以下预备知识:

HTML (超文本标记语言): 能够理解 HTML 的基本结构和常用标签,例如 <div><span><p><a><img><ul><ol><li><form><input> 等。
CSS (层叠样式表): 了解 CSS 的基本语法和常用属性,例如选择器、盒模型、布局方式 (Flexbox, Grid)、颜色、字体、背景等,能够使用 CSS 美化网页样式。
JavaScript (JS): 掌握 JavaScript 的基础语法,包括变量、数据类型、运算符、流程控制语句 (if, for, while)、函数、对象、数组等。 了解 ES6+ 的新特性,例如箭头函数、解构赋值、Promise、async/await、模块化等。

如果你对上述预备知识还不太熟悉,建议先学习相关的基础知识,再开始本书的学习。网上有很多优秀的 HTML、CSS 和 JavaScript 教程,可以帮助你快速入门。 掌握扎实的基础知识,将为你的 React 学习打下坚实的基础。

希望本书能成为你学习 React Web 开发的得力助手,祝你学习愉快,收获满满!🚀

REVIEW PASS

2. chapter 2: 起步准备:React 开发环境搭建

工欲善其事,必先利其器。在开始 React Web 开发之旅之前,搭建一个高效、顺畅的开发环境至关重要。本章将引导你一步步完成 React 开发环境的搭建,包括 Node.js 和包管理器的安装配置、代码编辑器的选择、以及两种常用的 React 项目快速搭建方式:Create React App (CRA) 和 Vite。

2.1 Node.js 和 npm(或 yarn / pnpm)的安装与配置

Node.js 是一个基于 Chrome V8 引擎的 JavaScript 运行环境,它使得 JavaScript 可以脱离浏览器在服务器端运行。对于 React 开发而言,Node.js 不仅是运行环境,更是构建工具和包管理器的基础。npm (Node Package Manager) 是 Node.js 默认的包管理器,用于安装、管理项目依赖的第三方库。除了 npm,yarn 和 pnpm 也是流行的包管理器,它们在性能和功能上各有特点。

安装 Node.js

访问 Node.js 官网 https://nodejs.org/,根据你的操作系统 (Operating System) 选择合适的版本进行下载安装。建议下载 LTS (Long-term Support, 长期支持) 版本,因为它更加稳定可靠。安装过程中,请务必勾选 “Add to PATH” 选项,这样可以将 Node.js 和 npm 命令添加到系统环境变量中,方便在命令行 (Command Line) 中直接使用。

安装完成后,打开命令行工具 (终端或命令提示符),输入以下命令检查 Node.js 和 npm 是否安装成功以及版本信息:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 node -v
2 npm -v

如果能正确输出 Node.js 和 npm 的版本号,则说明安装成功。🎉

配置 npm 镜像 (Mirror) (可选)

由于 npm 默认的 registry (仓库) 服务器在国外,国内用户访问速度可能较慢。为了提高包下载速度,可以配置 npm 镜像到国内的镜像站点,例如淘宝 npm 镜像 (taobao npm registry)。

在命令行中执行以下命令配置 npm 镜像:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 npm config set registry https://registry.npmmirror.com

配置完成后,可以使用以下命令验证是否配置成功:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 npm config get registry

如果输出 https://registry.npmmirror.com,则说明镜像配置成功。🚀

yarn 和 pnpm 的安装与使用 (可选)

除了 npm,yarn 和 pnpm 也是优秀的包管理器,它们在包安装速度、磁盘空间利用率等方面有所优势。你可以根据个人喜好选择使用。

  • yarn 的安装

如果已经安装了 npm,可以使用 npm 全局安装 yarn:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 npm install -g yarn

安装完成后,使用以下命令检查 yarn 版本:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 yarn -v
  • pnpm 的安装

同样可以使用 npm 全局安装 pnpm:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 npm install -g pnpm

安装完成后,使用以下命令检查 pnpm 版本:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 pnpm -v

yarn 和 pnpm 的基本命令与 npm 类似,例如 yarn add <package-name>pnpm add <package-name> 用于安装依赖包。在后续的项目创建和依赖管理中,你可以选择 npm、yarn 或 pnpm 其中之一进行使用。本书后续示例默认使用 npm,如果你选择 yarn 或 pnpm,只需将相应的 npm 命令替换为 yarn 或 pnpm 命令即可。

2.2 代码编辑器选择与推荐(VSCode,Sublime Text,WebStorm)

代码编辑器是开发者编写代码的主要工具。一个优秀的编辑器可以极大地提高开发效率和编码体验。对于 React 开发,以下几款编辑器都是不错的选择:

VSCode (Visual Studio Code) 🌟

VSCode 是 Microsoft 开发的一款免费、开源、跨平台的代码编辑器。它拥有强大的功能、丰富的扩展插件 (Extensions) 和良好的用户体验,是目前最流行的前端开发编辑器之一。

  • 优点
    ▮▮▮▮⚝ 免费开源:完全免费使用,并且开源,社区活跃。
    ▮▮▮▮⚝ 强大的功能:内置代码提示 (IntelliSense)、自动补全、代码调试、Git 集成等功能。
    ▮▮▮▮⚝ 丰富的扩展插件:拥有庞大的扩展插件市场,可以安装各种插件来增强功能,例如 React 代码片段 (React Snippets)、ESLint 代码检查、Prettier 代码格式化等。
    ▮▮▮▮⚝ 良好的 React 支持:对 React、JSX、TypeScript 等技术栈支持良好。
    ▮▮▮▮⚝ 跨平台:支持 Windows、macOS 和 Linux 操作系统。

  • 推荐插件
    ▮▮▮▮⚝ ESLint:JavaScript 代码质量检查工具。
    ▮▮▮▮⚝ Prettier:代码格式化工具。
    ▮▮▮▮⚝ Reactjs code snippetsES7+ React/Redux/React-Native snippets:React 代码片段,快速生成 React 代码。
    ▮▮▮▮⚝ Bracket Pair Colorizer:括号颜色高亮,提高代码可读性。
    ▮▮▮▮⚝ Auto Rename Tag:自动重命名 HTML/JSX 标签。

VSCode 官方网站:https://code.visualstudio.com/

WebStorm 💎

WebStorm 是 JetBrains 公司开发的一款强大的 JavaScript IDE (Integrated Development Environment, 集成开发环境)。它是收费软件,但功能非常全面,尤其在大型项目和团队协作方面表现出色。

  • 优点
    ▮▮▮▮⚝ 功能强大:集成了代码编辑、代码调试、单元测试、版本控制、构建工具等功能,开箱即用。
    ▮▮▮▮⚝ 智能代码提示:代码提示和自动补全非常智能,能够深度理解 JavaScript 和 React 代码。
    ▮▮▮▮⚝ 优秀的 React 支持:对 React 生态系统支持非常完善,例如 JSX 语法高亮、组件跳转、代码重构等。
    ▮▮▮▮⚝ 强大的调试功能:提供强大的 JavaScript 和 Node.js 调试功能。

  • 缺点
    ▮▮▮▮⚝ 收费软件:需要购买许可证才能使用,但提供 30 天免费试用。
    ▮▮▮▮⚝ 资源占用较高:相比 VSCode,WebStorm 资源占用稍高。

WebStorm 官方网站:https://www.jetbrains.com/webstorm/

Sublime Text 📝

Sublime Text 是一款轻量级、快速、可高度定制的代码编辑器。它也是收费软件,但可以免费试用,只是会偶尔弹出购买提示。

  • 优点
    ▮▮▮▮⚝ 轻量快速:启动速度快,运行流畅,资源占用低。
    ▮▮▮▮⚝ 高度可定制:可以通过安装插件和修改配置文件来高度定制编辑器的功能和外观。
    ▮▮▮▮⚝ 强大的快捷键:支持丰富的快捷键操作,提高编码效率。
    ▮▮▮▮⚝ 跨平台:支持 Windows、macOS 和 Linux 操作系统。

  • 缺点
    ▮▮▮▮⚝ 插件生态相对较小:相比 VSCode,Sublime Text 的插件生态相对较小。
    ▮▮▮▮⚝ 收费软件:虽然可以免费试用,但会弹出购买提示。

Sublime Text 官方网站:https://www.sublimetext.com/

选择建议

  • 初学者:推荐使用 VSCode,免费、易用、插件丰富,社区支持强大,非常适合入门学习。
  • 中高级工程师:可以根据个人需求选择 VSCodeWebStorm。如果注重功能全面性和智能代码提示,可以选择 WebStorm;如果喜欢轻量快速和高度定制,可以选择 VSCode 并安装必要的插件。
  • Sublime Text 适合喜欢轻量级编辑器,并对定制性有较高要求的开发者。

无论选择哪款编辑器,熟悉其基本操作和常用快捷键,并根据 React 开发需求安装必要的插件,都能有效提升开发效率。

2.3 Create React App (CRA) 快速搭建 React 项目

Create React App (CRA) 是 Facebook 官方提供的零配置 React 项目脚手架工具。它可以帮助开发者快速搭建一个现代化的 React 开发环境,无需手动配置 webpack、Babel 等构建工具。CRA 预配置了合理的默认设置,使得初学者可以专注于 React 代码的编写,而不用花费过多精力在环境配置上。

安装 Create React App

如果还没有全局安装 CRA,可以使用 npm 安装:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 npm install -g create-react-app

安装完成后,可以使用以下命令检查 CRA 版本:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 create-react-app --version

创建 React 项目

在命令行中,切换到你想要创建项目的目录,然后执行以下命令创建新的 React 项目:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 create-react-app my-app

其中 my-app 是你的项目名称,你可以根据需要自定义项目名称。CRA 会自动创建一个名为 my-app 的文件夹,并在其中初始化一个新的 React 项目。这个过程可能需要几分钟,请耐心等待。⏳

启动开发服务器

项目创建完成后,进入项目目录:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 cd my-app

然后启动开发服务器:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 npm start

CRA 会自动启动一个本地开发服务器,并在浏览器中打开你的 React 应用。默认情况下,应用会运行在 http://localhost:3000。 🎉 修改 src 目录下的代码,浏览器会自动刷新,实时预览修改效果。

CRA 项目结构

CRA 创建的项目具有清晰的目录结构:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 my-app/
2 ├── node_modules/ # 项目依赖包
3 ├── public/ # 静态资源文件 (例如 index.html, favicon.ico)
4 ├── src/ # 源代码目录
5 │ ├── App.js # 根组件
6 │ ├── index.js # 入口文件
7 │ ├── index.css # 全局样式
8 │ ├── logo.svg # React Logo
9 │ ├── ...
10 ├── package.json # 项目配置文件,包含依赖信息、脚本命令等
11 ├── package-lock.json # 锁定依赖版本,确保项目依赖一致性
12 ├── README.md # 项目 README 文件
13 ├── .gitignore # Git 忽略文件配置
14 └── ...

src 目录是开发的主要目录,你将在其中编写 React 组件、样式和逻辑代码。public 目录存放静态资源文件,例如 HTML 入口文件 index.htmlpackage.json 文件是项目配置文件,包含了项目名称、版本、依赖包信息、脚本命令等。

CRA 提供了一套开箱即用的 React 开发环境,非常适合初学者快速上手 React 开发。

2.4 Vite 构建工具介绍与使用

Vite (法语意为 "快速的") 是一个由 Evan You (Vue.js 作者) 开发的新一代前端构建工具。相比传统的 webpack 构建工具,Vite 具有极速冷启动即时热模块替换 (HMR)闪电般快速的打包速度 等优势。在 React 开发中,Vite 也是一个非常优秀的构建工具选择。

使用 Vite 创建 React 项目

Vite 官方提供了创建各种框架项目 (包括 React) 的脚手架。在命令行中执行以下命令,选择 React 模板创建项目:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 npm create vite@latest my-vite-app

根据提示,选择 ReactJavaScript (或 TypeScript) 模板。Vite 会自动创建一个名为 my-vite-app 的文件夹,并在其中初始化一个新的 Vite React 项目。

安装依赖并启动开发服务器

进入项目目录:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 cd my-vite-app

安装项目依赖:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 npm install

启动开发服务器:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 npm run dev

Vite 会启动一个本地开发服务器,并在浏览器中打开你的 React 应用。默认情况下,应用会运行在 http://localhost:5173。 Vite 的冷启动速度非常快,几乎是瞬间完成。修改代码后,HMR 也能实现即时更新,无需刷新整个页面。⚡️

Vite 项目结构

Vite 创建的 React 项目结构与 CRA 类似,但也有一些区别:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 my-vite-app/
2 ├── node_modules/
3 ├── public/
4 ├── src/
5 │ ├── App.jsx # 根组件 (注意文件扩展名是 .jsx 或 .tsx)
6 │ ├── main.jsx # 入口文件 (注意文件扩展名是 .jsx 或 .tsx)
7 │ ├── index.css
8 │ ├── assets/ # 静态资源目录
9 │ ├── ...
10 ├── index.html # HTML 入口文件 (在项目根目录下)
11 ├── package.json
12 ├── package-lock.json
13 ├── vite.config.js # Vite 配置文件
14 ├── README.md
15 └── ...

与 CRA 不同的是,Vite 的 HTML 入口文件 index.html 位于项目根目录下,而不是 public 目录。入口文件 main.jsx (或 main.tsx) 的文件扩展名是 .jsx.tsx,而不是 .js。Vite 项目根目录下还有一个 vite.config.js 文件,用于配置 Vite 的构建行为。

Vite 以其快速的开发体验和优异的性能,成为了新一代前端项目的构建工具首选。对于 React 项目,Vite 也是一个值得尝试的优秀选择。

本章总结

本章介绍了 React 开发环境的搭建过程,包括 Node.js 和包管理器的安装配置、代码编辑器的选择推荐、以及使用 CRA 和 Vite 快速创建 React 项目的方法。完成本章的学习,你已经拥有了一个基本的 React 开发环境,可以开始你的 React Web 开发之旅了。下一章,我们将深入 React 的核心概念:组件、JSX 和 Props。🚀

REVIEW PASS

3. chapter 3: React 核心概念:组件、JSX 与 Props

React 的核心理念在于组件化 (Componentization) 开发。组件是构建用户界面的独立、可复用的代码块。理解组件、JSX 语法以及 Props 属性,是掌握 React 开发的基石。本章将深入剖析这三个核心概念,为你后续的 React 学习打下坚实的基础。

3.1 组件 (Component) 的本质:UI 构建的基石

组件是 React 应用的构建模块,它将 UI 界面拆分成独立、可管理和可复用的部分。可以将组件视为一个个独立的乐高积木,通过组合这些积木,可以搭建出复杂的应用程序。

组件的概念和重要性

在传统的 Web 开发模式中,UI 往往由 HTML、CSS 和 JavaScript 代码混合而成,代码耦合度高,难以维护和复用。React 组件化的思想,将 UI 和逻辑封装在独立的组件中,实现了关注点分离 (Separation of Concerns),提高了代码的可维护性、可测试性和复用性。

组件的优点:

可复用性 (Reusability):组件可以被多次使用在不同的场景中,减少了重复代码的编写,提高了开发效率。例如,一个按钮组件可以在应用的多个页面和模块中使用。
可维护性 (Maintainability):组件的独立性使得代码结构更加清晰,易于理解和维护。当需要修改 UI 或逻辑时,只需修改相应的组件,而不会影响到其他部分。
可测试性 (Testability):组件的独立性使得单元测试更加容易。可以针对单个组件进行测试,确保其功能的正确性。
提高开发效率 (Development Efficiency):组件的复用性减少了重复开发的工作量,提高了开发效率。同时,组件化的开发模式也更符合现代前端工程化的理念。

组件的分类:函数式组件 (Function Components) 和 类组件 (Class Components)

在 React 中,组件主要分为两种类型:函数式组件 (Function Components)类组件 (Class Components)

  • 函数式组件 (Function Components)

函数式组件是使用 JavaScript 函数定义的组件。它是 React 16.8 版本引入 Hooks 之后推荐的组件编写方式。函数式组件更加简洁、易于理解和测试。

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 // 函数式组件示例
2 function Welcome(props) {
3 return <h1>你好, {props.name}</h1>;
4 }

在上面的例子中,Welcome 就是一个函数式组件。它接收一个 props 参数(代表属性),并返回一个 JSX 元素 (JSX element),描述了组件的 UI 结构。

  • 类组件 (Class Components)

类组件是使用 ES6 class 语法定义的组件。在 Hooks 出现之前,类组件是 React 中主要的组件编写方式。类组件功能更强大,可以管理组件自身的状态 (state) 和生命周期 (lifecycle)。

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 // 类组件示例
2 import React from 'react';
3
4 class Welcome extends React.Component {
5 render() {
6 return <h1>你好, {this.props.name}</h1>;
7 }
8 }

在上面的例子中,Welcome 是一个类组件,它继承自 React.Component。类组件必须实现 render() 方法,该方法返回 JSX 元素,描述组件的 UI 结构。通过 this.props 访问组件的属性。

选择建议

在现代 React 开发中,推荐优先使用函数式组件。函数式组件结合 Hooks,可以实现类组件的所有功能,并且代码更加简洁、易于理解和维护。类组件主要在维护旧项目或需要使用某些类组件特有的生命周期方法时使用。本书后续示例将主要使用函数式组件。

组件的生命周期 (Lifecycle) (简要提及)

组件的生命周期是指组件从创建、挂载到页面、更新再到卸载的过程。类组件拥有完整的生命周期方法,可以在组件的不同阶段执行特定的操作,例如在组件挂载后发送网络请求、在组件更新后更新 DOM 等。

函数式组件通过 Hooks (例如 useEffect) 也可以模拟类组件的生命周期行为。关于组件生命周期的详细内容,将在后续章节深入讲解。

3.2 JSX 语法详解:在 JavaScript 中编写 HTML

JSX (JavaScript XML) 是 React 的一个核心特性,它是一种 JavaScript 的语法扩展,允许你在 JavaScript 代码中编写类似 HTML 的结构。JSX 最终会被 Babel 编译成标准的 JavaScript 代码,用于创建 React 元素 (React elements)。

JSX 的概念和优势

JSX 并非必须的,你也可以使用纯 JavaScript 代码来创建 React 元素。但是,JSX 具有以下优势:

声明式 (Declarative):JSX 使得 UI 结构更加直观和易于理解,更符合声明式编程的理念。可以直接描述 UI 的外观,而无需关心底层的 DOM 操作细节。
类似 HTML (HTML-like):JSX 的语法与 HTML 非常相似,前端开发者更容易上手,学习成本低。
提高开发效率 (Development Efficiency):使用 JSX 可以更快速地编写和组织 UI 结构,提高开发效率。
防止注入攻击 (Prevent Injection Attacks):JSX 默认会对值进行转义,可以有效地防止 XSS (Cross-Site Scripting, 跨站脚本攻击) 注入攻击,提高应用的安全性。

JSX 的基本语法规则

  • HTML 标签与组件标签

JSX 中可以使用标准的 HTML 标签 (例如 <div>, <span>, <p>),也可以使用自定义的 React 组件标签 (例如 <Welcome />)。React 会根据标签的首字母大小写来区分 HTML 标签和组件标签。HTML 标签首字母小写,组件标签首字母大写

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 // HTML 标签
2 <div>这是一个 div 元素</div>
3
4 // 组件标签 (假设 Welcome 组件已定义)
5 <Welcome name="张三" />
  • JSX 表达式

在 JSX 中,可以使用花括号 {} 包裹 JavaScript 表达式,在 UI 中动态地插入 JavaScript 值。

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 function formatName(user) {
2 return user.firstName + ' ' + user.lastName;
3 }
4
5 const user = {
6 firstName: '李',
7 lastName: '四'
8 };
9
10 const element = (
11 <h1>
12 你好, {formatName(user)}!
13 </h1>
14 );

在上面的例子中,{formatName(user)} 就是一个 JSX 表达式,它会执行 formatName(user) 函数,并将返回的值插入到 <h1> 标签中。

  • JSX 属性 (Attributes)

HTML 标签和组件标签都可以设置属性,用于传递数据或配置组件的行为。JSX 属性的语法与 HTML 属性类似,但有一些区别:

▮▮▮▮⚝ 驼峰命名法 (CamelCase):对于 HTML 属性中包含连字符 - 的属性 (例如 class, tabindex),在 JSX 中需要使用驼峰命名法 (例如 className, tabIndex)。
▮▮▮▮⚝ 布尔属性 (Boolean Attributes):对于布尔属性 (例如 disabled, required),在 JSX 中可以直接写属性名,表示值为 true。或者显式地设置为 truefalse
▮▮▮▮⚝ 自定义属性 (Custom Attributes):可以为组件设置自定义属性,通过 props 传递给组件。

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 // JSX 属性示例
2 <div className="container" tabIndex="0">
3 <input type="text" disabled />
4 <Welcome name="王五" age={25} />
5 </div>
  • JSX 嵌套 (Nesting)

JSX 元素可以嵌套使用,构建复杂的 UI 结构。

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 const element = (
2 <div>
3 <h1>欢迎来到 React 世界!</h1>
4 <p>开始你的 React 开发之旅吧。</p>
5 </div>
6 );
  • JSX 是表达式 (JSX is an Expression)

JSX 本身也是 JavaScript 表达式,可以赋值给变量、作为函数参数或返回值。

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 function getGreeting(user) {
2 if (user) {
3 return <h1>你好, {formatName(user)}!</h1>;
4 }
5 return <h1>你好, 陌生人。</h1>;
6 }

JSX 的特性:表达式、条件渲染、列表渲染 (简要提及)

JSX 除了基本的语法规则外,还支持一些高级特性,例如:

  • 表达式 (Expressions):如前所述,可以使用花括号 {} 在 JSX 中嵌入 JavaScript 表达式,实现动态内容渲染。
  • 条件渲染 (Conditional Rendering):可以根据条件判断,渲染不同的 JSX 结构。常用的条件渲染方式有:if/else 语句、三元运算符 ?:、逻辑与 && 运算符。
  • 列表渲染 (List Rendering):可以遍历数组,动态渲染列表项。通常使用 map() 方法结合 JSX 表达式来实现列表渲染。

关于条件渲染和列表渲染的详细内容,将在后续章节深入讲解。

3.3 Props (属性):组件间数据传递的桥梁

Props (properties 的缩写) 是 React 中用于组件之间传递数据的机制。Props 允许父组件向子组件传递数据,子组件通过 props 接收和使用父组件传递的数据。Props 是单向数据流 (unidirectional data flow) 的重要组成部分,保证了数据流的可预测性和组件的独立性。

Props 的概念和作用

Props 的作用类似于 HTML 标签的属性,用于配置组件的行为和外观。Props 是从组件外部传递给组件的数据,是组件的输入 (input)。

Props 的特点:

只读 (Read-only):组件自身不能修改 props 的值。Props 由父组件传递,子组件只能读取 props 的值,不能修改。如果要修改组件自身的数据,需要使用 State (状态),将在下一章讲解。
任意类型 (Any Type):Props 可以传递任意类型的数据,包括字符串、数字、布尔值、对象、数组、函数、甚至 JSX 元素。
可选 (Optional):Props 可以是可选的,父组件可以选择是否向子组件传递某些 props。

Props 的传递方式:父组件向子组件传递数据

父组件通过在子组件标签上设置属性的方式,向子组件传递 props。

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 // 父组件 App
2 function App() {
3 const name = "React 小白";
4 return (
5 <div>
6 <Welcome name={name} /> {/* 向 Welcome 组件传递 name prop */}
7 </div>
8 );
9 }
10
11 // 子组件 Welcome
12 function Welcome(props) {
13 return <h1>你好, {props.name}</h1>; {/* 子组件通过 props.name 访问父组件传递的 name prop */}
14 }

在上面的例子中,App 组件是父组件,Welcome 组件是子组件。App 组件在 <Welcome> 标签上设置了 name={name} 属性,将 name 变量的值 "React 小白" 传递给了 Welcome 组件。Welcome 组件通过 props.name 访问父组件传递的 name prop,并在 <h1> 标签中渲染出来。

Props 的类型检查 (PropTypes 或 TypeScript) (简要提及)

为了提高组件的健壮性和可维护性,可以对 props 进行类型检查,确保父组件传递给子组件的 props 符合预期的类型。React 提供了两种 props 类型检查的方式:

  • PropTypes:React 官方提供的运行时 (runtime) 类型检查库。可以使用 PropTypes 定义组件 props 的类型,并在开发环境中进行类型检查。
  • TypeScript:一种静态类型 (static type) 的 JavaScript 超集。可以使用 TypeScript 定义组件 props 的类型接口 (interface),在编译时 (compile-time) 进行类型检查。

关于 PropTypes 和 TypeScript 的详细用法,将在后续章节深入讲解。

Props 的只读性 (Immutability)

Props 的只读性是 React 单向数据流的核心原则之一。子组件不应该修改 props 的值,任何修改都应该由父组件发起。这种单向数据流的设计,使得数据流向更加清晰可控,易于追踪和调试。

如果子组件需要修改自身的数据,应该使用 State (状态) 来管理。State 是组件内部的状态,组件可以控制和修改自身的状态。State 的概念将在下一章详细介绍。

本章总结

本章深入讲解了 React 的三个核心概念:组件、JSX 和 Props。组件是 UI 构建的基本单元,JSX 是一种在 JavaScript 中编写 HTML 的语法扩展,Props 是组件之间传递数据的桥梁。理解这三个概念,是掌握 React 开发的关键一步。下一章,我们将继续学习 React 的另一个核心概念:State (状态) 与事件处理。🚀

REVIEW PASS

4. chapter 4: State(状态)与事件处理

在 React 应用中,构建动态和交互式的用户界面,离不开 State (状态)事件处理 (Event Handling)。State 用于管理组件内部的数据,当 State 发生变化时,React 会自动更新组件的 UI。事件处理则用于响应用户的交互行为,例如点击、输入、鼠标移动等,使得应用能够与用户进行实时的互动。本章将深入探讨 State 的概念、useState Hook 的使用以及 React 中的事件处理机制。

4.1 State 的概念与作用:管理组件内部数据

State 是 React 组件中用于管理动态数据的核心机制。与 Props 不同,State 是组件自身内部的状态,组件可以读取和修改自己的 State,并且当 State 发生变化时,组件会重新渲染,更新 UI 以反映最新的数据状态。

State 的概念

可以将 State 理解为组件的内部数据存储。它存储了组件在不同时刻需要展示和操作的数据。State 是可变的 (Mutable),组件可以通过特定的方法更新 State 的值,从而触发 UI 的更新。

State 与 Props 的区别:

数据来源:Props 由父组件传递给子组件,是外部数据;State 是组件自身内部管理的数据,是内部数据。
可变性:Props 是只读的,子组件不能修改 Props 的值;State 是可变的,组件可以修改自身 State 的值。
用途:Props 主要用于父子组件之间的数据传递和组件配置;State 主要用于管理组件自身的动态数据和 UI 状态。

State 的作用

State 在 React 应用中扮演着至关重要的角色,主要作用包括:

驱动 UI 更新 (UI Updates):当 State 的值发生变化时,React 会自动触发组件的重新渲染,根据新的 State 值更新 UI 界面,实现动态的用户界面。
响应用户交互 (User Interactions):State 可以用于记录和响应用户的交互行为。例如,可以使用 State 来记录按钮的点击次数、输入框中的文本内容、复选框的选中状态等,并根据用户的交互更新 UI。
管理组件内部状态 (Component Internal State):State 用于管理组件自身的内部状态,例如组件的显示/隐藏状态、动画状态、加载状态等。

State 的初始化

在函数式组件中,通常使用 useState Hook 来声明和初始化 State。在类组件中,State 通过构造函数 constructor 初始化。

  • 函数式组件中使用 useState 初始化 State
1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 import React, { useState } from 'react';
2
3 function Counter() {
4 // 声明一个名为 count state 变量初始值为 0
5 const [count, setCount] = useState(0);
6
7 return (
8 <div>
9 <p>当前计数: {count}</p>
10 <button>增加计数</button>
11 </div>
12 );
13 }

在上面的例子中,useState(0) 返回一个包含两个元素的数组:第一个元素 count 是当前的 State 值,第二个元素 setCount 是更新 State 值的函数。初始 State 值设置为 0

  • 类组件中使用 constructor 初始化 State
1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 import React from 'react';
2
3 class Counter extends React.Component {
4 constructor(props) {
5 super(props);
6 // 初始化 state
7 this.state = {
8 count: 0
9 };
10 }
11
12 render() {
13 return (
14 <div>
15 <p>当前计数: {this.state.count}</p>
16 <button>增加计数</button>
17 </div>
18 );
19 }
20 }

在类组件中,State 是通过 this.state 对象进行管理的。在构造函数 constructor 中,通过 this.state = { count: 0 } 初始化 State。

4.2 useState Hook:函数式组件的状态管理

useState 是 React Hooks 中最基础也是最重要的 Hook 之一。它使得函数式组件也能够拥有管理 State 的能力,从而可以使用函数式组件构建复杂的、有状态的交互式 UI。

useState 的基本用法

useState Hook 接收一个初始值 (initial value) 作为参数,返回一个数组,数组包含两个元素:

  • 当前 State 值 (current state value):用于在组件中读取当前的 State 值。
  • 更新 State 值的函数 (state setter function):用于更新 State 的值,并触发组件的重新渲染。
1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 const [stateValue, setStateValue] = useState(initialValue);
  • initialValue:State 的初始值,可以是任何 JavaScript 数据类型,例如数字、字符串、布尔值、对象、数组等。初始值只在组件首次渲染时生效,后续渲染时会被忽略。
  • stateValue:当前的 State 值,在组件的渲染过程中保持不变,直到 setStateValue 函数被调用更新 State。
  • setStateValue:更新 State 值的函数。调用 setStateValue 函数会触发组件的重新渲染,React 会使用新的 State 值重新执行组件函数,并更新 UI。

更新 State 的方式

更新 State 值需要使用 useState 返回的 setState 函数,而不是直接修改 State 变量。直接修改 State 变量不会触发组件的重新渲染,UI 也不会更新。

  • 直接更新 State 值
1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 import React, { useState } from 'react';
2
3 function Counter() {
4 const [count, setCount] = useState(0);
5
6 const handleIncrement = () => {
7 // 使用 setCount 函数更新 state
8 setCount(count + 1);
9 };
10
11 return (
12 <div>
13 <p>当前计数: {count}</p>
14 <button onClick={handleIncrement}>增加计数</button>
15 </div>
16 );
17 }

在上面的例子中,handleIncrement 函数通过调用 setCount(count + 1) 来更新 count State 的值。当点击 "增加计数" 按钮时,handleIncrement 函数会被调用,count State 的值会递增,组件会重新渲染,UI 会显示最新的计数。

  • 使用函数式更新 (Functional Updates)

当新的 State 值依赖于之前的 State 值时,建议使用函数式更新的方式来更新 State。函数式更新接收一个函数作为参数,这个函数接收前一个 State 值 (previous state value) 作为参数,并返回新的 State 值 (new state value)

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 import React, { useState } from 'react';
2
3 function Counter() {
4 const [count, setCount] = useState(0);
5
6 const handleIncrement = () => {
7 // 使用函数式更新prevState 代表前一个 state
8 setCount(prevState => prevState + 1);
9 };
10
11 const handleDecrement = () => {
12 // 使用函数式更新prevState 代表前一个 state
13 setCount(prevState => prevState - 1);
14 };
15
16 return (
17 <div>
18 <p>当前计数: {count}</p>
19 <button onClick={handleIncrement}>增加</button>
20 <button onClick={handleDecrement}>减少</button>
21 </div>
22 );
23 }

使用函数式更新的好处是,可以确保在批量更新 (batch updates) 的情况下,State 的更新是基于最新的 State 值,避免出现 State 值不一致的问题。

State 的批量更新 (Batch Updates)

React 会将多个 State 更新操作合并 (batch) 成一次更新,以提高性能。这意味着在同一个事件处理函数中,多次调用 setState 函数,只会触发一次组件的重新渲染。

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 import React, { useState } from 'react';
2
3 function BatchUpdateExample() {
4 const [count, setCount] = useState(0);
5
6 const handleBatchIncrement = () => {
7 // 多次调用 setCount 函数只触发一次重新渲染
8 setCount(count + 1);
9 setCount(count + 1);
10 setCount(count + 1);
11 };
12
13 console.log('Component rendered'); // 观察组件渲染次数
14
15 return (
16 <div>
17 <p>当前计数: {count}</p>
18 <button onClick={handleBatchIncrement}>批量增加计数</button>
19 </div>
20 );
21 }

在上面的例子中,尽管 handleBatchIncrement 函数中调用了三次 setCount(count + 1),但组件只会重新渲染一次,count 的值最终只会增加 1。这是因为 React 将这三次 State 更新操作批量处理了。

4.3 事件处理:响应用户交互

React 使用合成事件 (Synthetic Events) 机制来处理 DOM 事件。合成事件是对原生 DOM 事件的封装,提供了跨浏览器兼容性和性能优化。React 事件处理方式与原生 DOM 事件处理类似,但有一些重要的区别。

React 合成事件 (Synthetic Events)

React 实现了一套合成事件系统,它在浏览器原生事件的基础上进行了封装,抹平了不同浏览器之间的事件差异,提供了统一的事件接口。合成事件对象 (Synthetic Event object) 包含了原生事件对象的所有属性,并且还扩展了一些 React 特有的属性和方法。

合成事件的优点:

跨浏览器兼容性 (Cross-browser Compatibility):合成事件解决了不同浏览器之间事件处理的差异性,保证了应用在不同浏览器上的行为一致性。
性能优化 (Performance Optimization):React 使用事件委托 (event delegation) 机制,将事件监听器 (event listeners) 绑定到组件树的根节点 (通常是 document),而不是每个 DOM 元素上。这样可以减少事件监听器的数量,提高性能。
统一的事件接口 (Unified Event Interface):合成事件提供了统一的事件对象接口,方便开发者使用。

React 事件处理方式

在 React 中,通过在 JSX 元素上设置事件处理属性 (event handler attributes) 来绑定事件监听器。事件处理属性的命名采用驼峰命名法 (camelCase),例如 onClickonChangeonSubmitonMouseOver 等。事件处理属性的值是一个函数,当事件发生时,该函数会被调用。

  • 绑定事件处理函数
1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 import React from 'react';
2
3 function ButtonExample() {
4 const handleClick = (event) => {
5 // event 是合成事件对象
6 console.log('Button clicked!', event);
7 alert('Button clicked!');
8 };
9
10 return (
11 <button onClick={handleClick}>点击我</button>
12 );
13 }

在上面的例子中,onClick={handleClick}handleClick 函数绑定到按钮的 click 事件上。当按钮被点击时,handleClick 函数会被调用,并接收一个合成事件对象 event 作为参数。

  • 事件对象 (Event Object)

事件处理函数接收的第一个参数是合成事件对象 (Synthetic Event object)。合成事件对象拥有与原生 DOM 事件对象相同的属性和方法,例如 event.target (触发事件的元素)、event.preventDefault() (阻止默认行为)、event.stopPropagation() (阻止事件冒泡) 等。

  • 常用的 React 事件

React 支持大多数常用的 DOM 事件,例如:

鼠标事件 (Mouse Events)onClick, onDoubleClick, onMouseOver, onMouseOut, onMouseMove, onMouseDown, onMouseUp
键盘事件 (Keyboard Events)onKeyDown, onKeyPress, onKeyUp
表单事件 (Form Events)onChange, onSubmit, onFocus, onBlur, onInput
触摸事件 (Touch Events)onTouchStart, onTouchMove, onTouchEnd, onTouchCancel
UI 事件 (UI Events)onScroll, onLoad, onError
焦点事件 (Focus Events)onFocus, onBlur
剪贴板事件 (Clipboard Events)onCopy, onCut, onPaste
组合事件 (Composition Events)onCompositionStart, onCompositionUpdate, onCompositionEnd
滚轮事件 (Wheel Events)onWheel

事件处理函数的 this 绑定 (类组件)

在类组件中,事件处理函数中的 this 默认不会绑定到组件实例。需要手动绑定 this,才能在事件处理函数中访问组件的 propsstate

  • 在构造函数中绑定 this (推荐):
1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 import React from 'react';
2
3 class ButtonExample extends React.Component {
4 constructor(props) {
5 super(props);
6 this.handleClick = this.handleClick.bind(this); // 在构造函数中绑定 this
7 }
8
9 handleClick() {
10 console.log('Button clicked!', this); // this 指向组件实例
11 alert('Button clicked!');
12 }
13
14 render() {
15 return (
16 <button onClick={this.handleClick}>点击我</button>
17 );
18 }
19 }
  • 使用箭头函数 (Arrow Functions)
1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 import React from 'react';
2
3 class ButtonExample extends React.Component {
4 handleClick = () => { // 使用箭头函数this 自动绑定到组件实例
5 console.log('Button clicked!', this); // this 指向组件实例
6 alert('Button clicked!');
7 };
8
9 render() {
10 return (
11 <button onClick={this.handleClick}>点击我</button>
12 );
13 }
14 }
  • 内联箭头函数 (Inline Arrow Functions)
1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 import React from 'react';
2
3 class ButtonExample extends React.Component {
4 render() {
5 return (
6 <button onClick={() => this.handleClick()}>点击我</button> // 内联箭头函数
7 );
8 }
9
10 handleClick() {
11 console.log('Button clicked!', this); // this 指向组件实例
12 alert('Button clicked!');
13 }
14 }

在函数式组件中,事件处理函数不需要手动绑定 this,因为函数式组件本身没有 this 上下文,事件处理函数中的 this 指向 undefined (在严格模式下) 或全局对象 (在非严格模式下),通常不需要关心 this 的绑定问题。

本章总结

本章深入讲解了 React 的 State 和事件处理机制。State 是组件内部动态数据的管理中心,useState Hook 使得函数式组件也能轻松管理 State。事件处理机制则使得 React 应用能够响应用户的交互行为。掌握 State 和事件处理,是构建交互式 React 应用的关键。下一章,我们将深入学习 React Hooks,探索更多强大的 Hooks 功能。🚀

REVIEW PASS

5. chapter 5: 深入理解 Hooks

React Hooks 是 React 16.8 版本引入的一项 революционный (gé mìng xìng, revolutionary) 功能。它允许你在函数式组件 (Function Components) 中使用 state 以及其他的 React 特性。Hooks 的出现,使得函数式组件拥有了类组件 (Class Components) 的所有能力,并且代码更加简洁、易于理解和复用。本章将深入探讨几个核心的 React Hooks,包括 useEffectuseContextuseRef 以及自定义 Hook,帮助你全面掌握 Hooks 的精髓,提升 React 开发技能。

5.1 useEffect Hook:处理副作用操作

useEffect Hook 是 React 中处理副作用 (Side Effects) 的主要工具。副作用是指在组件渲染之外发生的操作,例如:数据获取 (data fetching)、DOM 操作 (DOM manipulation)、设置定时器 (timers)、日志记录 (logging) 等。useEffect 允许你在函数式组件中执行这些副作用操作,并控制副作用的执行时机和清理 (cleanup)。

什么是副作用?

在 React 组件的世界里,渲染 (rendering) 的目标是根据 props 和 state 生成 UI 界面。而副作用,则是指那些不属于渲染过程,但需要在组件生命周期中执行的操作。这些操作可能会影响组件之外的世界,例如:

数据获取 (Data Fetching):从服务器获取数据,更新组件状态。
DOM 操作 (DOM Manipulation):直接修改 DOM 元素,例如设置焦点、滚动位置等。
定时器 (Timers):使用 setTimeoutsetInterval 设置定时器。
事件监听 (Event Listeners):添加或移除事件监听器。
日志记录 (Logging):打印日志信息到控制台或发送到服务器。

这些副作用操作需要在特定的时机执行,并且可能需要在组件卸载时进行清理,以避免内存泄漏 (memory leaks) 和其他问题。useEffect Hook 就是为了解决这些问题而诞生的。

useEffect 的基本用法

useEffect 接收两个参数:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 useEffect(effectFunction, dependencyArray);
  • effectFunction (副作用函数):这是一个函数,包含了你要执行的副作用逻辑。React 会在每次渲染后调用这个函数(默认情况下)。
  • dependencyArray (依赖项数组):这是一个可选的数组,用于指定 effectFunction 依赖的值。React 会根据依赖项数组的变化来决定是否重新执行 effectFunction

不同的 dependencyArray 取值,决定了 useEffect 的执行时机:

  • dependencyArray 为空数组 []effectFunction 只会在组件首次渲染后执行一次,类似于类组件的 componentDidMount 生命周期方法。常用于执行只需要在组件挂载时执行一次的副作用,例如:初始数据获取、添加全局事件监听器等。
  • dependencyArray 包含依赖项 [dep1, dep2, ...]effectFunction 会在组件首次渲染后执行一次,并且在每次依赖项数组中的任何一个值发生变化时重新执行。类似于类组件的 componentDidUpdate 生命周期方法,但更精细地控制了更新条件。常用于根据 props 或 state 的变化执行副作用,例如:根据搜索关键词获取数据、监听某个 state 值的变化并执行相应操作等。
  • dependencyArray 不传 (省略)effectFunction 会在每次组件渲染后都执行,包括首次渲染和后续更新渲染。类似于类组件的 componentDidMountcomponentDidUpdate 生命周期方法的组合。需要谨慎使用,因为每次渲染都执行副作用可能会导致性能问题或无限循环。

副作用清理 (Cleanup)

有些副作用操作需要在组件卸载时进行清理,例如:

取消数据请求:防止组件卸载后,请求返回的数据更新已卸载的组件状态,导致错误。
移除定时器:防止组件卸载后,定时器仍然执行,导致内存泄漏。
取消事件监听:防止组件卸载后,事件监听器仍然生效,导致意料之外的行为。

为了进行副作用清理,effectFunction 可以返回一个清理函数 (cleanup function)。React 会在组件卸载时,以及每次组件重新渲染之前(如果依赖项数组发生变化),先执行上一次 effect 的清理函数,然后再执行新的 effect 函数。

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 useEffect(() => {
2 // 副作用逻辑
3
4 return () => {
5 // 清理逻辑
6 };
7 }, [dependencyArray]);

示例 1:使用 useEffect 获取数据

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 import React, { useState, useEffect } from 'react';
2
3 function DataFetching() {
4 const [data, setData] = useState(null);
5 const [loading, setLoading] = useState(true);
6 const [error, setError] = useState(null);
7
8 useEffect(() => {
9 const fetchData = async () => {
10 try {
11 const response = await fetch('https://api.example.com/data');
12 if (!response.ok) {
13 throw new Error(`HTTP error! status: ${response.status}`);
14 }
15 const json = await response.json();
16 setData(json);
17 setLoading(false);
18 } catch (e) {
19 setError(e);
20 setLoading(false);
21 }
22 };
23
24 fetchData();
25 }, []); // 依赖项数组为空只在组件挂载时执行一次
26
27 if (loading) {
28 return <p>加载中...</p>;
29 }
30
31 if (error) {
32 return <p>Error: {error.message}</p>;
33 }
34
35 return (
36 <div>
37 <pre>{JSON.stringify(data, null, 2)}</pre>
38 </div>
39 );
40 }

在这个例子中,useEffect 的副作用函数 fetchData 发起数据请求。依赖项数组为空 [],所以数据请求只会在组件首次渲染后执行一次。加载状态 loading 和错误状态 error 用于处理加载中和错误情况。

示例 2:使用 useEffect 设置定时器并清理

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 import React, { useState, useEffect } from 'react';
2
3 function Timer() {
4 const [seconds, setSeconds] = useState(0);
5
6 useEffect(() => {
7 const intervalId = setInterval(() => {
8 setSeconds(prevSeconds => prevSeconds + 1);
9 }, 1000);
10
11 // 返回清理函数在组件卸载时清除定时器
12 return () => clearInterval(intervalId);
13 }, []); // 依赖项数组为空只在组件挂载时设置定时器
14
15 return (
16 <div>
17 <p>已过去 {seconds} </p>
18 </div>
19 );
20 }

在这个例子中,useEffect 设置了一个每秒更新 seconds 状态的定时器。清理函数 clearInterval(intervalId) 在组件卸载时被调用,清除了定时器,防止内存泄漏。

useEffect 的注意事项

避免无限循环:如果 effectFunction 中更新了依赖项数组中的状态,并且依赖项数组包含了这个状态,可能会导致无限循环执行 useEffect。要仔细考虑依赖项数组的设置,或者使用函数式更新来避免循环。
Effect 的执行顺序:React 会在浏览器绘制 (paint) 之后异步执行 useEffect。这意味着 useEffect 不会阻塞浏览器的渲染。如果需要同步执行副作用,可以使用 useLayoutEffect Hook (较少使用)。
把 Effect 逻辑放在 Effect 函数内部:避免在 useEffect 外部定义变量或函数,然后在 Effect 函数内部使用,这样容易引入依赖项错误。应该把 Effect 逻辑尽可能地封装在 effectFunction 内部。

useEffect 是处理 React 组件副作用的核心 Hook,合理使用 useEffect 可以有效地管理数据获取、DOM 操作、定时器等副作用操作,并确保组件的性能和稳定性。💡

5.2 useContext Hook:跨组件共享状态

在 React 应用中,数据通常通过 props 从父组件传递到子组件。但是,当组件层级很深时,逐层传递 props 可能会变得繁琐,这就是所谓的 "prop drilling" (props 逐层传递) 问题。useContext Hook 和 Context API (上下文 API) 提供了一种优雅的方式,可以在不显式传递 props 的情况下,在组件树中共享数据

Context API 的概念

Context API 提供了一种在组件树中全局共享某些值的方式。你可以创建一个 Context 对象,然后在组件树的任何地方订阅 (consume) 这个 Context 的值,而无需手动逐层传递 props。Context 非常适合共享那些对于整个组件树都是 "全局" 的数据,例如:当前用户 (current user)、主题 (theme)、语言 (language) 等。

Context API 的使用步骤

Context API 的使用通常包括以下三个步骤:

  1. 创建 Context 对象: 使用 React.createContext(defaultValue) 创建一个 Context 对象。defaultValue 是一个可选的默认值,当组件在组件树中没有找到匹配的 Provider 时,会使用这个默认值。
1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 const MyContext = React.createContext(null); // 创建一个 Context 对象,默认值为 null
  1. Provider 提供 Context 值: 使用 Context.Provider 组件包裹需要共享数据的组件子树。Provider 组件接收一个 value prop,用于指定要共享的 Context 值。所有在 Provider 组件子树中的组件都可以访问到这个 value 值。
1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 import React from 'react';
2 import ChildComponent from './ChildComponent';
3 const MyContext = React.createContext('默认值');
4
5 function ParentComponent() {
6 const contextValue = 'Context 共享的值';
7
8 return (
9 <MyContext.Provider value={contextValue}> {/* 使用 Provider 提供 Context */}
10 <ChildComponent /> {/* ChildComponent 及其子组件都可以访问 contextValue */}
11 </MyContext.Provider>
12 );
13 }
  1. Consumer 消费 Context 值: 在需要使用 Context 值的组件中,可以使用 useContext(Context) Hook 来消费 (consume) Context 值。useContext(Context) 返回当前 Context 的值,即最近的 Provider 提供的 value prop 值。
1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 import React, { useContext } from 'react';
2 import MyContext from './MyContext'; // 引入 Context 对象
3
4 function ChildComponent() {
5 const contextValue = useContext(MyContext); // 使用 useContext Hook 消费 Context
6
7 return (
8 <div>
9 <p>Context : {contextValue}</p>
10 </div>
11 );
12 }

useContext Hook 的使用

useContext Hook 接收一个 Context 对象 作为参数,返回当前 Context 的值。useContext 只能在 Provider 组件的子组件树中使用,否则会使用 Context 对象创建时的 defaultValue

示例:使用 Context 实现主题切换

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 import React, { createContext, useContext, useState } from 'react';
2
3 // 1. 创建 ThemeContext
4 const ThemeContext = createContext('light'); // 默认主题为 'light'
5
6 function App() {
7 const [theme, setTheme] = useState('light');
8
9 const toggleTheme = () => {
10 setTheme(prevTheme => prevTheme === 'light' ? 'dark' : 'light');
11 };
12
13 return (
14 // 2. 使用 ThemeContext.Provider 提供 theme
15 <ThemeContext.Provider value={theme}>
16 <div className={`app ${theme}`}>
17 <Header />
18 <Content />
19 <button onClick={toggleTheme}>切换主题</button>
20 </div>
21 </ThemeContext.Provider>
22 );
23 }
24
25 function Header() {
26 return (
27 <header>
28 <h1>主题切换示例</h1>
29 </header>
30 );
31 }
32
33 function Content() {
34 // 3. 使用 useContext(ThemeContext) 消费 theme
35 const theme = useContext(ThemeContext);
36
37 return (
38 <div className="content">
39 <p>当前主题: {theme}</p>
40 <p>这是一个使用 Context API 实现主题切换的示例</p>
41 </div>
42 );
43 }

在这个例子中,ThemeContext 用于共享主题 (theme) 值。App 组件使用 ThemeContext.Provider 将当前主题 theme 值提供给其子组件树。Content 组件使用 useContext(ThemeContext) Hook 消费 theme 值,并根据主题值渲染不同的 UI 样式。

useContext 的注意事项

性能优化:当 Provider 的 value prop 发生变化时,所有消费该 Context 的组件都会重新渲染。如果 Context 值变化频繁,可能会导致性能问题。需要谨慎使用 Context 共享大量或频繁变化的数据。
状态管理:Context API 主要用于全局状态共享,而不是作为状态管理工具 (例如 Redux, Zustand) 的替代品。对于复杂的应用状态管理,仍然建议使用专门的状态管理库。
避免过度使用:不要为了避免 props 传递而过度使用 Context。Context 应该用于真正需要全局共享的数据,而不是所有的数据传递场景。

useContext 和 Context API 提供了一种便捷的方式在 React 组件树中共享数据,有效地解决了 prop drilling 问题,使得代码更加清晰和易于维护。🛠️

5.3 useRef Hook:访问 DOM 元素与存储变量

useRef Hook 主要用于两个目的:

  1. 访问 DOM 元素或组件实例: 获取 DOM 元素的引用,或类组件的实例,从而可以进行直接的 DOM 操作或调用组件方法。
  2. 存储可变的变量,但不会触发组件重新渲染: 创建一个可以跨渲染周期保持不变的变量,并且修改这个变量不会触发组件的重新渲染。

useRef 的基本用法

useRef Hook 接收一个 初始值 (initial value) 作为参数,返回一个 ref 对象 (ref object)。ref 对象是一个普通的 JavaScript 对象,它有一个 current 属性,current 属性的初始值就是 useRef 的初始值。

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 const refContainer = useRef(initialValue);
  • initialValue (初始值):ref 对象的 current 属性的初始值。可以是任何 JavaScript 数据类型,例如 null0{}[] 等。
  • refContainer (ref 对象)useRef 返回的 ref 对象。它是一个持久化的容器,在组件的整个生命周期内保持不变。通过 refContainer.current 属性可以访问或修改 ref 对象的值。

访问 DOM 元素

可以将 useRef 创建的 ref 对象绑定到 JSX 元素的 ref 属性上,React 会在组件挂载后,将该 DOM 元素的引用赋值给 ref 对象的 current 属性。

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 import React, { useRef, useEffect } from 'react';
2
3 function FocusInput() {
4 const inputRef = useRef(null); // 创建一个 ref 对象初始值为 null
5
6 useEffect(() => {
7 // 组件挂载后inputRef.current 指向 input DOM 元素
8 inputRef.current.focus(); // 调用 input DOM 元素的 focus() 方法使其获得焦点
9 }, []); // 空依赖项数组只在组件挂载时执行
10
11 return (
12 <input type="text" ref={inputRef} /> // ref 对象绑定到 input 元素的 ref 属性
13 );
14 }

在这个例子中,inputRef 通过 ref={inputRef} 绑定到 <input> 元素。useEffect 在组件挂载后,通过 inputRef.current.focus() 使 input 元素获得焦点。

存储可变的变量

useRef 创建的 ref 对象,其 current 属性可以被修改,并且修改 refContainer.current 的值不会触发组件的重新渲染。这使得 useRef 非常适合存储一些不需要引起 UI 更新的可变值,例如:

存储定时器 ID:在 useEffect 中设置定时器,并将定时器 ID 存储在 ref 中,方便在清理函数中清除定时器。
存储上一次的 props 或 state 值:在组件更新后,比较当前的 props 或 state 值与上一次的值。
存储计数器:用于记录某些操作的次数,但不需要在 UI 上显示计数结果。

示例:使用 useRef 存储计数器,但不触发重新渲染

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 import React, { useRef, useState } from 'react';
2
3 function RenderCounter() {
4 const [count, setCount] = useState(0);
5 const renderCountRef = useRef(0); // 创建 ref 对象初始值为 0
6
7 renderCountRef.current = renderCountRef.current + 1; // 每次组件渲染递增 ref.current
8
9 const increment = () => {
10 setCount(count + 1); // 更新 state触发组件重新渲染
11 };
12
13 return (
14 <div>
15 <p>State Count: {count}</p>
16 <p>Render Count (ref): {renderCountRef.current}</p> {/* 显示 ref.current 的值 */}
17 <button onClick={increment}>增加 State Count</button>
18 </div>
19 );
20 }

在这个例子中,renderCountRef 用于记录组件的渲染次数。每次组件渲染时,renderCountRef.current 的值都会递增,但由于修改 refContainer.current 不会触发重新渲染,所以组件只会因为 count state 的变化而重新渲染。renderCountRef.current 的值会持续累加,但不会影响 UI 的更新。

useRefuseState 的区别

特性useStateuseRef
用途管理组件状态,驱动 UI 更新访问 DOM 元素,存储不触发重新渲染的可变值
数据变化修改 state 值会触发组件重新渲染修改 ref.current 值不会触发组件重新渲染
持久性state 值在组件重新渲染后会根据新的 state 值更新ref 对象及其 current 属性在组件整个生命周期内保持不变
返回值返回一个包含 state 值和 setState 函数的数组返回一个 ref 对象,通过 current 属性访问值

useRef 提供了一种在函数式组件中访问 DOM 元素和存储可变值的有效方式。理解 useRef 的用途和与 useState 的区别,可以帮助你更好地选择合适的 Hook 来解决不同的开发场景。 🧰

5.4 自定义 Hook (Custom Hook):逻辑复用的利器

自定义 Hook 是 React Hooks 的一个强大特性。它允许你提取组件逻辑,并在多个组件之间复用这些逻辑。自定义 Hook 本质上就是一个 JavaScript 函数,它的名称以 use 开头,并且可以调用其他的 Hook。

自定义 Hook 的概念和优势

当你在多个组件中发现有相同的逻辑 (例如:数据获取、事件监听、状态管理等) 时,可以将这些逻辑提取到一个自定义 Hook 中。然后在需要使用这些逻辑的组件中,调用这个自定义 Hook 即可。

自定义 Hook 的优势:

代码复用 (Code Reusability):将组件逻辑提取到自定义 Hook 中,可以在多个组件之间复用相同的逻辑,减少代码重复,提高开发效率。
逻辑分离 (Logic Separation):将组件的 UI 渲染逻辑和业务逻辑分离,使得组件代码更加清晰、易于理解和维护。
提高组件可测试性 (Component Testability):自定义 Hook 使得组件的逻辑更易于单元测试。可以单独测试自定义 Hook,确保逻辑的正确性。
提高代码可读性 (Code Readability):使用自定义 Hook 可以将复杂的组件逻辑分解成更小的、可复用的模块,提高代码的可读性和可维护性。

自定义 Hook 的创建规则

创建自定义 Hook 需要遵循以下规则:

  1. 函数名称以 use 开头:这是 React 官方的约定,React 会根据函数名称是否以 use 开头来判断是否是 Hook。例如:useFetchDatauseThemeuseWindowSize 等。
  2. 可以在自定义 Hook 中调用其他的 Hook:自定义 Hook 内部可以调用 useStateuseEffectuseContext 等其他的 Hook,也可以调用其他的自定义 Hook。
  3. 自定义 Hook 本身也是一个函数:它只是一个普通的 JavaScript 函数,但遵循了特定的命名约定和 Hook 使用规则。

示例:创建自定义 Hook useFetch 用于数据获取

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 import { useState, useEffect } from 'react';
2
3 // 自定义 Hook useFetch,用于数据获取
4 function useFetch(url) {
5 const [data, setData] = useState(null);
6 const [loading, setLoading] = useState(true);
7 const [error, setError] = useState(null);
8
9 useEffect(() => {
10 const fetchData = async () => {
11 try {
12 const response = await fetch(url);
13 if (!response.ok) {
14 throw new Error(`HTTP error! status: ${response.status}`);
15 }
16 const json = await response.json();
17 setData(json);
18 setLoading(false);
19 } catch (e) {
20 setError(e);
21 setLoading(false);
22 }
23 };
24
25 fetchData();
26 }, [url]); // 依赖项数组包含 url,当 url 变化时重新获取数据
27
28 return { data, loading, error }; // 返回 data, loading, error 状态
29 }
30
31 export default useFetch;

这个 useFetch 自定义 Hook 封装了数据获取的逻辑。它接收一个 url 参数,发起数据请求,并返回 dataloadingerror 状态。

在组件中使用 useFetch 自定义 Hook:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 import React from 'react';
2 import useFetch from './useFetch'; // 引入 useFetch 自定义 Hook
3
4 function UserList() {
5 const { data: users, loading, error } = useFetch('https://api.example.com/users'); // 调用 useFetch Hook
6
7 if (loading) {
8 return <p>加载用户列表中...</p>;
9 }
10
11 if (error) {
12 return <p>Error: {error.message}</p>;
13 }
14
15 return (
16 <ul>
17 {users.map(user => (
18 <li key={user.id}>{user.name}</li>
19 ))}
20 </ul>
21 );
22 }
23
24 function ProductList() {
25 const { data: products, loading, error } = useFetch('https://api.example.com/products'); // 调用 useFetch Hook
26
27 if (loading) {
28 return <p>加载商品列表中...</p>;
29 }
30
31 if (error) {
32 return <p>Error: {error.message}</p>;
33 }
34
35 return (
36 <ul>
37 {products.map(product => (
38 <li key={product.id}>{product.name} - ${product.price}</li>
39 ))}
40 </ul>
41 );
42 }

UserListProductList 组件中,都使用了 useFetch 自定义 Hook 来获取数据。通过自定义 Hook,数据获取的逻辑被复用,组件代码更加简洁和专注于 UI 渲染。

自定义 Hook 的注意事项

Hook 调用规则:自定义 Hook 也需要遵循 Hook 的调用规则,只能在 React 函数组件或其他的 Hook 中调用,不能在普通的 JavaScript 函数中调用。
逻辑抽象程度:自定义 Hook 应该抽象出通用的、可复用的逻辑,而不是过于具体或业务相关的逻辑。
命名清晰:自定义 Hook 的名称应该清晰地表达其功能,方便其他开发者理解和使用。

自定义 Hook 是 React Hooks 中最灵活和强大的特性之一。合理地使用自定义 Hook 可以极大地提高代码的复用性和可维护性,构建更加模块化和可扩展的 React 应用。 🚀

本章总结

本章深入探讨了 React Hooks 的核心概念和常用 Hook,包括 useEffectuseContextuseRef 以及自定义 Hook。useEffect 用于处理副作用操作,useContext 用于跨组件共享状态,useRef 用于访问 DOM 元素和存储变量,自定义 Hook 用于逻辑复用。掌握这些 Hooks,你将能够更加高效地开发 React 应用,并编写出更简洁、可维护的代码。下一章,我们将学习 React 中的条件渲染和列表渲染。🚀

REVIEW PASS

6. chapter 6: 条件渲染与列表渲染

构建动态用户界面是现代 Web 应用的核心需求。React 提供了强大的机制来实现条件渲染 (Conditional Rendering)列表渲染 (List Rendering),使得我们可以根据不同的条件展示不同的 UI 内容,并高效地渲染动态数据列表。本章将深入探讨 React 中的条件渲染和列表渲染技术,助你掌握构建动态 UI 的关键技能。

6.1 条件渲染:根据条件展示不同内容

条件渲染是指根据不同的条件,在组件中渲染不同的 JSX 结构或组件。这使得我们可以根据应用的状态或用户的交互,动态地展示不同的 UI 界面。React 提供了多种实现条件渲染的方式,每种方式都有其适用的场景。

if/else 语句

最直观的条件渲染方式是使用 JavaScript 的 if/else 语句。可以在组件的函数体内部使用 if/else 语句,根据条件返回不同的 JSX 元素。

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 import React from 'react';
2
3 function Greeting(props) {
4 const isLoggedIn = props.isLoggedIn;
5 if (isLoggedIn) {
6 return <h1>欢迎回来!</h1>;
7 } else {
8 return <h1>请登录</h1>;
9 }
10 }
11
12 function App() {
13 return (
14 <div>
15 <Greeting isLoggedIn={true} /> {/* 渲染 "欢迎回来!" */}
16 <Greeting isLoggedIn={false} /> {/* 渲染 "请登录。" */}
17 </div>
18 );
19 }

在上面的例子中,Greeting 组件根据 isLoggedIn prop 的值,使用 if/else 语句返回不同的 <h1> 元素。当 isLoggedIntrue 时,显示 "欢迎回来!",否则显示 "请登录。"。

三元运算符 ?: (Conditional Operator)

三元运算符 ?: 是一种简洁的条件表达式,也常用于条件渲染。它适用于简单的二选一的条件判断。

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 import React from 'react';
2
3 function Greeting(props) {
4 const isLoggedIn = props.isLoggedIn;
5 return (
6 isLoggedIn ? (
7 <h1>欢迎回来!</h1>
8 ) : (
9 <h1>请登录</h1>
10 )
11 );
12 }
13
14 function App() {
15 return (
16 <div>
17 <Greeting isLoggedIn={true} />
18 <Greeting isLoggedIn={false} />
19 </div>
20 );
21 }

这个例子使用三元运算符 isLoggedIn ? <h1>...</h1> : <h1>...</h1> 实现了与 if/else 语句相同的功能,代码更加简洁。

逻辑与运算符 && (Logical AND Operator)

逻辑与运算符 && 在 JavaScript 中具有短路求值 (short-circuit evaluation) 的特性。当 && 运算符左侧的表达式为真值 (truthy value) 时,会返回右侧的表达式的值;否则,返回左侧表达式的值。利用这个特性,可以实现条件为真时才渲染的场景。

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 import React from 'react';
2
3 function Mailbox(props) {
4 const unreadMessages = props.unreadMessages;
5 return (
6 <div>
7 <h1>你好!</h1>
8 {unreadMessages.length > 0 &&
9 <h2>
10 您有 {unreadMessages.length} 条未读消息
11 </h2>
12 }
13 </div>
14 );
15 }
16
17 function App() {
18 const messages = ['React', 'Re: React', 'Re:Re: React'];
19 return (
20 <div>
21 <Mailbox unreadMessages={messages} /> {/* 渲染 "您有 3 条未读消息。" */}
22 <Mailbox unreadMessages={[]} /> {/* 不渲染任何内容 */}
23 </div>
24 );
25 }

Mailbox 组件中,只有当 unreadMessages.length > 0 为真时,<h2> 元素才会被渲染。如果 unreadMessages 数组为空,则 && 运算符左侧的表达式为假值 (falsy value),&& 运算符会短路求值,直接返回左侧的值 (假值),右侧的 JSX 表达式不会被执行,因此 <h2> 元素不会被渲染。

使用变量存储组件

可以将需要条件渲染的组件或 JSX 元素赋值给变量,然后根据条件选择渲染哪个变量。这种方式适用于条件分支较多,或需要渲染的组件结构比较复杂的情况,可以使代码更加清晰易读。

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 import React from 'react';
2
3 function LoginButton(props) {
4 return (
5 <button onClick={props.onClick}>
6 登录
7 </button>
8 );
9 }
10
11 function LogoutButton(props) {
12 return (
13 <button onClick={props.onClick}>
14 退出
15 </button>
16 );
17 }
18
19 function Greeting(props) {
20 const isLoggedIn = props.isLoggedIn;
21 let button; // 定义变量 button
22
23 if (isLoggedIn) {
24 button = <LogoutButton onClick={props.onLogoutClick} />; // 根据条件赋值不同的组件
25 } else {
26 button = <LoginButton onClick={props.onLoginClick} />;
27 }
28
29 return (
30 <div>
31 <GreetingMessage isLoggedIn={isLoggedIn} />
32 {button} {/* 渲染变量 button */}
33 </div>
34 );
35 }
36
37 function GreetingMessage(props) {
38 const isLoggedIn = props.isLoggedIn;
39 if (isLoggedIn) {
40 return <h1>欢迎用户!</h1>;
41 } else {
42 return <h1>请先登录</h1>;
43 }
44 }
45
46 function App() {
47 const [isLoggedIn, setIsLoggedIn] = React.useState(false);
48
49 const handleLoginClick = () => {
50 setIsLoggedIn(true);
51 };
52
53 const handleLogoutClick = () => {
54 setIsLoggedIn(false);
55 };
56
57 return (
58 <div>
59 <Greeting
60 isLoggedIn={isLoggedIn}
61 onLoginClick={handleLoginClick}
62 onLogoutClick={handleLogoutClick}
63 />
64 </div>
65 );
66 }

在这个例子中,Greeting 组件根据 isLoggedIn 的值,将不同的按钮组件 (LoginButtonLogoutButton) 赋值给 button 变量,然后渲染 button 变量。这种方式使得条件渲染的逻辑更加清晰,尤其是在条件分支较多时。

选择建议

  • 对于简单的二选一条件渲染,可以使用三元运算符 ?:if/else 语句。三元运算符更简洁,if/else 语句更直观。
  • 对于条件为真时才渲染的场景,可以使用逻辑与运算符 &&
  • 对于条件分支较多或需要渲染的组件结构复杂的情况,可以使用变量存储组件的方式,提高代码可读性。

6.2 列表渲染:高效渲染动态列表

列表渲染是指根据数组数据,动态地渲染一系列相似的组件或 JSX 元素。在 Web 应用中,列表渲染非常常见,例如:商品列表、文章列表、用户列表等。React 提供了 map() 方法结合 JSX 表达式来实现高效的列表渲染。

使用 map() 方法进行列表渲染

map() 是 JavaScript 数组的一个常用方法,它接收一个函数作为参数,对数组中的每个元素执行该函数,并返回一个新的数组,新数组的元素是函数执行的结果。在 React 中,我们可以使用 map() 方法遍历数组数据,为每个数组元素生成对应的 JSX 元素,从而实现列表渲染。

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 import React from 'react';
2
3 function NumberList(props) {
4 const numbers = props.numbers;
5 const listItems = numbers.map((number) =>
6 <li>{number}</li> // 为每个 number 生成一个 <li> 元素
7 );
8 return (
9 <ul>{listItems}</ul> // 渲染 <li> 元素列表
10 );
11 }
12
13 function App() {
14 const numbers = [1, 2, 3, 4, 5];
15 return (
16 <NumberList numbers={numbers} />
17 );
18 }

NumberList 组件中,numbers.map((number) => <li>{number}</li>) 遍历 numbers 数组,为每个 number 生成一个 <li> 元素,并将生成的 <li> 元素数组赋值给 listItems 变量。最后,在 <ul> 标签中渲染 listItems 数组,实现了数字列表的渲染。

Keys 的重要性:优化列表渲染性能

在渲染列表时,React 强烈建议为列表中的每个元素添加 key 属性。key 属性是一个特殊的字符串属性,用于帮助 React 识别列表中哪些元素发生了变化 (添加、删除、移动、更新)。key 属性的值需要满足以下条件:

  • 唯一性 (Uniqueness):在同一个列表中,每个元素的 key 值必须是唯一的。
  • 稳定性 (Stability)key 值应该在列表数据更新后保持稳定,不应该是动态变化的或随机生成的。

为什么需要 Keys?

当列表数据发生变化时,React 需要判断哪些列表项需要更新、添加或删除。如果没有 key,React 只能通过元素的索引 (index) 来识别列表项,这会导致以下问题:

性能问题 (Performance Issues):当列表项的顺序发生变化时 (例如,在列表的开头或中间插入或删除元素),React 可能会错误地更新或重新渲染不必要的列表项,导致性能下降。
组件状态丢失 (Component State Loss):如果列表项包含组件状态 (例如,输入框的值、复选框的选中状态),当列表项的顺序发生变化时,React 可能会错误地复用组件实例,导致组件状态丢失或错乱。

使用 Keys 的好处

  • 性能优化 (Performance Optimization):使用 key 后,React 可以更准确地识别列表项的变化,只更新、添加或删除必要的列表项,避免不必要的渲染,提高性能。
  • 组件状态保持 (Component State Preservation):使用 key 后,React 可以正确地关联列表项和组件实例,即使列表项的顺序发生变化,组件状态也能得到正确地保持。

如何选择 Keys

选择合适的 key 值非常重要,常见的 key 值来源有:

  • 数据项的唯一 ID (Unique ID from Data):如果列表数据本身就包含唯一 ID (例如,数据库表的主键、API 返回的唯一标识符),这是最佳的 key 值选择。例如,如果渲染用户列表,可以使用用户的 ID 作为 key
1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 function UserList(props) {
2 const users = props.users;
3 const userListItems = users.map((user) =>
4 <li key={user.id}>{user.name}</li> // 使用 user.id 作为 key
5 );
6 return (
7 <ul>{userListItems}</ul>
8 );
9 }
  • 使用索引 index 作为 Key (不推荐,除非列表项顺序不变):在某些情况下,如果列表数据没有唯一 ID,并且列表项的顺序不会发生变化 (例如,静态列表),可以考虑使用数组索引 index 作为 key。但是,强烈不推荐在列表项顺序可能发生变化的情况下使用索引作为 key,因为这会导致性能问题和组件状态丢失。
1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 function NumberList(props) {
2 const numbers = props.numbers;
3 const listItems = numbers.map((number, index) =>
4 <li key={index}>{number}</li> // 使用 index 作为 key (不推荐,除非列表顺序不变)
5 );
6 return (
7 <ul>{listItems}</ul>
8 );
9 }

▮▮▮▮警告:在列表项顺序可能发生变化的情况下,绝对不要使用索引 index 作为 key。这会严重影响性能,并可能导致组件状态错误。

  • 生成唯一的 ID (Generate Unique ID):如果列表数据没有唯一 ID,并且列表项顺序可能变化,可以考虑在数据处理阶段为每个数据项生成唯一的 ID (例如,使用 UUID 库)。然后使用生成的唯一 ID 作为 key

列表渲染的最佳实践

始终为列表项添加 key 属性:除非你非常确定列表是静态的,并且永远不会重新排序或过滤,否则请始终为列表项添加 key 属性。
使用稳定的、唯一的 keykey 值应该是稳定的 (在数据更新后保持不变) 且唯一的 (在同一个列表中唯一)。
避免使用索引 index 作为 key (除非列表顺序不变):在列表项顺序可能变化的情况下,避免使用索引 index 作为 key,使用数据项的唯一 ID 或生成唯一 ID。
Key 只需要在同级元素中唯一:Key 的唯一性只需要在同一个父组件的直接子组件列表中保证即可,不需要全局唯一。

错误示例:未使用 Key 或使用 Index 作为 Key (当列表顺序变化时)

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 // 错误示例 1: 未使用 key
2 function InefficientListWithoutKey(props) {
3 const names = props.names;
4 const listItems = names.map((name) => <li>{name}</li>); // 没有 key 属性
5 return <ul>{listItems}</ul>;
6 }
7
8 // 错误示例 2: 使用 index 作为 key (当列表顺序可能变化时)
9 function InefficientListWithIndexKey(props) {
10 const names = props.names;
11 const listItems = names.map((name, index) => <li key={index}>{name}</li>); // 使用 index 作为 key
12 return <ul>{listItems}</ul>;
13 }

正确示例:使用数据项的唯一 ID 作为 Key

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 // 正确示例: 使用数据项的唯一 ID 作为 key
2 function EfficientListWithDataIdKey(props) {
3 const items = props.items;
4 const listItems = items.map((item) => <li key={item.id}>{item.text}</li>); // 使用 item.id 作为 key
5 return <ul>{listItems}</ul>;
6 }

本章总结

本章深入讲解了 React 中的条件渲染和列表渲染。条件渲染使得我们可以根据条件动态展示不同的 UI 内容,React 提供了多种条件渲染方式,如 if/else 语句、三元运算符、逻辑与运算符和组件变量。列表渲染使得我们可以高效地渲染动态数据列表,map() 方法是列表渲染的核心,而 key 属性对于列表渲染的性能和组件状态保持至关重要。掌握条件渲染和列表渲染,是构建动态、交互式 React 应用的基础。下一章,我们将学习 React 中的表单处理。🚀

REVIEW PASS

7. chapter 7: 表单 (Form) 处理

表单 (Form) 是 Web 应用中用户交互的重要组成部分。无论是用户注册、登录、信息填写还是数据搜索,都离不开表单的应用。React 提供了强大的表单处理机制,允许开发者构建交互性强、用户体验良好的表单。本章将深入探讨 React 中的表单处理,包括受控组件 (Controlled Components) 与非受控组件 (Uncontrolled Components) 的概念,以及表单验证与提交等关键技术。

7.1 受控组件与非受控组件

在 React 中处理表单元素(如 <input><textarea><select> 等)时,有两种主要的方式:受控组件 (Controlled Components)非受控组件 (Uncontrolled Components)。这两种方式在数据管理和组件行为控制上有着本质的区别。

受控组件 (Controlled Components)

在受控组件中,表单元素的值 (value) 由 React 组件的 State (状态) 管理。当用户在表单元素中输入内容时,会触发 事件处理函数 (Event Handler),事件处理函数会更新组件的 State,State 的变化会触发组件的重新渲染,从而更新表单元素的值。简而言之,表单元素的值始终由 React State 驱动

  • 特点
    ▮▮▮▮⚝ State 驱动:表单元素的值与组件的 State 紧密绑定,State 是唯一的数据源。
    ▮▮▮▮⚝ 实时控制:可以实时控制表单元素的 value 属性,例如实现输入验证、格式化输入等功能。
    ▮▮▮▮⚝ React 单向数据流:符合 React 的单向数据流原则,数据流向清晰可控。
    ▮▮▮▮⚝ 代码量稍多:需要编写更多的事件处理函数来更新 State。

  • 示例:受控的文本输入框

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 import React, { useState } from 'react';
2
3 function ControlledInput() {
4 const [inputValue, setInputValue] = useState(''); // 使用 State 管理输入框的值
5
6 const handleChange = (event) => {
7 setInputValue(event.target.value); // 事件处理函数更新 State
8 };
9
10 return (
11 <div>
12 <input
13 type="text"
14 value={inputValue} // value 属性绑定 State
15 onChange={handleChange} // onChange 事件绑定事件处理函数
16 />
17 <p>输入的值: {inputValue}</p>
18 </div>
19 );
20 }

在这个例子中,inputValue State 管理着输入框的值。handleChange 函数在输入框内容改变时被触发,通过 setInputValue 更新 inputValue State。input 元素的 value 属性绑定了 inputValue State,onChange 事件绑定了 handleChange 函数,从而实现了受控组件。

  • 受控组件的优势场景
    ▮▮▮▮⚝ 需要对用户输入进行实时验证或格式化处理。
    ▮▮▮▮⚝ 需要根据用户输入动态更新 UI。
    ▮▮▮▮⚝ 需要完全控制表单元素的值和行为。

非受控组件 (Uncontrolled Components)

在非受控组件中,表单元素的值由 DOM 自身管理。React 组件并不直接控制表单元素的值,而是通过 Ref (引用) 获取表单元素在 DOM 中的值。用户在表单元素中的输入,直接反映在 DOM 元素上,React 组件只是在需要时通过 Ref 获取 DOM 中的值。简而言之,表单元素的值由 DOM 自身维护

  • 特点
    ▮▮▮▮⚝ DOM 驱动:表单元素的值由 DOM 自身维护,React 组件不直接控制。
    ▮▮▮▮⚝ 代码简洁:代码量较少,不需要编写过多的事件处理函数。
    ▮▮▮▮⚝ 操作 DOM:需要使用 Ref 直接操作 DOM 元素,获取表单值。
    ▮▮▮▮⚝ 与原生 HTML 表单类似:更接近原生 HTML 表单的行为模式。

  • 示例:非受控的文本输入框

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 import React, { useRef } from 'react';
2
3 function UncontrolledInput() {
4 const inputRef = useRef(null); // 创建 Ref 对象
5
6 const handleSubmit = (event) => {
7 event.preventDefault(); // 阻止默认的表单提交行为
8 alert(`输入的值: ${inputRef.current.value}`); // 通过 Ref 获取 input 元素的值
9 };
10
11 return (
12 <form onSubmit={handleSubmit}>
13 <input type="text" ref={inputRef} /> {/* ref 属性绑定 Ref 对象 */}
14 <button type="submit">提交</button>
15 </form>
16 );
17 }

在这个例子中,inputRef Ref 对象绑定到 input 元素。handleSubmit 函数在表单提交时被触发,通过 inputRef.current.value 直接获取 input 元素在 DOM 中的值。input 元素的值由 DOM 自身维护,React 组件只是在需要时通过 Ref 获取。

  • 非受控组件的优势场景
    ▮▮▮▮⚝ 需要集成第三方 DOM 库。
    ▮▮▮▮⚝ 处理文件上传等复杂表单元素。
    ▮▮▮▮⚝ 将 React 代码集成到非 React 的环境中,需要与原生 DOM 交互。
    ▮▮▮▮⚝ 某些性能优化的场景,避免频繁的 State 更新。

受控组件 vs 非受控组件 的选择

特性受控组件 (Controlled Components)非受控组件 (Uncontrolled Components)
数据源React StateDOM
数据流单向数据流 (State -> UI -> Event -> State)数据流向不明显 (DOM -> Event -> 获取 DOM 值)
控制权完全控制表单元素的值和行为部分控制,主要通过 DOM 自身管理
代码量稍多,需要编写事件处理函数更新 State较少,代码更简洁
适用场景大部分表单场景,需要实时控制和验证的场景集成第三方 DOM 库、文件上传、与原生 DOM 交互、某些性能优化场景
心智负担较高,需要理解 State 和事件处理机制较低,更接近原生 HTML 表单,但 React 理念有所偏离
推荐程度推荐,更符合 React 的理念,数据流清晰,易于维护和测试不推荐,除非有特殊需求,代码可读性和可维护性稍差,与 React 理念有所偏离

总结

在大多数情况下,推荐使用受控组件。受控组件更符合 React 的单向数据流原则,使得表单数据的管理和控制更加清晰可控,易于进行表单验证、格式化输入等操作,也更易于进行单元测试。非受控组件在某些特殊场景下可能更适用,但需要权衡其优缺点,谨慎选择。

7.2 表单验证与提交

表单验证 (Form Validation) 是确保用户输入数据符合预期的重要步骤。表单提交 (Form Submission) 是将用户填写的数据发送到服务器进行处理的过程。React 表单处理中,表单验证和提交通常与受控组件结合使用,以实现更精细的控制和更好的用户体验。

表单验证 (Form Validation)

表单验证可以在客户端 (Client-side) 和服务端 (Server-side) 进行。客户端验证可以提高用户体验,及时反馈错误信息,减少不必要的服务器请求。服务端验证是必不可少的,可以确保数据的安全性和完整性。本节主要讨论客户端表单验证。

  • 客户端表单验证的类型
    ▮▮▮▮⚝ 必填项验证 (Required Field Validation):确保必填字段不能为空。
    ▮▮▮▮⚝ 格式验证 (Format Validation):验证输入内容是否符合特定的格式要求,例如邮箱格式、电话号码格式、密码强度等。
    ▮▮▮▮⚝ 长度验证 (Length Validation):限制输入内容的长度,例如最小长度、最大长度。
    ▮▮▮▮⚝ 数值范围验证 (Range Validation):验证输入数值是否在指定的范围内。
    ▮▮▮▮⚝ 自定义验证 (Custom Validation):根据业务需求,进行自定义的验证逻辑。

  • 在受控组件中实现表单验证

在受控组件中,可以在事件处理函数中进行表单验证,并使用 State 来管理验证错误信息。

示例:带验证的表单

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 import React, { useState } from 'react';
2
3 function ValidatedForm() {
4 const [name, setName] = useState('');
5 const [email, setEmail] = useState('');
6 const [password, setPassword] = useState('');
7 const [errors, setErrors] = useState({}); // State 管理错误信息
8
9 const validateForm = () => {
10 let tempErrors = {};
11 if (!name) {
12 tempErrors.name = '姓名不能为空';
13 }
14 if (!email) {
15 tempErrors.email = '邮箱不能为空';
16 } else if (!/\S+@\S+\.\S+/.test(email)) { // 邮箱格式验证
17 tempErrors.email = '邮箱格式不正确';
18 }
19 if (!password) {
20 tempErrors.password = '密码不能为空';
21 } else if (password.length < 6) { // 密码长度验证
22 tempErrors.password = '密码长度不能少于 6 位';
23 }
24 setErrors(tempErrors);
25 return Object.keys(tempErrors).length === 0; // 返回表单是否验证通过
26 };
27
28 const handleSubmit = (event) => {
29 event.preventDefault();
30 if (validateForm()) { // 提交前进行表单验证
31 alert('表单提交成功!');
32 // 发送表单数据到服务器 ...
33 } else {
34 alert('表单验证失败,请检查错误信息。');
35 }
36 };
37
38 return (
39 <form onSubmit={handleSubmit}>
40 <div>
41 <label htmlFor="name">姓名:</label>
42 <input
43 type="text"
44 id="name"
45 value={name}
46 onChange={(e) => setName(e.target.value)}
47 />
48 {errors.name && <p className="error">{errors.name}</p>} {/* 显示错误信息 */}
49 </div>
50 <div>
51 <label htmlFor="email">邮箱:</label>
52 <input
53 type="email"
54 id="email"
55 value={email}
56 onChange={(e) => setEmail(e.target.value)}
57 />
58 {errors.email && <p className="error">{errors.email}</p>} {/* 显示错误信息 */}
59 </div>
60 <div>
61 <label htmlFor="password">密码:</label>
62 <input
63 type="password"
64 id="password"
65 value={password}
66 onChange={(e) => setPassword(e.target.value)}
67 />
68 {errors.password && <p className="error">{errors.password}</p>} {/* 显示错误信息 */}
69 </div>
70 <button type="submit">提交</button>
71 </form>
72 );
73 }

在这个例子中,validateForm 函数包含了表单验证逻辑,验证姓名、邮箱和密码的必填性、邮箱格式和密码长度。errors State 管理错误信息。在 handleSubmit 函数中,先调用 validateForm 进行表单验证,如果验证通过,则提交表单,否则显示错误提示信息。错误信息会在对应的输入框下方显示。

表单提交 (Form Submission)

表单提交是将用户填写的数据发送到服务器进行处理的过程。在 React 中,表单提交通常在 form 元素的 onSubmit 事件处理函数中进行。

  • 阻止默认提交行为

在 React 中处理表单提交时,通常需要调用 event.preventDefault() 来阻止浏览器默认的表单提交行为 (页面跳转或刷新)。然后,在事件处理函数中自定义表单提交的逻辑,例如使用 fetchaxios 等库发送 AJAX 请求。

  • 发送表单数据到服务器

表单数据可以以多种格式发送到服务器,常见的格式有:

▮▮▮▮⚝ URL-encoded (application/x-www-form-urlencoded):默认的表单提交格式,数据以键值对的形式编码在 URL 中或请求体中。
▮▮▮▮⚝ JSON (application/json):将表单数据转换为 JSON 格式发送。
▮▮▮▮⚝ Multipart/form-data (multipart/form-data):用于上传文件,可以包含文本和二进制数据。

示例:表单提交到服务器

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 import React, { useState } from 'react';
2
3 function ContactForm() {
4 const [name, setName] = useState('');
5 const [email, setEmail] = useState('');
6 const [message, setMessage] = useState('');
7 const [submissionStatus, setSubmissionStatus] = useState(null); // State 管理提交状态
8
9 const handleSubmit = async (event) => {
10 event.preventDefault();
11 setSubmissionStatus('loading'); // 设置提交状态为加载中
12
13 try {
14 const response = await fetch('/api/contact', { // 发送 POST 请求到服务器
15 method: 'POST',
16 headers: {
17 'Content-Type': 'application/json', // 设置请求头为 JSON 格式
18 },
19 body: JSON.stringify({ name, email, message }), // 将表单数据转换为 JSON 字符串
20 });
21
22 if (response.ok) {
23 setSubmissionStatus('success'); // 设置提交状态为成功
24 // 清空表单 ...
25 setName('');
26 setEmail('');
27 setMessage('');
28 } else {
29 setSubmissionStatus('error'); // 设置提交状态为错误
30 }
31 } catch (error) {
32 console.error('提交错误:', error);
33 setSubmissionStatus('error'); // 设置提交状态为错误
34 }
35 };
36
37 return (
38 <form onSubmit={handleSubmit}>
39 {submissionStatus === 'loading' && <p>提交中...</p>} {/* 显示加载状态 */}
40 {submissionStatus === 'success' && <p className="success">提交成功!</p>} {/* 显示成功状态 */}
41 {submissionStatus === 'error' && <p className="error">提交失败请稍后重试</p>} {/* 显示错误状态 */}
42
43 <div>
44 <label htmlFor="name">姓名:</label>
45 <input
46 type="text"
47 id="name"
48 value={name}
49 onChange={(e) => setName(e.target.value)}
50 />
51 </div>
52 <div>
53 <label htmlFor="email">邮箱:</label>
54 <input
55 type="email"
56 id="email"
57 value={email}
58 onChange={(e) => setEmail(e.target.value)}
59 />
60 </div>
61 <div>
62 <label htmlFor="message">留言:</label>
63 <textarea
64 id="message"
65 value={message}
66 onChange={(e) => setMessage(e.target.value)}
67 />
68 </div>
69 <button type="submit" disabled={submissionStatus === 'loading'}> {/* 提交按钮加载中禁用 */}
70 提交
71 </button>
72 </form>
73 );
74 }

在这个例子中,handleSubmit 函数使用 fetch API 发送 POST 请求到 /api/contact 接口,将表单数据以 JSON 格式发送到服务器。submissionStatus State 管理表单提交状态 (loading, success, error),并根据状态显示不同的 UI 反馈。提交按钮在加载中状态时被禁用,防止重复提交。

本章总结

本章深入探讨了 React 中的表单处理,重点介绍了受控组件和非受控组件的概念、特点和适用场景,并深入讲解了如何在受控组件中实现表单验证和提交。掌握表单处理是构建交互式 Web 应用的关键技能。合理选择受控组件或非受控组件,并结合表单验证和提交技术,可以构建出用户体验良好、功能完善的 React 表单。下一章,我们将学习 React 中的路由管理。🚀

REVIEW PASS

8. chapter 8: 路由 (Routing) 管理

在构建复杂的单页应用程序 (SPA) 时,路由 (Routing) 管理至关重要。路由负责管理应用程序的不同视图 (Views) 或页面 (Pages),并允许用户在这些视图之间进行导航,而无需重新加载整个页面。React Router 是 React 生态系统中最流行的路由库,它提供了一套强大的工具,用于声明式地配置和管理 React 应用的路由。本章将深入介绍 React Router 的核心概念和使用方法,助你掌握 React 应用的路由管理技能。

8.1 React Router 介绍与安装

React Router 是一个为 React 应用设计的声明式路由库。它允许你定义应用的 URL 路径 (Paths) 与组件 (Components) 之间的映射关系,并在用户导航到不同路径时,渲染相应的组件。React Router 提供了多种路由模式和组件,以适应不同的应用场景。

为什么需要路由?

在传统的 Web 应用中,每个 URL 路径通常对应一个服务器端的页面,浏览器通过请求不同的 URL 获取不同的 HTML 页面。而在单页应用程序 (SPA) 中,整个应用只有一个 HTML 页面,所有的 UI 组件都运行在同一个页面中。路由的作用就是在 SPA 中模拟多页面的导航体验,实现以下目标:

组织应用结构 (Organize Application Structure):将应用的不同功能模块组织成不同的页面或视图,使得应用结构更清晰、易于维护。
模拟多页面导航 (Simulate Multi-Page Navigation):允许用户通过浏览器地址栏、前进/后退按钮、链接等方式在不同视图之间导航,就像在多页面应用中一样。
状态管理 (State Management):路由可以与状态管理库结合使用,实现基于路由的状态持久化和共享。
代码分割 (Code Splitting):可以将不同的路由路径对应的组件进行代码分割,提高应用的加载性能。

React Router 的核心概念

React Router 的核心概念包括:

Router 组件 (Router Components):例如 <BrowserRouter><HashRouter><MemoryRouter>。Router 组件是路由系统的根组件,它负责监听浏览器 URL 的变化,并根据配置的路由规则进行匹配和渲染。不同的 Router 组件适用于不同的路由模式 (例如:BrowserRouter 使用 HTML5 History API,HashRouter 使用 URL Hash)。
Route 组件 (Route Components):例如 <Route>。Route 组件用于定义 URL 路径与组件之间的映射关系。每个 <Route> 组件都关联一个路径 (path) 和一个组件 (component)。当浏览器 URL 匹配到 Route 组件定义的路径时,该 Route 组件关联的组件会被渲染。
Navigation 组件 (Navigation Components):例如 <Link><NavLink>。Navigation 组件用于在应用中创建导航链接,用户点击链接时,会触发路由切换,渲染新的组件。
Hook (Hooks):例如 useNavigateuseParamsuseLocation。React Router 提供了 Hooks,用于在函数式组件中访问路由信息和进行路由操作,例如:程序化导航、获取路由参数、获取当前 URL 等。

安装 React Router

在开始使用 React Router 之前,需要先安装 React Router 的核心库。React Router 提供了多个库,用于不同的应用场景。最常用的库是 react-router-dom,它包含了用于 Web 浏览器环境的 Router 组件和 Hooks。

使用 npm 安装 react-router-dom

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 npm install react-router-dom

或使用 yarn 安装:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 yarn add react-router-dom

安装完成后,就可以在 React 应用中引入和使用 React Router 的组件和 Hooks 了。

8.2 配置路由:定义页面路径与组件的映射

配置路由是使用 React Router 的核心步骤。配置路由的目标是定义应用的 URL 路径 (Paths) 与组件 (Components) 之间的映射关系。React Router 使用声明式的方式配置路由,通过 <BrowserRouter><Routes><Route> 组件来实现路由配置。

<BrowserRouter> 组件

<BrowserRouter> 组件是 React Router 提供的 Router 组件之一,它使用 HTML5 History API ( pushState, replaceState ) 来管理路由历史记录。<BrowserRouter> 适用于大多数 Web 应用场景,它能够创建干净的 URL 路径,例如 example.com/usersexample.com/products,而不是带有 # 号的 Hash URL。

基本用法

<BrowserRouter> 组件作为应用的最外层容器包裹起来,通常在 index.jsApp.js 文件中使用。

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 // index.js App.js
2 import React from 'react';
3 import ReactDOM from 'react-dom/client';
4 import App from './App';
5 import { BrowserRouter } from 'react-router-dom'; // 引入 BrowserRouter
6
7 const root = ReactDOM.createRoot(document.getElementById('root'));
8 root.render(
9 <React.StrictMode>
10 <BrowserRouter> {/* 使用 BrowserRouter 包裹 App 组件 */}
11 <App />
12 </BrowserRouter>
13 </React.StrictMode>
14 );

<Routes> 组件和 <Route> 组件

<Routes> 组件用于包裹一组 <Route> 组件。<Routes> 组件会独占式地匹配其子组件 <Route>。也就是说,当浏览器 URL 匹配到多个 <Route> 组件定义的路径时,<Routes> 只会渲染第一个匹配成功的 <Route> 组件。

<Route> 组件用于定义具体的路由规则,它接收以下主要 props:

  • path (string):定义 URL 路径规则。可以使用动态路由参数 (例如 :id)。
  • element (React.ReactNode):指定路径匹配成功时渲染的 React 组件。

基本用法

<BrowserRouter> 组件内部使用 <Routes> 组件包裹一组 <Route> 组件,定义应用的路由规则。

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 // App.js
2 import React from 'react';
3 import { Routes, Route } from 'react-router-dom'; // 引入 Routes Route
4 import Home from './pages/Home';
5 import About from './pages/About';
6 import Products from './pages/Products';
7
8 function App() {
9 return (
10 <div>
11 <Routes> {/* 使用 Routes 组件包裹 Route 组件 */}
12 <Route path="/" element={<Home />} /> {/* 定义路径 "/" 对应的组件为 Home */}
13 <Route path="/about" element={<About />} /> {/* 定义路径 "/about" 对应的组件为 About */}
14 <Route path="/products" element={<Products />} /> {/* 定义路径 "/products" 对应的组件为 Products */}
15 </Routes>
16 </div>
17 );
18 }

在这个例子中,定义了三个路由规则:

  • 路径 / 匹配时,渲染 Home 组件。
  • 路径 /about 匹配时,渲染 About 组件。
  • 路径 /products 匹配时,渲染 Products 组件。

当浏览器 URL 变为 //about/products 时,React Router 会根据配置的路由规则,渲染相应的组件。

路由匹配规则

React Router 的路由匹配是按顺序进行的。<Routes> 组件会遍历其子组件 <Route>,并从上到下依次匹配浏览器 URL 与 <Route> 组件定义的 path。一旦找到第一个匹配成功的 <Route> 组件,就会渲染该组件,并停止后续的匹配。

路径匹配规则

  • 精确匹配 (Exact Match):默认情况下,<Route path> 进行的是前缀匹配 (Prefix Match)。例如,<Route path="/users"> 会匹配 /users/users/123/users/settings 等以 /users 开头的路径。如果需要进行精确匹配 (Exact Match),可以使用 end 属性: <Route path="/" end element={<Home />} />,这样只有路径 / 才能匹配成功,/users/ 等路径将不会匹配。
  • 动态路由参数 (Dynamic Route Parameters):可以使用冒号 : 开头的路径段定义动态路由参数。例如,<Route path="/products/:id" element={<ProductDetail />} /> 中的 :id 就是一个动态路由参数。当 URL 匹配到 /products/123/products/456 等路径时,ProductDetail 组件会被渲染,并且可以通过 useParams Hook 获取到参数 id 的值 (例如 id: "123"id: "456")。
  • 通配符 * (Wildcard):可以使用通配符 * 匹配任意路径。通常用于定义 404 Not Found 页面。例如,<Route path="*" element={<NotFound />} /> 会匹配所有前面 <Route> 组件没有匹配到的路径。通配符 <Route> 应该放在 <Routes> 组件的最后,作为默认的匹配规则。

示例:更复杂的路由配置

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 // App.js
2 import React from 'react';
3 import { Routes, Route } from 'react-router-dom';
4 import Home from './pages/Home';
5 import About from './pages/About';
6 import Products from './pages/Products';
7 import ProductDetail from './pages/ProductDetail';
8 import NotFound from './pages/NotFound';
9
10 function App() {
11 return (
12 <div>
13 <Routes>
14 <Route path="/" element={<Home />} /> {/* 精确匹配路径 "/" */}
15 <Route path="/about" element={<About />} />
16 <Route path="/products" element={<Products />} />
17 <Route path="/products/:id" element={<ProductDetail />} /> {/* 动态路由参数 :id */}
18 <Route path="*" element={<NotFound />} /> {/* 通配符 "*" 匹配所有未匹配路径404 页面 */}
19 </Routes>
20 </div>
21 );
22 }

在这个例子中,增加了动态路由 /products/:id 和通配符路由 /* 的配置。当 URL 为 /products/123 时,会渲染 ProductDetail 组件,并可以通过 useParams Hook 获取到 id 参数的值为 "123"。当 URL 为前面所有路由规则都无法匹配的路径时 (例如 /settings/admin 等),会渲染 NotFound 组件,显示 404 页面。

8.3 导航与路由参数

路由配置完成后,需要提供导航方式,让用户可以在应用的不同页面之间跳转。React Router 提供了 <Link> 组件用于声明式导航,以及 useNavigate Hook 用于程序化导航。同时,还需要学习如何获取路由参数,以便在组件中动态地获取 URL 中的参数值。

<Link> 组件:声明式导航

<Link> 组件用于创建声明式导航链接。它类似于 HTML 的 <a> 标签,但 <Link> 组件在点击时,会阻止浏览器默认的页面跳转行为,而是通过 React Router 内部机制进行路由切换,实现 SPA 的无刷新页面跳转。

基本用法

<Link> 组件使用 to prop 指定要跳转的路径。

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 import React from 'react';
2 import { Link } from 'react-router-dom'; // 引入 Link
3
4 function Navigation() {
5 return (
6 <nav>
7 <ul>
8 <li>
9 <Link to="/">首页</Link> {/* 跳转到路径 "/" */}
10 </li>
11 <li>
12 <Link to="/about">关于我们</Link> {/* 跳转到路径 "/about" */}
13 </li>
14 <li>
15 <Link to="/products">产品列表</Link> {/* 跳转到路径 "/products" */}
16 </li>
17 </ul>
18 </nav>
19 );
20 }

在这个例子中,<Link to="/">首页</Link> 创建了一个链接,点击后会跳转到路径 /<Link to="/about">关于我们</Link><Link to="/products">产品列表</Link> 同理。

<NavLink> 组件:高亮当前激活链接

<NavLink> 组件是 <Link> 组件的增强版,它在 <Link> 的基础上,增加了高亮当前激活链接的功能。当 <NavLink> 组件的 to prop 与当前浏览器 URL 匹配时,<NavLink> 组件会被添加一个 active 类名 (默认类名,可以自定义)。可以利用这个 active 类名,使用 CSS 样式高亮当前激活的导航链接,提升用户体验。

基本用法

<NavLink> 组件的使用方式与 <Link> 组件类似,也使用 to prop 指定跳转路径。

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 import React from 'react';
2 import { NavLink } from 'react-router-dom'; // 引入 NavLink
3
4 function Navigation() {
5 return (
6 <nav>
7 <ul>
8 <li>
9 <NavLink to="/" className={({ isActive }) => isActive ? 'active' : ''}>首页</NavLink> {/* 使用 NavLink并根据 isActive 动态添加类名 */}
10 </li>
11 <li>
12 <NavLink to="/about" className={({ isActive }) => isActive ? 'active' : ''}>关于我们</NavLink>
13 </li>
14 <li>
15 <NavLink to="/products" className={({ isActive }) => isActive ? 'active' : ''}>产品列表</NavLink>
16 </li>
17 </ul>
18 </nav>
19 );
20 }

在这个例子中,<NavLink> 组件使用了 className prop,并传入一个函数。该函数接收一个 isActive 参数,当 <NavLink>to prop 与当前 URL 匹配时,isActivetrue,否则为 false。根据 isActive 的值,动态地添加或不添加 active 类名。可以自定义 CSS 样式,为 .active 类名的链接设置高亮样式。

useNavigate Hook:程序化导航

useNavigate Hook 提供了程序化导航的能力。它返回一个 navigate 函数,可以调用该函数进行路由跳转。程序化导航通常用于在 JavaScript 代码中根据某些条件或事件触发路由跳转,例如:表单提交成功后跳转到首页、登录成功后跳转到用户中心等。

基本用法

在函数式组件中,调用 useNavigate() Hook 获取 navigate 函数,然后调用 navigate(path) 函数进行路由跳转。

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 import React from 'react';
2 import { useNavigate } from 'react-router-dom'; // 引入 useNavigate
3
4 function LoginButton() {
5 const navigate = useNavigate(); // 获取 navigate 函数
6
7 const handleLogin = () => {
8 // ... 执行登录逻辑 ...
9 // 登录成功后程序化跳转到首页
10 navigate('/'); // 调用 navigate('/') 跳转到路径 "/"
11 };
12
13 return (
14 <button onClick={handleLogin}>登录</button>
15 );
16 }

在这个例子中,LoginButton 组件在登录成功后,调用 navigate('/') 函数,程序化跳转到首页。navigate 函数还支持第二个参数,用于传递路由状态 (state) 和跳转选项 (options),例如:

  • navigate('/products', { replace: true }):使用 replace: true 选项,表示使用 history.replaceState 进行跳转,替换当前的路由历史记录,而不是 push 新的记录。
  • navigate('/product/123', { state: { from: '/products' } }):使用 state 选项,传递路由状态数据,可以在目标组件中使用 useLocation Hook 获取到 state 数据。

useParams Hook:获取路由参数

useParams Hook 用于在函数式组件中获取动态路由参数。它返回一个对象,包含了当前匹配的路由参数键值对。useParams Hook 只能在动态路由 <Route path="/products/:id"> 对应的组件及其子组件中使用。

基本用法

在动态路由组件中,调用 useParams() Hook 获取路由参数对象,然后通过参数名访问参数值。

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 // ProductDetail.js
2 import React from 'react';
3 import { useParams } from 'react-router-dom'; // 引入 useParams
4
5 function ProductDetail() {
6 const { id } = useParams(); // 获取路由参数对象解构出 id 参数
7
8 return (
9 <div>
10 <h1>产品详情</h1>
11 <p>产品 ID: {id}</p> {/* 显示产品 ID */}
12 {/* ... 根据产品 ID 获取产品详情数据并渲染 ... */}
13 </div>
14 );
15 }

在这个例子中,ProductDetail 组件通过 useParams() Hook 获取到路由参数对象,并解构出 id 参数。id 参数的值就是 URL 中动态路由段 :id 匹配到的值 (例如,如果 URL 是 /products/123,则 id 的值为 "123" )。

本章总结

本章深入介绍了 React Router 的核心概念和使用方法,包括 React Router 的介绍与安装、路由配置 (使用 <BrowserRouter><Routes><Route>)、导航 (使用 <Link><NavLink>useNavigate>) 和路由参数获取 (使用 useParams>)。掌握 React Router 的路由管理技能,可以构建出结构清晰、导航流畅、用户体验良好的 React 单页应用程序。下一章,我们将学习 React 中的状态管理方案。🚀

REVIEW PASS

9. chapter 9: 状态管理方案

在 React 应用中,组件之间的数据共享和状态管理是一个核心问题。对于简单的应用,Props 和 State 已经足够满足需求。然而,当应用变得复杂,组件层级加深,跨组件共享状态的需求增多时,传统的 Props 逐层传递 (Prop Drilling) 会变得繁琐且难以维护。为了解决这些问题,React 生态系统中涌现出了多种状态管理方案。本章将深入探讨 React 提供的 Context API,以及流行的状态管理库 Redux 和 Redux Toolkit,并简要介绍一些轻量级状态管理库,帮助你根据不同的应用场景选择合适的方案。

9.1 Context API 深入:复杂状态管理的替代方案

我们在第五章已经初步接触了 Context API,了解了它如何用于跨组件共享主题 (Theme) 等全局数据。Context API 不仅可以用于简单的全局数据共享,也可以作为一种轻量级的状态管理方案,尤其适用于中小型应用局部复杂状态管理的场景。本节将深入探讨 Context API 的高级用法和适用场景。

9.1.1 Context API 的高级用法

除了基本的数据共享功能,Context API 还提供了一些高级用法,使其在状态管理方面更具灵活性。

9.1.1.1 动态 Context 值

Context Provider 的 value prop 可以是动态的,这意味着 Provider 提供的 Context 值可以根据组件的 State 或 Props 动态变化。当 Context 值发生变化时,所有消费该 Context 的组件都会重新渲染,从而实现状态的动态更新。

示例:动态 Context 值实现计数器

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 import React, { createContext, useContext, useState } from 'react';
2
3 // 创建 CounterContext
4 const CounterContext = createContext({
5 count: 0,
6 increment: () => {},
7 decrement: () => {},
8 });
9
10 function CounterProvider({ children }) {
11 const [count, setCount] = useState(0);
12
13 const increment = () => {
14 setCount(prevCount => prevCount + 1);
15 };
16
17 const decrement = () => {
18 setCount(prevCount => prevCount - 1);
19 };
20
21 const value = { // 动态 Context 包含 count 和更新函数
22 count,
23 increment,
24 decrement,
25 };
26
27 return (
28 <CounterContext.Provider value={value}>
29 {children}
30 </CounterContext.Provider>
31 );
32 }
33
34 function CounterDisplay() {
35 const { count } = useContext(CounterContext); // 消费 count
36
37 return (
38 <p>当前计数: {count}</p>
39 );
40 }
41
42 function CounterButtons() {
43 const { increment, decrement } = useContext(CounterContext); // 消费 increment decrement 函数
44
45 return (
46 <div>
47 <button onClick={increment}>增加</button>
48 <button onClick={decrement}>减少</button>
49 </div>
50 );
51 }
52
53 function App() {
54 return (
55 <CounterProvider> {/* 使用 CounterProvider 提供 Context */}
56 <div>
57 <CounterDisplay />
58 <CounterButtons />
59 </div>
60 </CounterProvider>
61 );
62 }

在这个例子中,CounterProvider 组件使用 useState 管理 count 状态,并将 count 值和 incrementdecrement 更新函数作为 Context 值提供给子组件。CounterDisplay 组件和 CounterButtons 组件分别消费 count 值和更新函数。当点击按钮时,incrementdecrement 函数被调用,更新 Context 值,CounterDisplay 组件会重新渲染,显示最新的计数。

9.1.1.2 多层 Context Provider

可以在组件树中嵌套使用多个 Context Provider,为不同的子树提供不同的 Context 值。内层的 Context Provider 会覆盖外层的 Provider 提供的 Context 值,但只会影响内层 Provider 子树中的组件。

示例:嵌套 Context Provider

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 import React, { createContext, useContext } from 'react';
2
3 const ThemeContext = createContext('light');
4 const LanguageContext = createContext('en');
5
6 function App() {
7 return (
8 <ThemeContext.Provider value="dark"> {/* 外层 ThemeContext Provider 提供 "dark" 主题 */}
9 <LanguageContext.Provider value="zh"> {/* 外层 LanguageContext Provider 提供 "zh" 语言 */}
10 <Layout />
11 </LanguageContext.Provider>
12 </ThemeContext.Provider>
13 );
14 }
15
16 function Layout() {
17 return (
18 <ThemeContext.Provider value="light"> {/* 内层 ThemeContext Provider 提供 "light" 主题覆盖外层 */}
19 <Content />
20 </ThemeContext.Provider>
21 );
22 }
23
24 function Content() {
25 const theme = useContext(ThemeContext); // 消费 ThemeContext
26 const language = useContext(LanguageContext); // 消费 LanguageContext
27
28 return (
29 <div>
30 <p>Theme: {theme}</p> {/* ThemeContext 的值将是内层 Provider 提供的 "light" */}
31 <p>Language: {language}</p> {/* LanguageContext 的值将是外层 Provider 提供的 "zh" */}
32 </div>
33 );
34 }

在这个例子中,Layout 组件使用了内层的 ThemeContext.Provider,将 ThemeContext 的值覆盖为 "light"。因此,Content 组件消费到的 ThemeContext 值是 "light",而 LanguageContext 的值仍然是外层 Provider 提供的 "zh"

9.1.1.3 Context Consumer 组件 (Class Component)

除了 useContext Hook,React 还提供了 Context.Consumer 组件,用于在类组件 (Class Component) 中消费 Context 值。Context.Consumer 组件使用 render props 模式,接收一个函数作为子组件 (child as a function),该函数接收当前的 Context 值作为参数,并返回需要渲染的 React 元素。

示例:使用 Context.Consumer 组件

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 import React, { createContext } from 'react';
2
3 const MessageContext = createContext('默认消息');
4
5 class MessageDisplay extends React.Component {
6 render() {
7 return (
8 <MessageContext.Consumer> {/* 使用 MessageContext.Consumer 组件 */}
9 {value => ( // render props 函数value Context
10 <p>消息: {value}</p>
11 )}
12 </MessageContext.Consumer>
13 );
14 }
15 }
16
17 function App() {
18 return (
19 <MessageContext.Provider value="Context 共享的消息">
20 <MessageDisplay />
21 </MessageContext.Provider>
22 );
23 }

在这个例子中,MessageDisplay 类组件使用 MessageContext.Consumer 组件消费 MessageContext 的值。render props 函数 value => <p>消息: {value}</p> 接收 Context 值 value,并渲染 <p> 元素。

9.1.2 Context API 的适用场景与局限性

Context API 作为一种状态管理方案,具有其独特的适用场景和局限性。

适用场景

全局配置类状态共享:例如:主题 (Theme)、语言 (Language)、用户认证信息 (Authentication Info) 等,这些状态通常需要在应用的多个组件中共享和访问,且更新频率较低。
组件库/UI 框架内部状态管理:Context API 非常适合用于组件库或 UI 框架内部的状态管理,例如:管理组件的主题样式、国际化配置等。
中小型应用的状态管理:对于中小型应用,如果状态逻辑不太复杂,Context API 可以作为一种轻量级的状态管理方案,避免引入额外的状态管理库。
局部复杂状态管理:对于组件树中某个局部模块的复杂状态管理,可以使用 Context API 将状态和状态更新逻辑封装在 Provider 组件中,提高代码的可维护性。

局限性

大型应用状态管理复杂性:对于大型应用,状态逻辑通常非常复杂,组件之间的状态依赖关系错综复杂。Context API 缺乏 Redux 等状态管理库提供的中央化状态管理、时间旅行调试、中间件等高级功能,难以有效地管理大型应用的状态。
性能优化挑战:当 Context Provider 的 value prop 发生变化时,所有消费该 Context 的组件都会重新渲染。如果 Context 值变化频繁,或者消费 Context 的组件数量很多,可能会导致性能问题。需要谨慎使用 Context 共享大量或频繁变化的数据。
调试和追踪状态变化困难:相比 Redux 等状态管理库提供的 DevTools 工具,Context API 缺乏直观的状态变化追踪和调试工具,当应用状态逻辑复杂时,调试和追踪状态变化会比较困难。

总结

Context API 是一种强大的全局状态共享机制,可以作为轻量级的状态管理方案,尤其适用于中小型应用或特定场景。但对于大型、复杂应用的状态管理,Redux 等更专业的状态管理库可能更合适。在选择状态管理方案时,需要根据应用的规模、复杂度和团队的技术栈等因素进行综合考虑。 ⚖️

9.2 Redux 核心概念:Store, Reducer, Action

Redux 是一个独立于 React 的 JavaScript 状态管理库,但它与 React 结合使用非常广泛,成为了 React 生态系统中最流行的状态管理方案之一。Redux 遵循 Flux 架构模式,通过 单向数据流 (Unidirectional Data Flow)可预测的状态容器 (Predictable State Container) 来管理应用的状态。本节将深入介绍 Redux 的核心概念:Store (仓库), Reducer (Reducer 函数), Action (Action 对象)。

9.2.1 Store (仓库)

Store 是 Redux 应用的核心,它是一个全局的状态容器 (Global State Container),负责存储整个应用的状态 (State)。Store 具有以下特点:

单一数据源 (Single Source of Truth):整个应用的状态都存储在一个 Store 中,保证了数据的一致性和可预测性。
状态只读 (State is Read-only):Store 中的状态是只读的 (Read-only),不能直接修改。任何状态的修改都必须通过 dispatch Action (派发 Action) 来触发。
使用 Reducer 函数更新状态 (State Updates via Reducers):状态的更新逻辑由 Reducer 函数 (Reducer Functions) 定义。Reducer 函数接收当前的 State 和一个 Action,根据 Action 的类型 (type) 返回新的 State

Store 的主要方法

  • getState():获取当前的 State。
  • dispatch(action):派发 Action,触发状态更新。
  • subscribe(listener):订阅 Store 的状态变化,当状态发生变化时,listener 函数会被调用。
  • replaceReducer(nextReducer):替换当前的 Reducer 函数 (用于代码分割和热重载)。

创建 Store

使用 Redux 提供的 createStore(reducer, [preloadedState], [enhancer]) 函数创建 Store。

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 import { createStore } from 'redux';
2 import rootReducer from './reducers'; // 引入 Reducer 函数
3
4 const store = createStore(rootReducer); // 创建 Store,传入 Reducer 函数
  • reducer (Reducer 函数):必须传入的参数,用于定义状态更新逻辑。
  • preloadedState (可选):初始 State,用于服务端渲染或状态持久化。
  • enhancer (可选):Store Enhancer,用于增强 Store 的功能,例如:应用中间件 (Middleware)、Redux DevTools 等。

9.2.2 Reducer (Reducer 函数)

Reducer 函数是 Redux 应用中负责状态更新的核心函数。Reducer 函数是一个纯函数 (Pure Function),它接收当前的 State 和一个 Action,根据 Action 的类型 (type) 返回新的 State

Reducer 函数的特点

纯函数 (Pure Function):Reducer 函数必须是一个纯函数,即:
▮▮▮▮⚝ 相同的输入,永远得到相同的输出 (给定相同的 State 和 Action,总是返回相同的 New State)。
▮▮▮▮⚝ 没有副作用 (Side Effects),不修改入参 (State),只返回新的 State。
接收 State 和 Action 参数:Reducer 函数接收两个参数:
▮▮▮▮⚝ state:当前的 State,首次调用时为 undefined,需要设置初始 State。
▮▮▮▮⚝ action:Action 对象,包含了状态更新的信息。
根据 Action 类型更新 State:Reducer 函数根据 Action 对象的 type 属性,判断 Action 的类型,并根据不同的类型执行不同的状态更新逻辑。
返回新的 State:Reducer 函数必须返回新的 State,而不是修改原有的 State。在 Redux 中,State 的不可变性 (Immutability) 是非常重要的原则。

Reducer 函数的示例

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 // counterReducer.js
2 const initialState = {
3 count: 0,
4 };
5
6 function counterReducer(state = initialState, action) { // 设置初始 State
7 switch (action.type) {
8 case 'INCREMENT':
9 return {
10 ...state, // 复制原 State
11 count: state.count + 1, // 更新 count 值,返回新的 State
12 };
13 case 'DECREMENT':
14 return {
15 ...state,
16 count: state.count - 1,
17 };
18 default:
19 return state; // 默认情况下返回原 State
20 }
21 }
22
23 export default counterReducer;

在这个例子中,counterReducer 函数接收 stateaction 参数。根据 action.type 的值,判断 Action 类型:

  • 如果 action.type'INCREMENT',则返回一个新的 State,count 值加 1。
  • 如果 action.type'DECREMENT',则返回一个新的 State,count 值减 1。
  • 如果 action.type 不是以上两种类型,则返回原 State (默认情况)。

合并 Reducer 函数 (Combine Reducers)

当应用的状态比较复杂时,可以将 Reducer 函数拆分成多个小的 Reducer 函数,分别负责管理 State 的不同部分。然后使用 Redux 提供的 combineReducers(reducers) 函数将这些小的 Reducer 函数合并成一个根 Reducer 函数 (Root Reducer)

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 // reducers/index.js
2 import { combineReducers } from 'redux';
3 import counterReducer from './counterReducer';
4 import userReducer from './userReducer';
5
6 const rootReducer = combineReducers({ // 合并 Reducer 函数
7 counter: counterReducer, // counter 状态由 counterReducer 管理
8 user: userReducer, // user 状态由 userReducer 管理
9 });
10
11 export default rootReducer;

combineReducers 函数接收一个对象作为参数,对象的 key 是 State 的 key,value 是对应的 Reducer 函数。合并后的根 Reducer 函数返回的 State 结构,与传入 combineReducers 的对象结构一致。

9.2.3 Action (Action 对象)

Action 对象是 Redux 应用中状态更新的唯一来源。Action 对象是一个普通的 JavaScript 对象,它描述了发生了什么 (What happened)。Action 对象必须包含一个 type 属性,用于标识 Action 的类型。Action 对象还可以包含 payload 属性,用于携带状态更新所需的数据 (可选)。

Action 对象的特点

描述 "发生了什么" (Describe "What happened"):Action 对象描述的是一个事件,例如:"增加计数"、"用户登录"、"获取商品列表" 等。
必须包含 type 属性type 属性是一个字符串常量,用于标识 Action 的类型。通常使用大写字母和下划线命名 type 常量,例如:INCREMENTUSER_LOGINFETCH_PRODUCTS_REQUEST
可以包含 payload 属性 (可选)payload 属性用于携带状态更新所需的数据,例如:用户 ID、商品列表数据等。payload 属性的命名可以自定义,通常使用 payloaddata
Action 对象是普通的 JavaScript 对象:Action 对象只是一个普通的 JavaScript 对象,没有任何特殊的方法或属性。

Action 创建函数 (Action Creators)

为了方便创建 Action 对象,通常会定义 Action 创建函数 (Action Creators)。Action 创建函数是一个函数,它返回一个 Action 对象。Action 创建函数可以接受参数,用于动态地生成 Action 对象的 payload 属性。

Action 创建函数示例

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 // actions/counterActions.js
2 export const INCREMENT = 'INCREMENT'; // 定义 Action type 常量
3 export const DECREMENT = 'DECREMENT';
4
5 export const increment = () => ({ // increment Action 创建函数,返回 INCREMENT Action
6 type: INCREMENT,
7 });
8
9 export const decrement = () => ({ // decrement Action 创建函数,返回 DECREMENT Action
10 type: DECREMENT,
11 });
12
13 // actions/userActions.js
14 export const USER_LOGIN_REQUEST = 'USER_LOGIN_REQUEST';
15 export const USER_LOGIN_SUCCESS = 'USER_LOGIN_SUCCESS';
16 export const USER_LOGIN_FAILURE = 'USER_LOGIN_FAILURE';
17
18 export const loginRequest = (username, password) => ({ // loginRequest Action 创建函数,携带 payload
19 type: USER_LOGIN_REQUEST,
20 payload: { username, password },
21 });
22
23 export const loginSuccess = (userInfo) => ({ // loginSuccess Action 创建函数,携带 payload
24 type: USER_LOGIN_SUCCESS,
25 payload: userInfo,
26 });
27
28 export const loginFailure = (error) => ({ // loginFailure Action 创建函数,携带 payload
29 type: USER_LOGIN_FAILURE,
30 payload: error,
31 });

派发 Action (Dispatching Actions)

在组件中需要更新状态时,需要派发 Action (Dispatch Action)。通过调用 store.dispatch(action) 方法派发 Action。dispatch 方法会将 Action 对象传递给 Reducer 函数,Reducer 函数根据 Action 类型更新 State,Store 的状态更新后,所有订阅了 Store 的组件都会重新渲染。

派发 Action 的示例

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 import React from 'react';
2 import { useDispatch, useSelector } from 'react-redux'; // 引入 Redux Hooks
3 import { increment, decrement } from './actions/counterActions'; // 引入 Action 创建函数
4
5 function CounterComponent() {
6 const count = useSelector(state => state.counter.count); // 使用 useSelector Hook 获取 count 状态
7 const dispatch = useDispatch(); // 使用 useDispatch Hook 获取 dispatch 函数
8
9 const handleIncrement = () => {
10 dispatch(increment()); // 派发 increment Action
11 };
12
13 const handleDecrement = () => {
14 dispatch(decrement()); // 派发 decrement Action
15 };
16
17 return (
18 <div>
19 <p>当前计数: {count}</p>
20 <button onClick={handleIncrement}>增加</button>
21 <button onClick={handleDecrement}>减少</button>
22 </div>
23 );
24 }

在这个例子中,CounterComponent 组件使用 useDispatch Hook 获取 dispatch 函数,使用 useSelector Hook 获取 count 状态。当点击按钮时,handleIncrementhandleDecrement 函数被调用,通过 dispatch(increment())dispatch(decrement()) 派发 Action。Redux Store 接收到 Action 后,会调用 Reducer 函数更新状态,CounterComponent 组件会重新渲染,显示最新的计数。

Redux 数据流 (Redux Data Flow)

Redux 的核心思想是 单向数据流 (Unidirectional Data Flow)。Redux 数据流的流程如下:

组件派发 Action (Dispatch Action):组件通过 dispatch(action) 派发 Action。
Action 到达 Reducer (Action Reaches Reducer):Action 对象被传递给 Reducer 函数。
Reducer 更新 State (Reducer Updates State):Reducer 函数根据 Action 类型,返回新的 State。
Store 更新状态 (Store Updates State):Store 接收到 Reducer 返回的新 State,更新 Store 的状态。
组件订阅状态变化 (Components Subscribe to State Changes):组件通过 useSelector Hook 订阅 Store 的状态变化。当 Store 的状态更新时,订阅了状态的组件会重新渲染,获取最新的 State 值。

Redux 通过单向数据流,保证了状态变化的可预测性和可追踪性,使得应用的状态管理更加规范和可维护。 🔄

9.3 Redux Toolkit:简化 Redux 开发

Redux Toolkit (RTK) 是 Redux 官方推荐的 Redux 开发工具集。RTK 旨在简化 Redux 开发,提供了一系列工具函数和 API,用于减少 Redux 样板代码 (Boilerplate Code),并提供最佳实践。RTK 包含以下核心模块:

configureStore():简化 Store 的配置,内置了 Redux DevTools 和常用的中间件。
createSlice():简化 Reducer 和 Action 创建,自动生成 Action Creators 和 Action Types。
createAsyncThunk():简化异步 Action 的处理,例如:数据请求、异步操作等。
createSelector():优化 Selector 性能,避免不必要的组件渲染。

9.3.1 configureStore():简化 Store 配置

configureStore() 函数是 RTK 提供的用于创建 Store 的函数,它相比 Redux 原生的 createStore() 函数,具有以下优势:

简化配置:只需要传入 Root Reducer,即可创建 Store,无需手动配置中间件和 Enhancer。
内置 Redux DevTools:默认集成了 Redux DevTools 扩展,方便调试。
默认添加常用中间件:默认添加了 redux-thunk 中间件 (用于处理异步 Action) 和 ImmutableStateInvariantMiddlewareSerializableStateInvariantMiddleware (用于检测 State 突变和不可序列化值)。

使用 configureStore() 创建 Store

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 // store/index.js
2 import { configureStore } from '@reduxjs/toolkit'; // 引入 configureStore
3 import rootReducer from './reducers';
4
5 const store = configureStore({ // 使用 configureStore 创建 Store
6 reducer: rootReducer, // 传入 Root Reducer
7 // middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(customMiddleware), // 自定义中间件 (可选)
8 // devTools: process.env.NODE_ENV !== 'production', // 禁用 Redux DevTools (生产环境) (可选)
9 });
10
11 export default store;

configureStore() 函数接收一个配置对象作为参数,常用的配置项包括:

  • reducer (Reducer 函数):必须传入的参数,Root Reducer 函数。
  • middleware (Function):可选参数,用于自定义中间件。可以使用 getDefaultMiddleware() 获取默认的中间件数组,并进行扩展。
  • devTools (boolean):可选参数,用于控制是否启用 Redux DevTools。默认为 true (开发环境启用)。

9.3.2 createSlice():简化 Reducer 和 Action 创建

createSlice() 函数是 RTK 提供的用于简化 Reducer 和 Action 创建的核心函数。createSlice() 可以根据传入的配置对象,自动生成 Reducer 函数、Action Types 和 Action Creators,极大地减少了 Redux 的样板代码。

使用 createSlice() 创建 Slice

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 // features/counterSlice.js
2 import { createSlice } from '@reduxjs/toolkit'; // 引入 createSlice
3
4 const counterSlice = createSlice({ // 使用 createSlice 创建 Slice
5 name: 'counter', // Slice 的名称,作为 Action Type 的前缀
6 initialState: { // Slice 的初始 State
7 value: 0,
8 },
9 reducers: { // Reducer 函数集合,key 为 Action Name,value 为 Reducer 函数
10 increment: (state) => { // increment Reducer 函数,直接修改 State (Immer.js 支持)
11 state.value += 1;
12 },
13 decrement: (state) => { // decrement Reducer 函数
14 state.value -= 1;
15 },
16 incrementByAmount: (state, action) => { // incrementByAmount Reducer 函数,接收 payload
17 state.value += action.payload;
18 },
19 },
20 });
21
22 export const { increment, decrement, incrementByAmount } = counterSlice.actions; // 导出 Action Creators
23 export default counterSlice.reducer; // 导出 Reducer 函数

createSlice() 函数接收一个配置对象作为参数,常用的配置项包括:

  • name (string):Slice 的名称,作为 Action Type 的前缀。例如,如果 name'counter',则 increment Action 的 Type 为 'counter/increment'
  • initialState (any):Slice 的初始 State。
  • reducers (Reducers Map):Reducer 函数集合。key 为 Action Name,value 为 Reducer 函数。在 Reducer 函数中,可以直接修改 State,RTK 内部使用了 Immer.js 库,实现了 State 的不可变更新。reducers 对象中的每个函数都会自动生成对应的 Action Creator。

createSlice() 返回一个 Slice 对象,包含了 actionsreducer 属性:

  • slice.actions:包含了自动生成的 Action Creators。例如,counterSlice.actions.increment() 返回一个 Action 对象 { type: 'counter/increment' }
  • slice.reducer:自动生成的 Reducer 函数。

使用 createSlice() 可以极大地简化 Reducer 和 Action 的创建过程,减少样板代码,提高开发效率。🚀

9.3.3 createAsyncThunk():简化异步 Action 处理

createAsyncThunk() 函数是 RTK 提供的用于简化异步 Action 处理的函数。createAsyncThunk() 可以自动处理异步 Action 的 pending (进行中)fulfilled (成功)rejected (失败) 三种状态,并生成对应的 Action Types 和 Action Creators。createAsyncThunk() 内部使用了 redux-thunk 中间件。

使用 createAsyncThunk() 创建异步 Thunk Action

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 // features/userSlice.js
2 import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'; // 引入 createAsyncThunk
3
4 // 创建异步 Thunk Action:fetchUserById
5 export const fetchUserById = createAsyncThunk( // 使用 createAsyncThunk 创建异步 Thunk Action
6 'users/fetchUserById', // Action Type 前缀
7 async (userId, thunkAPI) => { // payloadCreator 回调函数,处理异步逻辑
8 const response = await fetch(`https://api.example.com/users/${userId}`);
9 const data = await response.json();
10 return data; // 返回 resolved payload,会触发 fulfilled Action
11 // return thunkAPI.rejectWithValue(error); // 返回 rejected payload,会触发 rejected Action
12 }
13 );
14
15 const userSlice = createSlice({
16 name: 'users',
17 initialState: {
18 user: null,
19 loading: 'idle', // 'idle' | 'pending' | 'success' | 'error'
20 error: null,
21 },
22 reducers: {},
23 extraReducers: (builder) => { // 处理 extra reducers,例如异步 Action 的状态
24 builder
25 .addCase(fetchUserById.pending, (state) => { // 处理 pending 状态
26 state.loading = 'pending';
27 })
28 .addCase(fetchUserById.fulfilled, (state, action) => { // 处理 fulfilled 状态
29 state.loading = 'success';
30 state.user = action.payload; // 更新 user 状态
31 })
32 .addCase(fetchUserById.rejected, (state, action) => { // 处理 rejected 状态
33 state.loading = 'error';
34 state.error = action.error.message; // 更新 error 状态
35 });
36 },
37 });
38
39 export default userSlice.reducer;

createAsyncThunk() 函数接收两个参数:

  • actionTypePrefix (string):Action Type 前缀。例如,如果 actionTypePrefix'users/fetchUserById',则 pending Action Type 为 'users/fetchUserById/pending'fulfilled Action Type 为 'users/fetchUserById/fulfilled'rejected Action Type 为 'users/fetchUserById/rejected'
  • payloadCreator (Async Function):Payload Creator 回调函数,用于处理异步逻辑。该函数接收两个参数:
    ▮▮▮▮⚝ arg:派发 Thunk Action 时传入的参数,例如 dispatch(fetchUserById(userId)) 中的 userId
    ▮▮▮▮⚝ thunkAPI:Thunk API 对象,包含一些有用的方法和属性,例如:dispatchgetStateextrarequestIdsignalrejectWithValue
    ▮▮▮▮payloadCreator 函数应该返回一个 Promise。如果 Promise resolved,返回值会作为 fulfilled Action 的 payload;如果 Promise rejected,可以使用 thunkAPI.rejectWithValue(error) 返回 rejected payload,触发 rejected Action。

createSlice()extraReducers 配置项中,可以使用 builder callback API 处理异步 Thunk Action 的 pending、fulfilled 和 rejected 状态。builder 对象提供 addCase(actionCreator, reducer) 方法,用于添加 extra reducers。actionCreator 可以是 createAsyncThunk() 返回的 Action Creator,例如 fetchUserById.pendingfetchUserById.fulfilledfetchUserById.rejected

使用 createAsyncThunk() 可以极大地简化异步 Action 的处理,减少异步 Action 的样板代码,提高异步状态管理的可维护性。 🛠️

9.4 Zustand / Recoil 等轻量级状态管理库

除了 Context API 和 Redux/Redux Toolkit,React 生态系统中还涌现出许多轻量级的状态管理库,例如:Zustand, Recoil, Jotai, Valtio, Hookstate 等。这些库通常具有以下特点:

轻量级 (Lightweight):库的体积小,API 简洁,学习成本低。
易用性 (Easy to Use):API 设计简洁直观,易于上手和使用。
高性能 (Performant):针对 React 的渲染机制进行了优化,具有良好的性能。
函数式 API (Functional API):大多采用 Hook-based API,与 React Hooks 完美结合。
无样板代码 (Less Boilerplate):相比 Redux,减少了大量的样板代码。

9.4.1 Zustand

Zustand 是一个小型、快速、可扩展的 React 状态管理库。它基于 Flux 原则,但 API 设计非常简洁,学习曲线平缓。Zustand 的核心概念是 Store (仓库)Selector (选择器)

Zustand 的特点

极简 API:只有一个核心 API create(set => store),用于创建 Store。
Hook-based API:使用 useStore(selector) Hook 消费 Store 中的状态。
高性能:基于订阅模式,只重新渲染订阅了状态变化的组件。
体积小:gzip 后只有 ~1KB。
支持中间件:可以扩展中间件,例如:devtools, persist, immer 等。

Zustand 的基本用法

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 import React from 'react';
2 import create from 'zustand'; // 引入 zustand
3
4 // 创建 Zustand Store
5 const useStore = create(set => ({ // create 函数接收 set 函数用于更新状态
6 count: 0,
7 increment: () => set(state => ({ count: state.count + 1 })), // 使用 set 函数更新状态
8 decrement: () => set(state => ({ count: state.count - 1 })),
9 }));
10
11 function CounterDisplay() {
12 const count = useStore(state => state.count); // 使用 useStore Hook 消费 count 状态
13
14 return (
15 <p>当前计数: {count}</p>
16 );
17 }
18
19 function CounterButtons() {
20 const increment = useStore(state => state.increment); // 使用 useStore Hook 消费 increment 函数
21 const decrement = useStore(state => state.decrement); // 使用 useStore Hook 消费 decrement 函数
22
23 return (
24 <div>
25 <button onClick={increment}>增加</button>
26 <button onClick={decrement}>减少</button>
27 </div>
28 );
29 }

在这个例子中,useStore Hook 创建了一个 Zustand Store,包含了 count 状态和 incrementdecrement 更新函数。CounterDisplay 组件和 CounterButtons 组件分别使用 useStore Hook 消费 count 状态和更新函数。

Zustand 以其极简的 API、高性能和易用性,成为了轻量级状态管理库的热门选择。 👍

9.4.2 Recoil

Recoil 是 Facebook 官方推出的 React 状态管理库,它旨在解决 React 在大规模应用中状态管理的挑战。Recoil 采用了一种原子化 (Atomic) 的状态管理模型,将状态拆分成 Atoms (原子)Selectors (选择器)

Recoil 的特点

原子化状态管理:将状态拆分成独立的 Atoms,组件可以直接订阅 Atom,实现细粒度的状态更新。
Selector 计算衍生状态:使用 Selector 从 Atoms 或其他 Selectors 计算衍生状态,避免重复计算。
数据流图 (Data-flow Graph):Recoil 内部维护一个数据流图,追踪状态的依赖关系,实现高效的状态更新。
时间旅行调试:Recoil DevTools 提供了时间旅行调试功能。
并发模式兼容:Recoil 专门为 React Concurrent Mode 设计。

Recoil 的基本用法

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 import React from 'react';
2 import { atom, useRecoilState, useRecoilValue } from 'recoil'; // 引入 recoil
3
4 // 创建 Recoil Atom
5 const countState = atom({ // atom 函数创建 Atom
6 key: 'countState', // Atom 的唯一 key
7 default: 0, // Atom 的默认值
8 });
9
10 function CounterDisplay() {
11 const count = useRecoilValue(countState); // 使用 useRecoilValue Hook 消费 Atom
12
13 return (
14 <p>当前计数: {count}</p>
15 );
16 }
17
18 function CounterButtons() {
19 const [count, setCount] = useRecoilState(countState); // 使用 useRecoilState Hook 消费 Atom 值和更新函数
20
21 const increment = () => {
22 setCount(prevCount => prevCount + 1);
23 };
24
25 const decrement = () => {
26 setCount(prevCount => prevCount - 1);
27 };
28
29 return (
30 <div>
31 <button onClick={increment}>增加</button>
32 <button onClick={decrement}>减少</button>
33 </div>
34 );
35 }
36
37 function App() {
38 return (
39 <RecoilRoot> {/* 使用 RecoilRoot 包裹应用 */}
40 <div>
41 <CounterDisplay />
42 <CounterButtons />
43 </div>
44 </RecoilRoot>
45 );
46 }

在这个例子中,countState Atom 使用 atom() 函数创建,表示计数状态。CounterDisplay 组件使用 useRecoilValue(countState) Hook 消费 countState Atom 的值。CounterButtons 组件使用 useRecoilState(countState) Hook 消费 countState Atom 的值和更新函数。需要使用 <RecoilRoot> 组件包裹应用的最外层组件。

Recoil 以其原子化的状态管理模型、高性能和时间旅行调试等特性,成为了大型 React 应用状态管理的新选择。 ✨

选择建议

  • 小型应用或简单状态管理:Context API 或 Zustand 可能更合适,API 简洁,易于上手。
  • 中大型应用或复杂状态管理:Redux/Redux Toolkit 或 Recoil 更合适,功能更强大,生态更完善。
  • 性能敏感型应用:Recoil 或 Zustand 在性能方面通常表现更优。
  • 团队技术栈和偏好:团队熟悉 Redux 生态,可以选择 Redux Toolkit;追求更简洁、现代化的 API,可以选择 Zustand 或 Recoil。

在选择状态管理方案时,需要综合考虑应用的规模、复杂度、性能需求、团队技术栈等因素,选择最适合当前项目的方案。 🧐

本章总结

本章深入探讨了 React 应用的状态管理方案,包括 Context API 的高级用法和适用场景,Redux 的核心概念 (Store, Reducer, Action) 和 Redux Toolkit 的简化开发工具,以及 Zustand 和 Recoil 等轻量级状态管理库的介绍。掌握这些状态管理方案,可以让你更好地管理 React 应用的状态,构建更复杂、可维护的应用。下一章,我们将学习 React 中的样式处理。🚀

REVIEW PASS

10. chapter 10: 样式 (Styling) 处理

为 React 应用添加样式 (Styling) 是构建用户友好的用户界面的重要环节。React 提供了多种样式处理方案,从传统的 CSS 到 CSS-in-JS,再到原子化 CSS 框架,开发者可以根据项目需求和个人偏好选择合适的方案。本章将深入探讨 React 中常用的样式处理方案,包括行内样式 (Inline Styles)、CSS Modules、Styled Components、Tailwind CSS 以及 CSS 预处理器 (Sass/Less),助你掌握 React 应用的样式处理技巧。

10.1 行内样式 (Inline Styles) 与 CSS Modules

行内样式 (Inline Styles) 和 CSS Modules 是两种常见的 React 样式处理方案。行内样式直接在 JSX 元素上使用 style 属性设置样式,CSS Modules 则通过模块化的方式管理 CSS 样式,避免样式冲突。

10.1.1 行内样式 (Inline Styles)

行内样式 (Inline Styles) 是最简单的 React 样式处理方案之一。它直接在 JSX 元素的 style 属性中,使用 JavaScript 对象来定义元素的样式。

行内样式的基本用法

在 JSX 元素上,使用 style 属性,并赋值一个 JavaScript 对象。对象的 key 是 CSS 属性名 (使用驼峰命名法,例如 backgroundColor, fontSize),value 是 CSS 属性值 (字符串或数字)。

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 import React from 'react';
2
3 function InlineStyleExample() {
4 const style = { // 定义样式对象
5 color: 'blue',
6 fontSize: '16px',
7 backgroundColor: '#f0f0f0',
8 padding: '10px',
9 border: '1px solid #ccc',
10 borderRadius: '5px',
11 };
12
13 return (
14 <div style={style}> {/* 使用 style 属性设置行内样式 */}
15 <p>This is a paragraph with inline styles.</p>
16 </div>
17 );
18 }

在这个例子中,定义了一个 style 对象,包含了多个 CSS 属性和值。然后将 style 对象赋值给 <div> 元素的 style 属性,从而为 <div> 元素设置了行内样式。

行内样式的优点与缺点

优点

简单直接:使用简单直接,易于理解和上手。
样式作用域局限在组件内部:样式只作用于当前组件,不会影响其他组件,避免了样式冲突。
动态样式:可以方便地根据组件的 Props 或 State 动态生成样式。

缺点

可维护性差:当样式复杂时,JSX 代码会变得臃肿,难以维护和阅读。
代码复用性差:样式难以复用,需要在每个需要相同样式的元素上重复定义。
不支持 CSS 伪类和媒体查询:行内样式不支持 CSS 伪类 (例如 :hover, :active) 和媒体查询 (Media Queries) 等高级 CSS 特性。
性能问题:每次组件渲染都可能重新计算行内样式,可能影响性能 (虽然在大多数情况下影响不大)。

适用场景

  • 简单的、局部的样式:例如,为某个特定的元素设置简单的样式,且样式不需要复用。
  • 动态样式:需要根据组件状态或 props 动态改变样式的场景。

不适用场景

  • 复杂的样式:样式规则较多,需要复用,或需要使用 CSS 高级特性。
  • 大型项目:难以维护和管理大量的行内样式。

10.1.2 CSS Modules

CSS Modules 是一种模块化的 CSS 方案,它可以将 CSS 样式限定在组件作用域内,避免全局样式污染和样式冲突。CSS Modules 通过编译时 (Compile-time) 的方式,将 CSS 类名 (Class Names) 转换为唯一的哈希值,并在 JavaScript 代码中通过对象的方式引用这些哈希值化的类名。

配置 CSS Modules

在使用 CSS Modules 之前,需要配置构建工具 (例如 webpack 或 Vite) 以支持 CSS Modules。如果使用 Create React App (CRA) 或 Vite 创建的项目,CSS Modules 已经默认配置好,无需额外配置。

创建 CSS Modules 文件

CSS Modules 文件的命名约定是 [组件名].module.css[组件名].module.scss (如果使用 Sass)。例如,Button.module.css

编写 CSS Modules 样式

在 CSS Modules 文件中,编写普通的 CSS 样式,类名可以使用驼峰命名法 (camelCase)短横线命名法 (kebab-case)

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 /* Button.module.css */
2 .button { /* 类名使用短横线命名法 */
3 background-color: #4CAF50;
4 color: white;
5 padding: 10px 20px;
6 border: none;
7 border-radius: 5px;
8 cursor: pointer;
9 }
10
11 .button:hover {
12 background-color: #367c39;
13 }
14
15 .primaryButton { /* 类名使用驼峰命名法 */
16 background-color: #007bff;
17 }
18
19 .primaryButton:hover {
20 background-color: #0056b3;
21 }

在 React 组件中使用 CSS Modules

在 React 组件中,需要先 import CSS Modules 文件。import 的结果是一个 styles 对象,styles 对象的 key 是 CSS Modules 文件中定义的类名,value 是编译后生成的唯一的哈希值化的类名

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 import React from 'react';
2 import styles from './Button.module.css'; // 引入 CSS Modules 文件命名为 styles 对象
3
4 function Button(props) {
5 return (
6 <div>
7 <button className={styles.button}> {/* 使用 styles.button 引用类名 */}
8 Default Button
9 </button>
10 <button className={`${styles.button} ${styles.primaryButton}`}> {/* 组合多个类名 */}
11 Primary Button
12 </button>
13 </div>
14 );
15 }

在这个例子中,import styles from './Button.module.css' 引入了 CSS Modules 文件,并将 import 结果赋值给 styles 对象。在 JSX 中,使用 className={styles.button} 引用了 CSS Modules 文件中定义的 .button 类名。编译后,styles.button 的值会是一个唯一的哈希值化的类名,例如 "Button_button__abc12".

CSS Modules 的优点与缺点

优点

模块化:CSS 样式限定在组件作用域内,避免全局样式污染和样式冲突。
类名唯一性:编译后生成的类名是唯一的,无需担心类名冲突。
局部作用域:CSS Modules 默认是局部作用域的,只作用于当前组件。
可以使用 CSS 预处理器:可以与 CSS 预处理器 (例如 Sass, Less) 结合使用。
易于维护:样式文件和组件代码分离,代码结构清晰,易于维护。

缺点

学习成本:需要学习 CSS Modules 的使用方式和配置。
动态样式稍复杂:动态样式需要使用 JavaScript 表达式拼接类名。
运行时开销:编译时需要生成哈希值化的类名,运行时需要处理类名映射关系,有一定的运行时开销 (但通常很小)。

适用场景

  • 中大型项目:需要模块化管理 CSS 样式,避免全局样式污染和样式冲突。
  • 组件库/UI 框架:构建可复用的组件库或 UI 框架。
  • 需要使用 CSS 预处理器:项目需要使用 Sass 或 Less 等 CSS 预处理器。

不适用场景

  • 小型项目:项目规模小,样式简单,全局样式冲突风险较低。
  • 需要频繁动态修改样式:动态样式场景较多,使用 CSS-in-JS 方案可能更方便。

总结

行内样式和 CSS Modules 是两种各有优缺点的 React 样式处理方案。行内样式简单直接,适用于简单的、局部的样式和动态样式场景;CSS Modules 模块化程度高,避免样式冲突,适用于中大型项目和组件库。在实际项目中,可以根据项目需求和团队偏好选择合适的方案,或将两者结合使用。 ⚖️

10.2 Styled Components:CSS-in-JS 的方案

Styled Components 是一种流行的 CSS-in-JS 库,它允许你在 JavaScript 代码中编写 CSS 样式,并将样式与 React 组件关联起来。Styled Components 使用 Tagged Templates (标签模板字符串) 语法,使得 CSS 样式代码更接近原生 CSS,并提供了强大的主题 (Theme) 和动态样式 (Dynamic Styles) 功能。

安装 Styled Components

使用 npm 安装 Styled Components:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 npm install styled-components

或使用 yarn 安装:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 yarn add styled-components

Styled Components 的基本用法

使用 styled.[HTML 标签名]styled(ReactComponent) 创建 Styled Component。Styled Component 是一个 React 组件,它包含了预定义的样式。

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 import React from 'react';
2 import styled from 'styled-components'; // 引入 styled-components
3
4 // 创建 Styled Button 组件
5 const StyledButton = styled.button` // 使用 styled.button 创建 Styled Component
6 background-color: #4CAF50;
7 color: white;
8 padding: 10px 20px;
9 border: none;
10 border-radius: 5px;
11 cursor: pointer;
12
13 &:hover { // 使用 & 符号引用自身实现 CSS 嵌套
14 background-color: #367c39;
15 }
16 `;
17
18 function StyledComponentsExample() {
19 return (
20 <div>
21 <StyledButton>Default Button</StyledButton> {/* 使用 StyledButton 组件 */}
22 </div>
23 );
24 }

在这个例子中,styled.button 创建了一个名为 StyledButton 的 Styled Component,它渲染一个 <button> 元素,并应用了定义的 CSS 样式。StyledButton 组件可以直接在 JSX 中使用,就像普通的 React 组件一样。

动态样式 (Dynamic Styles)

Styled Components 支持动态样式,可以根据组件的 Props 动态地修改样式。在 Styled Component 的模板字符串中,可以使用 props 函数 访问组件的 Props,并根据 Props 的值动态生成样式。

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 import React from 'react';
2 import styled from 'styled-components';
3
4 // 创建带动态样式的 Styled Button 组件
5 const DynamicStyledButton = styled.button`
6 background-color: ${props => props.primary ? '#007bff' : '#4CAF50'}; // 根据 props.primary 动态设置背景色
7 color: white;
8 padding: 10px 20px;
9 border: none;
10 border-radius: 5px;
11 cursor: pointer;
12
13 &:hover {
14 background-color: ${props => props.primary ? '#0056b3' : '#367c39'}; // 动态设置 hover 时的背景色
15 }
16 `;
17
18 function DynamicStyledComponentsExample() {
19 return (
20 <div>
21 <DynamicStyledButton>Default Button</DynamicStyledButton> {/* 默认 Button */}
22 <DynamicStyledButton primary>Primary Button</DynamicStyledButton> {/* primary Button */}
23 </div>
24 );
25 }

在这个例子中,DynamicStyledButton 组件的 background-color:hover 时的 background-color 样式都使用了 props 函数 props => props.primary ? '...' : '...'。根据 props.primary 的值,动态地设置不同的背景色。

主题 (Theme)

Styled Components 提供了强大的主题 (Theme) 功能,可以将应用的全局样式变量 (例如:颜色、字体、间距等) 集中管理,并在 Styled Components 中通过 theme 对象 访问和使用这些变量。

定义 Theme

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 // theme.js
2 export const theme = {
3 colors: {
4 primary: '#007bff',
5 secondary: '#6c757d',
6 success: '#28a745',
7 error: '#dc3545',
8 },
9 fonts: {
10 primary: 'Arial, sans-serif',
11 secondary: 'Roboto, sans-serif',
12 },
13 spacing: {
14 small: '8px',
15 medium: '16px',
16 large: '24px',
17 },
18 };

使用 ThemeProvider 组件提供 Theme

在应用的最外层组件 (通常是 App.jsindex.js),使用 ThemeProvider 组件包裹应用,并将 Theme 对象作为 theme prop 传递给 ThemeProvider

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 // App.js
2 import React from 'react';
3 import { ThemeProvider } from 'styled-components'; // 引入 ThemeProvider
4 import { theme } from './theme'; // 引入 Theme 对象
5 import Button from './Button'; // 假设 Button 组件使用了 Theme
6
7 function App() {
8 return (
9 <ThemeProvider theme={theme}> {/* 使用 ThemeProvider 提供 Theme */}
10 <div>
11 <Button primary>Primary Button</Button> {/* Button 组件可以使用 Theme */}
12 </div>
13 </ThemeProvider>
14 );
15 }

在 Styled Components 中使用 Theme

在 Styled Component 的模板字符串中,可以通过 theme 对象 访问 Theme 中定义的样式变量。

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 // Button.js
2 import styled from 'styled-components';
3
4 const ThemedButton = styled.button`
5 background-color: ${props => props.theme.colors.primary}; // 使用 theme.colors.primary 访问主题颜色
6 color: white;
7 padding: ${props => props.theme.spacing.medium}; // 使用 theme.spacing.medium 访问主题间距
8 border: none;
9 border-radius: 5px;
10 font-family: ${props => props.theme.fonts.primary}; // 使用 theme.fonts.primary 访问主题字体
11 cursor: pointer;
12
13 &:hover {
14 background-color: ${props => props.theme.colors.primary}; // 动态设置 hover 时的背景色
15 opacity: 0.8;
16 }
17 `;

在这个例子中,ThemedButton 组件使用了 props.theme.colors.primaryprops.theme.spacing.mediumprops.theme.fonts.primary 访问 Theme 中定义的颜色、间距和字体变量。

Styled Components 的优点与缺点

优点

组件化:样式与组件紧密绑定,样式作用域局限在组件内部,避免样式冲突。
动态样式:可以方便地根据 Props 或 Theme 动态生成样式。
主题 (Theme):提供强大的主题功能,方便管理和复用全局样式变量。
CSS 代码组织:将 CSS 代码写在 JavaScript 文件中,更符合组件化的开发模式。
CSS 高级特性:支持 CSS 预处理器语法 (例如 Sass 嵌套、变量、mixin 等)。

缺点

学习成本:需要学习 CSS-in-JS 的概念和 Styled Components 的 API。
运行时开销:运行时需要动态生成 CSS 样式,并注入到 DOM 中,有一定的运行时开销 (虽然在大多数情况下影响不大)。
CSS 代码分散在 JavaScript 文件中:CSS 代码分散在各个 JavaScript 文件中,可能不符合传统 CSS 开发者的习惯。
与其他 CSS 方案集成稍复杂:与其他 CSS 方案 (例如 CSS Modules, Tailwind CSS) 集成可能稍复杂。

适用场景

  • 中大型项目:需要组件化管理样式,使用主题和动态样式功能。
  • 组件库/UI 框架:构建可复用的组件库或 UI 框架。
  • 前端工程化程度较高的项目:团队接受 CSS-in-JS 的开发模式。

不适用场景

  • 小型项目:项目规模小,样式简单,引入 Styled Components 可能会增加不必要的复杂度。
  • 团队技术栈偏传统 CSS:团队习惯传统的 CSS 开发模式,学习和迁移成本较高。
  • 需要与其他 CSS 方案深度集成:项目需要与 CSS Modules 或 Tailwind CSS 等其他 CSS 方案深度集成。

总结

Styled Components 是一种强大的 CSS-in-JS 方案,它提供了组件化、动态样式、主题等高级功能,适用于中大型项目和组件库。但 Styled Components 也存在一定的学习成本和运行时开销。在选择样式处理方案时,需要根据项目需求、团队技术栈和性能需求等因素进行权衡。 ⚖️

10.3 Tailwind CSS:原子化 CSS 框架

Tailwind CSS 是一种 原子化 CSS (Utility-First CSS) 框架。与传统的 CSS 框架 (例如 Bootstrap, Materialize CSS) 不同,Tailwind CSS 不提供预定义的组件样式,而是提供了一套细粒度的原子化 CSS 类名 (例如 text-center, bg-blue-500, p-4, m-2)。开发者可以通过组合这些原子化类名,快速、灵活地构建自定义的组件样式。

安装 Tailwind CSS

使用 npm 安装 Tailwind CSS:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 npm install -D tailwindcss postcss autoprefixer
2 npx tailwindcss init -p

或使用 yarn 安装:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 yarn add -D tailwindcss postcss autoprefixer
2 yarn tailwindcss init -p

安装完成后,需要配置 Tailwind CSS。

配置 Tailwind CSS

  • 配置 Tailwind 配置文件 tailwind.config.js

在项目根目录下,会生成 tailwind.config.js 文件。在该文件中,可以配置 Tailwind CSS 的主题、变体、插件等。

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 /** @type {import('tailwindcss').Config} */
2 module.exports = {
3 content: [ // 配置需要应用 Tailwind CSS 的文件
4 "./src/**/*.{js,jsx,ts,tsx}",
5 "./public/index.html",
6 ],
7 theme: { // 自定义主题配置 (可选)
8 extend: {
9 colors: {
10 primary: '#007bff',
11 secondary: '#6c757d',
12 },
13 },
14 },
15 plugins: [], // 添加 Tailwind CSS 插件 (可选)
16 }
  • 配置 PostCSS 配置文件 postcss.config.js

在项目根目录下,会生成 postcss.config.js 文件。在该文件中,配置 PostCSS 插件,包括 tailwindcssautoprefixer

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 module.exports = {
2 plugins: {
3 tailwindcss: {}, // 引入 tailwindcss 插件
4 autoprefixer: {}, // 引入 autoprefixer 插件,自动添加 CSS 浏览器前缀
5 },
6 }
  • 在 CSS 文件中引入 Tailwind CSS 指令

在全局 CSS 文件 (例如 index.cssApp.css) 中,引入 Tailwind CSS 的指令 @tailwind base, @tailwind components, @tailwind utilities

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 /* index.css 或 App.css */
2 @tailwind base; /* 引入 Tailwind CSS base styles,例如 normalize.css */
3 @tailwind components; /* 引入 Tailwind CSS components styles,例如预定义的组件样式 (通常为空) */
4 @tailwind utilities; /* 引入 Tailwind CSS utilities styles,原子化 CSS 类名 */

在 React 组件中使用 Tailwind CSS

在 React 组件中,直接在 JSX 元素的 className 属性中使用 Tailwind CSS 的原子化类名。

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 import React from 'react';
2
3 function TailwindCSSExample() {
4 return (
5 <div>
6 <button className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"> {/* 使用 Tailwind CSS 原子化类名 */}
7 Tailwind Button
8 </button>
9 <div className="flex items-center justify-center h-screen"> {/* 使用 flex 布局类名 */}
10 <div className="bg-gray-100 p-8 rounded shadow-md w-96"> {/* 使用背景色内边距圆角阴影宽度类名 */}
11 <h2 className="text-2xl font-bold mb-4">Tailwind CSS Example</h2> {/* 使用字体大小字体粗细外边距类名 */}
12 <p className="text-gray-700">This is an example of using Tailwind CSS in React.</p> {/* 使用文本颜色类名 */}
13 </div>
14 </div>
15 </div>
16 );
17 }

在这个例子中,<button><div> 元素的 className 属性中使用了 Tailwind CSS 的原子化类名,例如 bg-blue-500 (背景色蓝色 500)、hover:bg-blue-700 (hover 时背景色蓝色 700)、text-white (文本白色)、p-4 (padding 4)、m-2 (margin 2) 等。通过组合这些原子化类名,可以快速构建出自定义的组件样式。

Tailwind CSS 的优点与缺点

优点

快速开发:通过组合原子化类名,可以快速构建 UI 界面,无需编写大量的 CSS 代码。
一致的设计:Tailwind CSS 提供了统一的设计系统,可以保持应用风格的一致性。
可定制性:可以通过 tailwind.config.js 文件高度定制 Tailwind CSS 的主题、颜色、字体、间距等。
性能优化:Tailwind CSS 只会生成项目中实际使用的 CSS 类名,减少 CSS 文件体积,提高性能。
响应式设计:Tailwind CSS 内置了响应式设计 (Responsive Design) 功能,可以使用 sm:, md:, lg:, xl: 等前缀为不同屏幕尺寸设置样式。

缺点

学习曲线:需要学习 Tailwind CSS 的原子化类名体系和配置。
HTML 类名臃肿:JSX 元素的 className 属性可能会变得很长,可读性稍差。
不适用于复杂、高度定制化的样式:对于复杂、高度定制化的样式,原子化类名可能不够灵活。
CSS 代码复用性稍差:原子化类名主要用于组件内部样式,跨组件复用样式可能稍复杂。

适用场景

  • 快速原型开发:需要快速搭建 UI 原型,快速迭代。
  • 设计系统明确的项目:项目有明确的设计系统,可以使用 Tailwind CSS 的原子化类名快速实现设计稿。
  • 需要响应式设计的项目:项目需要支持多种屏幕尺寸的响应式设计。
  • 前端工程化程度较高的项目:团队接受原子化 CSS 的开发模式。

不适用场景

  • 需要高度定制化样式的项目:项目需要大量定制化的、非标准化的 UI 样式。
  • 团队技术栈偏传统 CSS:团队习惯传统的 CSS 开发模式,学习和迁移成本较高。
  • 需要与其他 CSS 方案深度集成:项目需要与 CSS Modules 或 Styled Components 等其他 CSS 方案深度集成。

总结

Tailwind CSS 是一种原子化 CSS 框架,它以其快速开发、一致的设计和可定制性等优点,成为了快速构建 React 应用 UI 的热门选择。但 Tailwind CSS 也存在一定的学习曲线和 HTML 类名臃肿等缺点。在选择样式处理方案时,需要根据项目需求、团队技术栈和开发效率等因素进行权衡。 ⚖️

10.4 CSS 预处理器 (Sass/Less)

CSS 预处理器 (CSS Preprocessors) 例如 Sass (Syntactically Awesome Stylesheets) 和 Less (Leaner Style Sheets) 是 CSS 的增强工具。它们在 CSS 的基础上,扩展了 CSS 的语法,增加了变量 (Variables)嵌套 (Nesting)混合 (Mixins)函数 (Functions)模块化 (Modules/Partials) 等高级特性,使得 CSS 的编写更加高效、灵活和可维护。CSS 预处理器需要通过编译 (Compile) 转换为标准的 CSS 代码,才能被浏览器解析和渲染。

Sass (SCSS) 和 Less 的基本特性

Sass 和 Less 是两种最流行的 CSS 预处理器,它们都提供了类似的高级特性,主要区别在于语法和一些细节功能。

Sass (SCSS)

  • 两种语法
    ▮▮▮▮⚝ SCSS (Sassy CSS):是 Sass 的主要语法,是 CSS 的超集,完全兼容 CSS 语法,使用 大括号 {}分号 ; 分隔代码块和语句,文件扩展名为 .scss。本书示例主要使用 SCSS 语法。
    ▮▮▮▮⚝ 缩进语法 (Indented Syntax):是 Sass 的旧语法,使用缩进代替大括号,使用换行符代替分号,代码更简洁,但不太直观,文件扩展名为 .sass
  • 高级特性:变量、嵌套、混合、函数、模块化 (使用 @use@forward 规则)、控制指令 (例如 @if, @for, @each, @while) 等。
  • 功能强大:Sass 功能强大,社区活跃,生态完善。

Less (Leaner Style Sheets)

  • CSS-like 语法:Less 语法更接近 CSS,学习曲线更平缓。
  • 高级特性:变量、嵌套、混合、函数、模块化 (使用 @import 规则)、条件语句 (使用 ifelse 混合)、循环语句 (使用 loop 混合) 等。
  • 易于上手:Less 语法简洁,易于学习和使用。

在 React 项目中使用 CSS 预处理器

在 React 项目中使用 CSS 预处理器,需要安装相应的预处理器和构建工具插件,并配置构建工具以编译预处理器文件。

  • 安装 Sass

使用 npm 安装 Sass:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 npm install -D sass

或使用 yarn 安装:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 yarn add -D sass
  • 安装 Less

使用 npm 安装 Less:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 npm install -D less less-loader

或使用 yarn 安装:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 yarn add -D less less-loader
  • 配置构建工具

如果使用 Create React App (CRA) 或 Vite 创建的项目,已经默认配置好 Sass 和 Less 的 loader,无需额外配置。只需要安装相应的预处理器即可。

  • 创建预处理器文件

创建 Sass 文件 (.scss) 或 Less 文件 (.less),编写预处理器代码。

示例:使用 Sass (SCSS) 编写样式

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 /* _variables.scss (Sass Partial 文件,用于定义变量) */
2 $primary-color: #007bff;
3 $secondary-color: #6c757d;
4 $font-size-base: 16px;
5 $padding-base: 10px;
1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 /* Button.scss */
2 @import './variables'; /* 引入 _variables.scss 文件 */
3
4 .button {
5 background-color: $primary-color; /* 使用变量 */
6 color: white;
7 padding: $padding-base * 2; /* 使用变量和运算 */
8 border: none;
9 border-radius: 5px;
10 cursor: pointer;
11
12 &:hover { /* 使用嵌套 */
13 background-color: darken($primary-color, 10%); /* 使用函数 */
14 }
15
16 &.primary { /* 使用父选择器 & 和类名组合 */
17 background-color: $secondary-color;
18
19 &:hover {
20 background-color: darken($secondary-color, 10%);
21 }
22 }
23 }
1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 import React from 'react';
2 import './Button.scss'; // 引入 Sass 文件
3
4 function SassExampleButton() {
5 return (
6 <div>
7 <button className="button">Default Button</button> {/* 使用 Sass 定义的类名 */}
8 <button className="button primary">Primary Button</button> {/* 使用 Sass 定义的类名和组合类名 */}
9 </div>
10 );
11 }

在这个例子中,使用了 Sass 的变量、嵌套、混合、函数、模块化等特性编写 CSS 样式。在 React 组件中,像引入普通的 CSS 文件一样引入 Sass 文件 (.scss) 即可。构建工具会自动编译 Sass 文件为 CSS 文件,并应用到组件中。

CSS 预处理器 (Sass/Less) 的优点与缺点

优点

提高 CSS 代码复用性:通过变量、混合、模块化等特性,可以提高 CSS 代码的复用性,减少重复代码。
提高 CSS 代码可维护性:通过嵌套、模块化等特性,可以提高 CSS 代码的结构化程度,使其更易于阅读和维护。
提高 CSS 编写效率:通过变量、混合、函数等特性,可以简化 CSS 代码的编写,提高开发效率。
可以使用 CSS 高级特性:Sass 和 Less 提供了许多 CSS 扩展特性,例如:变量、嵌套、混合、函数、模块化、控制指令等。

缺点

学习成本:需要学习 CSS 预处理器的语法和特性。
编译步骤:需要额外的编译步骤将预处理器代码转换为 CSS 代码。
运行时开销:编译过程会增加一定的构建时间,运行时可能需要处理一些额外的代码 (例如 Source Maps)。

适用场景

  • 中大型项目:需要提高 CSS 代码的复用性和可维护性,需要使用 CSS 高级特性。
  • 团队技术栈偏传统 CSS:团队习惯传统的 CSS 开发模式,但希望提高 CSS 开发效率和代码质量。
  • 需要与其他 CSS 方案集成:CSS 预处理器可以与 CSS Modules, Tailwind CSS 等其他 CSS 方案结合使用。

不适用场景

  • 小型项目:项目规模小,样式简单,引入 CSS 预处理器可能会增加不必要的复杂度。
  • 前端工程化程度较低的项目:团队对前端工程化工具链不太熟悉,引入 CSS 预处理器可能会增加学习和配置成本。

总结

CSS 预处理器 (Sass/Less) 是 CSS 的强大扩展工具,它们提供了变量、嵌套、混合、函数、模块化等高级特性,可以提高 CSS 代码的复用性、可维护性和编写效率。CSS 预处理器适用于中大型项目,或需要使用 CSS 高级特性的项目。在选择样式处理方案时,可以根据项目需求、团队技术栈和开发效率等因素,将 CSS 预处理器与其他 CSS 方案结合使用,构建更高效、可维护的 React 应用样式系统。 🚀

本章总结

本章深入探讨了 React 中常用的样式处理方案,包括行内样式、CSS Modules、Styled Components、Tailwind CSS 以及 CSS 预处理器 (Sass/Less)。每种方案都有其独特的优点、缺点和适用场景。在实际项目中,开发者可以根据项目规模、复杂度、团队技术栈、性能需求和个人偏好等因素,选择合适的样式处理方案,或将多种方案结合使用,构建灵活、高效、可维护的 React 应用样式系统。下一章,我们将学习 React 组件的进阶技巧。🚀

REVIEW PASS

11. chapter 11: React 组件进阶

在掌握了 React 组件的基础知识之后,为了构建更复杂、更可维护的应用,我们需要深入学习组件的进阶技巧。本章将探讨 React 组件进阶 topics,包括组件组合与复用模式、高阶组件 (HOC)、Render Props、Forwarding Refs 和 Portals。这些进阶技巧将帮助你提升组件的设计能力,编写更灵活、更高效、更可复用的 React 代码。

11.1 组件组合与复用模式

组件组合 (Component Composition) 是 React 中最核心的组件复用模式。React 提倡使用组合 (Composition) 而不是继承 (Inheritance) 来复用组件逻辑和 UI 结构。组件组合通过灵活地组合小的、可复用的组件来构建复杂的 UI 界面,提高了代码的可维护性和可扩展性。

11.1.1 Composition vs. Inheritance in React

在面向对象编程中,继承 (Inheritance) 是一种常见的代码复用方式。但在 React 中,官方更推荐使用组合 (Composition) 来实现代码复用。

继承 (Inheritance) 的问题

紧耦合 (Tight Coupling):继承会创建父类和子类之间的紧密耦合关系。子类依赖于父类的实现细节,父类的修改可能会影响到所有子类。
脆弱的基类问题 (Fragile Base Class Problem):当基类发生变化时,可能会导致子类行为异常,难以维护和预测。
层级复杂 (Deep Hierarchy):过度使用继承可能会导致复杂的继承层级,难以理解和维护。
不灵活 (Inflexible):继承是静态的,在运行时无法动态地改变组件的行为。

组合 (Composition) 的优势

松耦合 (Loose Coupling):组合通过 Props 将组件组合在一起,组件之间是松耦合的。组件只依赖于 Props 接口,不依赖于其他组件的实现细节。
高复用性 (High Reusability):小的、独立的组件更容易被复用在不同的场景中。
灵活性 (Flexibility):组合是动态的,可以在运行时灵活地组合不同的组件,实现不同的 UI 结构和行为。
易于维护 (Easy to Maintain):组件的独立性使得代码结构更清晰,易于理解和维护。

React 提倡 "组合优于继承 (Composition Over Inheritance)" 的原则。在 React 中,应该优先使用组件组合来复用代码和构建 UI,而不是使用类组件的继承。

11.1.2 常见的组合模式

React 提供了多种组件组合模式,常用的模式包括:

11.1.2.1 Children Prop

Children Prop 是 React 中最基本也是最常用的组合模式。组件可以通过 children prop 接收任意类型的子组件 (包括 JSX 元素、组件实例、字符串、数字等),并在组件的渲染输出中渲染这些子组件。

示例:使用 children prop 创建容器组件

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 import React from 'react';
2
3 // Container 组件接收 children prop
4 function Container(props) {
5 return (
6 <div className="container">
7 {props.children} {/* 渲染 children prop */}
8 </div>
9 );
10 }
11
12 function App() {
13 return (
14 <Container> {/* 使用 Container 组件传入子组件 */}
15 <h1>Welcome to My App</h1>
16 <p>This is the content of my application.</p>
17 </Container>
18 );
19 }

在这个例子中,Container 组件接收 children prop,并在 <div> 元素中渲染 props.children。在 App 组件中使用 Container 组件时,将 <h1><p> 元素作为子组件传递给 Container 组件。Container 组件作为一个通用的容器,可以复用于不同的场景,只需要传入不同的 children prop 即可。

11.1.2.2 Specialization (特殊化)

Specialization (特殊化) 模式是指创建一个更通用的组件,然后通过组合和配置这个通用组件来创建更具体的、特殊化的组件。通常使用 Props 来配置通用组件的行为和外观,从而实现组件的特殊化。

示例:使用 Specialization 模式创建不同样式的 Button 组件

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 import React from 'react';
2
3 // Button 组件通用 Button 组件
4 function Button(props) {
5 return (
6 <button className={`button ${props.className}`} onClick={props.onClick}>
7 {props.children}
8 </button>
9 );
10 }
11
12 // PrimaryButton 组件特殊化的 Primary Button
13 function PrimaryButton(props) {
14 return (
15 <Button {...props} className="primary-button" /> {/* 组合 Button 组件并配置 className prop */}
16 );
17 }
18
19 // SecondaryButton 组件特殊化的 Secondary Button
20 function SecondaryButton(props) {
21 return (
22 <Button {...props} className="secondary-button" /> {/* 组合 Button 组件并配置 className prop */}
23 );
24 }
25
26 function App() {
27 return (
28 <div>
29 <Button>Default Button</Button> {/* 使用通用 Button 组件 */}
30 <PrimaryButton>Primary Button</PrimaryButton> {/* 使用 Specialization 组件 */}
31 <SecondaryButton>Secondary Button</SecondaryButton> {/* 使用 Specialization 组件 */}
32 </div>
33 );
34 }

在这个例子中,Button 组件是一个通用的 Button 组件,接收 classNameonClick 等 props。PrimaryButtonSecondaryButton 组件通过组合 Button 组件,并分别配置 className prop 为 "primary-button""secondary-button",从而创建了特殊化的 Primary Button 和 Secondary Button 组件。Specialization 模式通过 Props 配置通用组件,实现了组件的复用和特殊化。

11.1.2.3 Container Components (容器组件)

Container Components (容器组件)Presentational Components (展示组件) 是一种组件设计模式,也称为 分离关注点 (Separation of Concerns) 模式。容器组件负责处理数据获取、状态管理、业务逻辑等,而展示组件只负责 UI 渲染,不关心数据和逻辑。容器组件通常会使用展示组件作为子组件,并将数据和回调函数作为 Props 传递给展示组件。

容器组件 (Container Components)

⚝ 负责数据获取 (Data Fetching)、状态管理 (State Management)、业务逻辑 (Business Logic)。
⚝ 通常是有状态组件 (Stateful Components),可以是类组件或函数式组件 (使用 Hooks)。
⚝ 不包含 UI 渲染逻辑,或只包含少量的容器布局相关的 UI 渲染。
⚝ 将数据和回调函数作为 Props 传递给展示组件。

展示组件 (Presentational Components)

⚝ 负责 UI 渲染 (UI Rendering)。
⚝ 通常是无状态组件 (Stateless Components),函数式组件更佳。
⚝ 不包含业务逻辑和状态管理。
⚝ 通过 Props 接收数据和回调函数,并根据 Props 渲染 UI。
⚝ 可复用性高,易于测试和维护。

示例:使用 Container/Presentational Components 模式构建用户列表

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 import React, { useState, useEffect } from 'react';
2
3 // UserListContainer 组件容器组件负责数据获取和状态管理
4 function UserListContainer() {
5 const [users, setUsers] = useState([]);
6 const [loading, setLoading] = useState(true);
7 const [error, setError] = useState(null);
8
9 useEffect(() => {
10 const fetchData = async () => {
11 try {
12 const response = await fetch('https://api.example.com/users');
13 if (!response.ok) {
14 throw new Error(`HTTP error! status: ${response.status}`);
15 }
16 const json = await response.json();
17 setUsers(json);
18 setLoading(false);
19 } catch (e) {
20 setError(e);
21 setLoading(false);
22 }
23 };
24
25 fetchData();
26 }, []);
27
28 if (loading) {
29 return <p>加载用户列表中...</p>;
30 }
31
32 if (error) {
33 return <p>Error: {error.message}</p>;
34 }
35
36 return (
37 <UserList users={users} /> {/* 渲染展示组件 UserList传递 users 数据 */}
38 );
39 }
40
41 // UserList 组件展示组件只负责 UI 渲染
42 function UserList(props) {
43 return (
44 <ul>
45 {props.users.map(user => (
46 <li key={user.id}>{user.name}</li>
47 ))}
48 </ul>
49 );
50 }
51
52 function App() {
53 return (
54 <UserListContainer /> {/* 使用容器组件 UserListContainer */}
55 );
56 }

在这个例子中,UserListContainer 组件是容器组件,负责数据获取、加载状态和错误处理。UserList 组件是展示组件,只负责渲染用户列表 UI。UserListContainer 组件将获取到的 users 数据作为 Props 传递给 UserList 组件。Container/Presentational Components 模式实现了组件的关注点分离,使得代码结构更清晰,组件更易于复用和测试。

组件组合模式总结

组件组合是 React 中最核心的组件复用模式。通过灵活运用 children prop、Specialization 和 Container/Presentational Components 等组合模式,可以构建出高度可复用、可维护的 React 应用。在实际开发中,应该优先考虑使用组件组合来复用代码,而不是使用类组件的继承。 🧩

11.2 高阶组件 (HOC)

高阶组件 (Higher-Order Component, HOC) 是 React 中一种用于复用组件逻辑的高级技巧。HOC 本身不是组件,而是一个函数,它接收一个组件作为参数,并返回一个新的增强后的组件。HOC 可以用来增强 (Enhance) 组件的功能,例如:添加 Props、注入状态、处理副作用等。

11.2.1 HOC 的定义与目的

HOC 的定义

高阶组件是一个函数,满足以下条件:

① 接收一个组件作为参数 (WrappedComponent)。
② 返回一个新的组件 (EnhancedComponent)。
③ 新组件 (EnhancedComponent) 会渲染 WrappedComponent,并在 WrappedComponent 的基础上添加一些额外的功能或 Props。

HOC 的目的

代码复用 (Code Reusability):将组件之间通用的逻辑 (例如:身份验证、日志记录、数据获取等) 提取到 HOC 中,实现逻辑的复用。
逻辑抽象 (Logic Abstraction):将组件的业务逻辑抽象出来,使得组件更专注于 UI 渲染。
增强组件功能 (Enhance Component Functionality):HOC 可以为组件添加额外的 Props、状态、生命周期方法或渲染逻辑,增强组件的功能。
关注点分离 (Separation of Concerns):将横切关注点 (Cross-cutting Concerns) (例如:日志记录、权限控制等) 从组件中分离出来,提高代码的可维护性。

11.2.2 HOC 的使用场景示例

HOC 在 React 中有很多使用场景,常见的场景包括:

身份验证 (Authentication):创建一个 HOC,用于验证用户是否已登录,如果未登录,则重定向到登录页面。
日志记录 (Logging):创建一个 HOC,用于记录组件的渲染性能、Props 变化等日志信息。
数据获取 (Data Fetching):创建一个 HOC,用于处理组件的数据获取逻辑,例如:从 API 获取数据,并作为 Props 传递给组件。
状态连接 (State Connection):将组件连接到全局状态管理库 (例如 Redux, Zustand),并将状态和状态更新函数作为 Props 传递给组件 (例如 Redux 的 connect HOC)。
样式注入 (Style Injection):为组件注入一些通用的样式 Props 或 Theme Props。

11.2.3 HOC 的实现模式

HOC 的实现模式主要有两种:

11.2.3.1 Props Proxy (Props 代理)

Props Proxy (Props 代理) 是 HOC 最常用的实现模式。Props Proxy HOC 返回的新组件 (EnhancedComponent) 会渲染 WrappedComponent,并在渲染 WrappedComponent 时,代理 (Proxy) WrappedComponent 的 Props,可以在代理过程中,修改、添加或删除 Props。

Props Proxy HOC 的示例:日志记录 HOC

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 import React from 'react';
2
3 // withLogging HOCProps Proxy 模式
4 function withLogging(WrappedComponent) {
5 return class EnhancedComponent extends React.Component {
6 componentDidMount() {
7 console.log(`[HOC] ${WrappedComponent.name} 组件已挂载`); // 组件挂载时记录日志
8 }
9
10 componentDidUpdate(prevProps) {
11 console.log(`[HOC] ${WrappedComponent.name} 组件已更新Props:`, this.props); // 组件更新时记录日志和 Props
12 }
13
14 render() {
15 const { ...restProps } = this.props; // 获取所有 Props
16 return <WrappedComponent {...restProps} />; // 渲染 WrappedComponent并传递 Props
17 }
18 };
19 }
20
21 function MyComponent(props) {
22 return (
23 <div>
24 <p>My Component, Prop: {props.message}</p>
25 </div>
26 );
27 }
28
29 const EnhancedComponent = withLogging(MyComponent); // 使用 withLogging HOC 增强 MyComponent
30
31 function App() {
32 return (
33 <EnhancedComponent message="Hello HOC!" /> {/* 使用增强后的组件 EnhancedComponent */}
34 );
35 }

在这个例子中,withLogging HOC 是一个 Props Proxy HOC。它返回的 EnhancedComponent 类组件,在 componentDidMountcomponentDidUpdate 生命周期方法中记录日志,并在 render 方法中渲染 WrappedComponent,并将接收到的 Props 原封不动地传递给 WrappedComponent

11.2.3.2 Inheritance Inversion (反向继承)

Inheritance Inversion (反向继承) 是 HOC 的另一种实现模式。Inheritance Inversion HOC 返回的新组件 (EnhancedComponent) 会继承 (Inherit) WrappedComponent。EnhancedComponent 可以访问 WrappedComponent 的 Props、State、生命周期方法和 render 方法,但通常不直接渲染 WrappedComponent,而是通过某种方式修改或增强 WrappedComponent 的行为。

Inheritance Inversion HOC 的示例:权限控制 HOC

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 import React from 'react';
2
3 // withAuthorization HOCInheritance Inversion 模式
4 function withAuthorization(WrappedComponent, allowedRoles) {
5 return class EnhancedComponent extends WrappedComponent { // EnhancedComponent 继承 WrappedComponent
6 render() {
7 const userRole = 'admin'; // 假设当前用户角色为 'admin'
8 if (!allowedRoles.includes(userRole)) { // 检查用户角色是否在允许的角色列表中
9 return <p>您没有权限访问该组件</p>; // 如果没有权限渲染提示信息
10 }
11 return super.render(); // 如果有权限调用 WrappedComponent render 方法渲染组件
12 }
13 };
14 }
15
16 class AdminPanel extends React.Component {
17 render() {
18 return (
19 <div>
20 <h1>Admin Panel</h1>
21 <p>Welcome to the admin panel.</p>
22 </div>
23 );
24 }
25 }
26
27 const AuthorizedAdminPanel = withAuthorization(AdminPanel, ['admin']); // 使用 withAuthorization HOC 增强 AdminPanel只允许 'admin' 角色访问
28
29 function App() {
30 return (
31 <AuthorizedAdminPanel /> {/* 使用增强后的组件 AuthorizedAdminPanel */}
32 );
33 }

在这个例子中,withAuthorization HOC 是一个 Inheritance Inversion HOC。它返回的 EnhancedComponent 类组件继承WrappedComponentEnhancedComponent 组件重写了 render 方法,在 render 方法中,首先检查用户角色是否在允许的角色列表中,如果没有权限,则渲染提示信息,如果有权限,则调用 super.render() 调用父类 (WrappedComponent) 的 render 方法渲染组件。

11.2.4 HOC 的缺点与替代方案

HOC 虽然是一种强大的组件复用技巧,但它也存在一些缺点:

Props 名称冲突 (Props Name Collisions):HOC 可能会为 WrappedComponent 添加新的 Props,如果新的 Props 名称与 WrappedComponent 自身已有的 Props 名称冲突,可能会导致意外的行为。
Ref 传递问题 (Ref Forwarding Issues):默认情况下,Ref 不会传递到 HOC 增强后的组件内部的 WrappedComponent。需要使用 React.forwardRef API 显式地传递 Ref。
Wrapper Hell (组件包裹地狱):过度使用 HOC 可能会导致组件被多层 HOC 包裹,形成 "Wrapper Hell",使得组件树结构复杂,难以理解和调试。
静态方法丢失 (Static Methods Loss):HOC 会返回一个新的组件,原始组件的静态方法会丢失。需要手动复制静态方法。
类型推断困难 (Type Inference Challenges):在使用 TypeScript 等静态类型语言时,HOC 的类型推断可能会比较复杂。

HOC 的替代方案

React Hooks 的出现,为组件逻辑复用提供了更简洁、更灵活的替代方案,例如:

自定义 Hook (Custom Hooks):可以将组件逻辑提取到自定义 Hook 中,并在多个组件中复用 Hook。自定义 Hook 比 HOC 更简洁、更易于理解和测试,也避免了 HOC 的一些缺点 (例如 Props 名称冲突、Ref 传递问题等)。
Render Props:Render Props 模式也可以用于组件逻辑复用,Render Props 比 HOC 更灵活,组件之间的关系更清晰。

在现代 React 开发中,推荐优先使用自定义 Hook 和 Render Props 来替代 HOC。HOC 主要在维护旧项目或需要与旧代码库兼容时使用。

HOC 总结

高阶组件 (HOC) 是一种用于复用组件逻辑的强大技巧。HOC 可以增强组件的功能,实现代码复用和逻辑抽象。但 HOC 也存在一些缺点,React Hooks 和 Render Props 提供了更现代、更简洁的替代方案。在选择组件复用方案时,需要根据项目需求和团队技术栈进行权衡。 ⚖️

11.3 Render Props

Render Props 是一种在 React 组件之间复用代码的模式。Render Props 是指组件的 Prop 是一个函数,该函数返回一个 React 元素。Render Props 组件不负责渲染 UI 内容,而是将渲染 UI 的控制权交给调用者,通过 Render Prop 函数将数据传递给调用者,由调用者根据数据渲染 UI。

11.3.1 Render Props 的定义与目的

Render Props 的定义

Render Props 模式是指组件满足以下条件:

① 组件接收一个名为 render 的 Prop (或其他约定名称,例如 children, component 等)。
render Prop 的值是一个函数。
③ 组件在内部调用 render Prop 函数,并将一些数据作为参数传递给 render 函数。
render 函数返回一个 React 元素,由组件的调用者决定如何渲染 UI。

Render Props 的目的

代码复用 (Code Reusability):将组件之间通用的 UI 渲染逻辑或状态逻辑提取到 Render Props 组件中,实现逻辑的复用。
灵活性 (Flexibility):Render Props 将 UI 渲染的控制权交给调用者,使得组件更灵活,可以适应不同的 UI 渲染需求。
组合性 (Composability):Render Props 可以与其他组件组合使用,构建更复杂的 UI 结构。
避免 Wrapper Hell:相比 HOC,Render Props 避免了组件的层层嵌套包裹,组件树结构更清晰。

11.3.2 Render Props 的使用场景示例

Render Props 在 React 中有很多使用场景,常见的场景包括:

鼠标追踪 (Mouse Tracking):创建一个 Render Props 组件,用于追踪鼠标的位置,并将鼠标位置数据传递给调用者,由调用者根据鼠标位置渲染 UI。
数据提供者 (Data Provider):创建一个 Render Props 组件,用于从 API 获取数据,并将数据传递给调用者,由调用者根据数据渲染 UI。
状态共享 (State Sharing):创建一个 Render Props 组件,用于管理一些共享的状态,并将状态和状态更新函数传递给调用者,由调用者根据状态渲染 UI。
动画控制 (Animation Control):创建一个 Render Props 组件,用于控制动画的播放和暂停,并将动画状态传递给调用者,由调用者根据动画状态渲染 UI。

11.3.3 Render Props 的实现模式

Render Props 的实现模式主要通过以下步骤:

定义 Render Prop 函数:在组件中定义一个 Prop,约定该 Prop 为 Render Prop 函数 (例如 render prop)。
调用 Render Prop 函数:在组件的 render 方法中,调用 Render Prop 函数,并将需要传递给调用者的数据作为参数传递给 Render Prop 函数。
渲染 Render Prop 函数的返回值:将 Render Prop 函数的返回值 (React 元素) 作为组件的渲染输出。

Render Props 组件示例:鼠标追踪组件 MouseTracker

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 import React from 'react';
2
3 class MouseTracker extends React.Component {
4 state = { x: 0, y: 0 };
5
6 handleMouseMove = (event) => {
7 this.setState({ x: event.clientX, y: event.clientY });
8 };
9
10 render() {
11 return (
12 <div style={{ height: '100vh' }} onMouseMove={this.handleMouseMove}>
13 {this.props.render(this.state)} {/* 调用 render prop 函数传递鼠标位置数据 */}
14 </div>
15 );
16 }
17 }
18
19 function App() {
20 return (
21 <MouseTracker
22 render={mouse => ( // 传递 render prop 函数
23 <p>鼠标位置: ({mouse.x}, {mouse.y})</p> // render prop 函数返回 React 元素
24 )}
25 />
26 );
27 }

在这个例子中,MouseTracker 组件是一个 Render Props 组件。它接收一个名为 render 的 Prop,render Prop 的值是一个函数。MouseTracker 组件在 render 方法中,调用 this.props.render(this.state),并将鼠标位置数据 this.state 作为参数传递给 render 函数。App 组件在使用 MouseTracker 组件时,传递了一个 render Prop 函数,该函数接收鼠标位置数据 mouse,并返回一个 <p> 元素显示鼠标位置。

使用不同 Render Props 函数,复用 MouseTracker 组件

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 import React from 'react';
2
3 // MouseTracker 组件 (同上) ...
4
5 function Cat(props) {
6 const mouse = props.mouse;
7 return (
8 <img src="/cat.jpg" style={{ position: 'absolute', left: mouse.x, top: mouse.y }} alt="Cat" />
9 );
10 }
11
12 function App() {
13 return (
14 <div>
15 <MouseTracker
16 render={mouse => ( // 传递不同的 render prop 函数
17 <p>鼠标位置: ({mouse.x}, {mouse.y})</p> // 渲染鼠标位置文本
18 )}
19 />
20 <MouseTracker
21 render={mouse => ( // 传递不同的 render prop 函数
22 <Cat mouse={mouse} /> // 渲染 Cat 组件根据鼠标位置移动 Cat 组件
23 )}
24 />
25 </div>
26 );
27 }

在这个例子中,App 组件两次使用了 MouseTracker 组件,但传递了不同的 render Prop 函数。第一个 MouseTracker 组件的 render Prop 函数渲染鼠标位置文本,第二个 MouseTracker 组件的 render Prop 函数渲染 Cat 组件,并根据鼠标位置移动 Cat 组件。通过传递不同的 render Prop 函数,MouseTracker 组件被复用于不同的 UI 渲染场景。

11.3.4 Render Props 的优点与缺点

优点

代码复用:Render Props 可以复用组件的 UI 渲染逻辑或状态逻辑。
灵活性:Render Props 将 UI 渲染的控制权交给调用者,组件更灵活。
组合性:Render Props 可以与其他组件组合使用,构建更复杂的 UI 结构。
避免 Wrapper Hell:相比 HOC,Render Props 避免了组件的层层嵌套包裹,组件树结构更清晰。
更清晰的组件关系:Render Props 通过 Props 显式地传递数据,组件之间的关系更清晰。

缺点

JSX 嵌套层级增加:使用 Render Props 可能会增加 JSX 的嵌套层级,代码可读性稍差。
Props 命名约定:需要约定 Render Prop 的 Prop 名称 (例如 render, children, component 等),需要一定的约定和规范。
类型检查稍复杂:在使用 TypeScript 等静态类型语言时,Render Props 的类型检查可能会稍复杂。

Render Props 的替代方案

React Hooks 的出现,为组件逻辑复用提供了更简洁、更灵活的替代方案,例如:自定义 Hook。

Render Props 总结

Render Props 是一种在 React 组件之间复用代码的模式,它通过 Render Prop 函数将 UI 渲染的控制权交给调用者,实现了组件的灵活性和复用性。Render Props 是一种有效的组件复用模式,但在现代 React 开发中,自定义 Hook 通常是更简洁、更推荐的替代方案。 在选择组件复用方案时,需要根据项目需求和团队技术栈进行权衡。 ⚖️

11.4 Forwarding Refs

Forwarding Refs (Ref 转发) 是一种 React 特性,它允许你将 Ref 自动地通过组件树向下传递给子组件的 DOM 元素或组件实例。Forwarding Refs 主要用于高阶组件 (HOC)Render Props 组件等场景,解决 Ref 无法直接传递到 WrappedComponent 或 Render Props 组件内部的问题。

11.4.1 Refs 的概念及其在函数式组件中的限制

Refs 的概念

Refs (References) 提供了一种访问 React 组件渲染的底层 DOM 元素或组件实例的方式。在类组件中,可以使用 React.createRef() 创建 Ref 对象,并将其绑定到 JSX 元素的 ref 属性上,通过 this.ref.current 访问 DOM 元素或组件实例。在函数式组件中,可以使用 useRef() Hook 创建 Ref 对象。

函数式组件中的 Ref 限制

默认情况下,不能直接在函数式组件上使用 ref 属性。因为函数式组件没有实例,ref 属性只能绑定到类组件或 DOM 元素上。如果需要在父组件中访问函数式组件内部的 DOM 元素或子组件实例,就需要使用 Forwarding Refs。

11.4.2 React.forwardRef API 及其用法

React.forwardRef 是 React 提供的用于实现 Ref 转发的 API。React.forwardRef 接收一个渲染函数 (render function) 作为参数,并返回一个新的 React 组件。渲染函数接收两个参数:propsrefprops 是组件接收的 Props,ref 是父组件传递给当前组件的 Ref 对象。在渲染函数中,需要手动将 ref 属性传递给子组件的 DOM 元素或组件实例,才能实现 Ref 转发。

React.forwardRef API 的基本用法

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 import React, { useRef, forwardRef } from 'react';
2
3 // 使用 forwardRef 创建可转发 Ref 的函数式组件 MyInput
4 const MyInput = forwardRef((props, ref) => { // forwardRef 接收渲染函数参数为 props ref
5 return (
6 <input type="text" ref={ref} {...props} /> // ref 属性传递给 input 元素
7 );
8 });
9
10 function ParentComponent() {
11 const inputRef = useRef(null);
12
13 const handleFocus = () => {
14 inputRef.current.focus(); // 通过 Ref 访问 MyInput 组件内部的 input 元素
15 };
16
17 return (
18 <div>
19 <MyInput ref={inputRef} placeholder="请输入内容" /> {/* inputRef 传递给 MyInput 组件的 ref 属性 */}
20 <button onClick={handleFocus}>聚焦输入框</button>
21 </div>
22 );
23 }

在这个例子中,MyInput 组件使用 forwardRef 创建,渲染函数接收 propsref 参数,并将 ref 属性传递给 <input> 元素。ParentComponent 组件创建 inputRef Ref 对象,并将其传递给 MyInput 组件的 ref 属性。通过 Forwarding Refs,ParentComponent 组件可以通过 inputRef.current 访问 MyInput 组件内部的 <input> DOM 元素。

11.4.3 Forwarding Refs 的使用场景示例

Forwarding Refs 主要用于以下场景:

HOC Ref 转发:解决 HOC 增强后的组件无法直接传递 Ref 到 WrappedComponent 的问题。
Render Props Ref 转发:解决 Render Props 组件无法直接传递 Ref 到 Render Prop 函数返回的组件的问题。
组件库 Ref 转发:在组件库中,需要将 Ref 转发到组件内部的 DOM 元素,以便使用者可以访问底层 DOM 元素。

示例:HOC Ref 转发

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 import React, { useRef, forwardRef } from 'react';
2
3 // withLogging HOC (Props Proxy 模式同上) ...
4
5 // 使用 forwardRef 包裹 WrappedComponent实现 Ref 转发
6 const MyComponentWithLogging = withLogging(forwardRef(MyComponent)); // 使用 forwardRef 包裹 MyComponent
7
8 function ParentComponent() {
9 const componentRef = useRef(null);
10
11 const handleLog = () => {
12 console.log('Component Ref:', componentRef.current); // 可以通过 Ref 访问 MyComponent 组件实例
13 };
14
15 return (
16 <div>
17 <MyComponentWithLogging ref={componentRef} message="Hello HOC Ref!" /> {/* componentRef 传递给增强后的组件 */}
18 <button onClick={handleLog}>Log Component Ref</button>
19 </div>
20 );
21 }

在这个例子中,MyComponent 组件使用 forwardRef 包裹,使得它可以接收 Ref 属性。withLogging HOC 增强 MyComponent 组件时,也使用了 forwardRef(MyComponent) 包裹。这样,当 ParentComponent 组件将 componentRef 传递给 MyComponentWithLogging 组件时,Ref 会通过 HOC 自动转发到 MyComponent 组件实例。

Forwarding Refs 总结

Forwarding Refs 是一种 React 特性,允许将 Ref 自动地向下传递给子组件的 DOM 元素或组件实例。Forwarding Refs 主要用于 HOC、Render Props 组件和组件库等场景,解决 Ref 传递问题,使得父组件可以访问子组件内部的 DOM 元素或组件实例。 🔑

11.5 Portals

Portals (传送门) 提供了一种将子组件渲染到 DOM 树中不同位置 的能力。通常情况下,React 组件渲染的 DOM 元素会作为其父组件 DOM 元素的子节点插入到 DOM 树中。Portals 允许组件将其子节点渲染到 DOM 树中的任何位置,而逻辑上仍然是 React 组件树的一部分

11.5.1 Portals 的定义与目的

Portals 的定义

Portals 是一种 React 特性,它允许组件将其子节点渲染到 DOM 树中的不同位置,而不是组件的 DOM 父节点下。

Portals 的目的

突破 DOM 结构限制:解决某些 UI 场景下,组件的 DOM 结构受到父组件 DOM 结构限制的问题,例如:Modal 对话框、Tooltip 提示框、Notification 通知栏等。这些 UI 元素通常需要渲染到 DOM 树的顶层 (例如 <body> 元素下),以避免受到父组件样式 (例如 overflow: hidden, z-index) 的影响。
提高组件灵活性:Portals 使得组件的渲染位置更灵活,可以根据 UI 需求将组件渲染到 DOM 树的任何位置。
事件冒泡和 Context 仍然有效:通过 Portals 渲染的组件,在逻辑上仍然是 React 组件树的一部分,事件冒泡 (Event Bubbling) 和 Context API 仍然可以正常工作。

11.5.2 ReactDOM.createPortal API 及其用法

ReactDOM.createPortal 是 React 提供的用于创建 Portals 的 API。ReactDOM.createPortal 接收两个参数:

children (ReactNode):需要渲染的子节点 (JSX 元素、组件实例、文本节点等)。
container (HTMLElement):DOM 容器元素,指定子节点要渲染到的 DOM 树中的位置。container 必须是一个已存在的 DOM 元素,通常是 document.body 或其他 DOM 元素。

ReactDOM.createPortal 函数返回一个 React 节点 (React Node),可以在组件的 render 方法中返回该节点,实现 Portals 渲染。

ReactDOM.createPortal API 的基本用法

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 import React from 'react';
2 import ReactDOM from 'react-dom'; // 引入 ReactDOM
3
4 function MyPortalComponent() {
5 return ReactDOM.createPortal( // 使用 createPortal 创建 Portal
6 <div>
7 <p>This is content rendered by a portal.</p>
8 </div>,
9 document.body // 指定渲染到 document.body
10 );
11 }
12
13 function App() {
14 return (
15 <div>
16 <h1>App Component</h1>
17 <MyPortalComponent /> {/* 使用 Portal 组件 */}
18 </div>
19 );
20 }

在这个例子中,MyPortalComponent 组件使用 ReactDOM.createPortal 创建 Portal。createPortal 函数的第一个参数是需要渲染的 <div> 元素,第二个参数是 document.body,表示将 <div> 元素渲染到 document.body 元素下。虽然 MyPortalComponent 组件在 App 组件中使用,但其渲染的 DOM 元素 <div> 并不是 App 组件 DOM 元素的子节点,而是直接插入到 document.body 元素下。

11.5.3 Portals 的使用场景示例

Portals 主要用于解决以下 UI 场景:

Modal 对话框 (Modal Dialog):将 Modal 对话框渲染到 document.body 下,避免受到父组件样式 (例如 overflow: hidden) 的影响,确保 Modal 对话框能够覆盖整个视口 (Viewport)。
Tooltip 提示框 (Tooltip):将 Tooltip 提示框渲染到 document.body 下,避免受到父组件布局和样式的限制,确保 Tooltip 提示框能够正确地定位和显示。
Notification 通知栏 (Notification):将 Notification 通知栏渲染到 document.body 下,避免受到父组件 DOM 结构的影响,确保 Notification 通知栏能够固定在屏幕的某个位置。

示例:使用 Portals 创建 Modal 对话框

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 import React, { useState } from 'react';
2 import ReactDOM from 'react-dom';
3
4 const modalRoot = document.getElementById('modal-root'); // 获取 modal-root DOM 元素
5
6 // Modal 组件使用 Portals 渲染
7 function Modal(props) {
8 if (!props.isOpen) {
9 return null;
10 }
11
12 return ReactDOM.createPortal( // 使用 createPortal 创建 Portal
13 <div className="modal-backdrop">
14 <div className="modal-content">
15 {props.children}
16 <button onClick={props.onClose}>关闭</button>
17 </div>
18 </div>,
19 modalRoot // 渲染到 modal-root DOM 元素下
20 );
21 }
22
23 function App() {
24 const [isModalOpen, setIsModalOpen] = useState(false);
25
26 const handleOpenModal = () => {
27 setIsModalOpen(true);
28 };
29
30 const handleCloseModal = () => {
31 setIsModalOpen(false);
32 };
33
34 return (
35 <div>
36 <h1>App Component</h1>
37 <button onClick={handleOpenModal}>打开 Modal</button>
38 <Modal isOpen={isModalOpen} onClose={handleCloseModal}> {/* 使用 Modal 组件 */}
39 <h2>Modal 对话框</h2>
40 <p>This is the content of the modal dialog.</p>
41 </Modal>
42 </div>
43 );
44 }

在这个例子中,Modal 组件是一个 Portal 组件。它使用 ReactDOM.createPortal 将 Modal 对话框的内容渲染到 modal-root DOM 元素下。modal-root DOM 元素通常在 index.html 文件中定义,放置在 <body> 元素的末尾,作为 Modal 对话框的渲染容器。App 组件使用 Modal 组件时,通过 isOpen prop 控制 Modal 对话框的显示和隐藏。

Portals 的事件冒泡 (Event Bubbling)

虽然 Portals 将组件渲染到 DOM 树的不同位置,但事件冒泡 (Event Bubbling) 仍然会沿着 React 组件树向上冒泡,而不是沿着 DOM 树冒泡。这意味着,在 Portals 组件内部触发的事件,仍然可以被 React 组件树中 Portals 组件的父组件捕获。

Portals 总结

Portals 提供了一种将组件渲染到 DOM 树中不同位置的能力,解决了组件 DOM 结构受到父组件 DOM 结构限制的问题。Portals 主要用于 Modal 对话框、Tooltip 提示框、Notification 通知栏等 UI 场景,使得这些 UI 元素能够突破 DOM 结构限制,正确地渲染和显示。 🚪

本章总结

本章深入探讨了 React 组件的进阶技巧,包括组件组合与复用模式 (Children Prop, Specialization, Container Components)、高阶组件 (HOC)、Render Props、Forwarding Refs 和 Portals。这些进阶技巧是构建复杂、可维护 React 应用的关键。掌握这些技巧,可以让你编写更灵活、更高效、更可复用的 React 代码,提升你的 React 开发技能。下一章,我们将学习 React 应用的性能优化。🚀

REVIEW PASS

12. chapter 12: 性能优化

React 以其高效的 Virtual DOM 和组件更新机制而闻名,但在构建大型、复杂的 React 应用时,仍然需要关注性能优化。优化 React 应用的性能,可以提升用户体验,减少资源消耗,提高应用的响应速度和流畅度。本章将深入探讨 React 应用的性能优化策略,包括代码分割 (Code Splitting)、懒加载 (Lazy Loading)、Memoization (记忆化) 和虚拟化 (Virtualization) 列表等关键技术,助你打造高性能的 React 应用。

12.1 代码分割 (Code Splitting)

代码分割 (Code Splitting) 是指将 JavaScript 代码分割成多个 bundle (代码包),按需加载 (On-demand Loading) 的技术。在传统的 Web 应用中,通常会将整个应用的代码打包成一个或少数几个大的 JavaScript 文件。当用户首次访问应用时,浏览器需要下载并解析整个 JavaScript 文件,这会导致首屏加载时间过长,影响用户体验。代码分割可以将应用的代码分割成多个小的 bundle,只在用户需要访问某个功能模块时,才加载对应的 bundle,从而减少首屏加载时间 (Initial Load Time),提高应用的初始加载性能 (Initial Performance)

12.1.1 代码分割的类型

代码分割主要有两种类型:

基于入口点的分割 (Entry Point Splitting):将应用的代码按照不同的入口点 (Entry Points) 分割成多个 bundle。例如,可以将应用的首页、登录页、商品列表页等作为不同的入口点,分别打包成不同的 bundle。当用户访问不同的页面时,只加载该页面对应的 bundle。

动态分割 (Dynamic Splitting):在应用运行时,动态地 (Dynamically) 加载需要的代码 bundle。例如,可以使用 import() 语法动态地加载组件或模块,实现按需加载 (On-demand Loading)。动态分割是代码分割的主要方式,可以更精细地控制代码的加载时机,提高性能。

12.1.2 代码分割的实现方式

在 React 应用中,代码分割的实现方式主要有以下几种:

使用 React.lazySuspense 进行组件懒加载 (Component Lazy Loading)React.lazySuspense 是 React 官方提供的用于组件懒加载的 API。React.lazy 用于动态导入组件 (Dynamic Import)Suspense 用于在组件加载时显示 Loading 指示器 (Loading Indicator)

使用 React.lazySuspense 实现组件懒加载

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 import React, { Suspense, lazy } from 'react';
2
3 // 使用 React.lazy 动态导入组件组件在首次渲染时才会加载
4 const LazyComponent = lazy(() => import('./LazyComponent')); // 动态导入 LazyComponent
5
6 function App() {
7 return (
8 <div>
9 <h1>Code Splitting Example</h1>
10 <Suspense fallback={<div>Loading...</div>}> {/* 使用 Suspense 包裹 LazyComponentfallback prop 指定 Loading 指示器 */}
11 <LazyComponent /> {/* 渲染 LazyComponent组件加载时会显示 Loading... */}
12 </Suspense>
13 </div>
14 );
15 }

在这个例子中,React.lazy(() => import('./LazyComponent')) 使用 import() 语法动态导入 LazyComponent 组件。LazyComponent 组件只有在首次渲染时才会加载,加载完成后才会显示组件内容。Suspense 组件的 fallback prop 指定了 Loading 指示器,在 LazyComponent 组件加载完成之前,会显示 <div>Loading...</div>

路由级代码分割 (Route-Based Code Splitting):结合 React Router 等路由库,将不同的路由路径对应的组件进行代码分割。当用户访问不同的路由路径时,只加载该路由路径对应的组件 bundle。

使用 React Router 和 React.lazy 实现路由级代码分割

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 import React, { Suspense, lazy } from 'react';
2 import { BrowserRouter as Router, Route, Routes } from 'react-router-dom';
3
4 // 使用 React.lazy 动态导入路由组件
5 const HomePage = lazy(() => import('./pages/HomePage')); // 动态导入 HomePage 组件
6 const AboutPage = lazy(() => import('./pages/AboutPage')); // 动态导入 AboutPage 组件
7 const ProductsPage = lazy(() => import('./pages/ProductsPage')); // 动态导入 ProductsPage 组件
8
9 function App() {
10 return (
11 <Router>
12 <Suspense fallback={<div>Loading route...</div>}> {/* 使用 Suspense 包裹 Routesfallback prop 指定路由 Loading 指示器 */}
13 <Routes>
14 <Route path="/" element={<HomePage />} /> {/* 路由路径 "/" 对应 HomePage 组件 */}
15 <Route path="/about" element={<AboutPage />} /> {/* 路由路径 "/about" 对应 AboutPage 组件 */}
16 <Route path="/products" element={<ProductsPage />} /> {/* 路由路径 "/products" 对应 ProductsPage 组件 */}
17 </Routes>
18 </Suspense>
19 </Router>
20 );
21 }

在这个例子中,HomePageAboutPageProductsPage 组件都使用 React.lazy 动态导入。当用户访问不同的路由路径时,React Router 会根据路由配置渲染对应的组件,Suspense 组件会在组件加载完成之前显示 Loading 指示器。路由级代码分割可以有效地减少首屏加载时间,提高应用的初始加载性能。

使用 import() 动态加载模块 (Dynamic Module Loading):除了组件懒加载和路由级代码分割,还可以使用 import() 语法动态加载 JavaScript 模块,实现更细粒度的代码分割。

使用 import() 动态加载模块示例

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 import React, { useState, useCallback } from 'react';
2
3 function DynamicModuleLoadingExample() {
4 const [dynamicModule, setDynamicModule] = useState(null);
5 const [loading, setLoading] = useState(false);
6
7 const loadModule = useCallback(async () => {
8 setLoading(true);
9 const module = await import('./dynamic-module'); // 使用 import() 动态加载模块 dynamic-module.js
10 setDynamicModule(() => module.default); // 将模块的 default export 设置为 dynamicModule 状态
11 setLoading(false);
12 }, []);
13
14 if (loading) {
15 return <p>Loading module...</p>;
16 }
17
18 const DynamicComponent = dynamicModule; // 获取动态加载的组件
19
20 return (
21 <div>
22 <h1>Dynamic Module Loading Example</h1>
23 <button onClick={loadModule} disabled={loading || dynamicModule}> {/* 点击按钮加载模块 */}
24 Load Dynamic Module
25 </button>
26 {DynamicComponent && <DynamicComponent />} {/* 动态渲染加载的组件 */}
27 </div>
28 );
29 }

在这个例子中,点击 "Load Dynamic Module" 按钮后,loadModule 函数会被调用,使用 import('./dynamic-module') 动态加载 dynamic-module.js 模块。模块加载完成后,将模块的 default export (假设是一个 React 组件) 设置为 dynamicModule 状态,并动态渲染 DynamicComponent 组件。

12.1.3 代码分割的收益与注意事项

代码分割的收益

减少首屏加载时间:只加载首屏需要的代码,减少初始 bundle 体积,缩短首屏加载时间。
提高初始加载性能:减少浏览器在首屏加载时需要解析和执行的 JavaScript 代码量,提高应用的初始加载性能。
按需加载资源:只在需要时才加载对应的代码和资源,节省带宽和资源。
优化用户体验:更快的首屏加载速度和更流畅的应用体验。

代码分割的注意事项

分割粒度:代码分割的粒度需要根据应用的需求进行权衡。分割粒度过细可能会增加请求数量,分割粒度过粗可能达不到优化的效果。
Loading 指示器:在代码 bundle 加载过程中,需要显示 Loading 指示器,提示用户正在加载中,避免用户感到卡顿或困惑。
服务端渲染 (SSR) 兼容性:代码分割需要与服务端渲染 (SSR) 兼容,确保在服务端渲染时也能正确处理代码分割。
构建工具配置:需要配置构建工具 (例如 webpack, Vite) 以支持代码分割,并进行合理的配置和优化。

代码分割总结

代码分割是一种重要的 React 应用性能优化技术,它可以有效地减少首屏加载时间,提高初始加载性能,优化用户体验。在构建大型 React 应用时,应该充分利用代码分割技术,提高应用的性能和用户体验。 ✂️

12.2 懒加载 (Lazy Loading) 组件与资源

懒加载 (Lazy Loading) 是一种延迟加载 (Deferred Loading) 资源的技术。在 Web 应用中,资源 (例如:组件、图片、视频、模块等) 可以分为首屏关键资源 (Above-the-fold Resources)非首屏资源 (Below-the-fold Resources)。首屏关键资源是指用户首次访问页面时立即需要的资源,而非首屏资源是指用户在滚动页面或进行交互后才需要的资源。懒加载的核心思想是:优先加载首屏关键资源,延迟加载非首屏资源。通过懒加载,可以减少首屏加载时间,提高应用的初始加载性能。

12.2.1 组件懒加载 (Component Lazy Loading)

组件懒加载 (Component Lazy Loading) 是指将组件的代码分割成独立的 bundle,只在组件即将渲染 (Just-in-time Rendering) 时才加载组件的代码。组件懒加载通常与代码分割结合使用,可以实现更精细的代码分割和按需加载。

组件懒加载的实现方式

使用 React.lazySuspense API 可以实现组件懒加载 (我们在 12.1 节已经介绍过 React.lazySuspense 的用法)。

组件懒加载的收益

减少初始 bundle 体积:将组件代码分割成独立的 bundle,减少初始 bundle 体积。
按需加载组件代码:只在组件即将渲染时才加载组件的代码,避免加载不必要的代码。
提高页面初始加载速度:减少首屏加载时需要解析和执行的 JavaScript 代码量,提高页面初始加载速度。
优化用户体验:更快的页面初始加载速度和更流畅的应用体验。

12.2.2 图片懒加载 (Image Lazy Loading)

图片懒加载 (Image Lazy Loading) 是指延迟加载页面中的图片资源。当图片滚动到可视区域 (Viewport) 时,才开始加载图片。图片懒加载可以减少页面初始加载时需要加载的图片数量,提高页面加载速度,节省带宽。

图片懒加载的实现方式

原生懒加载 (Native Lazy Loading):现代浏览器 (Chrome 76+, Firefox 75+, Safari 14+) 已经原生支持图片懒加载。只需要在 <img> 标签上添加 loading="lazy" 属性即可启用原生懒加载。

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 <img src="image.jpg" loading="lazy" alt="Lazy-loaded image">

loading 属性的可选值:

  • lazy:启用懒加载,图片在滚动到可视区域附近时加载。
  • eager:立即加载图片 (默认值)。
  • auto:浏览器自行决定是否懒加载。

Intersection Observer API:对于不支持原生懒加载的浏览器,可以使用 Intersection Observer API 实现图片懒加载。Intersection Observer API 可以监听元素是否进入或离开可视区域。当图片元素进入可视区域时,开始加载图片。

使用 Intersection Observer API 实现图片懒加载

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 import React, { useRef, useEffect, useState } from 'react';
2
3 function LazyLoadImage({ src, alt }) {
4 const [isLoaded, setIsLoaded] = useState(false);
5 const imageRef = useRef(null);
6
7 useEffect(() => {
8 const observer = new IntersectionObserver((entries) => { // 创建 IntersectionObserver 实例
9 entries.forEach(entry => {
10 if (entry.isIntersecting) { // 图片进入可视区域
11 const image = imageRef.current;
12 image.src = src; // 开始加载图片
13 image.onload = () => { // 图片加载完成后设置 isLoaded true
14 setIsLoaded(true);
15 };
16 observer.unobserve(image); // 取消监听
17 }
18 });
19 });
20
21 observer.observe(imageRef.current); // 监听 imageRef 对应的 DOM 元素
22
23 return () => observer.disconnect(); // 组件卸载时取消监听
24 }, [src]);
25
26 return (
27 <img
28 ref={imageRef}
29 data-src={src} // 使用 data-src 存储图片 URL初始 src 为占位符或空
30 src={isLoaded ? src : 'placeholder.png'} // 加载完成后显示真实图片加载前显示占位符
31 alt={alt}
32 style={{ opacity: isLoaded ? 1 AlBeRt63EiNsTeIn 'opacity 0.5s' }} // 加载完成后显示图片添加过渡动画
33 />
34 );
35 }

在这个例子中,LazyLoadImage 组件使用 Intersection Observer API 监听图片元素是否进入可视区域。当图片进入可视区域时,开始加载图片,加载完成后显示真实图片。加载前显示占位符图片或空白,并添加过渡动画,提升用户体验。

图片懒加载的收益

减少页面初始加载时间:延迟加载非首屏图片,减少初始加载时需要加载的图片数量,缩短页面加载时间。
节省带宽:只加载用户实际看到的图片,节省带宽资源。
提高页面性能:减少浏览器在页面初始加载时需要处理的图片资源,提高页面渲染性能。
优化用户体验:更快的页面加载速度和更流畅的滚动体验。

12.2.3 其他资源懒加载

除了组件和图片,还可以对其他资源进行懒加载,例如:

  • 视频懒加载 (Video Lazy Loading):延迟加载视频资源,当视频滚动到可视区域或用户点击播放按钮时才加载视频。
  • iframe 懒加载 (iframe Lazy Loading):延迟加载 iframe 资源,当 iframe 滚动到可视区域时才加载 iframe 内容。
  • 模块懒加载 (Module Lazy Loading):使用 import() 动态加载 JavaScript 模块,实现模块的按需加载 (我们在 12.1.2 节已经介绍过模块懒加载)。
  • 数据懒加载 (Data Lazy Loading):延迟加载数据,当组件需要数据时或用户触发某个操作时才加载数据。例如,分页加载、滚动加载等。

懒加载总结

懒加载是一种重要的 Web 应用性能优化技术,它可以有效地减少首屏加载时间,提高初始加载性能,优化用户体验。在 React 应用中,可以对组件、图片、视频、模块、数据等多种资源进行懒加载,提高应用的整体性能和用户体验。 ⏳

12.3 Memoization:useMemo, useCallback, React.memo

Memoization (记忆化) 是一种缓存 (Caching) 技术,用于缓存函数的计算结果组件的渲染结果。当函数或组件接收到相同的输入 (参数或 Props) 时,直接从缓存中返回之前计算的结果,避免重复计算,提高性能。React 提供了 useMemo, useCallbackReact.memo 等 API,用于实现 Memoization。

12.3.1 useMemo Hook:缓存计算结果

useMemo Hook 用于缓存计算结果。它接收一个 计算函数 (factory function)依赖项数组 (dependency array) 作为参数,返回一个 缓存值 (memoized value)useMemo Hook 会在组件首次渲染时执行计算函数,并将计算结果缓存起来。在后续渲染中,如果依赖项数组没有发生变化,useMemo Hook 会直接返回缓存的计算结果,不再重新执行计算函数。

useMemo Hook 的基本用法

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 import React, { useMemo, useState } from 'react';
2
3 function UseMemoExample() {
4 const [count, setCount] = useState(0);
5 const [factor, setFactor] = useState(2);
6
7 // 使用 useMemo 缓存计算结果只有当 factor count 变化时才重新计算
8 const expensiveValue = useMemo(() => { // 计算函数
9 console.log('Calculating expensive value...'); // 观察计算函数是否被重复执行
10 let result = count;
11 for (let i = 0; i < 100000000; i++) { // 模拟耗时计算
12 result *= factor;
13 }
14 return result;
15 }, [count, factor]); // 依赖项数组count factor
16
17 return (
18 <div>
19 <p>Count: {count}</p>
20 <p>Factor: {factor}</p>
21 <p>Expensive Value: {expensiveValue}</p>
22 <button onClick={() => setCount(count + 1)}>Increment Count</button>
23 <button onClick={() => setFactor(factor + 1)}>Increment Factor</button>
24 </div>
25 );
26 }

在这个例子中,expensiveValue 使用 useMemo Hook 缓存计算结果。计算函数模拟了一个耗时计算,依赖项数组为 [count, factor]。当 countfactor 状态发生变化时,useMemo Hook 会重新执行计算函数,更新缓存值。如果 countfactor 状态没有变化,useMemo Hook 会直接返回缓存的 expensiveValue,避免重复计算。

12.3.2 useCallback Hook:缓存函数

useCallback Hook 用于缓存函数。它接收一个 回调函数 (callback function)依赖项数组 (dependency array) 作为参数,返回一个 缓存的回调函数 (memoized callback function)useCallback Hook 会在组件首次渲染时创建回调函数,并将回调函数缓存起来。在后续渲染中,如果依赖项数组没有发生变化,useCallback Hook 会直接返回缓存的回调函数,不再重新创建回调函数。useCallback Hook 主要用于优化传递给子组件的回调函数,避免子组件因回调函数引用变化而导致不必要的重新渲染。

useCallback Hook 的基本用法

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 import React, { useState, useCallback } from 'react';
2
3 // 子组件接收回调函数 onClick
4 const Button = React.memo(({ onClick, children }) => { // 使用 React.memo 优化子组件避免不必要的渲染
5 console.log('Button 组件渲染'); // 观察 Button 组件是否被重复渲染
6 return (
7 <button onClick={onClick}>{children}</button>
8 );
9 });
10
11 function UseCallbackExample() {
12 const [count, setCount] = useState(0);
13
14 // 使用 useCallback 缓存回调函数只有当 count 变化时才重新创建回调函数
15 const increment = useCallback(() => { // 回调函数
16 setCount(count + 1);
17 }, [count]); // 依赖项数组count
18
19 return (
20 <div>
21 <p>Count: {count}</p>
22 <Button onClick={increment}>Increment Count</Button> {/* 将缓存的回调函数 increment 传递给子组件 Button */}
23 </div>
24 );
25 }

在这个例子中,increment 回调函数使用 useCallback Hook 缓存。依赖项数组为 [count]。当 count 状态发生变化时,useCallback Hook 会重新创建 increment 回调函数。如果 count 状态没有变化,useCallback Hook 会直接返回缓存的 increment 回调函数。Button 子组件使用 React.memo 进行优化,只有当 Props 发生变化时才重新渲染。由于 increment 回调函数使用了 useCallback Hook 缓存,在 count 状态没有变化时,increment 回调函数的引用保持不变,Button 子组件可以避免不必要的重新渲染。

12.3.3 React.memo:缓存组件渲染结果

React.memo 是一个 高阶组件 (HOC),用于缓存组件的渲染结果React.memo 接收一个组件作为参数,并返回一个新的 记忆化组件 (memoized component)。记忆化组件会浅比较 (Shallow Compare) Props 的变化。如果 Props 没有发生变化,记忆化组件会直接返回之前渲染的结果,不再重新渲染组件。React.memo 主要用于优化函数式组件,避免组件因父组件重新渲染而导致不必要的重新渲染。

React.memo 的基本用法

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 import React from 'react';
2
3 // 使用 React.memo 记忆化函数式组件 MyComponent
4 const MyComponent = React.memo((props) => { // React.memo 接收函数式组件作为参数
5 console.log('MyComponent 组件渲染'); // 观察 MyComponent 组件是否被重复渲染
6 return (
7 <div>
8 <p>My Component, Prop: {props.value}</p>
9 </div>
10 );
11 });
12
13 function ReactMemoExample() {
14 const [count, setCount] = useState(0);
15
16 return (
17 <div>
18 <p>Count: {count}</p>
19 <MyComponent value={count} /> {/* 渲染记忆化组件 MyComponent */}
20 <button onClick={() => setCount(count + 1)}>Increment Count</button>
21 </div>
22 );
23 }

在这个例子中,MyComponent 组件使用 React.memo 记忆化。ReactMemoExample 组件的 count 状态变化时,ReactMemoExample 组件会重新渲染。但由于 MyComponent 组件使用了 React.memo 记忆化,并且 value Prop 的值 (即 count 状态) 也发生了变化,所以 MyComponent 组件也会重新渲染。

React.memo 的第二个参数:自定义比较函数

React.memo 还可以接收第二个参数,自定义比较函数 (arePropsEqual function)。自定义比较函数接收两个参数:prevProps (上一次的 Props) 和 nextProps (当前的 Props)。如果自定义比较函数返回 true,则表示 Props 没有发生变化,记忆化组件会直接返回之前渲染的结果,不再重新渲染组件。如果返回 false,则表示 Props 发生了变化,记忆化组件会重新渲染组件。

使用自定义比较函数的 React.memo 示例

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 import React from 'react';
2
3 // 使用 React.memo 和自定义比较函数记忆化组件 MyComponent
4 const MyComponent = React.memo((props) => {
5 console.log('MyComponent 组件渲染');
6 return (
7 <div>
8 <p>My Component, Prop: {props.value.count}</p>
9 </div>
10 );
11 }, (prevProps, nextProps) => { // 自定义比较函数
12 return prevProps.value.count === nextProps.value.count; // 只比较 value prop count 属性
13 });
14
15 function ReactMemoWithCompareExample() {
16 const [value, setValue] = useState({ count: 0, text: 'Hello' });
17
18 return (
19 <div>
20 <p>Count: {value.count}</p>
21 <MyComponent value={value} /> {/* 渲染记忆化组件 MyComponent传递 value prop */}
22 <button onClick={() => setValue({ ...value, count: value.count + 1 })}>Increment Count</button> {/* 更新 count 属性 */}
23 <button onClick={() => setValue({ ...value, text: 'World' })}>Change Text</button> {/* 更新 text 属性count 属性不变 */}
24 </div>
25 );
26 }

在这个例子中,MyComponent 组件使用了自定义比较函数。自定义比较函数 (prevProps, nextProps) => prevProps.value.count === nextProps.value.count 只比较 value Prop 的 count 属性。当点击 "Increment Count" 按钮时,value.count 属性发生变化,MyComponent 组件会重新渲染。当点击 "Change Text" 按钮时,value.text 属性发生变化,但 value.count 属性没有变化,自定义比较函数返回 trueMyComponent 组件不会重新渲染。

Memoization 总结

Memoization 是一种有效的 React 应用性能优化技术,它可以缓存计算结果、函数和组件的渲染结果,避免不必要的重复计算和组件渲染,提高应用的性能。合理使用 useMemo, useCallbackReact.memo 等 Memoization API,可以显著提升 React 应用的性能和用户体验。 🗄️

12.4 虚拟化 (Virtualization) 列表

虚拟化 (Virtualization) 列表,也称为 窗口化 (Windowing) 列表滚动视口 (Scrollport),是一种用于高效渲染大数据列表 的技术。当需要渲染包含大量数据项 (例如:几千、几万甚至几十万条数据) 的列表时,如果一次性渲染所有列表项,会导致性能问题,例如:页面加载缓慢、滚动卡顿、内存占用过高等。虚拟化列表的核心思想是:只渲染可视区域 (Viewport) 内的列表项,当用户滚动列表时,动态地加载和渲染新的可视区域内的列表项,卸载移出可视区域的列表项。通过虚拟化列表,可以大幅度减少 DOM 元素的数量,提高大数据列表的渲染性能和滚动流畅度。

12.4.1 虚拟化列表的原理

虚拟化列表的原理如下:

计算可视区域 (Viewport) 的起始索引和结束索引:根据滚动位置和列表项的高度,计算出当前可视区域内应该渲染的列表项的起始索引和结束索引。
只渲染可视区域内的列表项:只渲染起始索引和结束索引之间的列表项,而不是渲染整个列表。
动态更新可视区域:当用户滚动列表时,根据新的滚动位置,重新计算可视区域的起始索引和结束索引,动态地加载和渲染新的可视区域内的列表项,卸载移出可视区域的列表项。
使用占位符 (Placeholder) 或空白区域:在可视区域外,可以使用占位符或空白区域来填充列表的滚动区域,保持滚动条的正确高度和滚动行为。

12.4.2 虚拟化列表的实现方式

在 React 应用中,可以使用一些现有的虚拟化列表库,例如:

react-window:React 官方推荐的虚拟化列表库,由 Brian Vaughn (React 核心团队成员) 开发。react-window 专注于高性能的列表和表格虚拟化,API 简洁、灵活,性能优秀。
react-virtualized:另一个流行的虚拟化列表库,功能更全面,支持列表、表格、Grid 等多种虚拟化组件,但 bundle 体积稍大。
react-virtuoso:一个功能丰富、性能优秀的虚拟化列表库,API 更现代、更易用,支持多种虚拟化模式和高级特性。

使用 react-window 实现虚拟化列表

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 import React from 'react';
2 import { FixedSizeList as List } from 'react-window'; // 引入 react-window FixedSizeList 组件
3
4 const rowHeight = 30; // 列表项高度
5
6 // 渲染列表项组件 Row
7 const Row = ({ index, style }) => ( // Row 组件接收 index style prop
8 <div className="list-item" style={style}> {/* style prop react-window 自动计算 */}
9 Row {index + 1}
10 </div>
11 );
12
13 function ReactWindowExample({ itemCount }) {
14 return (
15 <List // 使用 FixedSizeList 组件创建虚拟化列表
16 height={300} // 列表高度
17 width={300} // 列表宽度
18 itemSize={rowHeight} // 列表项高度
19 itemCount={itemCount} // 列表项总数
20 itemData={undefined} // 传递给 Row 组件的额外数据 (可选)
21 >
22 {Row} {/* 渲染列表项组件 Row */}
23 </List>
24 );
25 }

在这个例子中,使用了 react-window 库的 FixedSizeList 组件创建虚拟化列表。FixedSizeList 组件接收 height, width, itemSize, itemCount 等 props,用于配置列表的高度、宽度、列表项高度和列表项总数。Row 组件是列表项组件,接收 index (列表项索引) 和 style (列表项样式) props,style prop 由 react-window 自动计算,包含了列表项的位置和大小信息。FixedSizeList 组件只会渲染可视区域内的列表项,当用户滚动列表时,动态地更新可视区域内的列表项。

虚拟化列表的收益

大幅度减少 DOM 元素数量:只渲染可视区域内的列表项,而不是渲染整个列表,大幅度减少 DOM 元素的数量。
提高大数据列表渲染性能:减少浏览器需要处理的 DOM 元素数量,提高大数据列表的渲染性能。
提高滚动流畅度:减少 DOM 元素的数量,提高列表的滚动流畅度,避免滚动卡顿。
降低内存占用:减少 DOM 元素的数量,降低内存占用,提高应用的整体性能。

虚拟化列表的适用场景

  • 大数据列表:需要渲染包含大量数据项 (几千、几万甚至几十万条数据) 的列表。
  • 长列表:列表高度超过一屏,需要滚动才能查看所有列表项。
  • 性能敏感型应用:对列表渲染性能和滚动流畅度有较高要求的应用。

虚拟化列表的局限性

  • 增加开发复杂度:使用虚拟化列表库需要学习和理解虚拟化列表的原理和 API,增加一定的开发复杂度。
  • 样式定制限制:虚拟化列表库通常对列表项的样式有一定的限制,例如:列表项高度需要固定或可预测。
  • 非所有场景适用:对于小数据量列表或非列表型数据展示,虚拟化列表可能没有明显的性能优势,反而会增加代码复杂度。

虚拟化列表总结

虚拟化列表是一种高效渲染大数据列表的关键技术,它可以大幅度减少 DOM 元素数量,提高大数据列表的渲染性能和滚动流畅度。对于需要渲染大数据列表的 React 应用,使用虚拟化列表库是优化性能的有效手段。 🪟

本章总结

本章深入探讨了 React 应用的性能优化策略,包括代码分割、懒加载、Memoization 和虚拟化列表。代码分割和懒加载可以减少首屏加载时间,提高初始加载性能;Memoization 可以缓存计算结果、函数和组件渲染结果,避免不必要的重复计算和渲染;虚拟化列表可以高效渲染大数据列表,提高大数据列表的渲染性能和滚动流畅度。掌握这些性能优化技术,可以让你打造高性能、用户体验优秀的 React 应用。 🎉

REVIEW PASS

perform step 5.

13. chapter 13:服务端渲染 (SSR) 与 Next.js

在现代 Web 应用开发中,用户体验至关重要。首屏加载速度、搜索引擎优化(SEO)以及应用的可访问性都是影响用户体验的关键因素。传统的客户端渲染(Client-Side Rendering, CSR)应用在这些方面存在一些局限性。为了解决这些问题,服务端渲染(Server-Side Rendering, SSR)应运而生。本章将深入探讨服务端渲染的概念、优势与应用场景,并重点介绍基于 React 的 SSR 框架——Next.js,帮助读者掌握构建高性能 React 应用的关键技术。

13.1 服务端渲染的优势与应用场景

13.1.1 客户端渲染 (CSR) 的局限性

传统的 React 应用通常采用客户端渲染模式。在这种模式下,浏览器首先加载一个基本的 HTML 页面,然后下载 JavaScript 代码并执行。React 应用在客户端完成组件的渲染和页面的构建。

首屏加载慢:浏览器需要下载、解析和执行大量的 JavaScript 代码才能渲染出完整的页面内容。在网络环境较差或设备性能较低的情况下,用户可能会长时间看到白屏,用户体验不佳。
SEO 不友好:搜索引擎爬虫通常难以执行 JavaScript 代码,因此无法抓取 CSR 应用动态生成的内容。这会影响应用在搜索引擎中的排名,不利于 SEO。
不利于可访问性:依赖 JavaScript 执行的应用对不支持 JavaScript 或禁用 JavaScript 的用户不友好。

13.1.2 服务端渲染 (SSR) 的优势

服务端渲染是一种在服务器端完成页面初始渲染的技术。服务器接收到用户的请求后,会执行 React 代码,将组件渲染成 HTML 字符串,然后将完整的 HTML 返回给浏览器。浏览器接收到 HTML 后可以直接展示页面内容,无需等待 JavaScript 代码下载和执行。

服务端渲染带来了以下显著优势:

更快的首屏加载速度:浏览器无需等待 JavaScript 代码下载和执行,可以直接渲染服务器返回的 HTML,显著提升首屏加载速度,改善用户体验。🚀
更好的 SEO:搜索引擎爬虫可以直接抓取服务器返回的 HTML 内容,更容易索引页面内容,提升 SEO 效果。 🔎
提升可访问性:即使浏览器不支持 JavaScript 或禁用 JavaScript,用户仍然可以访问到页面的基本内容。 ♿️
更佳的性能:对于一些性能较低的设备,服务端渲染可以减轻客户端的渲染压力。

13.1.3 服务端渲染的应用场景

服务端渲染并非适用于所有场景。它主要适用于以下类型的应用:

内容型网站:例如博客、新闻网站、电商网站的商品详情页等,这些网站对首屏加载速度和 SEO 有较高要求。
需要快速展示关键内容的 Web 应用:例如营销活动页面、落地页等,需要第一时间向用户展示核心信息。
对 SEO 有较高要求的 Web 应用:例如企业官网、在线教育平台等,需要通过搜索引擎获取流量。

对于交互复杂的富客户端应用,例如后台管理系统、Web IDE 等,客户端渲染可能更适合,因为这些应用对 SEO 的要求不高,且更注重客户端的交互体验。

13.2 Next.js 快速上手:创建 SSR 应用

Next.js 是一个基于 React 的开源框架,专门用于构建服务端渲染和静态网站。它提供了开箱即用的 SSR 功能,极大地简化了 React SSR 应用的开发流程。

13.2.1 Next.js 的核心特性

Next.js 提供了以下核心特性,使其成为构建 React SSR 应用的理想选择:

服务端渲染 (SSR) 和静态生成 (SSG):Next.js 支持服务端渲染和静态生成两种预渲染方式,可以根据不同的页面需求选择合适的渲染策略。
文件系统路由:Next.js 使用文件系统作为路由,pages 目录下的文件会自动生成对应的路由,简化了路由配置。 📁
API 路由:Next.js 允许在 pages/api 目录下创建 API 路由,方便构建后端 API 接口。 ⚙️
代码分割和优化:Next.js 内置了代码分割、图片优化、字体优化等性能优化策略,提升应用性能。 ⚡️
开发体验:Next.js 提供了快速刷新、错误提示等优秀开发体验,提升开发效率。 👨‍💻

13.2.2 创建 Next.js 项目

使用 create-next-app 脚手架可以快速创建一个 Next.js 项目。

打开终端,执行以下命令:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 npx create-next-app my-nextjs-app
2 cd my-nextjs-app

或者使用 yarn 或 pnpm:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 yarn create next-app my-nextjs-app
2 cd my-nextjs-app
1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 pnpm create next-app my-nextjs-app
2 cd my-nextjs-app

执行命令后,脚手架会引导你进行项目配置,例如是否使用 TypeScript、ESLint 等。根据你的需求选择配置即可。

项目创建完成后,进入项目目录 my-nextjs-app

13.2.3 启动开发服务器

在项目根目录下,执行以下命令启动开发服务器:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 npm run dev

或者使用 yarn 或 pnpm:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 yarn dev
1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 pnpm dev

启动成功后,访问 http://localhost:3000,即可看到 Next.js 默认的欢迎页面。

13.2.4 第一个 Next.js 页面

打开 pages/index.js 文件,可以看到如下代码:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 ```javascript
2 import Head from 'next/head'
3 import styles from '../styles/Home.module.css'
4
5 export default function Home() {
6 return (
7
8
9
10 Welcome to
11
12 Next.js!
13
14
15
16
17
18
19
20 Get started by editing `pages/index.js`
21
22
23
24 Documentation
25 Learn about Next.js features and API.
26
27
28 Learn
29 Learn about Next.js in an interactive course with quizzes!
30
31
32 Examples
33 Discover and deploy boilerplate example Next.js projects.
34
35
36 Deploy
37 Instantly deploy your Next.js site to a public URL with Vercel.
38
39
40
41
42
43 )
44 }
45 ```

这是一个标准的 React 函数组件。Next.js 将 pages 目录下的 index.js 文件自动映射为根路由 /

修改 pages/index.js 文件内容,例如修改 Welcome to Next.js!Hello Next.js!,保存后页面会自动刷新,显示修改后的内容。这就是 Next.js 的快速刷新功能。

13.3 Next.js 核心特性:页面路由、数据获取

13.3.1 文件系统路由

Next.js 使用文件系统路由,pages 目录下的每个文件都会自动生成一个路由。

pages/index.js -> /
pages/about.js -> /about
pages/posts/index.js -> /posts
pages/posts/[id].js -> /posts/:id (动态路由)

创建页面

要创建一个新的页面,只需要在 pages 目录下创建一个新的 .js.jsx 文件即可。例如,在 pages 目录下创建一个 about.js 文件:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 ```javascript
2 function AboutPage() {
3 return (
4
5
6
7
8 About Us
9
10
11
12
13
14
15 This is the about page.
16
17
18
19
20 );
21 }
22
23 export default AboutPage;
24 ```

访问 http://localhost:3000/about 即可看到 About 页面的内容。

动态路由

Next.js 支持动态路由,用于处理参数化的路由。例如,要创建一个文章详情页,路由路径为 /posts/:id,可以在 pages/posts 目录下创建一个 [id].js 文件。

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 ```javascript
2 import { useRouter } from 'next/router';
3
4 function PostDetailPage() {
5 const router = useRouter();
6 const { id } = router.query;
7
8 return (
9
10
11
12
13 Post Detail - {id}
14
15
16
17
18
19
20 Post ID: {id}
21
22
23
24
25 );
26 }
27
28 export default PostDetailPage;
29 ```

访问 http://localhost:3000/posts/123,页面会显示 "Post Detail - 123" 和 "Post ID: 123"。

13.3.2 数据获取 (Data Fetching)

Next.js 提供了多种数据获取方式,以满足不同场景的需求。

getStaticProps (静态生成)

getStaticProps 函数在构建时(build time)运行,用于获取构建时需要的数据。它只在服务器端运行,不会包含在客户端 JavaScript bundle 中。适用于博客文章、商品列表等静态内容。

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 ```javascript
2 function HomePage({ posts }) {
3 return (
4
5
6
7
8 Blog Posts
9
10
11
12
13
14
15 {posts.map((post) => (
16
17 {post.title}
18
19 ))}
20
21
22
23
24 );
25 }
26
27 export async function getStaticProps() {
28 // 在构建时获取 posts 数据
29 const res = await fetch('https://api.example.com/posts');
30 const posts = await res.json();
31
32 return {
33 props: {
34 posts,
35 },
36 };
37 }
38
39 export default HomePage;
40 ```

getServerSideProps (服务端渲染)

getServerSideProps 函数在每次请求时(request time)运行,用于获取每次请求都需要动态更新的数据。它也在服务器端运行。适用于用户身份验证、实时数据展示等动态内容。

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 ```javascript
2 function UserProfilePage({ user }) {
3 return (
4
5
6
7
8 User Profile
9
10
11
12
13
14
15 Username: {user.username}
16 Email: {user.email}
17
18
19
20
21 );
22 }
23
24 export async function getServerSideProps(context) {
25 const { req, res } = context;
26 // 获取用户认证信息 (例如从 cookies 中)
27 const token = req.cookies.token;
28
29 // 验证 token,获取用户信息
30 const user = await fetchUser(token);
31
32 if (!user) {
33 return {
34 redirect: {
35 destination: '/login',
36 permanent: false,
37 },
38 };
39 }
40
41 return {
42 props: {
43 user,
44 },
45 };
46 }
47
48 export default UserProfilePage;
49 ```

getStaticPaths (配合 getStaticProps 用于动态路由)

对于使用动态路由的静态页面,需要使用 getStaticPaths 函数预先生成所有可能的路由路径。

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 ```javascript
2 function PostDetailPage({ post }) {
3 return (
4
5
6
7
8 Post Detail - {post.title}
9
10
11
12
13
14
15
16 Title: {post.title}
17
18
19 Content: {post.content}
20
21
22
23
24
25 );
26 }
27
28 export async function getStaticPaths() {
29 // 获取所有 post 的 id
30 const res = await fetch('https://api.example.com/posts');
31 const posts = await res.json();
32
33 const paths = posts.map((post) => ({
34 params: { id: post.id.toString() },
35 }));
36
37 return {
38 paths,
39 fallback: false, // 如果路径不存在,则返回 404 页面
40 };
41 }
42
43 export async function getStaticProps({ params }) {
44 const { id } = params;
45 // 根据 id 获取 post 数据
46 const res = await fetch(`https://api.example.com/posts/${id}`);
47 const post = await res.json();
48
49 return {
50 props: {
51 post,
52 },
53 };
54 }
55
56 export default PostDetailPage;
57 ```

客户端数据获取 (Client-side Data Fetching)

对于一些不重要的、用户交互后才需要的数据,可以在客户端进行数据获取,例如使用 useEffect Hook 和 fetch API。

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 ```javascript
2 import { useState, useEffect } from 'react';
3
4 function CommentsSection({ postId }) {
5 const [comments, setComments] = useState([]);
6
7 useEffect(() => {
8 async function fetchComments() {
9 const res = await fetch(`/api/posts/${postId}/comments`);
10 const data = await res.json();
11 setComments(data);
12 }
13 fetchComments();
14 }, [postId]);
15
16 return (
17
18
19
20
21 Comments
22
23
24
25
26
27
28 {comments.map((comment) => (
29
30 {comment.content} - {comment.author}
31
32 ))}
33
34
35
36
37 );
38 }
39
40 export default CommentsSection;
41 ```

13.3.3 API 路由

Next.js 允许在 pages/api 目录下创建 API 路由,用于构建后端 API 接口。pages/api 目录下的文件会被处理为 API 路由,而不是页面路由。

例如,在 pages/api 目录下创建一个 hello.js 文件:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 ```javascript
2 export default function handler(req, res) {
3 res.status(200).json({ message: 'Hello from Next.js API!' });
4 }
5 ```

访问 /api/hello 路由,即可获取 JSON 响应 {"message": "Hello from Next.js API!"}

API 路由可以用于处理表单提交、数据库操作、第三方 API 调用等后端逻辑。

13.4 部署 Next.js 应用

Next.js 应用可以部署到多种平台,例如 Vercel、Netlify、AWS、Docker 等。

13.4.1 部署到 Vercel

Vercel 是 Next.js 官方推荐的部署平台,提供了最佳的 Next.js 应用部署体验。

注册 Vercel 账号:访问 vercel.com 注册账号。
安装 Vercel CLI:在终端执行 npm install -g vercelyarn global add vercel
登录 Vercel:在项目根目录下执行 vercel login,按照提示完成登录。
部署应用:在项目根目录下执行 vercel 命令,Vercel CLI 会自动检测 Next.js 项目并进行部署。

Vercel 会自动构建和部署 Next.js 应用,并提供一个唯一的 URL 用于访问你的应用。每次代码更新,只需要重新执行 vercel 命令即可更新部署。

13.4.2 其他部署平台

除了 Vercel,Next.js 应用也可以部署到其他平台,例如:

Netlify:类似于 Vercel 的 Serverless 平台,也提供了良好的 Next.js 支持。
AWS (Amazon Web Services):可以使用 AWS EC2、AWS S3 + CloudFront、AWS Amplify 等服务部署 Next.js 应用。
Docker:可以使用 Docker 容器化 Next.js 应用,部署到任何支持 Docker 的平台。

具体的部署步骤会根据不同的平台有所差异,可以参考 Next.js 官方文档和各平台的部署指南。

总结

本章深入介绍了服务端渲染 (SSR) 的概念、优势与应用场景,并详细讲解了基于 React 的 SSR 框架 Next.js。我们学习了 Next.js 的核心特性,包括文件系统路由、数据获取方式和 API 路由,并了解了如何快速创建和部署 Next.js 应用。掌握 Next.js 可以帮助开发者构建高性能、SEO 友好的 React 应用,提升用户体验和应用价值。在接下来的章节中,我们将继续深入学习 React 生态系统的其他重要技术和最佳实践。


本章关键词:

  • 服务端渲染 (SSR)
  • 客户端渲染 (CSR)
  • 首屏加载速度
  • 搜索引擎优化 (SEO)
  • Next.js
  • 文件系统路由
  • API 路由
  • getStaticProps
  • getServerSideProps
  • getStaticPaths
  • Vercel
  • 部署

练习与思考:

① 尝试创建一个 Next.js 项目,并创建一个包含静态内容和动态内容的页面。
② 比较 getStaticPropsgetServerSideProps 的使用场景,并分析它们的优缺点。
③ 尝试将你的 Next.js 应用部署到 Vercel 或其他平台。
④ 思考在哪些场景下服务端渲染是必要的,哪些场景下客户端渲染更合适?

希望本章内容能够帮助你深入理解服务端渲染和 Next.js,为构建更优秀的 React 应用打下坚实的基础。 🚀📚 Happy coding! 🎉


(本章完)

perform step 5.

14. chapter 14:React 测试 (Testing)

软件测试是软件开发生命周期中至关重要的一环。对于 React Web 应用而言,测试不仅能确保代码的质量和稳定性,还能提高开发效率,降低维护成本。通过编写充分的测试用例,我们可以尽早发现和修复 bug,保证应用在各种场景下的正确运行。本章将深入探讨 React 测试的不同类型和方法,包括单元测试 (Unit Test)、集成测试 (Integration Test) 和端到端测试 (End-to-End Test, E2E Test),并介绍常用的测试工具和最佳实践,帮助读者构建健壮可靠的 React 应用。

14.1 单元测试 (Unit Test):Jest 与 React Testing Library

单元测试是针对软件中最小可测试单元(通常是函数或组件)进行的测试。在 React 应用中,单元测试主要用于验证组件的逻辑和行为是否符合预期。Jest 和 React Testing Library (RTL) 是 React 单元测试中最常用的工具组合。

14.1.1 Jest:JavaScript 测试框架

Jest 是 Facebook 开源的一款流行的 JavaScript 测试框架,它具有以下特点:

零配置:开箱即用,无需复杂的配置即可开始编写测试。
快速:并行执行测试,提高测试速度。 🚀
强大的断言库:内置丰富的断言方法,方便编写各种测试用例。
Mocking 功能:内置 Mocking 功能,方便模拟模块依赖和 API 调用。
代码覆盖率:内置代码覆盖率工具,帮助评估测试的充分性。
Snapshot 测试:支持 Snapshot 测试,方便进行 UI 组件的回归测试。

14.1.2 React Testing Library (RTL):用户行为导向的测试库

React Testing Library (RTL) 是一个轻量级的 React 测试库,它鼓励以用户行为为导向进行组件测试。RTL 的核心理念是:测试组件应该像用户使用组件一样进行。这意味着我们应该关注组件的输入 (props) 和输出 (UI),而不是组件的内部实现细节。

RTL 提供了以下核心 API:

render:渲染 React 组件,返回组件的容器。
screen:提供各种查询方法,用于在渲染后的组件中查找元素,例如:
▮▮▮▮ screen.getByRole('button', { name: /Click me/i }):根据 ARIA role 和文本内容查找按钮元素。
▮▮▮▮
screen.getByText('Hello World'):根据文本内容查找元素。
▮▮▮▮ screen.getByLabelText('Username'):根据 label 文本查找表单元素。
▮▮▮▮
screen.getByPlaceholderText('Enter your email'):根据 placeholder 文本查找输入框元素。
▮▮▮▮ screen.getByAltText('User Avatar'):根据 alt 文本查找图片元素。
▮▮▮▮
screen.queryBy...screen.findBy...:提供异步和非严格查找方法。
fireEvent:模拟用户与组件的交互事件,例如:
▮▮▮▮ fireEvent.click(button):模拟点击事件。
▮▮▮▮
fireEvent.change(input, { target: { value: 'test' } }):模拟输入框内容改变事件。
▮▮▮▮* fireEvent.submit(form):模拟表单提交事件。

14.1.3 配置 Jest 和 RTL

如果使用 Create React App (CRA) 或 Vite 创建的 React 项目,Jest 和 RTL 通常已经预配置好了。

对于 CRA 项目,测试文件需要放在 src 目录下的 __tests__ 目录中,或者文件名以 .test.js.spec.js 结尾。

对于 Vite 项目,需要在项目根目录下安装 @testing-library/react@testing-library/jest-dom

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 npm install --save-dev @testing-library/react @testing-library/jest-dom

或者使用 yarn 或 pnpm:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 yarn add -D @testing-library/react @testing-library/jest-dom
1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 pnpm add -D @testing-library/react @testing-library/jest-dom

然后在 vite.config.js 文件中配置 test 选项:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 ```javascript
2 import { defineConfig } from 'vite'
3 import react from '@vitejs/plugin-react'
4
5 // https://vitejs.dev/config/
6 export default defineConfig({
7 plugins: [react()],
8 test: {
9 globals: true,
10 environment: 'jsdom',
11 setupFiles: './src/test/setupTests.js', // (可选) setup 文件路径
12 },
13 })
14 ```

src/test 目录下创建一个 setupTests.js 文件 (如果 vite.config.js 中配置了 setupFiles),用于添加全局的 setup 代码,例如引入 @testing-library/jest-dom/extend-expect 以扩展 Jest 的断言方法:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 ```javascript
2 import '@testing-library/jest-dom/extend-expect';
3 ```

14.1.4 编写第一个单元测试

假设我们有一个简单的计数器组件 Counter.js

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 ```javascript
2 import React, { useState } from 'react';
3
4 function Counter() {
5 const [count, setCount] = useState(0);
6
7 const increment = () => {
8 setCount(count + 1);
9 };
10
11 const decrement = () => {
12 setCount(count - 1);
13 };
14
15 return (
16
17
18
19 Count: {count}
20
21
22
23 Increment
24
25
26 Decrement
27
28
29
30 );
31 }
32
33 export default Counter;
34 ```

src/__tests__ 目录下创建一个 Counter.test.js 文件,编写单元测试用例:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 ```javascript
2 import React from 'react';
3 import { render, screen, fireEvent } from '@testing-library/react';
4 import Counter from '../components/Counter'; // 假设 Counter 组件在 components 目录下
5
6 describe('Counter Component', () => {
7 it('should render initial count correctly', () => {
8 render(<Counter />);
9 const countElement = screen.getByText(/Count: 0/i); // 使用正则匹配文本内容
10 expect(countElement).toBeInTheDocument(); // 断言元素在文档中
11 });
12
13 it('should increment count when increment button is clicked', () => {
14 render(<Counter />);
15 const incrementButton = screen.getByRole('button', { name: /Increment/i });
16 fireEvent.click(incrementButton);
17 const countElement = screen.getByText(/Count: 1/i);
18 expect(countElement).toBeInTheDocument();
19 });
20
21 it('should decrement count when decrement button is clicked', () => {
22 render(<Counter />);
23 const decrementButton = screen.getByRole('button', { name: /Decrement/i });
24 fireEvent.click(decrementButton);
25 const countElement = screen.getByText(/Count: -1/i);
26 expect(countElement).toBeInTheDocument();
27 });
28 });
29 ```

测试用例解释:

describe('Counter Component', ...): 使用 describe 函数创建一个测试套件 (test suite),用于组织相关的测试用例。
it('should render initial count correctly', ...): 使用 it 函数创建一个测试用例 (test case),描述测试的目的。
render(<Counter />): 使用 render 函数渲染 Counter 组件。
screen.getByText(/Count: 0/i): 使用 screen.getByText 查询方法,根据文本内容 "Count: 0" 查找元素。/i 表示忽略大小写。
expect(countElement).toBeInTheDocument(): 使用 Jest 的断言方法 toBeInTheDocument,断言查找到的 countElement 在文档中。
screen.getByRole('button', { name: /Increment/i }): 使用 screen.getByRole 查询方法,根据 ARIA role "button" 和文本内容 "Increment" 查找按钮元素。
fireEvent.click(incrementButton): 使用 fireEvent.click 模拟点击 incrementButton 事件。

运行测试

在项目根目录下,执行以下命令运行测试:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 npm test

或者使用 yarn 或 pnpm:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 yarn test
1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 pnpm test

Jest 会自动查找并执行测试文件,并输出测试结果。如果所有测试用例都通过,则会显示测试通过的信息。

14.1.5 Mocking 模块依赖

在单元测试中,我们通常需要隔离被测试组件的依赖,例如 API 调用、第三方库等。Jest 提供了强大的 Mocking 功能,可以方便地模拟模块依赖。

假设 Counter 组件需要从外部 API 获取初始 count 值:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 ```javascript
2 import React, { useState, useEffect } from 'react';
3 import { fetchInitialCount } from '../api'; // 假设 API 请求函数
4
5 function Counter() {
6 const [count, setCount] = useState(0);
7
8 useEffect(() => {
9 async function loadInitialCount() {
10 const initialCount = await fetchInitialCount();
11 setCount(initialCount);
12 }
13 loadInitialCount();
14 }, []);
15
16 const increment = () => {
17 setCount(count + 1);
18 };
19
20 const decrement = () => {
21 setCount(count - 1);
22 };
23
24 return (
25
26
27
28 Count: {count}
29
30
31
32 Increment
33
34
35 Decrement
36
37
38
39 );
40 }
41
42 export default Counter;
43 ```

Counter.test.js 中,我们可以使用 jest.mock 模拟 fetchInitialCount 函数的返回值:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 ```javascript
2 import React from 'react';
3 import { render, screen, fireEvent, waitFor } from '@testing-library/react';
4 import Counter from '../components/Counter';
5 import { fetchInitialCount } from '../api';
6
7 jest.mock('../api', () => ({ // 模拟整个 '../api' 模块
8 fetchInitialCount: jest.fn(() => Promise.resolve(10)), // 模拟 fetchInitialCount 函数
9 }));
10
11 describe('Counter Component with API', () => {
12 it('should render initial count from API correctly', async () => {
13 render(<Counter />);
14 await waitFor(() => screen.getByText(/Count: 10/i)); // 使用 waitFor 等待异步操作完成
15 const countElement = screen.getByText(/Count: 10/i);
16 expect(countElement).toBeInTheDocument();
17 expect(fetchInitialCount).toHaveBeenCalledTimes(1); // 断言 API 函数被调用了一次
18 });
19
20 // ... 其他测试用例
21 });
22 ```

Mocking 解释:

jest.mock('../api', ...): 使用 jest.mock 函数模拟 '../api' 模块。
fetchInitialCount: jest.fn(() => Promise.resolve(10)): 在模拟模块中,使用 jest.fn 创建一个 mock 函数 fetchInitialCount,并使用 mockImplementation 或简写形式 mockReturnValue 设置 mock 函数的返回值。这里我们模拟 fetchInitialCount 函数返回一个 resolved Promise,值为 10。
await waitFor(() => screen.getByText(/Count: 10/i)): 由于 fetchInitialCount 是异步操作,我们需要使用 waitFor 函数等待页面内容更新。waitFor 会定期检查回调函数,直到回调函数不抛出错误或超时。
expect(fetchInitialCount).toHaveBeenCalledTimes(1): 使用 Jest 的 mock 函数断言方法 toHaveBeenCalledTimes,断言 fetchInitialCount mock 函数被调用了一次。

14.1.6 Snapshot 测试

Snapshot 测试是一种用于 UI 组件回归测试的技术。它可以将组件的渲染结果(通常是 DOM 结构)序列化成一个 snapshot 文件,并在后续测试中与之前的 snapshot 文件进行比较。如果组件的 UI 发生意外改变,Snapshot 测试会失败,提示开发者检查 UI 变更是否符合预期。

要使用 Snapshot 测试,需要安装 jest-serializer-html (如果使用 CRA,则无需安装,已内置):

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 npm install --save-dev jest-serializer-html

或者使用 yarn 或 pnpm:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 yarn add -D jest-serializer-html

Counter.test.js 中添加 Snapshot 测试用例:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 ```javascript
2 import React from 'react';
3 import { render } from '@testing-library/react';
4 import Counter from '../components/Counter';
5
6 describe('Counter Component Snapshot', () => {
7 it('should match snapshot', () => {
8 const { container } = render(<Counter />);
9 expect(container).toMatchSnapshot(); // 生成或比较 snapshot
10 });
11 });
12 ```

运行 Snapshot 测试

第一次运行 Snapshot 测试时,Jest 会生成一个 snapshot 文件 __snapshots__/Counter.test.js.snap,其中包含了 Counter 组件的初始渲染结果。

后续运行测试时,Jest 会将组件的当前渲染结果与 snapshot 文件进行比较。如果两者一致,则测试通过;如果不一致,则测试失败,并提示 snapshot 发生了变更。

更新 Snapshot

如果组件的 UI 变更符合预期,需要更新 snapshot 文件。可以使用以下命令更新 snapshot:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 npm test -- -u

或者使用 yarn 或 pnpm:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 yarn test -u
1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 pnpm test -u

Jest 会将新的渲染结果写入 snapshot 文件,并更新测试结果。

14.2 集成测试 (Integration Test)

集成测试是测试软件模块之间的交互和协作是否正常。在 React 应用中,集成测试通常用于测试组件之间的交互、组件与状态管理库 (例如 Redux, Zustand) 的集成、组件与路由的集成等。

集成测试的范围比单元测试更大,但比端到端测试更小。它可以帮助我们发现模块之间的集成问题,确保各个模块能够协同工作。

14.2.1 集成测试策略

组件集成测试:测试多个组件之间的交互,例如父组件和子组件之间的 props 传递、事件处理、状态共享等。
组件与状态管理库集成测试:测试组件是否能够正确地从状态管理库中读取数据和更新数据。
组件与路由集成测试:测试组件是否能够正确地处理路由变化、导航、路由参数等。
API 集成测试:测试前端应用与后端 API 的集成,例如数据请求、数据提交、错误处理等。可以使用 Mock Service Worker (MSW) 或其他工具模拟 API 请求。

14.2.2 组件集成测试示例

假设我们有一个 TodoList 组件和一个 TodoItem 组件。TodoList 组件负责渲染 todo 列表,TodoItem 组件负责渲染单个 todo 项。我们需要测试 TodoListTodoItem 组件是否能够正确地协同工作。

TodoList.js:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 ```javascript
2 import React, { useState } from 'react';
3 import TodoItem from './TodoItem';
4
5 function TodoList() {
6 const [todos, setTodos] = useState([
7 { id: 1, text: 'Learn React Testing', completed: false },
8 { id: 2, text: 'Write Unit Tests', completed: false },
9 ]);
10
11 const toggleComplete = (id) => {
12 const updatedTodos = todos.map(todo =>
13 todo.id === id ? { ...todo, completed: !todo.completed } : todo
14 );
15 setTodos(updatedTodos);
16 };
17
18 return (
19
20
21
22 {todos.map(todo => (
23
24 <TodoItem key={todo.id} todo={todo} onToggleComplete={toggleComplete} />
25
26 ))}
27
28
29
30 );
31 }
32
33 export default TodoList;
34 ```

TodoItem.js:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 ```javascript
2 import React from 'react';
3
4 function TodoItem({ todo, onToggleComplete }) {
5 return (
6
7
8
9 <input
10 type="checkbox"
11 checked={todo.completed}
12 onChange={() => onToggleComplete(todo.id)}
13 />
14 {todo.text}
15
16
17
18 );
19 }
20
21 export default TodoItem;
22 ```

TodoList.test.js (集成测试):

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 ```javascript
2 import React from 'react';
3 import { render, screen, fireEvent } from '@testing-library/react';
4 import TodoList from '../components/TodoList';
5
6 describe('TodoList Integration', () => {
7 it('should render todo items correctly', () => {
8 render(<TodoList />);
9 const todoItems = screen.getAllRole('listitem'); // 获取所有 listitem 元素 (TodoItem)
10 expect(todoItems).toHaveLength(2); // 断言渲染了 2 个 todo item
11 expect(screen.getByText(/Learn React Testing/i)).toBeInTheDocument();
12 expect(screen.getByText(/Write Unit Tests/i)).toBeInTheDocument();
13 });
14
15 it('should toggle todo item completion when checkbox is clicked', () => {
16 render(<TodoList />);
17 const checkbox1 = screen.getByRole('checkbox', { name: /Learn React Testing/i });
18 expect(checkbox1).not.toBeChecked(); // 初始状态未选中
19 fireEvent.click(checkbox1);
20 expect(checkbox1).toBeChecked(); // 点击后选中
21 });
22 });
23 ```

集成测试解释:

screen.getAllRole('listitem'): 使用 screen.getAllRole 获取所有 role 为 "listitem" 的元素,即 TodoItem 组件渲染的 list item。
expect(checkbox1).not.toBeChecked()expect(checkbox1).toBeChecked(): 使用 Jest 的扩展断言方法 toBeCheckednot.toBeChecked 断言 checkbox 的选中状态。

14.3 端到端测试 (E2E Test):Cypress

端到端测试 (E2E Test) 是从用户角度出发,模拟用户完整的使用流程,验证应用的整体功能是否正常。E2E 测试覆盖的范围最广,可以测试应用的 UI、用户交互、数据流程、后端 API 集成等。

Cypress 是一个流行的 JavaScript 端到端测试框架,它具有以下特点:

易用性:API 简洁易懂,上手快速。
快速可靠:测试执行速度快,稳定性高。 🚀
Debuggable:提供友好的测试运行界面和调试工具,方便调试测试用例。
Time Travel:测试执行过程中可以 time travel,查看每个步骤的快照。
自动等待:内置自动等待机制,无需显式等待元素加载和动画完成。
Mocking 和 Stubbing:支持 Mocking 和 Stubbing API 请求,方便测试不同场景。

14.3.1 安装 Cypress

在项目根目录下安装 Cypress:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 npm install --save-dev cypress

或者使用 yarn 或 pnpm:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 yarn add -D cypress
1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 pnpm add -D cypress

14.3.2 启动 Cypress Test Runner

在项目根目录下,执行以下命令启动 Cypress Test Runner:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 npx cypress open

或者使用 yarn 或 pnpm:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 yarn cypress open
1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 pnpm cypress open

Cypress Test Runner 会打开一个图形界面,显示测试文件列表和运行状态。

14.3.3 创建第一个 E2E 测试

在 Cypress 项目的 cypress/e2e 目录下创建一个测试文件,例如 todo.cy.js

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 ```javascript
2 describe('Todo App E2E Tests', () => {
3 it('should add a new todo item', () => {
4 cy.visit('http://localhost:3000'); // 访问应用地址
5 cy.get('[data-testid="new-todo-input"]').type('Learn Cypress E2E Testing{enter}'); // 输入 todo text 并回车
6 cy.get('[data-testid="todo-item"]').should('have.length', 1); // 断言 todo item 列表长度为 1
7 cy.get('[data-testid="todo-item"]').first().should('contain.text', 'Learn Cypress E2E Testing'); // 断言第一个 todo item 的文本内容
8 });
9
10 it('should toggle todo item completion', () => {
11 cy.visit('http://localhost:3000');
12 cy.get('[data-testid="todo-item"]').first().find('input[type="checkbox"]').check(); // 选中第一个 todo item 的 checkbox
13 cy.get('[data-testid="todo-item"]').first().should('have.class', 'completed'); // 断言第一个 todo item 包含 'completed' class
14 cy.get('[data-testid="todo-item"]').first().find('input[type="checkbox"]').uncheck(); // 取消选中 checkbox
15 cy.get('[data-testid="todo-item"]').first().should('not.have.class', 'completed'); // 断言第一个 todo item 不包含 'completed' class
16 });
17 });
18 ```

E2E 测试解释:

cy.visit('http://localhost:3000'): 使用 cy.visit 命令访问应用地址。
cy.get('[data-testid="new-todo-input"]'): 使用 cy.get 命令根据 data-testid 属性选择元素。Cypress 推荐使用 data-testid 属性来选择元素,以避免选择器过于依赖 UI 结构。
.type('Learn Cypress E2E Testing{enter}'): 使用 .type 命令模拟用户输入文本,{enter} 表示模拟回车键。
.should('have.length', 1): 使用 .should 命令进行断言,'have.length', 1 表示断言元素列表的长度为 1。
.should('contain.text', 'Learn Cypress E2E Testing'): 使用 .should 命令进行断言,'contain.text', 'Learn Cypress E2E Testing' 表示断言元素包含文本内容 "Learn Cypress E2E Testing"。
.find('input[type="checkbox"]'): 使用 .find 命令在父元素下查找子元素。
.check().uncheck(): 使用 .check.uncheck 命令模拟选中和取消选中 checkbox。
.should('have.class', 'completed').should('not.have.class', 'completed'): 使用 .should 命令进行断言,'have.class', 'completed' 表示断言元素包含 'completed' class,'not.have.class', 'completed' 表示断言元素不包含 'completed' class。

运行 E2E 测试

在 Cypress Test Runner 图形界面中,点击 todo.cy.js 文件,即可运行 E2E 测试。Cypress 会自动启动浏览器,访问应用地址,并执行测试用例。测试执行过程中,可以在 Cypress Test Runner 中实时查看测试步骤和结果。

14.3.4 Cypress Best Practices

使用 data-testid 属性选择元素:避免选择器过于依赖 UI 结构,提高测试的稳定性。
编写简洁的测试用例:每个测试用例只测试一个小的功能点,避免测试用例过于复杂。
合理使用 Mocking 和 Stubbing:对于不必要的外部依赖 (例如第三方 API),可以使用 Cypress 的 cy.intercept 命令进行 Mocking 和 Stubbing,提高测试速度和稳定性。
使用 Cypress Dashboard:Cypress Dashboard 是 Cypress 官方提供的测试仪表盘服务,可以记录测试运行结果、截图、视频等,方便进行测试分析和管理。

总结

本章深入探讨了 React 测试的不同类型和方法,包括单元测试、集成测试和端到端测试。我们学习了如何使用 Jest 和 React Testing Library 进行单元测试,如何进行组件集成测试,以及如何使用 Cypress 进行端到端测试。掌握这些测试技术可以帮助开发者构建高质量、高可靠性的 React 应用。在实际开发中,应该根据项目需求和测试目标,选择合适的测试类型和策略,构建完善的测试体系。


本章关键词:

  • 单元测试 (Unit Test)
  • 集成测试 (Integration Test)
  • 端到端测试 (E2E Test)
  • Jest
  • React Testing Library (RTL)
  • Snapshot 测试
  • Mocking
  • Cypress
  • data-testid
  • cy.visit
  • cy.get
  • cy.type
  • cy.click
  • cy.check
  • cy.uncheck
  • cy.intercept
  • Cypress Dashboard

练习与思考:

① 为你在之前章节中创建的 React 组件编写单元测试用例,覆盖组件的主要功能和边界情况。
② 尝试为一个包含多个组件交互的场景编写集成测试用例。
③ 学习 Cypress 官方文档,尝试为一个简单的 React 应用编写端到端测试用例,覆盖用户的主要使用流程。
④ 思考不同类型的测试在软件测试金字塔中的位置和作用,以及如何根据项目需求选择合适的测试策略。

希望本章内容能够帮助你深入理解 React 测试,并在实际项目中应用测试技术,提升代码质量和开发效率。 🚀📚 Happy testing! 🎉


(本章完)

perform step 5.

15. chapter 15:TypeScript 与 React

随着 Web 应用复杂度的不断提升,JavaScript 的动态类型特性在大型项目中逐渐显露出一些局限性,例如类型错误难以在编译时发现、代码可维护性降低等。TypeScript 作为 JavaScript 的超集,引入了静态类型系统,为 JavaScript 开发带来了更强的类型安全性和更好的开发体验。将 TypeScript 应用于 React 项目,可以显著提高代码质量、增强可维护性,并提升团队协作效率。本章将系统地介绍 TypeScript 的基础知识,并深入探讨如何在 React 项目中有效地使用 TypeScript,帮助读者构建更健壮、更易于维护的 React 应用。

15.1 TypeScript 基础回顾:类型系统入门

在深入 React 与 TypeScript 的结合之前,我们先回顾一下 TypeScript 的基础知识,重点关注类型系统。如果你已经熟悉 TypeScript,可以跳过本节。

15.1.1 静态类型与动态类型

JavaScript 是一门动态类型语言。这意味着变量的类型在运行时才会被确定,并且变量的类型可以随时改变。

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 ```javascript
2 let message = "hello"; // message 被推断为字符串类型
3 message = 123; // 运行时不会报错,message 的类型变为数字类型
4 ```

动态类型语言的灵活性很高,但也容易在运行时出现类型错误,且难以在开发阶段发现。

TypeScript 是一门静态类型语言。这意味着变量的类型在编译时就必须确定,并且变量的类型在声明后不能随意改变。

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 ```typescript
2 let message: string = "hello"; // 显式声明 message 为字符串类型
3 message = 123; // 编译时报错:Type 'number' is not assignable to type 'string'.
4 ```

静态类型语言可以在编译时发现类型错误,提高代码的可靠性,并提供更好的代码提示和自动补全功能。

15.1.2 基本类型 (Primitive Types)

TypeScript 提供了与 JavaScript 对应的一系列基本类型:

boolean: 布尔值,truefalse
number: 数字,包括整数和浮点数。
string: 字符串。
null: 空值。
undefined: 未定义值。
symbol: 符号 (ES6 新增)。
bigint: 任意精度的整数 (ES2020 新增)。

除了基本类型,TypeScript 还引入了一些新的类型:

void: 表示没有返回值的函数。
any: 表示任意类型,可以绕过类型检查 (应尽量避免使用)。
unknown: 表示未知类型,比 any 更安全,需要进行类型断言或类型守卫才能使用。
never: 表示永不存在的值的类型,例如总是抛出错误的函数或无限循环的函数。

15.1.3 对象类型 (Object Types)

对象类型用于描述 JavaScript 对象。可以使用接口 (Interface)、类型别名 (Type Alias) 或类 (Class) 来定义对象类型。

接口 (Interface)

接口用于描述对象的结构,定义对象应该包含哪些属性以及属性的类型。

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 ```typescript
2 interface User {
3 id: number;
4 name: string;
5 email?: string; // 可选属性,使用 ? 标识
6 }
7
8 const user: User = {
9 id: 1,
10 name: "Alice",
11 // email: "alice@example.com" // 可选属性,可以省略
12 };
13 ```

类型别名 (Type Alias)

类型别名用于给一个类型起一个新的名字。可以用于基本类型、对象类型、联合类型、交叉类型等。

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 ```typescript
2 type Point = {
3 x: number;
4 y: number;
5 };
6
7 const origin: Point = { x: 0, y: 0 };
8 ```

类 (Class)

类不仅可以定义对象的结构,还可以包含方法。类本身也可以作为一种类型。

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 ```typescript
2 class Person {
3 name: string;
4 age: number;
5
6 constructor(name: string, age: number) {
7 this.name = name;
8 this.age = age;
9 }
10
11 greet(): void {
12 console.log(`Hello, my name is ${this.name} and I am ${this.age} years old.`);
13 }
14 }
15
16 const person: Person = new Person("Bob", 30);
17 person.greet();
18 ```

15.1.4 函数类型 (Function Types)

函数类型用于描述函数的参数类型和返回值类型。

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 ```typescript
2 // 完整函数类型定义
3 function add(x: number, y: number): number {
4 return x + y;
5 }
6
7 // 类型别名定义函数类型
8 type AddFunction = (x: number, y: number) => number;
9 const subtract: AddFunction = (x, y) => x - y; // 类型推断参数类型和返回值类型
10
11 // 箭头函数类型定义
12 const multiply = (x: number, y: number): number => {
13 return x * y;
14 };
15
16 // 可选参数和默认参数
17 function greet(name: string, greeting?: string, punctuation: string = "!"): string {
18 const greetWord = greeting || "Hello";
19 return `${greetWord}, ${name}${punctuation}`;
20 }
21
22 console.log(greet("Alice")); // Hello, Alice!
23 console.log(greet("Bob", "Hi")); // Hi, Bob!
24 console.log(greet("Charlie", "Hey", "?")); // Hey, Charlie?
25 ```

15.1.5 联合类型 (Union Types) 和交叉类型 (Intersection Types)

联合类型 (Union Types)

联合类型表示一个变量可以取多种类型中的一种。使用 | 符号分隔类型。

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 ```typescript
2 type StringOrNumber = string | number;
3
4 let value: StringOrNumber;
5 value = "hello"; // OK
6 value = 123; // OK
7 // value = true; // Error: Type 'boolean' is not assignable to type 'string | number'.
8 ```

交叉类型 (Intersection Types)

交叉类型将多个类型合并成一个类型,合并后的类型拥有所有类型的特性。使用 & 符号连接类型。

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 ```typescript
2 interface Colorful {
3 color: string;
4 }
5
6 interface Shape {
7 shape: string;
8 }
9
10 type ColorfulShape = Colorful & Shape;
11
12 const colorfulShape: ColorfulShape = {
13 color: "red",
14 shape: "circle",
15 };
16 ```

15.1.6 泛型 (Generics)

泛型允许我们在定义函数、接口或类的时候,不预先指定具体的类型,而是在使用的时候再指定类型。泛型可以提高代码的复用性和灵活性。

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 ```typescript
2 // 泛型函数
3 function identity<T>(arg: T): T {
4 return arg;
5 }
6
7 let outputString = identity<string>("myString"); // 类型参数显式指定为 string
8 let outputNumber = identity(100); // 类型参数类型推断为 number
9
10 // 泛型接口
11 interface Box<T> {
12 value: T;
13 }
14
15 let stringBox: Box<string> = { value: "hello" };
16 let numberBox: Box<number> = { value: 42 };
17
18 // 泛型类
19 class GenericNumber<T> {
20 zeroValue: T;
21 add: (x: T, y: T) => T;
22 }
23
24 let myGenericNumber = new GenericNumber<number>();
25 myGenericNumber.zeroValue = 0;
26 myGenericNumber.add = function(x, y) { return x + y; };
27 ```

15.1.7 类型推断 (Type Inference)

TypeScript 具有强大的类型推断能力。在很多情况下,可以省略类型注解,TypeScript 编译器会自动推断出变量、函数等的类型。

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 ```typescript
2 let message = "hello"; // 类型推断为 string
3 let count = 10; // 类型推断为 number
4
5 function add(x: number, y: number) { // 返回值类型推断为 number
6 return x + y;
7 }
8 ```

虽然类型推断很方便,但在某些情况下,显式地添加类型注解可以提高代码的可读性和可维护性,尤其是在复杂的类型场景下。

15.2 在 React 项目中配置 TypeScript

在 React 项目中使用 TypeScript 非常简单。无论是使用 Create React App (CRA) 还是 Vite 创建项目,都可以方便地配置 TypeScript。

15.2.1 Create React App (CRA) 创建 TypeScript 项目

使用 CRA 创建 TypeScript 项目,只需要在创建项目时指定 --template typescript 选项。

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 npx create-react-app my-ts-react-app --template typescript
2 cd my-ts-react-app

或者使用 yarn:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 yarn create react-app my-ts-react-app --template typescript
2 cd my-ts-react-app

CRA 会自动配置好 TypeScript 环境,包括安装 TypeScript 依赖、配置 tsconfig.json 文件等。

创建的 TypeScript React 项目具有以下特点:

① 文件扩展名:组件文件使用 .tsx 扩展名,普通 TypeScript 文件使用 .ts 扩展名。
tsconfig.json 文件:项目根目录下包含 tsconfig.json 文件,用于配置 TypeScript 编译选项。
③ 类型检查:CRA 启动开发服务器时,会自动进行 TypeScript 类型检查,并在控制台显示类型错误。

15.2.2 Vite 创建 TypeScript 项目

使用 Vite 创建 TypeScript React 项目更加简单,只需选择 React-TypeScript 模板即可。

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 npm create vite@latest my-ts-react-app

或者使用 yarn 或 pnpm:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 yarn create vite my-ts-react-app
1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 pnpm create vite my-ts-react-app

在选择模板时,选择 react-tsreact-ts-swc 模板。

Vite 创建的 TypeScript React 项目同样会自动配置好 TypeScript 环境,并包含 tsconfig.json 文件。

15.2.3 tsconfig.json 配置文件

tsconfig.json 文件是 TypeScript 项目的核心配置文件,用于配置 TypeScript 编译器的行为。常见的配置选项包括:

compilerOptions: 编译器选项,例如:
▮▮▮▮ target: 指定编译目标 ECMAScript 版本,例如 "es5", "es6", "esnext"。
▮▮▮▮
module: 指定模块化规范,例如 "commonjs", "esmodule", "amd"。
▮▮▮▮ jsx: 指定 JSX 编译模式,通常设置为 "react-jsx" 或 "react-jsxdev"。
▮▮▮▮
strict: 启用严格模式,建议设置为 true,以获得更强的类型检查。
▮▮▮▮ esModuleInterop: 允许 CommonJS 模块和 ES 模块互操作,通常设置为 true
▮▮▮▮
lib: 指定需要包含的类型声明文件库,例如 ["dom", "esnext"]。
▮▮▮▮ baseUrlpaths: 配置模块解析的基准路径和路径别名。
includeexclude: 指定需要包含和排除的文件或目录。
extends*: 继承其他 tsconfig.json 配置文件。

通常情况下,CRA 和 Vite 创建的 TypeScript 项目已经提供了合理的 tsconfig.json 默认配置,开发者可以根据项目需求进行调整。

15.3 TypeScript 与 React Hooks 的结合

TypeScript 可以与 React Hooks 完美结合,为函数组件提供类型安全的支持。我们需要关注以下几个方面:

15.3.1 useState Hook 类型定义

useState Hook 用于在函数组件中管理状态。在使用 TypeScript 时,需要为 useState Hook 的状态值定义类型。

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 ```typescript
2 import React, { useState } from 'react';
3
4 interface CounterProps {
5 initialCount?: number; // 可选属性
6 }
7
8 const Counter: React.FC<CounterProps> = ({ initialCount = 0 }) => {
9 const [count, setCount] = useState<number>(initialCount); // 显式指定 count 的类型为 number
10
11 const increment = () => {
12 setCount(count + 1);
13 };
14
15 return (
16
17
18
19 Count: {count}
20
21
22
23 Increment
24
25
26
27 );
28 };
29
30 export default Counter;
31 ```

类型定义解释:

useState<number>(initialCount): 使用泛型语法 <number> 显式指定 useState Hook 管理的状态 count 的类型为 number
React.FC<CounterProps>: 使用 React.FC<CounterProps> (Function Component) 类型注解函数组件 CounterCounterProps 接口定义了组件的 props 类型。React.FCReact.FunctionComponent 的简写形式。

如果不显式指定类型,TypeScript 编译器通常也能根据初始值推断出状态的类型。但为了代码的可读性和类型安全,建议显式指定类型。

15.3.2 useEffect Hook 类型定义

useEffect Hook 用于处理副作用操作,例如数据请求、DOM 操作、定时器等。useEffect Hook 本身不需要显式的类型定义,但需要注意处理副作用函数中的类型。

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 ```typescript
2 import React, { useState, useEffect } from 'react';
3
4 interface User {
5 id: number;
6 name: string;
7 }
8
9 const UserProfile: React.FC = () => {
10 const [user, setUser] = useState<User | null>(null); // 状态类型为 User 或 null
11
12 useEffect(() => {
13 async function fetchUser() {
14 const response = await fetch('/api/user');
15 const data: User = await response.json(); // 显式断言响应数据类型为 User
16 setUser(data);
17 }
18 fetchUser();
19 }, []);
20
21 if (!user) {
22 return
23
24 Loading user data...
25 ;
26 }
27
28 return (
29
30
31
32 User Profile
33
34
35
36
37
38 Username: {user.name}
39 ID: {user.id}
40
41
42
43
44 );
45 };
46
47 export default UserProfile;
48 ```

类型定义解释:

useState<User | null>(null): user 状态的类型定义为 User | null,表示 user 可以是 User 类型或 null。初始值设置为 null
const data: User = await response.json(): 使用类型断言 as Userresponse.json() 的结果断言为 User 类型。确保后续使用 data 时具有正确的类型信息。

15.3.3 useContext Hook 类型定义

useContext Hook 用于消费 Context 对象,获取 Context 提供的值。在使用 TypeScript 时,需要为 Context 对象的值定义类型。

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 ```typescript
2 import React, { createContext, useContext } from 'react';
3
4 interface ThemeContextValue {
5 theme: 'light' | 'dark';
6 toggleTheme: () => void;
7 }
8
9 const ThemeContext = createContext<ThemeContextValue | undefined>(undefined); // Context value 类型定义为 ThemeContextValue 或 undefined
10
11 interface ThemeProviderProps {
12 children: React.ReactNode;
13 }
14
15 const ThemeProvider: React.FC<ThemeProviderProps> = ({ children }) => {
16 // ... ThemeProvider 组件逻辑,提供 theme 和 toggleTheme
17 const value: ThemeContextValue = {
18 theme: 'light',
19 toggleTheme: () => { /* ... */ },
20 };
21
22 return (
23 <ThemeContext.Provider value={value}>
24 {children}
25 </ThemeContext.Provider>
26 );
27 };
28
29 const ThemeConsumer: React.FC = () => {
30 const themeContext = useContext(ThemeContext); // 类型推断为 ThemeContextValue | undefined
31
32 if (!themeContext) {
33 throw new Error('useTheme must be used within a ThemeProvider');
34 }
35
36 const { theme, toggleTheme } = themeContext;
37
38 return (
39
40
41 Current Theme: {theme}
42
43 Toggle Theme
44
45
46
47 );
48 };
49 ```

类型定义解释:

createContext<ThemeContextValue | undefined>(undefined): 创建 ThemeContext 时,使用泛型语法 <ThemeContextValue | undefined> 指定 Context value 的类型为 ThemeContextValueundefinedundefined 作为默认值,表示在没有 Provider 的情况下 Context 的值。
const themeContext = useContext(ThemeContext): useContext(ThemeContext) 的返回值类型被推断为 ThemeContextValue | undefined
类型守卫: 在使用 themeContext 前,需要进行类型守卫,判断 themeContext 是否存在,以避免 undefined 错误。

15.3.4 useReducer Hook 类型定义

useReducer Hook 用于管理复杂的状态逻辑。在使用 TypeScript 时,需要为 state、action 和 reducer 函数定义类型。

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 ```typescript
2 import React, { useReducer } from 'react';
3
4 interface State {
5 count: number;
6 }
7
8 type Action =
9 | { type: 'increment' }
10 | { type: 'decrement' }
11 | { type: 'reset'; payload: number };
12
13 const initialState: State = { count: 0 };
14
15 function reducer(state: State, action: Action): State {
16 switch (action.type) {
17 case 'increment':
18 return { count: state.count + 1 };
19 case 'decrement':
20 return { count: state.count - 1 };
21 case 'reset':
22 return { count: action.payload };
23 default:
24 return state;
25 }
26 }
27
28 const CounterWithReducer: React.FC = () => {
29 const [state, dispatch] = useReducer(reducer, initialState); // 类型推断 state 为 State, dispatch 为 Dispatch<Action>
30
31 return (
32
33
34
35 Count: {state.count}
36
37
38
39 Increment
40
41
42 Decrement
43
44
45 Reset to 10
46
47
48
49 );
50 };
51
52 export default CounterWithReducer;
53 ```

类型定义解释:

interface State: 定义 state 的接口类型,包含 count 属性。
type Action: 定义 action 的联合类型,包含 'increment', 'decrement', 'reset' 三种 action 类型,每种 action 类型可以携带不同的 payload。
reducer(state: State, action: Action): State: reducer 函数的参数和返回值都显式定义了类型。
useReducer(reducer, initialState): useReducer Hook 的返回值 statedispatch 的类型会自动推断为 StateDispatch<Action>

15.3.5 自定义 Hook 类型定义

自定义 Hook 是复用组件逻辑的有效方式。在使用 TypeScript 时,需要为自定义 Hook 的参数和返回值定义类型。

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 ```typescript
2 import { useState, useEffect } from 'react';
3
4 interface UseFetchResult<T> {
5 data: T | null;
6 loading: boolean;
7 error: Error | null;
8 }
9
10 function useFetch<T>(url: string): UseFetchResult<T> { // 泛型自定义 Hook
11 const [data, setData] = useState<T | null>(null);
12 const [loading, setLoading] = useState<boolean>(true);
13 const [error, setError] = useState<Error | null>(null);
14
15 useEffect(() => {
16 async function fetchData() {
17 try {
18 const response = await fetch(url);
19 if (!response.ok) {
20 throw new Error(`HTTP error! status: ${response.status}`);
21 }
22 const json = await response.json() as T; // 类型断言
23 setData(json);
24 setLoading(false);
25 } catch (e) {
26 setError(e instanceof Error ? e : new Error('Unknown error'));
27 setLoading(false);
28 }
29 }
30 fetchData();
31 }, [url]);
32
33 return { data, loading, error };
34 }
35
36 interface Posts {
37 id: number;
38 title: string;
39 body: string;
40 }
41
42 const PostList: React.FC = () => {
43 const { data: posts, loading, error } = useFetch<Posts[]>('https://jsonplaceholder.typicode.com/posts'); // 使用泛型指定数据类型为 Posts[]
44
45 if (loading) {
46 return
47
48 Loading posts...
49 ;
50 }
51
52 if (error) {
53 return
54
55 Error: {error.message}
56 ;
57 }
58
59 return (
60
61
62
63 Posts
64
65
66
67
68 {posts?.map(post => (
69
70
71 {post.title}
72
73
74 ))}
75
76
77
78 );
79 };
80
81 export default PostList;
82 ```

类型定义解释:

interface UseFetchResult<T>: 定义泛型接口 UseFetchResult<T>,用于描述 useFetch Hook 的返回值类型,包含 data, loading, error 三个属性,其中 data 的类型为泛型类型 T
function useFetch<T>(url: string): UseFetchResult<T>: 定义泛型函数 useFetch<T>,类型参数 T 用于指定请求返回数据的类型。函数的返回值类型为 UseFetchResult<T>
useFetch<Posts[]>('https://...'): 在 PostList 组件中使用 useFetch Hook 时,使用泛型语法 <Posts[]> 指定请求返回的数据类型为 Posts[] (Posts 数组)。

15.4 使用 TypeScript 提升组件的可维护性

TypeScript 在 React 组件开发中带来的类型安全性和静态检查,可以显著提升组件的可维护性。

15.4.1 更早发现类型错误

TypeScript 可以在编译时发现类型错误,例如 props 类型错误、state 类型错误、函数参数类型错误等。这可以避免很多潜在的运行时错误,减少调试时间。

15.4.2 增强代码可读性和可理解性

类型注解可以清晰地表达组件的 props 类型、state 类型、函数参数类型和返回值类型,提高代码的可读性和可理解性。其他开发者在阅读和维护代码时,可以更容易地理解组件的接口和行为。

15.4.3 提升代码重构的安全性

在重构代码时,TypeScript 的类型检查可以帮助开发者更安全地进行代码修改。如果修改导致类型错误,TypeScript 编译器会及时报错,防止引入潜在的 bug。

15.4.4 改善团队协作效率

在团队协作开发中,TypeScript 的类型系统可以作为一种沟通工具,明确组件之间的接口和数据类型。开发者可以根据类型定义更好地理解和使用其他成员编写的组件,减少沟通成本,提高协作效率。

15.4.5 更好的代码提示和自动补全

TypeScript 编译器和编辑器 (例如 VSCode) 可以根据类型信息提供更准确、更智能的代码提示和自动补全功能。这可以提高开发效率,减少代码输入错误。

总结

本章深入探讨了 TypeScript 与 React 的结合应用。我们回顾了 TypeScript 的基础类型系统,学习了如何在 React 项目中配置 TypeScript 环境,并详细讲解了 TypeScript 如何与 React Hooks (useState, useEffect, useContext, useReducer, 自定义 Hook) 结合使用,进行类型定义和类型检查。最后,我们总结了 TypeScript 如何提升 React 组件的可维护性。掌握 TypeScript 与 React 的结合,可以帮助开发者构建更健壮、更易于维护、更高效的 React 应用。在接下来的章节中,我们将继续深入学习 React 生态系统的其他重要技术和最佳实践。


本章关键词:

  • TypeScript
  • 静态类型
  • 动态类型
  • 类型注解 (Type Annotation)
  • 类型推断 (Type Inference)
  • 基本类型 (Primitive Types)
  • 对象类型 (Object Types)
  • 函数类型 (Function Types)
  • 联合类型 (Union Types)
  • 交叉类型 (Intersection Types)
  • 泛型 (Generics)
  • tsconfig.json
  • React.FC (Function Component)
  • useState Hook
  • useEffect Hook
  • useContext Hook
  • useReducer Hook
  • 自定义 Hook (Custom Hook)
  • 类型安全 (Type Safety)
  • 可维护性 (Maintainability)

练习与思考:

① 将你在之前章节中创建的 React 组件迁移到 TypeScript,添加类型注解,并确保代码通过 TypeScript 类型检查。
② 尝试创建一个包含复杂状态逻辑的 React 组件,使用 useReducer Hook 和 TypeScript 进行类型定义。
③ 编写一个自定义 Hook,使用泛型来提高 Hook 的复用性,并添加完整的 TypeScript 类型定义。
④ 思考在大型 React 项目中,使用 TypeScript 的优势和潜在的挑战,以及如何克服这些挑战。

希望本章内容能够帮助你深入理解 TypeScript 与 React 的结合,并在实际项目中应用 TypeScript,提升 React 应用的质量和可维护性。 🚀📚 Happy typing! 🎉


(本章完)

perform step 5.

16. chapter 16:React 性能监控与调试

构建高性能的 React 应用是提升用户体验的关键。性能问题不仅会导致应用响应缓慢、用户体验下降,还可能影响应用的搜索引擎排名和用户转化率。因此,对 React 应用进行性能监控与调试至关重要。本章将深入探讨 React 性能监控与调试的常用工具和技术,包括 React DevTools、性能分析工具 Profiler,以及错误监控与日志记录,帮助读者全面掌握 React 应用性能优化的方法,打造流畅高效的用户体验。

16.1 React DevTools:开发者工具详解

React DevTools 是 Chrome 和 Firefox 浏览器的扩展程序,专门用于调试 React 应用。它提供了一系列强大的功能,可以帮助开发者深入了解 React 组件的结构、属性、状态、性能等信息,是 React 开发中不可或缺的调试利器。

16.1.1 安装 React DevTools

根据你使用的浏览器,安装对应的 React DevTools 扩展程序:

Chrome: 在 Chrome Web Store 中搜索 "React Developer Tools" 并安装。 🌐 Chrome Web Store Link
Firefox: 在 Firefox Browser Add-ons 中搜索 "React Developer Tools" 并安装。 🦊 Firefox Add-ons Link
其他浏览器: 对于基于 Chromium 的浏览器 (例如 Edge, Brave),可以使用 Chrome 版本的 React DevTools。

安装完成后,打开开发者工具 (通常按 F12 键),如果当前页面运行着 React 应用,你将看到 "⚛️ Components" 和 "⚛️ Profiler" 两个新的选项卡。

16.1.2 Components 选项卡:组件结构与属性查看

"⚛️ Components" 选项卡用于查看 React 组件的层级结构、props、state、hooks、context 等信息。

组件树 (Component Tree):Components 选项卡左侧显示组件的树状结构。你可以展开和折叠组件节点,查看组件的父子关系。

组件详情面板 (Component Details Panel):选中组件树中的某个组件后,右侧的组件详情面板会显示该组件的详细信息,包括:
▮▮▮▮ Props (属性):显示组件的 props 及其值。可以实时查看 props 的变化。
▮▮▮▮
State (状态):显示函数组件的 state hooks 或类组件的 this.state。可以实时查看 state 的变化。
▮▮▮▮ Hooks (钩子):对于函数组件,显示组件使用的 hooks 及其值。例如 useState, useEffect, useContext 等。
▮▮▮▮
Context (上下文):显示组件消费的 Context 对象及其值。
▮▮▮▮ Source (源码)*:点击 "⚛️" 图标可以跳转到组件的源码位置 (如果 sourcemap 配置正确)。

搜索组件 (Search Components):可以使用顶部的搜索框输入组件名称或文本内容,快速查找组件树中的目标组件。

选择元素 (Select an element in the page to inspect it):点击 Components 选项卡左上角的 "选择元素" 图标 ( 🎯 ),然后在页面上点击某个元素,React DevTools 会自动定位到渲染该元素的 React 组件。

强制更新组件 (Force Update):在组件详情面板中,可以点击 "Force Update" 按钮强制组件重新渲染。这在某些调试场景下很有用。

跳转到组件定义 (Jump to source):在组件详情面板中,点击组件名称旁边的 "⚛️" 图标,可以跳转到组件的源代码位置 (需要 sourcemap 支持)。

16.1.3 Profiler 选项卡:性能分析与瓶颈定位

"⚛️ Profiler" 选项卡用于分析 React 组件的渲染性能,帮助开发者定位性能瓶颈。

启动/停止性能分析 (Start/Stop recording):点击 Profiler 选项卡左上角的 "记录" 按钮 ( ● ) 启动性能分析。在应用中进行用户交互或操作,React DevTools 会记录组件的渲染信息。再次点击 "记录" 按钮停止性能分析。

渲染火焰图 (Flame Chart):Profiler 选项卡的主视图是渲染火焰图。火焰图以时间轴为横轴,纵轴表示组件的层级关系。每个色块代表一个组件的渲染过程,色块的宽度表示渲染耗时,颜色深浅表示渲染耗时的相对大小。

▮▮▮▮ 颜色含义
▮▮▮▮▮▮▮▮
绿色: 表示组件本次渲染是 "committed",即实际发生了 DOM 更新。
▮▮▮▮▮▮▮▮ 黄色: 表示组件本次渲染是 "re-rendered",但由于优化 (例如 memoization) 或没有 props/state 变化,没有发生 DOM 更新。
▮▮▮▮▮▮▮▮
灰色: 表示组件本次渲染被 "wasted",即由于父组件的渲染导致子组件也进行了不必要的渲染。

组件渲染耗时统计 (Ranked Chart):Profiler 选项卡底部显示组件渲染耗时统计图表。图表按照组件渲染耗时从高到低排序,方便开发者快速找到性能瓶颈组件。

组件详情面板 (Component Details Panel):选中火焰图或统计图表中的某个组件后,右侧的组件详情面板会显示该组件的详细渲染信息,包括:
▮▮▮▮ Self Duration (自身耗时):组件自身渲染所花费的时间,不包括子组件的渲染时间。
▮▮▮▮
Total Duration (总耗时):组件自身渲染及其所有子组件渲染所花费的总时间。
▮▮▮▮ Rendered during this commit (本次 commit 渲染的组件):显示本次 commit 中渲染了哪些组件。
▮▮▮▮
Why did this render? (为什么渲染?):显示组件本次渲染的原因,例如 props 变化、state 变化、forceUpdate 等。
▮▮▮▮ Props diff (Props 差异):显示本次渲染前后 props 的变化 (如果 props 是对象或数组,会显示详细的差异)。
▮▮▮▮
State diff (State 差异):显示本次渲染前后 state 的变化 (如果 state 是对象或数组,会显示详细的差异)。

筛选渲染 commit (Filter commits):可以使用顶部的 "Filter commits" 输入框,根据 commit 的序号或耗时筛选渲染 commit。

导入/导出性能数据 (Import/Export profile):Profiler 选项卡支持导入和导出性能数据,方便开发者分享和分析性能数据。

16.1.4 使用 React DevTools 进行性能调试

使用 React DevTools 进行性能调试的一般流程如下:

启动 React DevTools Profiler: 打开 React DevTools,切换到 "⚛️ Profiler" 选项卡,点击 "记录" 按钮开始性能分析。
复现性能问题: 在应用中操作,复现性能问题场景,例如页面加载缓慢、交互卡顿等。
停止性能分析: 完成操作后,点击 "记录" 按钮停止性能分析。
分析渲染火焰图和统计图表: 查看渲染火焰图和统计图表,找到渲染耗时较高的组件。关注颜色为绿色和黄色的色块,以及 "wasted" 的渲染。
查看组件详情面板: 选中性能瓶颈组件,查看组件详情面板,分析组件渲染的原因、props 差异、state 差异等信息。
优化性能瓶颈组件: 根据分析结果,优化性能瓶颈组件。常见的优化手段包括:
▮▮▮▮ 避免不必要的渲染: 使用 React.memo, useMemo, useCallback 等 memoization 技术,避免组件在 props 或 state 没有变化时重新渲染。
▮▮▮▮
组件拆分: 将大型组件拆分成更小的、更细粒度的组件,减少单个组件的渲染负担。
▮▮▮▮ 列表虚拟化: 对于长列表,使用虚拟化技术 (例如 react-window, react-virtualized),只渲染可视区域内的列表项。
▮▮▮▮
代码分割: 使用代码分割技术 (例如 React.lazy, import()),将应用代码分割成更小的 bundle,按需加载。
▮▮▮▮ 图片优化: 优化图片大小和格式,使用 CDN 加速图片加载。
▮▮▮▮
服务端渲染 (SSR): 对于首屏加载性能敏感的应用,考虑使用服务端渲染。
重复步骤 ①-⑥: 优化后,重新进行性能分析,验证优化效果。

16.2 性能分析工具:Profiler API 的使用

除了 React DevTools,React 还提供了 Profiler API,允许开发者在代码中 programmatic 地测量组件的渲染性能。Profiler API 可以与 React DevTools Profiler 选项卡配合使用,也可以用于自定义性能分析工具的开发。

16.2.1 <Profiler> 组件

<Profiler> 是 React 提供的内置组件,用于测量其子树的渲染性能。<Profiler> 组件接受两个 props:

id: string: Profiler 组件的唯一标识符,用于在 onRender 回调函数中区分不同的 Profiler 组件。
onRender: (id, phase, actualDuration, baseDuration, startTime, commitTime, interactions) => void: 渲染完成时的回调函数。每次 <Profiler> 组件及其子树完成渲染时,onRender 回调函数会被调用。

onRender 回调函数接受以下参数:

id: string: Profiler 组件的 id prop。
phase: "mount" | "update": 渲染阶段,"mount" 表示组件首次挂载,"update" 表示组件更新。
actualDuration: number: 本次渲染的实际耗时 (包括组件自身及其子组件的渲染时间)。
baseDuration: number: 组件及其子树在不进行任何优化情况下的理论渲染耗时。可以作为优化的基准参考值。
startTime: number: 本次渲染的开始时间 (performance.now() 的返回值)。
commitTime: number: 本次渲染 commit 完成的时间 (performance.now() 的返回值)。
interactions: Set<Interaction>: 本次渲染相关的 Interactions (用于 tracing 异步操作,通常为空)。

16.2.2 使用 <Profiler> 组件测量性能

在需要测量性能的组件或组件子树外层包裹 <Profiler> 组件,并提供 idonRender props。

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 ```javascript
2 import React, { Profiler, useState } from 'react';
3
4 function MyComponent() {
5 const [count, setCount] = useState(0);
6
7 const onRenderCallback = (
8 id, // 发生 commit 的 Profiler 树的 id
9 phase, // "mount" 或 "update"
10 actualDuration, // 本次渲染的实际耗时
11 baseDuration, // 预估的不包含 memoization 等优化的理想渲染耗时
12 startTime, // 本次渲染的开始时间
13 commitTime, // 本次渲染的 commit 时间
14 interactions // 本次渲染相关的 interactions
15 ) => {
16 console.log(`Profiler id: ${id}`);
17 console.log(`Phase: ${phase}`);
18 console.log(`Actual Duration: ${actualDuration}`);
19 console.log(`Base Duration: ${baseDuration}`);
20 // ... 可以将性能数据上报到分析服务
21 };
22
23 return (
24 <Profiler id="MyComponentProfiler" onRender={onRenderCallback}>
25
26
27 Count: {count}
28
29
30
31 Increment
32
33
34 </Profiler>
35 );
36 }
37
38 export default MyComponent;
39 ```

MyComponent 组件及其子树发生渲染时,onRenderCallback 函数会被调用,并在控制台输出性能数据。开发者可以根据需要,将性能数据上报到后端分析服务,进行更深入的性能分析和监控。

16.2.3 结合 React DevTools Profiler 使用

<Profiler> 组件可以与 React DevTools Profiler 选项卡配合使用。当应用中使用了 <Profiler> 组件时,React DevTools Profiler 选项卡会显示 <Profiler> 组件的性能数据,并在火焰图中用紫色边框标记 <Profiler> 组件。

开发者可以使用 <Profiler> 组件精确测量特定组件或组件子树的性能,并在 React DevTools Profiler 选项卡中查看更详细的性能数据和火焰图。

16.2.4 注意事项

生产环境禁用 Profiler: <Profiler> 组件会增加额外的性能开销,因此在生产环境中应该禁用 <Profiler> 组件。可以使用条件编译或环境变量来控制 <Profiler> 组件的启用状态。

避免过度使用 Profiler: <Profiler> 组件应该只用于测量性能瓶颈组件或关键组件的性能,避免在整个应用中过度使用 <Profiler> 组件,影响应用性能。

16.3 错误监控与日志记录

除了性能问题,React 应用的错误也需要及时监控和处理。错误监控可以帮助开发者尽早发现和修复 bug,提高应用的稳定性和用户体验。

16.3.1 错误边界 (Error Boundaries)

React 16 引入了错误边界 (Error Boundaries) 的概念。错误边界是一种 React 组件,可以捕获其子组件树中发生的 JavaScript 错误,并优雅地展示降级 UI,而不是崩溃整个应用。错误边界类似于 JavaScript 的 try...catch 语句,但用于组件层面。

创建错误边界组件

错误边界组件必须是类组件,并实现 static getDerivedStateFromError(error)componentDidCatch(error, errorInfo) 两个生命周期方法中的至少一个。

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 ```javascript
2 import React from 'react';
3
4 interface ErrorBoundaryState {
5 hasError: boolean;
6 error: Error | null;
7 errorInfo: React.ErrorInfo | null;
8 }
9
10 class ErrorBoundary extends React.Component<{}, ErrorBoundaryState> {
11 constructor(props) {
12 super(props);
13 this.state = { hasError: false, error: null, errorInfo: null };
14 }
15
16 static getDerivedStateFromError(error: Error) {
17 // 更新 state 使下一次渲染显示降级 UI
18 return { hasError: true, error: error, errorInfo: null };
19 }
20
21 componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
22 // 你可以在这里将错误日志上报给服务器
23 console.error("Caught an error: ", error, errorInfo);
24 this.setState({ errorInfo: errorInfo }); // 保存 errorInfo 到 state
25 }
26
27 render() {
28 if (this.state.hasError) {
29 // 你可以自定义降级 UI 渲染
30 return (
31
32
33
34 Something went wrong.
35
36
37 Error: {this.state.error?.message}
38
39
40 <details style={{ whiteSpace: 'pre-wrap' }}>
41 {this.state.errorInfo?.componentStack}
42 </details>
43
44
45
46 );
47 }
48
49 return this.props.children;
50 }
51 }
52
53 export default ErrorBoundary;
54 ```

使用错误边界组件

将错误边界组件包裹在可能发生错误的组件树外层。

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 ```javascript
2 import ErrorBoundary from './ErrorBoundary';
3 import MyComponent from './MyComponent';
4
5 function App() {
6 return (
7
8
9
10 <ErrorBoundary>
11 <MyComponent />
12 </ErrorBoundary>
13
14
15
16 );
17 }
18
19 export default App;
20 ```

MyComponent 组件或其子组件抛出错误时,ErrorBoundary 组件会捕获错误,并渲染降级 UI,而不是导致整个应用崩溃。

错误边界的局限性

只能捕获子组件树中的错误: 错误边界只能捕获其子组件树中发生的错误,无法捕获自身组件或父组件的错误。
无法捕获异步错误: 错误边界无法捕获异步代码 (例如 Promise, setTimeout) 中发生的错误。对于异步错误,需要使用 try...catch 或其他错误处理机制。
无法捕获服务端渲染错误: 错误边界只能捕获客户端渲染过程中的错误,无法捕获服务端渲染过程中的错误。

16.3.2 错误日志上报

componentDidCatch 生命周期方法可以用于将错误日志上报到服务器。可以使用 fetch API 或第三方错误监控服务 (例如 Sentry, Bugsnag) 将错误信息 (error, errorInfo) 发送到后端,进行错误分析和监控。

使用 Sentry 进行错误监控

Sentry 是一款流行的错误监控服务,提供了 React 集成库,可以方便地将 React 应用的错误上报到 Sentry 平台。

安装 Sentry SDK:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 npm install --save @sentry/react @sentry/browser

或者使用 yarn 或 pnpm:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 yarn add @sentry/react @sentry/browser
1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 pnpm add @sentry/react @sentry/browser

初始化 Sentry SDK: 在应用的入口文件 (例如 index.jsindex.tsx) 中初始化 Sentry SDK。

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 ```javascript
2 import * as Sentry from "@sentry/react";
3 import { BrowserTracing } from "@sentry/tracing";
4
5 Sentry.init({
6 dsn: "YOUR_SENTRY_DSN", // 替换为你的 Sentry DSN
7 integrations: [new BrowserTracing()],
8 // Set tracesSampleRate to 1.0 to capture 100%
9 // of transactions for performance monitoring.
10 // We recommend adjusting this value in production
11 tracesSampleRate: 1.0,
12 // ... 其他配置
13 });
14
15 // ... 渲染 React 应用
16 ```

配置 ErrorBoundary 集成: Sentry SDK 提供了 ErrorBoundary 组件,可以替代自定义的 ErrorBoundary 组件,自动上报错误信息到 Sentry。

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 ```javascript
2 import * as Sentry from "@sentry/react";
3 import App from './App';
4
5 function Root() {
6 return (
7 <Sentry.ErrorBoundary fallback={
8
9 Something went wrong
10 }>
11 <App />
12 </Sentry.ErrorBoundary>
13 );
14 }
15
16 export default Root;
17 ```

配置完成后,当 React 应用发生错误时,错误信息会自动上报到 Sentry 平台,开发者可以在 Sentry 平台上查看错误详情、错误堆栈、用户环境信息等,进行错误分析和修复。

16.3.3 日志记录 (Logging)

除了错误监控,日志记录也是应用监控的重要组成部分。可以使用 console.log, console.warn, console.error 等方法在开发环境中记录日志。在生产环境中,应该使用更专业的日志记录方案,例如:

前端日志库: 例如 loglevel, js-logger 等,提供更丰富的日志级别、日志格式化、日志输出目标 (例如控制台、文件、远程服务器) 等功能。
后端日志服务: 将前端日志上报到后端日志服务 (例如 ELK Stack, Splunk, Graylog),进行集中式日志管理和分析。

16.4 React 最佳实践

在 React 应用开发中,遵循一些最佳实践可以帮助我们更好地进行性能监控和调试:

尽早进行性能测试: 在开发阶段就应该开始进行性能测试,而不是等到应用上线后才关注性能问题。可以使用 React DevTools Profiler 选项卡和 <Profiler> 组件进行性能测试,尽早发现和解决性能瓶颈。
关注组件渲染性能: React 应用的性能瓶颈通常出现在组件渲染方面。应该重点关注组件的渲染次数、渲染耗时、不必要的渲染等问题,并使用 memoization, 组件拆分, 列表虚拟化等技术进行优化。
使用错误边界处理错误: 使用 ErrorBoundary 组件捕获组件错误,避免应用崩溃,并提供友好的降级 UI。
集成错误监控服务: 集成 Sentry, Bugsnag 等错误监控服务,及时发现和处理生产环境中的错误。
合理使用日志记录: 在开发和生产环境中合理使用日志记录,方便问题排查和监控。
持续监控和优化: 性能优化是一个持续的过程。应该定期进行性能监控和分析,根据监控数据进行持续优化。

总结

本章深入探讨了 React 性能监控与调试的工具和技术。我们详细介绍了 React DevTools 的 Components 和 Profiler 选项卡的使用方法,学习了如何使用 Profiler API 进行 programmatic 性能测量,并了解了错误边界和错误监控的重要性。掌握这些工具和技术可以帮助开发者有效地监控和调试 React 应用的性能和错误,构建高质量、高可靠性的 React 应用。在接下来的章节中,我们将继续深入学习 React 生态系统的其他重要技术和最佳实践。


本章关键词:

  • React DevTools
  • Components 选项卡
  • Profiler 选项卡
  • 渲染火焰图 (Flame Chart)
  • 性能分析 (Performance Profiling)
  • <Profiler> 组件
  • onRender 回调函数
  • 错误边界 (Error Boundaries)
  • static getDerivedStateFromError
  • componentDidCatch
  • 错误监控 (Error Monitoring)
  • Sentry
  • Bugsnag
  • 日志记录 (Logging)
  • 性能优化 (Performance Optimization)

练习与思考:

① 使用 React DevTools Profiler 选项卡分析一个复杂的 React 组件的渲染性能,并找出性能瓶颈。
② 在一个 React 组件中使用 <Profiler> 组件,测量组件的渲染耗时,并将性能数据输出到控制台或上报到后端服务。
③ 创建一个 ErrorBoundary 组件,包裹一个可能抛出错误的子组件,并测试错误边界组件是否能够正确捕获和处理错误。
④ 集成 Sentry 或 Bugsnag 等错误监控服务到你的 React 应用中,并测试错误上报功能。
⑤ 思考在实际项目中,如何结合使用 React DevTools, Profiler API, 错误边界和错误监控服务,构建完善的 React 应用监控体系。

希望本章内容能够帮助你深入理解 React 性能监控与调试,并在实际项目中应用这些技术,提升 React 应用的质量和用户体验。 🚀📚 Happy debugging! 🎉


(本章完)

perform step 5.

17. chapter 17:React 最佳实践

随着 React 应用规模和复杂度的增长,遵循最佳实践对于保证代码质量、提高开发效率、降低维护成本至关重要。React 最佳实践涵盖了组件设计、代码组织、项目结构、代码风格等多个方面。本章将系统地总结 React 开发中的最佳实践,包括组件设计原则、代码组织与项目结构、代码风格指南等,帮助读者构建可维护、可扩展、高质量的 React 应用。

17.1 组件设计原则:单一职责与关注点分离

良好的组件设计是构建可维护 React 应用的基石。组件设计应遵循一些核心原则,例如单一职责原则 (Single Responsibility Principle, SRP) 和关注点分离原则 (Separation of Concerns, SoC)。

17.1.1 单一职责原则 (SRP)

单一职责原则 (SRP) 指的是一个组件应该只负责一项职责。换句话说,一个组件应该只有一个明确的、单一的功能。

SRP 的优点:

提高组件的可读性和可理解性:单一职责的组件代码更简洁、逻辑更清晰,易于理解和维护。
提高组件的可复用性:单一职责的组件更专注于完成特定的任务,更容易在不同的场景下复用。
降低组件的维护成本:当需求变更或 bug 出现时,更容易定位到具体的组件,修改和调试的范围更小,降低维护成本。
提高组件的可测试性:单一职责的组件更容易进行单元测试,测试用例更简洁、覆盖更全面。

反例:违反 SRP 的组件

假设我们有一个 ProductCard 组件,同时负责以下职责:

⚝ 展示商品信息(图片、标题、价格、描述)
⚝ 处理用户点击 "添加到购物车" 的操作
⚝ 处理商品图片加载失败的情况
⚝ 格式化商品价格

这样的 ProductCard 组件就违反了 SRP 原则,因为它承担了过多的职责。当需要修改价格格式化逻辑时,我们可能需要修改 ProductCard 组件的代码,即使价格格式化与商品展示、购物车操作等功能逻辑无关。

改进:遵循 SRP 的组件

为了遵循 SRP 原则,我们可以将 ProductCard 组件拆分成更小的、职责更单一的组件或函数:

ProductCard 组件: 只负责展示商品的基本信息 (图片、标题、描述),接收 props 传递商品数据。
AddToCartButton 组件: 专门负责处理 "添加到购物车" 的操作,接收商品 ID 作为 props。
ImageWithFallback 组件: 处理图片加载失败的情况,接收图片 URL 和 fallback 图片 URL 作为 props。
formatPrice 函数: 专门负责格式化商品价格,接收价格数值作为参数,返回格式化后的价格字符串。

拆分后的组件和函数职责更单一、更内聚,提高了代码的可读性、可复用性和可维护性。

17.1.2 关注点分离原则 (SoC)

关注点分离原则 (SoC) 指的是将软件系统中的不同关注点 (concerns) 分离到不同的模块中。在 React 组件开发中,SoC 通常体现在将展示逻辑 (presentation logic) 和业务逻辑 (business logic) 分离到不同的组件中。

SoC 的优点:

提高代码的可维护性:不同关注点的代码修改互不影响,降低代码修改的风险。
提高代码的可测试性:展示逻辑组件和业务逻辑组件可以独立进行测试。
提高代码的可复用性:展示逻辑组件可以更容易地在不同的业务场景下复用,只需更换不同的业务逻辑组件。
提高团队协作效率:团队成员可以根据关注点划分工作,并行开发,提高协作效率。

Presentational Components (展示型组件) 和 Container Components (容器型组件)

一种常见的 SoC 实践是将组件分为 Presentational Components (展示型组件)Container Components (容器型组件) 两种类型。

Presentational Components (展示型组件)

⚝ 也称为 UI 组件、dumb 组件、纯组件 (Pure Components)。
⚝ 只负责 UI 的渲染,关注 UI 的外观和交互细节。
⚝ 不包含业务逻辑,不依赖应用的状态管理 (例如 Redux, Zustand)。
⚝ 数据通过 props 传入,事件处理通过 props 回调函数传递给父组件。
⚝ 易于复用和测试。

Container Components (容器型组件)

⚝ 也称为 Smart 组件、stateful 组件。
⚝ 负责业务逻辑、数据获取、状态管理。
⚝ 将数据和业务逻辑传递给 Presentational Components 进行渲染。
⚝ 通常包含状态管理逻辑 (例如使用 useState, useReducer, Context API, Redux 等)。
⚝ 较难复用,测试时需要 mock 依赖的状态管理和数据获取逻辑。

示例:Presentational 和 Container 组件分离

假设我们需要创建一个用户列表组件,从 API 获取用户数据并展示用户姓名和邮箱。

Presentational Component: UserListUI.js

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 ```javascript
2 import React from 'react';
3
4 interface User {
5 id: number;
6 name: string;
7 email: string;
8 }
9
10 interface UserListUIProps {
11 users: User[];
12 onUserClick: (userId: number) => void;
13 }
14
15 const UserListUI: React.FC<UserListUIProps> = ({ users, onUserClick }) => {
16 return (
17
18
19
20 {users.map(user => (
21
22
23 {user.name}
24
25 {user.email}
26
27
28
29 ))}
30
31
32
33 );
34 };
35
36 export default UserListUI;
37 ```

UserListUI 组件只负责渲染用户列表 UI,接收 users 数据和 onUserClick 回调函数作为 props。

Container Component: UserListContainer.js

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 ```javascript
2 import React, { useState, useEffect } from 'react';
3 import UserListUI from './UserListUI';
4 import { fetchUsers } from '../api'; // 假设 API 请求函数
5
6 const UserListContainer: React.FC = () => {
7 const [users, setUsers] = useState<User[]>([]);
8 const [loading, setLoading] = useState<boolean>(true);
9 const [error, setError] = useState<Error | null>(null);
10
11 useEffect(() => {
12 async function loadUsers() {
13 try {
14 const data = await fetchUsers();
15 setUsers(data);
16 setLoading(false);
17 } catch (err) {
18 setError(err instanceof Error ? err : new Error('Failed to fetch users'));
19 setLoading(false);
20 }
21 }
22 loadUsers();
23 }, []);
24
25 const handleUserClick = (userId: number) => {
26 alert(`User ID clicked: ${userId}`); // 示例:点击用户后的操作
27 };
28
29 if (loading) {
30 return
31
32 Loading users...
33 ;
34 }
35
36 if (error) {
37 return
38
39 Error: {error.message}
40 ;
41 }
42
43 return (
44 <UserListUI users={users} onUserClick={handleUserClick} />
45 );
46 };
47
48 export default UserListContainer;
49 ```

UserListContainer 组件负责数据获取、状态管理和业务逻辑,将获取到的 users 数据和 handleUserClick 回调函数传递给 UserListUI 组件进行渲染。

通过 Presentational 和 Container 组件分离,我们将 UI 渲染逻辑和业务逻辑有效分离,提高了代码的可维护性、可测试性和可复用性。

17.1.3 组件复用与组合模式

React 推崇组件化开发,组件复用是提高开发效率、降低代码冗余的关键。组件复用可以通过以下方式实现:

Props 灵活配置: 设计组件时,尽可能将组件的行为和外观参数化,通过 props 灵活配置组件,使其适应不同的场景。例如,Button 组件可以接收 type, size, variant, onClick 等 props,实现不同样式和功能的按钮。
组件组合 (Composition):React 强大的组合能力允许我们将小的、可复用的组件组合成更复杂的 UI 结构。常见的组件组合模式包括:
▮▮▮▮ Containment (容器):使用 props.children 将子组件传递给父组件,实现布局组件、容器组件等。例如,Layout 组件、Card 组件、Modal 组件等。
▮▮▮▮
Specialization (特化):创建高阶组件 (HOC) 或 Render Props 组件,复用组件的逻辑,并允许使用者自定义组件的渲染方式。例如,withLoading HOC, withAuth HOC, List 组件的 Render Props pattern 等。

示例:组件组合 - Containment

Card 组件:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 ```javascript
2 import React from 'react';
3
4 interface CardProps {
5 title?: React.ReactNode;
6 children: React.ReactNode;
7 }
8
9 const Card: React.FC<CardProps> = ({ title, children }) => {
10 return (
11
12
13 {title &&
14
15 {title}
16
17 }
18
19 {children}
20
21
22
23 );
24 };
25
26 export default Card;
27 ```

使用 Card 组件组合其他组件:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 ```javascript
2 import React from 'react';
3 import Card from './Card';
4 import UserProfile from './UserProfile';
5 import UserListContainer from './UserListContainer';
6
7 function Dashboard() {
8 return (
9
10
11
12 <Card title="User Profile">
13 <UserProfile userId={123} />
14 </Card>
15
16
17 <Card title="User List">
18 <UserListContainer />
19 </Card>
20
21
22
23 );
24 }
25
26 export default Dashboard;
27 ```

Card 组件作为一个容器组件,通过 props.children 接收子组件,并提供统一的 Card 样式和布局。Dashboard 组件通过组合 Card, UserProfile, UserListContainer 组件,构建出复杂的仪表盘 UI。

17.2 代码组织与项目结构

良好的代码组织和项目结构对于大型 React 应用的可维护性和可扩展性至关重要。常见的代码组织和项目结构模式包括:

17.2.1 按功能模块 (Feature-based) 组织

按功能模块组织项目结构,将相关的文件 (组件、样式、测试、API 接口等) 放在同一个功能模块目录下。

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 src/
2 ├── features/
3 ├── user/
4 ├── components/
5 ├── UserProfile.js
6 ├── UserList.js
7 ├── containers/
8 ├── UserProfileContainer.js
9 ├── UserListContainer.js
10 ├── hooks/
11 ├── useUser.js
12 ├── contexts/
13 ├── UserContext.js
14 ├── api/
15 ├── userApi.js
16 ├── User.module.css
17 └── index.js (user feature 入口)
18 ├── product/
19 └── ... (product feature 相关文件)
20 ├── order/
21 └── ... (order feature 相关文件)
22 ├── shared/ (跨功能模块的通用组件、hooksutils )
23 ├── components/
24 ├── Button.js
25 ├── Input.js
26 ├── Card.js
27 └── ...
28 ├── hooks/
29 ├── useDebounce.js
30 ├── useLocalStorage.js
31 └── ...
32 ├── utils/
33 ├── format.js
34 ├── validation.js
35 └── ...
36 ├── app/ (应用级别的组件、布局、路由等)
37 ├── components/
38 ├── AppLayout.js
39 ├── AppHeader.js
40 ├── AppFooter.js
41 └── ...
42 ├── App.js (根组件)
43 ├── App.module.css
44 └── index.js (app 入口)
45 ├── pages/ (Next.js pages 目录 路由页面组件)
46 ├── styles/ (全局样式)
47 ├── assets/ (静态资源,例如图片、字体)
48 ├── services/ (全局服务,例如 authService, analyticsService)
49 ├── contexts/ (全局 Context)
50 ├── hooks/ (全局 Hooks)
51 ├── utils/ (全局工具函数)
52 ├── constants/ (常量定义)
53 ├── types/ (TypeScript 类型定义)
54 ├── config/ (项目配置)
55 ├── index.js (应用入口)
56 └── ...

按功能模块组织的优点:

高内聚、低耦合:功能模块内部高内聚,模块之间低耦合,易于维护和扩展。
易于理解和查找:根据功能模块划分目录,易于理解项目结构,快速找到相关文件。
利于团队协作:团队成员可以根据功能模块划分工作,并行开发,减少代码冲突。
模块化复用:功能模块可以作为独立的模块进行复用,例如在不同的项目中复用用户模块、产品模块等。

17.2.2 按类型 (Type-based) 组织

按类型组织项目结构,将相同类型的文件 (组件、容器、hooks、样式等) 放在同一个目录下。

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 src/
2 ├── components/ (展示型组件)
3 ├── UserProfile.js
4 ├── UserList.js
5 ├── Button.js
6 ├── Input.js
7 ├── Card.js
8 └── ...
9 ├── containers/ (容器型组件)
10 ├── UserProfileContainer.js
11 ├── UserListContainer.js
12 └── ...
13 ├── hooks/ (自定义 Hooks)
14 ├── useUser.js
15 ├── useDebounce.js
16 ├── useLocalStorage.js
17 └── ...
18 ├── contexts/ (Context 对象)
19 ├── UserContext.js
20 └── ...
21 ├── utils/ (工具函数)
22 ├── format.js
23 ├── validation.js
24 └── ...
25 ├── api/ (API 接口)
26 ├── userApi.js
27 └── ...
28 ├── pages/ (Next.js pages 目录 路由页面组件)
29 ├── styles/ (全局样式 组件样式模块)
30 ├── assets/ (静态资源)
31 ├── services/ (全局服务)
32 ├── constants/ (常量定义)
33 ├── types/ (TypeScript 类型定义)
34 ├── config/ (项目配置)
35 ├── App.js (根组件)
36 ├── index.js (应用入口)
37 └── ...

按类型组织的优点:

类型结构清晰:不同类型的文件放在不同的目录下,类型结构一目了然。
易于查找同类型文件:需要查找某种类型的文件时,例如所有组件、所有 hooks,可以快速在对应的目录下找到。

按类型组织的缺点:

功能模块分散:相关功能模块的文件分散在不同的类型目录下,不利于理解功能模块的完整性。
维护成本较高:当需要修改某个功能模块时,可能需要在多个类型目录下修改文件,维护成本较高。

选择哪种组织方式?

选择按功能模块组织还是按类型组织,取决于项目规模、团队习惯和个人偏好。

小型项目:按类型组织可能更简洁直观。
中大型项目:按功能模块组织更易于管理和维护,尤其是在团队协作开发中。

在实际项目中,也可以结合两种组织方式的优点,例如,在功能模块目录下再按类型细分,或者在类型目录下再按功能模块分组。

17.2.3 Atomic Design (原子设计)

Atomic Design 是一种组件组织方法论,将 UI 组件划分为五个不同的层级:

Atoms (原子):最小的、不可再分的 UI 元素,例如 Button, Input, Label, Icon, Typography (文本排版) 等。原子组件通常只关注 UI 呈现,不包含业务逻辑。
Molecules (分子):由多个原子组件组合而成的相对简单的 UI 组件,例如 SearchForm (由 Input, Button 组成), UserCard (由 Avatar, Typography, Button 组成) 等。分子组件可以包含一些简单的逻辑,但主要还是关注 UI 组合。
Organisms (组织):由多个分子组件或原子组件组合而成的相对复杂的 UI 组件,例如 Header (由 Logo, Navigation, SearchForm 组成), ProductList (由 ProductCard 分子组件重复渲染而成) 等。组织组件开始关注页面结构和布局。
Templates (模板):页面级别的结构,定义页面的整体布局和骨架,但不包含实际内容。模板组件通常用于定义页面 regions (区域) 和 placeholders (占位符)。
Pages (页面):Templates 的具体实例,将模板与实际内容 (Organisms, Molecules, Atoms) 组合,构成最终的页面。Pages 组件通常与路由关联。

Atomic Design 的优点:

组件复用性高:原子组件、分子组件、组织组件等都具有很高的复用性,可以在不同的页面和场景下组合使用。
结构清晰、易于维护:Atomic Design 将组件分层组织,结构清晰,易于理解和维护。
设计与开发协同:Atomic Design 提供了一套通用的语言和框架,方便设计师和开发者协同工作,保证 UI 设计的一致性和可复用性。
可扩展性强:基于原子组件构建更复杂的分子、组织组件,可以方便地扩展和构建新的 UI 界面。

Atomic Design 的项目结构示例:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 src/
2 ├── components/
3 │ ├── atoms/
4 │ │ ├── Button/
5 │ │ │ ├── Button.js
6 │ │ │ ├── Button.module.css
7 │ │ │ └── index.js
8 │ │ ├── Input/
9 │ │ ├── Icon/
10 │ │ ├── Typography/
11 │ │ └── ...
12 │ ├── molecules/
13 │ │ ├── SearchForm/
14 │ │ │ ├── SearchForm.js
15 │ │ │ ├── SearchForm.module.css
16 │ │ │ └── index.js
17 │ │ ├── UserCard/
18 │ │ ├── ProductCard/
19 │ │ └── ...
20 │ ├── organisms/
21 │ │ ├── Header/
22 │ │ │ ├── Header.js
23 │ │ │ ├── Header.module.css
24 │ │ │ └── index.js
25 │ │ ├── ProductList/
26 │ │ ├── ShoppingCart/
27 │ │ └── ...
28 │ ├── templates/
29 │ │ ├── HomePageTemplate.js
30 │ │ ├── ProductDetailPageTemplate.js
31 │ │ └── ...
32 │ ├── pages/
33 │ │ ├── HomePage.js
34 │ │ ├── ProductDetailPage.js
35 │ │ └── ...
36 ├── App.js
37 └── index.js

在 React 项目中应用 Atomic Design

Atomic Design 是一种组织组件的思路,可以与功能模块组织或类型组织方式结合使用。例如,可以在功能模块目录下,再使用 Atomic Design 的层级结构组织组件。或者在按类型组织的组件目录下,再使用 Atomic Design 的层级结构细分组件。

17.3 代码风格指南 (ESLint, Prettier)

统一的代码风格对于团队协作开发至关重要。代码风格指南可以帮助团队成员编写风格一致、易于阅读和维护的代码。在 React 项目中,可以使用 ESLint 和 Prettier 等工具强制执行代码风格指南。

17.3.1 ESLint:代码质量和风格检查

ESLint 是一款流行的 JavaScript 代码检查工具,可以帮助开发者发现代码中的潜在错误和风格问题。

配置 ESLint

安装 ESLint 及相关插件:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 npm install --save-dev eslint eslint-plugin-react @typescript-eslint/parser @typescript-eslint/eslint-plugin

或者使用 yarn 或 pnpm:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 yarn add -D eslint eslint-plugin-react @typescript-eslint/parser @typescript-eslint/eslint-plugin
1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 pnpm add -D eslint eslint-plugin-react @typescript-eslint/parser @typescript-eslint/eslint-plugin

创建 .eslintrc.js 配置文件: 在项目根目录下创建 .eslintrc.js 文件,配置 ESLint 规则。

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 ```javascript
2 module.exports = {
3 parser: '@typescript-eslint/parser', // 使用 TypeScript parser
4 plugins: [
5 'react',
6 '@typescript-eslint',
7 ],
8 extends: [
9 'eslint:recommended', // 推荐的 ESLint 规则
10 'plugin:react/recommended', // 推荐的 React 规则
11 'plugin:@typescript-eslint/recommended', // 推荐的 TypeScript 规则
12 ],
13 env: {
14 browser: true, // 浏览器环境
15 es6: true, // ES6+ 语法
16 node: true, // Node.js 环境
17 jest: true, // Jest 测试环境
18 },
19 rules: {
20 // 自定义规则 (覆盖 extends 中的规则)
21 'react/prop-types': 'off', // 关闭 prop-types 检查 (TypeScript 已提供类型检查)
22 '@typescript-eslint/explicit-function-return-type': 'off', // 关闭函数返回类型显式声明检查 (类型推断已足够)
23 // ... 其他自定义规则
24 },
25 settings: {
26 react: {
27 version: 'detect', // 自动检测 React 版本
28 },
29 },
30 };
31 ```

配置 package.json scripts: 在 package.json 文件中添加 ESLint 检查命令。

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 {
2 "scripts": {
3 "lint": "eslint './src/**/*.{js,jsx,ts,tsx}'", // 检查 src 目录下所有 js, jsx, ts, tsx 文件
4 "lint:fix": "eslint --fix './src/**/*.{js,jsx,ts,tsx}'" // 自动修复部分 ESLint 错误
5 },
6 // ...
7 }
8 ```

常用 ESLint 规则插件:

eslint-plugin-react: React 相关的 ESLint 规则,例如检查 JSX 语法、propTypes、hooks 规则等。
@typescript-eslint/parser@typescript-eslint/eslint-plugin: TypeScript 相关的 ESLint 规则和 parser,用于检查 TypeScript 代码。
eslint-plugin-jsx-a11y: JSX 无障碍 (Accessibility) 相关的 ESLint 规则,检查 JSX 语法是否符合无障碍标准。
eslint-plugin-import: import 相关的 ESLint 规则,检查 import 语句的规范性。
eslint-plugin-react-hooks: React Hooks 相关的 ESLint 规则,强制执行 Hooks 的使用规则 (例如 Rules of Hooks)。

运行 ESLint 检查

在终端中执行 npm run lintyarn lintpnpm lint 命令,ESLint 会检查项目代码,并输出代码风格和错误信息。执行 npm run lint:fix 可以自动修复部分 ESLint 错误。

17.3.2 Prettier:代码格式化工具

Prettier 是一款代码格式化工具,可以自动格式化代码,使其符合统一的代码风格。Prettier 可以与 ESLint 集成使用,ESLint 负责代码质量和风格检查,Prettier 负责代码格式化。

配置 Prettier

安装 Prettier:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 npm install --save-dev prettier eslint-config-prettier eslint-plugin-prettier

或者使用 yarn 或 pnpm:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 yarn add -D prettier eslint-config-prettier eslint-plugin-prettier
1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 pnpm add -D prettier eslint-config-prettier eslint-plugin-prettier

创建 .prettierrc.js 配置文件: 在项目根目录下创建 .prettierrc.js 文件,配置 Prettier 格式化规则 (可选,可以使用默认配置)。

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 ```javascript
2 module.exports = {
3 semi: false, // 去除分号
4 trailingComma: 'all', // 尾随逗号
5 singleQuote: true, // 单引号
6 printWidth: 100, // 代码宽度
7 tabWidth: 2, // tab 宽度
8 useTabs: false, // 使用空格代替 tab
9 };
10 ```

修改 .eslintrc.js 配置文件: 在 .eslintrc.js 文件中添加 Prettier 相关配置,禁用 ESLint 中与 Prettier 冲突的格式化规则,并启用 Prettier 插件。

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 ```javascript
2 module.exports = {
3 // ...
4 extends: [
5 // ...
6 'prettier', // 禁用 ESLint 中与 Prettier 冲突的格式化规则
7 'plugin:prettier/recommended', // 启用 Prettier 插件,将 Prettier 错误作为 ESLint 错误提示
8 ],
9 rules: {
10 // ...
11 'prettier/prettier': 'error', // Prettier 格式化错误提示级别设置为 error
12 },
13 };
14 ```

配置 package.json scripts: 在 package.json 文件中添加 Prettier 格式化命令。

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 {
2 "scripts": {
3 "format": "prettier --write './src/**/*.{js,jsx,ts,tsx,json,css,scss,md}'", // 格式化 src 目录下所有指定类型的文件
4 "format:check": "prettier --check './src/**/*.{js,jsx,ts,tsx,json,css,scss,md}'" // 检查代码是否符合 Prettier 格式化规则
5 },
6 // ...
7 }
8 ```

常用 Prettier 配置选项:

semi: 是否添加分号,默认 true (添加分号),建议设置为 false (去除分号)。
trailingComma: 尾随逗号,默认 es5 (ES5 尾随逗号),建议设置为 all (所有可能的地方添加尾随逗号)。
singleQuote: 是否使用单引号,默认 false (双引号),建议设置为 true (单引号)。
printWidth: 代码宽度,默认 80,可以根据团队习惯调整,例如 100120
tabWidth: tab 宽度,默认 2
useTabs: 是否使用 tab 缩进,默认 false (使用空格),可以设置为 true (使用 tab)。

运行 Prettier 格式化

在终端中执行 npm run formatyarn formatpnpm format 命令,Prettier 会自动格式化项目代码,使其符合 Prettier 规则。执行 npm run format:check 可以检查代码是否符合 Prettier 格式化规则,但不进行格式化。

17.3.3 代码注释与文档

良好的代码注释和文档对于代码的可读性和可维护性至关重要。

代码注释

组件注释: 在组件文件头部添加组件注释,描述组件的功能、props 类型、使用场景等。
函数注释: 在函数定义上方添加函数注释,描述函数的功能、参数、返回值、注意事项等。
复杂逻辑注释: 对于代码中复杂的逻辑部分,添加注释解释代码的意图和实现思路。

文档

组件文档: 对于可复用的组件库或 UI 组件库,编写详细的组件文档,包括组件的 API (props, events, slots)、示例代码、使用场景、注意事项等。可以使用 Storybook, Docz, Styleguidist 等工具生成组件文档。
项目文档: 对于大型项目,编写项目文档,描述项目的功能、架构、模块划分、开发规范、部署流程等。

总结

本章系统地总结了 React 开发中的最佳实践,包括组件设计原则 (SRP, SoC)、代码组织与项目结构 (按功能模块、按类型、Atomic Design)、代码风格指南 (ESLint, Prettier) 等。遵循这些最佳实践可以帮助开发者构建可维护、可扩展、高质量的 React 应用,提高开发效率,降低维护成本,并提升团队协作效率。在实际项目中,应该根据项目规模、团队习惯和具体需求,灵活应用这些最佳实践,构建适合自身项目的最佳实践体系。


本章关键词:

  • 最佳实践 (Best Practices)
  • 单一职责原则 (SRP)
  • 关注点分离原则 (SoC)
  • Presentational Components (展示型组件)
  • Container Components (容器型组件)
  • 组件复用 (Component Reusability)
  • 组件组合 (Component Composition)
  • Containment (容器)
  • Atomic Design (原子设计)
  • 功能模块组织 (Feature-based Organization)
  • 类型组织 (Type-based Organization)
  • 代码风格指南 (Code Style Guide)
  • ESLint
  • Prettier
  • 代码注释 (Code Comments)
  • 文档 (Documentation)

练习与思考:

① 分析你在之前章节中创建的 React 组件,是否符合单一职责原则和关注点分离原则?如果不符合,尝试重构组件,使其更符合这些原则。
② 选择一个开源的 React 项目,分析其项目结构和代码组织方式,思考其优缺点。
③ 配置 ESLint 和 Prettier 到你的 React 项目中,并根据团队或个人喜好,自定义 ESLint 和 Prettier 规则。
④ 为一个你创建的可复用 React 组件编写详细的组件文档,包括 API 文档和示例代码。
⑤ 思考在大型 React 项目中,如何有效地推行和落地代码风格指南和最佳实践,保证团队代码质量和开发效率。

希望本章内容能够帮助你深入理解 React 最佳实践,并在实际项目中应用这些实践,提升 React 应用的质量和可维护性。 🚀📚 Happy coding with best practices! 🎉


(本章完)

perform step 5.

18. chapter 18:无障碍 (Accessibility) 与 React

Web 无障碍 (Accessibility, a11y) 是指让所有人,包括有残疾的人,都能平等地使用 Web 内容和功能。构建无障碍的 Web 应用不仅是道德责任,也是法律合规的要求,同时还能提升用户体验,扩大用户群体。对于 React Web 应用而言,无障碍同样至关重要。本章将深入探讨 Web 无障碍的重要性、标准 (WCAG),以及如何在 React 应用中实践无障碍,包括 ARIA 属性、语义化 HTML、无障碍测试工具等,帮助读者构建更包容、更友好的 React 应用。

18.1 Web 无障碍的重要性与标准 (WCAG)

18.1.1 Web 无障碍的重要性

Web 无障碍不仅仅是为残疾人士提供便利,更是一种普惠的设计理念,能够提升所有用户的体验。无障碍的重要性体现在以下几个方面:

社会责任与道德义务:每个人都应该有平等地获取信息的权利。Web 无障碍是尊重人权、履行社会责任的体现。 🧑‍🤝‍🧑
法律合规要求:许多国家和地区都有相关的法律法规要求 Web 内容必须满足一定的无障碍标准。例如,美国的《美国残疾人法案》(ADA)、欧盟的《欧洲无障碍法案》(EAA)、中国的《信息无障碍环境建设无障碍设计规范》等。遵守这些法律法规是企业的基本义务。 ⚖️
提升用户体验:无障碍设计可以提升所有用户的体验,例如清晰的页面结构、易于理解的内容、键盘可操作性等,都能让普通用户受益。 ⬆️
扩大用户群体:构建无障碍的 Web 应用可以吸引更广泛的用户群体,包括残疾人士、老年人、使用辅助技术的人等,扩大用户覆盖范围。 🌍
SEO 优化:搜索引擎爬虫与辅助技术 (如屏幕阅读器) 在某种程度上都依赖于语义化的 HTML 结构来理解页面内容。无障碍的 Web 应用通常具有更好的 SEO 效果。 🔎

18.1.2 Web 内容无障碍指南 (WCAG)

Web 内容无障碍指南 (Web Content Accessibility Guidelines, WCAG) 是由万维网联盟 (W3C) 发布的国际标准,旨在为 Web 内容的可访问性提供统一的、可衡量的标准。WCAG 是目前国际上最权威、最广泛接受的 Web 无障碍标准。

WCAG 最新版本是 WCAG 2.1,它基于四个核心原则 (POUR):

Perceivable (可感知):信息和用户界面组件必须以用户可感知的方式呈现。这包括:
▮▮▮▮ 提供文本替代 (Text Alternatives):为非文本内容 (如图片、视频) 提供文本替代,例如 alt 属性、字幕、文字稿。
▮▮▮▮
提供时间性媒体替代 (Time-based Media):为时间性媒体 (如音频、视频) 提供替代形式,例如字幕、音频描述。
▮▮▮▮ 内容可适应 (Adaptable):内容可以以不同的方式呈现 (例如,更简单的布局),而不会丢失信息或结构。
▮▮▮▮
可区分 (Distinguishable):让用户更容易看到和听到内容,包括前景与背景对比度、颜色使用、音频控制等。

Operable (可操作):用户界面组件和导航必须可操作。这包括:
▮▮▮▮ 键盘可访问 (Keyboard Accessible):所有功能都可以通过键盘操作访问。
▮▮▮▮
有足够的时间 (Enough Time):为用户提供足够的时间来阅读和使用内容。
▮▮▮▮ 避免癫痫发作 (Seizures and Physical Reactions):不设计可能引起癫痫发作的内容。
▮▮▮▮
可导航 (Navigable):提供导航方式,帮助用户找到内容、确定当前位置、跳过重复内容。
▮▮▮▮ 输入模式* (Input Modalities):让不同输入设备的用户都能方便操作,例如鼠标、键盘、触摸屏、语音输入等。

Understandable (可理解):信息和用户界面的操作必须是可理解的。这包括:
▮▮▮▮ 可读性 (Readable):让文本内容易于阅读和理解。
▮▮▮▮
可预测 (Predictable):让网页以可预测的方式呈现和操作。
▮▮▮▮ 输入辅助* (Input Assistance):帮助用户避免和纠正错误。

Robust (鲁棒):内容必须足够健壮,以便能够被各种用户代理 (包括辅助技术) 可靠地解释。这包括:
▮▮▮▮ 兼容性* (Compatible):最大程度地兼容当前和未来的用户代理 (包括辅助技术)。

WCAG 2.1 定义了三个一致性等级:A, AA, AAA。A 级是最低级别,AAA 级是最高级别。通常建议至少达到 AA 级一致性。

18.2 React 中的无障碍实践:ARIA 属性、语义化 HTML

在 React 应用中实现无障碍,主要需要关注以下两个方面:使用 ARIA 属性增强语义化,以及编写语义化的 HTML 结构。

18.2.1 ARIA 属性 (Accessible Rich Internet Applications)

ARIA (Accessible Rich Internet Applications) 是一组 W3C 规范,定义了一系列 HTML 属性,用于增强 Web 内容的语义化,特别是对于动态 Web 应用和富客户端应用。ARIA 属性可以向辅助技术 (如屏幕阅读器) 传递更多关于页面元素角色、状态和属性的信息,从而提升无障碍性。

常用的 ARIA 属性可以分为以下几类:

Roles (角色)role 属性用于定义元素的语义角色。例如:
▮▮▮▮ <div role="button">Click me</div>:将 div 元素标记为按钮角色。
▮▮▮▮
<nav role="navigation">...</nav>:将 nav 元素标记为导航角色。
▮▮▮▮ <main role="main">...</main>:将 main 元素标记为主要内容角色。
▮▮▮▮
<form role="search">...</form>:将 form 元素标记为搜索表单角色。
▮▮▮▮ <article role="article">...</article>:将 article 元素标记为文章角色。
▮▮▮▮
<aside role="complementary">...</aside>:将 aside 元素标记为补充内容角色。
▮▮▮▮ <banner role="banner">...</banner>:将 banner 元素标记为站点 banner 角色。
▮▮▮▮
<complementary role="complementary">...</complementary>:将 complementary 元素标记为补充内容角色。
▮▮▮▮ <contentinfo role="contentinfo">...</contentinfo>:将 contentinfo 元素标记为站点 contentinfo 角色。
▮▮▮▮
<dialog role="dialog">...</dialog>:将 dialog 元素标记为对话框角色。
▮▮▮▮ <document role="document">...</document>:将 document 元素标记为文档角色。
▮▮▮▮
<feed role="feed">...</feed>:将 feed 元素标记为 feed 角色。
▮▮▮▮ <figure role="figure">...</figure>:将 figure 元素标记为 figure 角色。
▮▮▮▮
<group role="group">...</group>:将 group 元素标记为 group 角色。
▮▮▮▮ <heading role="heading" aria-level="1">...</heading>:将元素标记为标题角色,并使用 aria-level 指定标题级别。
▮▮▮▮
<img role="img" alt="Description of the image">:将 img 元素标记为图片角色 (通常 <img> 标签默认具有 role="img")。
▮▮▮▮ <list role="list">...</list>:将 list 元素标记为列表角色。
▮▮▮▮
<listitem role="listitem">...</listitem>:将 listitem 元素标记为列表项角色。
▮▮▮▮ <marquee role="marquee">...</marquee>:将 marquee 元素标记为跑马灯角色。
▮▮▮▮
<meter role="meter" aria-valuemin="0" aria-valuemax="100" aria-valuenow="75" aria-label="Disk usage">:将 meter 元素标记为计量器角色,并使用 aria-valuemin, aria-valuemax, aria-valuenow, aria-label 等属性描述计量器状态。
▮▮▮▮ <navigation role="navigation">...</navigation>:将 navigation 元素标记为导航角色。
▮▮▮▮
<none role="none">...</none><presentation role="presentation">...</presentation>:移除元素的语义角色,使其对辅助技术不可见 (慎用)。
▮▮▮▮ <note role="note">...</note>:将 note 元素标记为笔记角色。
▮▮▮▮
<progressbar role="progressbar" aria-valuemin="0" aria-valuemax="100" aria-valuenow="50" aria-label="Loading...">:将 progressbar 元素标记为进度条角色,并使用 aria-valuemin, aria-valuemax, aria-valuenow, aria-label 等属性描述进度条状态。
▮▮▮▮ <radiogroup role="radiogroup">...</radiogroup>:将 radiogroup 元素标记为单选按钮组角色。
▮▮▮▮
<region role="region" aria-label="Custom Region">...</region>:将 region 元素标记为区域角色,并使用 aria-label 提供区域的标签。
▮▮▮▮ <rowgroup role="rowgroup">...</rowgroup>:将 rowgroup 元素标记为行组角色 (表格)。
▮▮▮▮
<search role="search">...</search>:将 search 元素标记为搜索角色。
▮▮▮▮ <separator role="separator" aria-orientation="vertical">:将 separator 元素标记为分隔符角色,并使用 aria-orientation 指定分隔符方向。
▮▮▮▮
<slider role="slider" aria-valuemin="0" aria-valuemax="100" aria-valuenow="30" aria-label="Volume">:将 slider 元素标记为滑块角色,并使用 aria-valuemin, aria-valuemax, aria-valuenow, aria-label 等属性描述滑块状态。
▮▮▮▮ <spinbutton role="spinbutton" aria-valuemin="0" aria-valuemax="100" aria-valuenow="50" aria-label="Quantity">:将 spinbutton 元素标记为微调按钮角色,并使用 aria-valuemin, aria-valuemax, aria-valuenow, aria-label 等属性描述微调按钮状态。
▮▮▮▮
<status role="status" aria-live="polite">...</status>:将 status 元素标记为状态消息角色,并使用 aria-live 指定状态消息的更新方式。
▮▮▮▮ <table role="table">...</table>:将 table 元素标记为表格角色。
▮▮▮▮
<tablist role="tablist">...</tablist>:将 tablist 元素标记为标签页列表角色。
▮▮▮▮ <term role="term">...</term>:将 term 元素标记为术语角色 (定义列表)。
▮▮▮▮
<toolbar role="toolbar">...</toolbar>:将 toolbar 元素标记为工具栏角色。
▮▮▮▮ <tooltip role="tooltip" aria-hidden="false">...</tooltip>:将 tooltip 元素标记为工具提示角色,并使用 aria-hidden 控制工具提示的显示隐藏。
▮▮▮▮
<tree role="tree">...</tree>:将 tree 元素标记为树形结构角色。

States (状态)aria- 开头的属性用于描述元素的状态。例如:
▮▮▮▮ aria-checked="true" / aria-checked="false" / aria-checked="mixed":用于复选框、单选按钮等,表示选中状态。
▮▮▮▮
aria-disabled="true" / aria-disabled="false":表示禁用状态。
▮▮▮▮ aria-expanded="true" / aria-expanded="false":用于可展开/折叠的元素 (如 Accordion, Tree View),表示展开/折叠状态。
▮▮▮▮
aria-hidden="true" / aria-hidden="false":表示元素是否对辅助技术可见。
▮▮▮▮ aria-pressed="true" / aria-pressed="false" / aria-pressed="mixed":用于按钮等,表示按下状态。
▮▮▮▮
aria-selected="true" / aria-selected="false":用于 TabPanel, Listbox, Treeitem 等,表示选中状态。
▮▮▮▮ aria-invalid="true" / aria-invalid="false":用于表单元素,表示输入是否有效。
▮▮▮▮
aria-busy="true" / aria-busy="false":表示元素是否处于忙碌状态 (例如加载中)。

Properties (属性)aria- 开头的属性用于描述元素的属性或特征。例如:
▮▮▮▮ aria-label="Close":为元素提供标签文本,用于屏幕阅读器朗读。
▮▮▮▮
aria-labelledby="elementId":使用另一个元素作为当前元素的标签。
▮▮▮▮ aria-describedby="elementId":使用另一个元素作为当前元素的描述。
▮▮▮▮
aria-valuemin="0" / aria-valuemax="100" / aria-valuenow="50":用于 Range 类型元素 (如 Slider, Progressbar),表示最小值、最大值和当前值。
▮▮▮▮ aria-orientation="horizontal" / aria-orientation="vertical":用于 Slider, Scrollbar, Separator 等,表示方向。
▮▮▮▮
aria-live="off" / aria-live="polite" / aria-live="assertive":用于动态更新的内容区域 (例如 Status Message, Chat Log),表示内容更新的优先级。

在 React 中使用 ARIA 属性

在 React 组件中,可以直接将 ARIA 属性添加到 JSX 元素上,就像添加普通的 HTML 属性一样。

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 ```jsx
2 import React, { useState } from 'react';
3
4 function AccessibleButton() {
5 const [isPressed, setIsPressed] = useState(false);
6
7 const handleClick = () => {
8 setIsPressed(!isPressed);
9 alert('Button clicked!');
10 };
11
12 return (
13 <button
14 role="button" // 指定 role 为 button
15 aria-pressed={isPressed} // 使用 aria-pressed 属性表示按钮的按下状态
16 aria-label="Click to toggle" // 使用 aria-label 为按钮提供屏幕阅读器文本
17 onClick={handleClick}
18 >
19 Toggle Button
20 </button>
21 );
22 }
23
24 export default AccessibleButton;
25 ```

ARIA 属性使用原则

不要滥用 ARIA: 只有在 HTML 语义化标签不足以表达元素语义时,才考虑使用 ARIA 属性。优先使用语义化的 HTML 标签。
正确使用 ARIA roles: 选择合适的 ARIA roles 来描述元素的角色,避免使用错误的 roles 导致语义混乱。
更新 ARIA states 和 properties: 当组件状态变化时,及时更新相关的 ARIA states 和 properties,保持辅助技术获取的信息与 UI 状态同步。
测试 ARIA 属性: 使用屏幕阅读器等辅助技术测试 ARIA 属性是否生效,确保无障碍性。

18.2.2 语义化 HTML (Semantic HTML)

语义化 HTML 指的是使用 HTML 标签来表达内容的结构和含义,而不是仅仅关注内容的样式和外观。语义化 HTML 有助于提升 Web 页面的可访问性、可维护性和 SEO。

常用的语义化 HTML 标签

结构化标签
▮▮▮▮ <header>: 页眉,通常包含站点标题、导航等。
▮▮▮▮
<nav>: 导航栏,用于页面导航链接。
▮▮▮▮ <main>: 页面主要内容区域。
▮▮▮▮
<article>: 独立的文章、帖子、新闻报道等内容。
▮▮▮▮ <aside>: 与主要内容相关的辅助信息,例如侧边栏、广告、相关链接等。
▮▮▮▮
<footer>: 页脚,通常包含版权信息、联系方式、站点地图等。
▮▮▮▮ <section>: 文档或应用中的一个区域,例如章节、主题分组等。
▮▮▮▮
<address>: 联系信息,例如作者、网站所有者的地址、邮箱、电话等。

文本语义化标签
▮▮▮▮ <h1> - <h6>: 标题,表示不同级别的标题。<h1> 表示最高级别标题,<h6> 表示最低级别标题。
▮▮▮▮
<p>: 段落。
▮▮▮▮ <ul>, <ol>, <li>: 无序列表、有序列表、列表项。
▮▮▮▮
<dl>, <dt>, <dd>: 定义列表、定义术语、定义描述。
▮▮▮▮ <figure>, <figcaption>: 表示独立的图片、图表等内容,<figcaption> 为图注。
▮▮▮▮
<blockquote>: 长引用。
▮▮▮▮ <cite>: 引用来源。
▮▮▮▮
<code>: 代码片段。
▮▮▮▮ <pre>: 预格式化文本。
▮▮▮▮
<strong>: 强调文本 (重要性)。
▮▮▮▮ <em>: 强调文本 (语气)。
▮▮▮▮
<abbr>: 缩写词。
▮▮▮▮ <q>: 行内引用。
▮▮▮▮
<time>: 日期和时间。

表单语义化标签
▮▮▮▮ <form>: 表单。
▮▮▮▮
<label>: 表单元素的标签,使用 for 属性关联表单元素 id
▮▮▮▮ <input>: 输入框,使用 type 属性指定输入类型 (text, email, password, checkbox, radio, etc.)。
▮▮▮▮
<textarea>: 多行文本输入框。
▮▮▮▮ <select>, <option>: 下拉选择框、选项。
▮▮▮▮
<button>: 按钮,使用 type 属性指定按钮类型 (button, submit, reset)。
▮▮▮▮* <fieldset>, <legend>: 表单字段集、字段集标题,用于分组表单元素。

表格语义化标签
▮▮▮▮ <table>: 表格。
▮▮▮▮
<caption>: 表格标题。
▮▮▮▮ <thead>, <tbody>, <tfoot>: 表头、表体、表尾。
▮▮▮▮
<tr>: 表格行。
▮▮▮▮ <th>: 表头单元格。
▮▮▮▮
<td>: 表格数据单元格。
▮▮▮▮* <colgroup>, <col>: 列组、列,用于定义表格列的样式和属性。

语义化 HTML 的优点

提升可访问性: 语义化 HTML 标签本身就具有一定的语义信息,辅助技术可以更好地理解页面结构和内容,提高可访问性。例如,屏幕阅读器可以根据 <h1> - <h6> 标签识别标题层级,根据 <ul>, <ol> 标签识别列表结构,根据 <nav> 标签识别导航区域。
提升可维护性: 语义化 HTML 代码结构清晰、易于理解,降低代码维护成本。
提升 SEO: 搜索引擎爬虫更容易理解语义化 HTML 代码,有利于 SEO 优化。
提高代码可读性: 语义化 HTML 代码更易于阅读和理解,提高开发效率。

React 中语义化 HTML 实践

在 React 组件开发中,应该尽可能使用语义化的 HTML 标签来构建 UI 结构。例如,使用 <header>, <nav>, <main>, <footer>, <article>, <section> 等结构化标签划分页面布局,使用 <h1> - <h6>, <p>, <ul>, <ol> 等文本语义化标签组织文本内容,使用 <form>, <label>, <input>, <button> 等表单语义化标签构建表单。

避免滥用 <div><span> 等无语义标签,除非确实没有更合适的语义化标签可用。

18.3 无障碍测试工具

为了确保 React 应用的无障碍性,需要使用无障碍测试工具进行测试和验证。常用的无障碍测试工具包括:

18.3.1 浏览器扩展

WAVE Evaluation Tool (Web Accessibility Evaluation Tool): 一款流行的浏览器扩展,可以快速评估 Web 页面的无障碍性,检测 WCAG 违规问题。 🌐 WAVE Chrome Extension / 🦊 WAVE Firefox Extension
axe DevTools: 一款强大的无障碍测试工具,提供浏览器扩展、CLI 工具、自动化测试库等多种形式,可以检测 WCAG, Section 508 等标准的违规问题。 🌐 axe DevTools Chrome Extension / 🦊 axe DevTools Firefox Extension
Accessibility Insights for Web: 微软开发的无障碍测试工具,提供浏览器扩展和 Windows 应用,可以进行快速评估、自动检查、人工检查等多种测试模式。 🌐 Accessibility Insights Chrome Extension / 🦊 Accessibility Insights Firefox Extension

18.3.2 在线工具

WebAIM Contrast Checker: 在线对比度检查工具,用于检查文本颜色和背景颜色之间的对比度是否满足 WCAG 标准。 🌐 WebAIM Contrast Checker
Color Contrast Analyzer (CCA): 一款桌面应用程序 (Windows/macOS),用于检查颜色对比度。 🌐 Color Contrast Analyzer

18.3.3 自动化测试库

react-axe: React 无障碍自动化测试库,可以在 React 组件渲染时自动进行无障碍检查,并在控制台输出违规信息。 📦 react-axe

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 npm install --save-dev react-axe

或者使用 yarn 或 pnpm:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 yarn add -D react-axe
1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 pnpm add -D react-axe

在应用入口文件 (例如 index.jsindex.tsx) 中引入 react-axe,在开发环境下启用无障碍检查。

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 if (process.env.NODE_ENV === 'development') {
2 const axe = require('react-axe');
3 axe(React, ReactDOM, 1000); // 延迟 1 秒后执行检查,避免影响 initial render 性能
4 }
5 ```

axe-core: axe DevTools 的核心引擎,可以用于编写自动化无障碍测试用例。 📦 axe-core

Testing Library 系列: React Testing Library 鼓励以用户行为为导向进行测试,也包含一些无障碍测试相关的工具和方法,例如使用 screen.getByRole 查询元素时,可以根据 ARIA roles 进行查询,并进行相关断言。 📦 @testing-library/jest-dom 提供了 toBeAccessible, toHaveNoViolations 等无障碍相关的 Jest 扩展断言方法。

18.3.4 屏幕阅读器 (Screen Reader)

最权威的无障碍测试方法是使用屏幕阅读器等辅助技术进行人工测试。常用的屏幕阅读器包括:

NVDA (NonVisual Desktop Access): 免费开源的 Windows 屏幕阅读器。 🌐 NVDA
JAWS (Job Access With Speech): 商业的 Windows 屏幕阅读器,功能强大。 🌐 JAWS
VoiceOver: macOS 和 iOS 系统内置的屏幕阅读器。 🍎 VoiceOver
TalkBack: Android 系统内置的屏幕阅读器。 🤖 TalkBack

使用屏幕阅读器测试时,需要模拟视障用户的操作方式,例如使用键盘导航、听取屏幕阅读器朗读的内容、操作表单元素、与动态内容交互等,评估 Web 应用的可访问性。

18.4 React 无障碍最佳实践

在 React 应用开发中,遵循以下无障碍最佳实践,可以构建更友好的应用:

语义化 HTML 优先: 尽可能使用语义化的 HTML 标签构建 UI 结构,例如 <header>, <nav>, <main>, <footer>, <article>, <section>, <h1> - <h6>, <ul>, <ol>, <form>, <label>, <button> 等。
合理使用 ARIA 属性: 在 HTML 语义化标签不足以表达元素语义时,使用 ARIA 属性增强语义化,例如 role, aria-label, aria-labelledby, aria-describedby, aria-expanded, aria-hidden, aria-live 等。
为图片和非文本内容提供文本替代: 为 <img> 标签添加 alt 属性,为视频和音频提供字幕和文字稿。
确保键盘可访问性: 所有交互组件 (例如按钮、链接、表单元素、自定义组件) 都应该可以通过键盘操作访问和操作。关注 Tab 键顺序、焦点管理、键盘事件处理等。
颜色对比度: 确保文本颜色和背景颜色之间有足够的对比度,满足 WCAG 对比度要求。
避免纯颜色传达信息: 不要仅仅使用颜色来传达重要信息,例如状态、错误提示等。颜色应该作为辅助手段,同时提供文本或其他视觉提示。
表单无障碍: 为表单元素添加 <label> 标签,使用 for 属性关联表单元素 id。为表单错误提示信息添加 aria-invalidaria-describedby 属性。
动态内容更新提示: 对于动态更新的内容区域 (例如 Status Message, Chat Log),使用 aria-live 属性提示屏幕阅读器内容更新。
焦点管理: 对于复杂的交互组件 (例如 Modal, Dropdown, Autocomplete),需要进行合理的焦点管理,确保键盘用户可以方便地操作组件。
使用无障碍测试工具: 在开发过程中和发布前,使用无障碍测试工具 (浏览器扩展、在线工具、自动化测试库) 进行测试和验证,尽早发现和修复无障碍问题。
进行人工测试: 使用屏幕阅读器等辅助技术进行人工测试,从用户角度评估应用的无障碍性。

总结

本章深入探讨了 Web 无障碍的重要性与标准 (WCAG),以及如何在 React 应用中实践无障碍。我们学习了如何使用 ARIA 属性增强语义化,如何编写语义化的 HTML 结构,以及常用的无障碍测试工具。遵循本章介绍的无障碍最佳实践,可以帮助开发者构建更包容、更友好的 React 应用,提升用户体验,扩大用户群体,并履行社会责任。在接下来的章节中,我们将继续深入学习 React 生态系统的其他重要技术和最佳实践。


本章关键词:

  • Web 无障碍 (Accessibility, a11y)
  • WCAG (Web Content Accessibility Guidelines)
  • POUR 原则 (Perceivable, Operable, Understandable, Robust)
  • ARIA (Accessible Rich Internet Applications)
  • role 属性
  • aria-label, aria-labelledby, aria-describedby
  • aria-checked, aria-disabled, aria-expanded, aria-hidden, aria-live
  • 语义化 HTML (Semantic HTML)
  • 结构化标签 (<header>, <nav>, <main>, <footer>, <article>, <section>)
  • 文本语义化标签 (<h1> - <h6>, <p>, <ul>, <ol>, <form>, <label>, <button>)
  • 无障碍测试工具 (Accessibility Testing Tools)
  • WAVE, axe DevTools, Accessibility Insights
  • react-axe
  • 屏幕阅读器 (Screen Reader)
  • NVDA, JAWS, VoiceOver, TalkBack

练习与思考:

① 选择一个你之前创建的 React 组件,评估其无障碍性,并使用 WAVE 或 axe DevTools 等工具进行测试,查看是否存在无障碍违规问题。
② 为该组件添加 ARIA 属性,增强其语义化,例如为自定义按钮组件添加 role="button"aria-label 属性。
③ 使用语义化 HTML 标签重构该组件的 UI 结构,例如使用 <button> 替代 <div> + onClick 实现按钮功能,使用 <ul> + <li> 替代 <div> 列表结构。
④ 使用屏幕阅读器 (例如 VoiceOver, NVDA) 测试重构后的组件,验证其无障碍性是否得到提升。
⑤ 思考在大型 React 项目中,如何系统地推进无障碍化,例如建立无障碍组件库、制定无障碍开发规范、进行无障碍培训和测试等。

希望本章内容能够帮助你深入理解 React 无障碍,并在实际项目中实践无障碍,构建更包容、更友好的 Web 应用。 🚀📚 Happy accessible coding! 🎉


(本章完)

perform step 5.

19. chapter 19:国际化 (i18n) 与本地化 (l10n)

在全球化的 Web 应用开发中,国际化 (Internationalization, i18n) 和本地化 (Localization, l10n) 是至关重要的环节。它们使得应用能够适应不同语言、文化和地区的用户,从而扩大用户群体,提升用户体验。国际化是指在软件设计和开发阶段,为产品将来支持多语言和地区文化特性做好准备。本地化则是指针对特定的语言和地区文化,修改软件以满足当地用户需求的过程,包括翻译文本、调整日期时间格式、货币符号、数字格式、文化习惯等。本章将深入探讨国际化与本地化的概念、流程,并重点介绍在 React 应用中实现国际化的常用方案和最佳实践,帮助读者构建面向全球用户的多语言 React 应用。

19.1 国际化的概念与流程

19.1.1 国际化 (i18n) 与本地化 (l10n) 的定义

国际化 (Internationalization, i18n):指在软件设计和开发阶段,使产品具备支持多种语言和地区文化特性的能力。国际化的核心目标是解耦代码和语言/地区相关的内容,使得应用的核心逻辑与具体的语言环境无关。国际化通常在开发初期进行,为后续的本地化工作奠定基础。 (i18n is often abbreviated as "i" + 18 letters + "n")

本地化 (Localization, l10n):指将国际化后的产品针对特定的语言和地区文化进行适配的过程。本地化包括翻译用户界面文本、调整日期、时间、货币、数字格式、以及根据当地文化习惯调整应用的行为和内容。本地化是在国际化基础上进行的,针对每个目标语言和地区进行定制。 (l10n is often abbreviated as "l" + 10 letters + "n")

简而言之,国际化是“准备”,本地化是“适配”。国际化是技术层面的准备工作,本地化是内容和文化层面的适配工作。

19.1.2 国际化与本地化的益处

实施国际化和本地化为 Web 应用带来诸多益处:

扩大用户群体:通过支持多种语言和地区,应用可以触达更广泛的全球用户,显著扩大用户群体和市场。 🌍
提升用户体验:用户可以使用母语操作应用,更符合当地文化习惯,提升用户体验和满意度。 😊
增强品牌形象:多语言支持体现了企业的国际化视野和对不同文化用户的尊重,有助于提升品牌形象和全球竞争力。 🏆
提高用户转化率:本地化的内容更容易被用户接受和信任,有助于提高用户转化率和业务增长。 📈
降低维护成本:良好的国际化设计可以降低后续新增语言和地区支持的成本,提高开发效率和可维护性。 🛠️

19.1.3 国际化与本地化工作流程

一个典型的国际化和本地化工作流程包括以下步骤:

国际化 (i18n) 开发
* 代码国际化改造
* 将用户界面文本、提示信息、错误消息等硬编码字符串从代码中提取出来,替换为国际化键 (i18n keys)
* 移除与特定地区文化相关的代码逻辑,例如日期时间格式化、货币符号处理等,使用国际化库提供的 API 进行处理。
* 确保应用可以根据不同的语言环境加载不同的语言资源文件 (locale files)
* 语言资源文件准备
* 创建默认语言 (通常是英文) 的语言资源文件,例如 JSON 或 YAML 格式。
* 在语言资源文件中,以 i18n keys 为键,以默认语言的文本内容为值。

本地化 (l10n) 翻译
* 翻译资源文件
* 将默认语言的语言资源文件导出,发送给翻译人员或翻译服务提供商。
* 翻译人员将默认语言的文本内容翻译成目标语言,并创建目标语言的语言资源文件。
* 文化适配 (可选):
* 根据目标地区的文化习惯,调整应用的行为和内容,例如图片、颜色、布局方向 (RTL 语言)。

本地化测试与验证
* 集成本地化资源
* 将翻译后的目标语言资源文件导入到应用中。
* 配置应用支持切换语言环境 (locale switching)
* 本地化功能测试
* 切换应用语言环境到目标语言,测试应用在目标语言环境下的显示和功能是否正常。
* 重点测试文本翻译质量、日期时间格式、数字格式、货币符号等本地化细节。
* 邀请目标语言母语用户进行用户体验测试,收集反馈并进行改进。

持续维护与更新
* 翻译内容管理
* 建立翻译内容管理平台或流程,方便管理和维护多语言资源文件。
* 当应用内容更新时,及时更新语言资源文件,并进行翻译和本地化。
* 本地化质量监控
* 定期检查和更新本地化质量,确保翻译准确性和用户体验。

19.2 React 国际化方案:react-intli18next

在 React 生态系统中,有许多优秀的国际化库可供选择。其中,react-intli18next 是两个最受欢迎且功能强大的库。

19.2.1 react-intl:由 FormatJS 维护的官方库

react-intl 是由 FormatJS 维护的官方 React 国际化库,是 React 官方推荐的国际化方案。react-intl 提供了丰富的 API 和组件,用于处理文本格式化、日期时间格式化、数字格式化、货币格式化、复数处理、性别处理等国际化需求。

react-intl 的主要特点:

官方维护:由 FormatJS 团队维护,质量可靠,更新及时。
功能全面:提供全面的国际化功能,满足各种复杂的国际化需求。
与 React 生态深度集成:与 React Hooks、Context API 等特性良好集成,使用方便。
性能优秀:经过优化,性能表现良好,适用于大型应用。
社区活跃:社区活跃,文档完善,学习资源丰富。

react-intl 的核心 API 和组件:

IntlProvider 组件: 作为 Provider 组件,包裹应用的根组件,提供国际化 Context,配置当前语言环境 (locale) 和语言资源 (messages)。

FormattedMessage 组件: 用于渲染国际化文本消息。接收 id prop (i18n key) 和 values prop (用于文本插值)。

useIntl Hook: 用于在函数组件中获取 intl 对象,访问 react-intl 提供的各种格式化 API。

intl 对象提供的格式化 API:
* formatMessage(descriptor, values?): 格式化文本消息。
* formatDate(value, options?): 格式化日期。
* formatTime(value, options?): 格式化时间。
* formatNumber(value, options?): 格式化数字。
* formatPlural(value, options?): 处理复数形式。
* formatRelativeTime(value, unit, options?): 格式化相对时间 (例如 "几分钟前", "明天")。
* formatDisplayName(value, options?): 格式化显示名称 (例如语言名称, 货币名称, 国家名称)。

react-intl 使用示例:

安装 react-intl:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 npm install react-intl

或者使用 yarn 或 pnpm:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 yarn add react-intl
1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 pnpm add react-intl

创建语言资源文件: 例如 src/locales/en.json (英文) 和 src/locales/zh.json (中文)。

src/locales/en.json:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 ```json
2 {
3 "greeting": "Hello, {name}!",
4 "product.price": "Price: {price, number, currency}",
5 "items.count": "{count, plural, =0 {No items} one {One item} other {# items}}"
6 }
7 ```

src/locales/zh.json:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 ```json
2 {
3 "greeting": "你好,{name}!",
4 "product.price": "价格:{price, number, currency}",
5 "items.count": "{count, plural, =0 {没有商品} one {一件商品} other {# 件商品}}"
6 }
7 ```

配置 IntlProvider: 在 src/App.jssrc/index.js 中配置 IntlProvider

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 import React from 'react';
2 import { IntlProvider, FormattedMessage } from 'react-intl';
3 import enMessages from './locales/en.json';
4 import zhMessages from './locales/zh.json';
5
6 const messages = {
7 'en': enMessages,
8 'zh': zhMessages,
9 };
10
11 const locale = 'en'; // 或根据用户语言设置动态获取
12
13 function App() {
14 return (
15 <IntlProvider locale={locale} messages={messages[locale]}>
16
17
18
19 <FormattedMessage id="greeting" values={{ name: 'User' }} />
20
21
22 <FormattedMessage id="product.price" values={{ price: 123.45 }} />
23
24
25 <FormattedMessage id="items.count" values={{ count: 0 }} />
26 <FormattedMessage id="items.count" values={{ count: 1 }} />
27 <FormattedMessage id="items.count" values={{ count: 5 }} />
28
29
30
31 </IntlProvider>
32 );
33 }
34
35 export default App;
36 ```

使用 FormattedMessage 组件: 在组件中使用 FormattedMessage 组件渲染国际化文本。

使用 useIntl Hook: 在函数组件中使用 useIntl Hook 获取 intl 对象,进行更灵活的格式化操作。

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 import React from 'react';
2 import { useIntl } from 'react-intl';
3
4 function MyComponent() {
5 const intl = useIntl();
6 const formattedPrice = intl.formatNumber(99.99, {
7 style: 'currency',
8 currency: 'USD',
9 });
10
11 return (
12
13
14 Formatted Price: {formattedPrice}
15
16 );
17 }
18
19 export default MyComponent;
20 ```

19.2.2 i18next:轻量级且灵活的国际化框架

i18next 是一个轻量级且非常流行的 JavaScript 国际化框架,不局限于 React,可以用于各种 JavaScript 项目。i18next 以其灵活性、可扩展性和强大的生态系统而著称。react-i18nexti18next 的 React 集成库。

i18next 的主要特点:

轻量级:核心库体积小巧,性能优秀。
灵活性:高度可配置,支持多种语言资源加载方式、插值方式、复数处理、上下文处理等。
可扩展性:提供丰富的插件和扩展,满足各种高级国际化需求。
框架无关:不局限于 React,可以用于各种 JavaScript 项目 (Vanilla JS, Vue, Angular, etc.)。
社区庞大:社区庞大,生态系统完善,插件丰富,文档详细。

i18next 的核心 API 和组件 (通过 react-i18next 提供):

I18nextProvider 组件: 作为 Provider 组件,包裹应用的根组件,提供 i18next 实例和配置。

useTranslation Hook: 用于在函数组件中获取 t 函数 (translate function) 和 i18n 实例。

withTranslation HOC: 用于在高阶组件中注入 t 函数和 i18n 实例。

Trans 组件: 用于处理包含 React 组件的复杂文本翻译。

t 函数: 核心的翻译函数,接收 i18n key 作为参数,返回翻译后的文本。支持插值、复数、上下文等功能。

i18next 使用示例:

安装 i18nextreact-i18next:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 npm install i18next react-i18next i18next-browser-languagedetector i18next-http-backend

或者使用 yarn 或 pnpm:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 yarn add i18next react-i18next i18next-browser-languagedetector i18next-http-backend
1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 pnpm add i18next react-i18next i18next-browser-languagedetector i18next-http-backend

这里我们同时安装了 i18next-browser-languagedetector (用于浏览器语言检测) 和 i18next-http-backend (用于从 HTTP 加载语言资源文件)。

创建 i18next 配置文件: 例如 src/i18n.js

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 import i18n from 'i18next';
2 import { initReactI18next } from 'react-i18next';
3 import LanguageDetector from 'i18next-browser-languagedetector';
4 import HttpApi from 'i18next-http-backend';
5
6 i18n
7 .use(HttpApi) // 后端加载语言资源
8 .use(LanguageDetector) // 浏览器语言检测
9 .use(initReactI18next) // passes i18n down to react-i18next
10 .init({
11 fallbackLng: 'en', // 默认语言
12 detection: {
13 order: ['localStorage', 'cookie', 'htmlTag', 'navigator'], // 语言检测顺序
14 caches: ['localStorage', 'cookie'], // 缓存语言到 localStorage 和 cookie
15 },
16 backend: {
17 loadPath: '/locales/{{lng}}/translation.json', // 语言资源文件加载路径
18 },
19 interpolation: {
20 escapeValue: false, // react already safes from xss => https://www.i18next.com/translation-function/interpolation#unescape
21 },
22 });
23
24 export default i18n;
25 ```

创建语言资源文件目录: 例如 public/locales/en/translation.json (英文) 和 public/locales/zh/translation.json (中文)。

public/locales/en/translation.json:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 ```json
2 {
3 "greeting": "Hello, {{name}}!",
4 "product.price": "Price: {{price, currency}}",
5 "items.count": "{{count}} item(s)"
6 }
7 ```

public/locales/zh/translation.json:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 ```json
2 {
3 "greeting": "你好,{{name}}!",
4 "product.price": "价格:{{price, currency}}",
5 "items.count": "{{count}} 件商品"
6 }
7 ```

配置 I18nextProvider: 在 src/App.jssrc/index.js 中配置 I18nextProvider

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 import React from 'react';
2 import { I18nextProvider, useTranslation } from 'react-i18next';
3 import i18n from './i18n'; // 导入 i18next 实例
4
5 function App() {
6 return (
7 <I18nextProvider i18n={i18n}>
8 <MyComponent />
9 </I18nextProvider>
10 );
11 }
12
13 function MyComponent() {
14 const { t } = useTranslation();
15
16 return (
17
18
19
20 {t('greeting', { name: 'User' })}
21
22
23 {t('product.price', { price: 123.45, currency: 'CNY' })}
24
25
26 {t('items.count', { count: 5 })}
27
28
29
30 );
31 }
32
33 export default App;
34 ```

使用 useTranslation Hook 和 t 函数: 在组件中使用 useTranslation Hook 获取 t 函数,调用 t('i18nKey', options?) 进行文本翻译。

使用 Trans 组件: 处理包含 React 组件的复杂文本翻译。

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 import React from 'react';
2 import { useTranslation, Trans } from 'react-i18next';
3
4 function MyComponent() {
5 const { t } = useTranslation();
6
7 return (
8
9
10
11 <Trans i18nKey="complex.text">
12 Edit
13
14 your profile
15
16 </Trans>
17
18
19
20 );
21 }
22
23 export default MyComponent;
24 ```

在语言资源文件中,配置 complex.text 如下:

public/locales/en/translation.json:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 ```json
2 {
3 "complex.text": "Edit <0>your profile</0>"
4 }
5 ```

public/locales/zh/translation.json:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 ```json
2 {
3 "complex.text": "编辑 <0>你的个人资料</0>"
4 }
5 ```

19.2.3 react-intl vs i18next:选择指南

react-intli18next 都是优秀的 React 国际化库,选择哪个库取决于项目的具体需求和偏好。

选择 react-intl 的场景:

⚝ 需要官方支持和长期维护的库。
⚝ 项目国际化需求复杂,需要全面的国际化功能 (文本格式化、日期时间格式化、数字格式化、货币格式化、复数、性别等)。
⚝ 团队熟悉 FormatJS 生态系统和 ICU Message Syntax。
⚝ 对性能有较高要求,需要经过优化的国际化库。

选择 i18next 的场景:

⚝ 需要轻量级、灵活、可扩展的国际化框架。
⚝ 项目国际化需求相对简单,主要关注文本翻译和基本格式化。
⚝ 需要跨框架的国际化方案 (不仅限于 React,可能需要支持 Vue, Angular 等)。
⚝ 团队喜欢 i18next 的简洁 API 和强大的插件生态系统。
⚝ 对 bundle size 敏感,需要尽可能小的国际化库。

通常来说,对于大型、复杂的 React 应用,如果对国际化功能和性能有较高要求,react-intl 是一个更稳妥的选择。对于中小型 React 应用,或者需要更轻量级、更灵活的国际化方案,i18next 也是一个不错的选择。

19.3 React 应用国际化实践

19.3.1 配置国际化库

根据项目需求和偏好,选择 react-intli18next,并按照官方文档进行配置。

react-intl 配置: 主要配置 IntlProvider 组件,设置 localemessages props。
i18next 配置: 主要配置 i18next 实例 (例如 src/i18n.js),包括语言检测、后端加载、默认语言、插值配置等,然后在 App.jsindex.js 中使用 I18nextProvider 包裹根组件。

19.3.2 提取和管理翻译文本

提取硬编码字符串: 在代码中搜索硬编码的字符串 (用户界面文本、提示信息、错误消息等),将它们替换为 i18n keys。

定义 i18n keys: 为每个需要国际化的文本定义一个唯一的 i18n key。i18n key 通常采用命名空间 (namespace) + 模块名 + 页面名 + 组件名 + 文本类型 的格式,例如 common.button.submit, product.detail.title, error.validation.email_invalid

管理语言资源文件: 创建和维护多语言的语言资源文件 (JSON, YAML, etc.)。可以使用在线翻译平台、翻译管理工具 (例如 Lokalise, Phrase, Crowdin) 或自建翻译管理系统来辅助管理翻译文本。

19.3.3 组件国际化

函数组件国际化: 使用 react-intlFormattedMessage 组件或 useIntl Hook,或者 react-i18nextuseTranslation Hook 和 t 函数进行文本翻译。

类组件国际化: 可以使用 react-intlinjectIntl HOC 或 withTranslation HOC (react-i18next) 将国际化 API 注入到类组件的 props 中。

处理复杂文本: 对于包含 React 组件、HTML 标签或需要更灵活控制的复杂文本,可以使用 react-intlFormattedHTMLMessage 组件或 react-i18nextTrans 组件。

19.3.4 处理复数、性别和日期/时间/数字格式

复数处理: 使用 react-intlFormattedPlural 组件或 intl.formatPlural API,或者 i18next 的复数规则和 t 函数的复数插值功能,根据数值动态选择合适的复数形式。

性别处理: 使用 react-intlFormattedSelect 组件或 intl.formatSelect API,或者 i18next 的上下文功能,根据性别动态选择合适的文本。

日期/时间/数字格式化: 使用 react-intlFormattedDate, FormattedTime, FormattedNumber 组件或 intl.formatDate, intl.formatTime, intl.formatNumber API,或者 i18next 的格式化插件,根据当前语言环境格式化日期、时间、数字和货币。

19.3.5 动态语言环境切换

实现动态语言环境切换功能,允许用户在运行时切换应用的语言。

语言切换组件: 创建一个语言切换组件 (例如下拉选择框、按钮组),列出应用支持的语言选项。

语言环境切换逻辑: 当用户选择新的语言时,更新应用的语言环境 (locale)。
▮▮▮▮ react-intl: 更新 IntlProviderlocale prop。
▮▮▮▮
i18next: 调用 i18n.changeLanguage(lng) 方法。

语言环境持久化: 将用户选择的语言环境持久化存储 (例如 localStorage, cookie),以便下次用户访问应用时,自动加载上次选择的语言环境。

19.4 React 国际化最佳实践

19.4.1 尽早规划国际化

在项目启动初期就应该考虑国际化需求,进行国际化规划和设计。尽早进行国际化改造,可以避免后期重构代码的成本。

19.4.2 有效使用翻译键 (i18n keys)

选择清晰、一致、易于维护的翻译键命名规范。使用命名空间、模块名、组件名等信息组织翻译键,提高翻译键的可读性和可管理性。

19.4.3 将翻译文本与代码分离

将翻译文本存储在独立的语言资源文件中,与代码逻辑分离。避免在代码中硬编码字符串。

19.4.4 测试国际化与本地化

进行充分的国际化和本地化测试,包括功能测试、界面显示测试、语言切换测试、翻译质量测试、用户体验测试等。使用自动化测试工具和人工测试相结合的方式,确保国际化和本地化质量。

19.4.5 考虑国际化性能

对于大型应用,需要关注国际化对性能的影响。例如,按需加载语言资源文件、缓存翻译结果、优化格式化操作等。

总结

本章深入探讨了国际化 (i18n) 和本地化 (l10n) 的概念、流程,并详细介绍了在 React 应用中实现国际化的两个主流方案:react-intli18next。我们学习了如何配置国际化库、提取和管理翻译文本、国际化组件、处理复数、性别和日期/时间/数字格式,以及动态语言环境切换。遵循本章介绍的国际化最佳实践,可以帮助开发者构建面向全球用户的多语言 React 应用,提升用户体验,扩大用户群体。在接下来的章节中,我们将继续深入学习 React 生态系统的其他重要技术和最佳实践。


本章关键词:

  • 国际化 (Internationalization, i18n)
  • 本地化 (Localization, l10n)
  • 语言资源文件 (Locale Files)
  • 国际化键 (i18n Keys)
  • react-intl
  • i18next
  • IntlProvider (react-intl)
  • I18nextProvider (react-i18next)
  • FormattedMessage (react-intl)
  • useIntl (react-intl)
  • useTranslation (react-i18next)
  • t 函数 (react-i18next)
  • 复数处理 (Pluralization)
  • 性别处理 (Gender)
  • 日期格式化 (Date Formatting)
  • 时间格式化 (Time Formatting)
  • 数字格式化 (Number Formatting)
  • 货币格式化 (Currency Formatting)
  • 动态语言环境切换 (Dynamic Locale Switching)

练习与思考:

① 选择 react-intli18next,为一个简单的 React 应用实现国际化,支持至少两种语言 (例如英文和中文)。
② 在应用中实现动态语言环境切换功能,允许用户切换语言。
③ 处理应用中的复数情况,例如商品数量、评论数量等,使用国际化库提供的复数处理功能。
④ 使用国际化库提供的日期/时间/数字格式化 API,在应用中格式化日期、时间、数字和货币。
⑤ 思考在大型 React 项目中,如何有效地管理和维护多语言资源文件,以及如何进行国际化测试和质量保证。

希望本章内容能够帮助你深入理解 React 国际化,并在实际项目中应用国际化技术,构建更具全球竞争力的 React 应用。 🚀📚 Happy internationalizing! 🎉


(本章完)

perform step 5.

20. chapter 20:安全 (Security) 最佳实践

Web 应用安全是至关重要的,尤其是在处理用户数据和敏感信息时。React 应用作为构建现代 Web 应用的重要技术,同样需要关注安全问题。常见的 Web 安全漏洞,如跨站脚本攻击 (XSS) 和跨站请求伪造 (CSRF),都可能对 React 应用造成威胁。本章将介绍常见的 Web 安全漏洞,并深入探讨在 React 应用开发中需要采取的安全防范措施和最佳实践,帮助读者构建更安全的 React 应用,保护用户数据和应用安全。

20.1 常见的 Web 安全漏洞 (XSS, CSRF)

20.1.1 跨站脚本攻击 (Cross-Site Scripting, XSS)

跨站脚本攻击 (XSS) 是一种常见的 Web 安全漏洞,攻击者通过在 Web 页面中注入恶意脚本 (通常是 JavaScript 代码),当用户访问被注入恶意脚本的页面时,恶意脚本会在用户的浏览器中执行,从而窃取用户 Cookie、Session 信息、用户凭证,甚至篡改页面内容、重定向用户到恶意网站等。

XSS 攻击主要分为三种类型:

反射型 XSS (Reflected XSS)
▮▮▮▮ 恶意脚本作为请求参数的一部分,通过 URL 传递给服务器。
▮▮▮▮
服务器端未对请求参数进行充分的过滤和转义,直接将恶意脚本反射回响应内容中。
▮▮▮▮ 当用户点击包含恶意脚本的链接或提交包含恶意脚本的表单时,恶意脚本会在用户的浏览器中执行。
▮▮▮▮
反射型 XSS 攻击通常是一次性的,攻击效果取决于用户是否点击恶意链接。

▮▮▮▮攻击流程示例:

▮▮▮▮1. 攻击者构造包含恶意 JavaScript 代码的 URL:https://example.com/search?query=<script>alert('XSS')</script>
▮▮▮▮2. 攻击者诱导用户点击该 URL。
▮▮▮▮3. 用户浏览器向 example.com 发送请求,URL 中包含恶意脚本作为 query 参数。
▮▮▮▮4. 服务器端接收到请求,未对 query 参数进行充分处理,直接将 query 参数的值 (包含恶意脚本) 嵌入到 HTML 响应中,返回给用户浏览器。
▮▮▮▮5. 用户浏览器解析 HTML 响应,执行嵌入的恶意脚本 alert('XSS'),弹出警告框。

存储型 XSS (Stored XSS)
▮▮▮▮ 恶意脚本被存储在服务器端的数据库、文件系统等持久化存储介质中。
▮▮▮▮
当用户请求包含恶意脚本的数据时,服务器端从存储介质中读取恶意脚本,并将其嵌入到响应内容中返回给用户浏览器。
▮▮▮▮ 存储型 XSS 攻击具有持久性,只要恶意脚本被存储在服务器端,所有访问受影响页面的用户都可能受到攻击。
▮▮▮▮
存储型 XSS 攻击的危害通常比反射型 XSS 更大。

▮▮▮▮攻击流程示例:

▮▮▮▮1. 攻击者在网站的留言板、评论区等允许用户输入内容的区域,提交包含恶意 JavaScript 代码的内容:<img src="x" onerror=alert('XSS')>
▮▮▮▮2. 服务器端接收到用户提交的内容,并将其存储到数据库中。
▮▮▮▮3. 其他用户访问留言板或评论区页面时,服务器端从数据库中读取包含恶意脚本的内容,并将其嵌入到 HTML 响应中返回给用户浏览器。
▮▮▮▮4. 用户浏览器解析 HTML 响应,执行嵌入的恶意脚本 onerror=alert('XSS') (当 <img> 标签加载图片失败时触发 onerror 事件),弹出警告框。

DOM-based XSS (DOM 型 XSS)
▮▮▮▮ 恶意脚本不经过服务器端,而是通过客户端 JavaScript 代码直接操作 DOM (Document Object Model) 导致的 XSS 漏洞。
▮▮▮▮
攻击者通过修改 URL、Cookie 等方式,将恶意数据注入到客户端 JavaScript 代码中。
▮▮▮▮ 客户端 JavaScript 代码在处理恶意数据时,没有进行充分的过滤和转义,直接将恶意数据渲染到 DOM 中,导致恶意脚本执行。
▮▮▮▮
DOM 型 XSS 攻击完全发生在客户端,服务器端无法直接防御。

▮▮▮▮攻击流程示例:

▮▮▮▮1. 攻击者构造包含恶意 JavaScript 代码的 URL:https://example.com/index.html#<script>alert('XSS')</script> (恶意脚本放在 URL Hash 中)。
▮▮▮▮2. 攻击者诱导用户点击该 URL。
▮▮▮▮3. 用户浏览器访问 example.com/index.html,加载页面和客户端 JavaScript 代码。
▮▮▮▮4. 客户端 JavaScript 代码从 window.location.hash 中获取 URL Hash 值 (包含恶意脚本)。
▮▮▮▮5. 客户端 JavaScript 代码在处理 URL Hash 值时,没有进行充分处理,直接使用 innerHTML 等方式将 URL Hash 值渲染到 DOM 中。
▮▮▮▮6. 恶意脚本在客户端浏览器中执行,弹出警告框。

20.1.2 跨站请求伪造 (Cross-Site Request Forgery, CSRF)

跨站请求伪造 (CSRF) 是一种利用用户已登录的身份,在用户不知情的情况下,以用户的名义伪造请求发送给服务器的攻击。攻击者通过 CSRF 攻击,可以冒充用户执行一些敏感操作,例如修改用户密码、转账、发布信息等。

CSRF 攻击通常发生在用户已经登录目标网站,并且目标网站没有有效的 CSRF 防护机制的情况下。

攻击流程示例:

  1. 用户 Alice 登录了银行网站 bank.example.com,并保持登录状态。
  2. 攻击者 Mallory 构造一个恶意网站 malicious.example.com,并在恶意网站中嵌入一个表单,该表单的目标 URL 指向 bank.example.com 的转账接口,并预先填写了转账金额和收款人账号等信息。
1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 ```html
2 <form action="https://bank.example.com/transfer" method="POST">
3 <input type="hidden" name="amount" value="1000">
4 <input type="hidden" name="recipient" value="Mallory">
5 <input type="submit" value="Click here to get a prize!">
6 </form>
7 <script>
8 document.forms[0].submit(); // 页面加载后自动提交表单
9 </script>
10 ```
  1. 攻击者 Mallory 通过各种方式 (例如邮件、社交媒体) 诱导用户 Alice 访问恶意网站 malicious.example.com
  2. 用户 Alice 在访问恶意网站时,恶意网站中嵌入的表单会自动提交到 bank.example.com/transfer 接口。由于 Alice 之前已经登录了 bank.example.com,浏览器在发送请求时会自动携带 Alice 的 Cookie 信息。
  3. bank.example.com 服务器接收到请求,验证 Cookie 信息,认为请求是用户 Alice 发起的合法请求,执行转账操作,将 1000 元转账给 Mallory。
  4. 用户 Alice 在不知情的情况下,被攻击者 Mallory 伪造请求转走了 1000 元。

CSRF 攻击的特点:

利用用户已登录的身份: CSRF 攻击依赖于用户在目标网站的登录状态。
伪造用户请求: 攻击者伪造的请求是用户本可以发起的合法请求。
跨站攻击: CSRF 攻击通常发生在攻击者网站 (恶意网站) 和目标网站 (受攻击网站) 之间。
隐蔽性: 用户在不知情的情况下被攻击,难以察觉。

20.2 React 应用安全防范

针对 XSS 和 CSRF 等常见的 Web 安全漏洞,在 React 应用开发中需要采取一系列安全防范措施,从前端和后端两个层面共同保障应用安全。

20.2.1 前端安全防范

HTML 转义 (HTML Escaping)

▮▮▮▮ 目的: 防止 XSS 攻击,特别是反射型和存储型 XSS 攻击。
▮▮▮▮
原理: 将用户输入的内容 (特别是可能包含 HTML 特殊字符的内容,例如 <, >, ", ', &) 进行 HTML 转义,将特殊字符转换为 HTML 实体编码。例如:
▮▮▮▮▮▮▮▮ < 转义为 &lt;
▮▮▮▮▮▮▮▮
> 转义为 &gt;
▮▮▮▮▮▮▮▮ " 转义为 &quot;
▮▮▮▮▮▮▮▮
' 转义为 &#39;
▮▮▮▮▮▮▮▮ & 转义为 &amp;
▮▮▮▮
React 防范: React 默认对 JSX 语法中的变量插值进行 HTML 转义,可以有效防止 XSS 攻击。例如:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 ```jsx
2 function MyComponent({ userInput }) {
3 return (
4
5
6 {userInput} {/* React 自动进行 HTML 转义 */}
7
8 );
9 }
10 ```

▮▮▮▮▮▮▮▮如果 userInput 变量包含恶意脚本,例如 <script>alert('XSS')</script>,React 会将其转义为 &lt;script&gt;alert('XSS')&lt;/script&gt;,作为普通文本渲染到页面上,而不是作为 HTML 代码执行。

▮▮▮▮ dangerouslySetInnerHTML 风险: 避免使用 dangerouslySetInnerHTML APIdangerouslySetInnerHTML 允许直接设置元素的 innerHTML不会进行任何 HTML 转义*,如果使用不当,容易引入 XSS 漏洞。只有在明确知道输入内容是安全可信的情况下,才谨慎使用 dangerouslySetInnerHTML

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 ```jsx
2 function MyComponent({ unsafeHTML }) {
3 return (
4
5
6 {/* ⚠️ 存在 XSS 风险,除非 unsafeHTML 内容完全可信 */}
7
8 );
9 }
10 ```

内容安全策略 (Content Security Policy, CSP)

▮▮▮▮ 目的: 增强 XSS 防御,限制浏览器加载和执行的资源来源,减少 XSS 攻击的危害。
▮▮▮▮
原理: CSP 是一种 HTTP 头部,由服务器端设置,告诉浏览器只允许加载来自特定来源的资源 (例如 JavaScript, CSS, 图片, 字体, 框架等)。如果加载的资源来源不符合 CSP 策略,浏览器会阻止资源加载和执行。
▮▮▮▮ 配置 CSP*: 在服务器端配置 HTTP 响应头 Content-Security-Policy。CSP 策略由一系列指令组成,用于定义不同类型资源的允许来源。例如:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://trusted-cdn.example.com; style-src 'self' https://trusted-cdn.example.com; img-src 'self' data:;

▮▮▮▮▮▮▮▮常用 CSP 指令:

▮▮▮▮▮▮▮▮ default-src: 设置所有类型资源的默认来源策略。
▮▮▮▮▮▮▮▮
script-src: 设置 JavaScript 资源的来源策略。
▮▮▮▮▮▮▮▮ style-src: 设置 CSS 资源的来源策略。
▮▮▮▮▮▮▮▮
img-src: 设置图片资源的来源策略。
▮▮▮▮▮▮▮▮ font-src: 设置字体资源的来源策略。
▮▮▮▮▮▮▮▮
connect-src: 设置 AJAX/Fetch 等网络请求的来源策略。
▮▮▮▮▮▮▮▮ media-src: 设置媒体资源 (音频、视频) 的来源策略。
▮▮▮▮▮▮▮▮
object-src: 设置 <object>, <embed>, <applet> 等嵌入式对象的来源策略。
▮▮▮▮▮▮▮▮ frame-src: 设置 <frame><iframe> 框架资源的来源策略。
▮▮▮▮▮▮▮▮
base-uri: 限制 <base> 标签的 URL。
▮▮▮▮▮▮▮▮ form-action: 限制表单提交的目标 URL。
▮▮▮▮▮▮▮▮
upgrade-insecure-requests: 指示浏览器将所有 HTTP 请求升级为 HTTPS。
▮▮▮▮▮▮▮▮* block-all-mixed-content: 阻止加载 HTTP 资源在 HTTPS 页面上 (混合内容)。

▮▮▮▮▮▮▮▮CSP 策略来源值:

▮▮▮▮▮▮▮▮ 'self': 只允许来自同一来源 (域名、协议、端口) 的资源。
▮▮▮▮▮▮▮▮
'none': 不允许加载任何来源的资源。
▮▮▮▮▮▮▮▮ 'unsafe-inline': 允许加载内联 JavaScript 和 CSS (例如 <script> 标签内的 JavaScript 代码, style 属性, <style> 标签)。生产环境应尽量避免使用 'unsafe-inline',容易引入 XSS 风险
▮▮▮▮▮▮▮▮
'unsafe-eval': 允许使用 eval(), Function(), setTimeout('string'), setInterval('string') 等字符串执行机制。生产环境应尽量避免使用 'unsafe-eval',容易引入 XSS 风险
▮▮▮▮▮▮▮▮ https://trusted-cdn.example.com: 允许加载来自指定域名 (例如 CDN 域名) 的资源。
▮▮▮▮▮▮▮▮
data:: 允许加载 data: URI 形式的资源 (例如 base64 编码的图片)。
▮▮▮▮▮▮▮▮* *: 允许加载来自任意来源的资源 (不推荐,降低 CSP 安全性)。

▮▮▮▮ React 应用 CSP 实践*: 在 React 应用中配置 CSP,可以有效限制恶意脚本的执行,降低 XSS 攻击的风险。需要根据应用实际情况,配置合适的 CSP 策略。例如,如果应用只加载来自同一来源的 JavaScript 和 CSS 资源,可以配置 script-src 'self'; style-src 'self';。如果需要加载来自 CDN 的资源,需要将 CDN 域名添加到 CSP 策略中。

输入验证 (Input Validation)

▮▮▮▮ 目的: 防止 XSS 和其他类型的注入攻击 (例如 SQL 注入, 命令注入)。
▮▮▮▮
原理: 对用户输入的数据进行严格的验证,只接受符合预期格式和类型的数据,拒绝非法输入。输入验证应该在前端和后端同时进行,前端验证用于快速反馈和用户体验,后端验证用于安全保障。
▮▮▮▮ 前端输入验证*: 在 React 组件中,可以使用表单验证库 (例如 react-hook-form, formik, yup, zod) 对用户输入进行验证。例如,验证邮箱格式、密码强度、用户名长度等。

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 ```jsx
2 import React from 'react';
3 import { useForm } from 'react-hook-form';
4 import { yupResolver } from '@hookform/resolvers/yup';
5 import * as yup from 'yup';
6
7 const schema = yup.object({
8 username: yup.string().required("Username is required"),
9 email: yup.string().email("Invalid email format").required("Email is required"),
10 password: yup.string().min(6, "Password must be at least 6 characters").required("Password is required"),
11 }).required();
12
13 function RegistrationForm() {
14 const { register, handleSubmit, formState:{ errors } } = useForm({
15 resolver: yupResolver(schema)
16 });
17 const onSubmit = data => console.log(data);
18
19 return (
20 <form onSubmit={handleSubmit(onSubmit)}>
21
22 <label htmlFor="username">Username:</label>
23 <input type="text" id="username" {...register("username")} />
24
25 {errors.username?.message}
26
27
28
29
30 <label htmlFor="email">Email:</label>
31 <input type="email" id="email" {...register("email")} />
32
33 {errors.email?.message}
34
35
36
37
38 <label htmlFor="password">Password:</label>
39 <input type="password" id="password" {...register("password")} />
40
41 {errors.password?.message}
42
43
44
45
46 <input type="submit" value="Register" />
47
48 </form>
49 );
50 }
51
52 export default RegistrationForm;
53 ```

▮▮▮▮ 后端输入验证: 后端也必须对接收到的用户输入进行二次验证*,前端验证只是辅助手段,不能完全依赖前端验证。后端验证可以防止绕过前端验证的恶意请求。后端验证可以使用服务器端框架提供的验证机制 (例如 Node.js 的 express-validator, Java 的 JSR 303 Bean Validation, Python 的 Django forms)。

Cookie 安全属性:

▮▮▮▮ 目的: 增强 Cookie 安全性,防止 Cookie 被窃取和滥用,减少 XSS 和 CSRF 攻击的风险。
▮▮▮▮
常用 Cookie 安全属性:
▮▮▮▮▮▮▮▮ HttpOnly: 设置为 HttpOnly 的 Cookie 只能通过 HTTP(S) 协议传输,无法被客户端 JavaScript 代码访问 (例如 document.cookie)。可以有效防止 XSS 攻击窃取 Cookie。生产环境 Cookie 应该默认设置 HttpOnly 属性*。

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 Set-Cookie: sessionId=xxx; HttpOnly

▮▮▮▮▮▮▮▮ Secure: 设置为 Secure 的 Cookie 只能在 HTTPS 连接下传输,防止 Cookie 在 HTTP 明文传输过程中被窃听。生产环境 Cookie 应该默认设置 Secure 属性*。

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 Set-Cookie: sessionId=xxx; Secure

▮▮▮▮▮▮▮▮ SameSite: 用于防御 CSRF 攻击。SameSite 属性指示浏览器在跨站请求时是否发送 Cookie。SameSite 属性有三个可选值:
▮▮▮▮▮▮▮▮▮▮▮▮
Strict: 最严格的模式。只有在同站请求 (Same-Site Request, 即请求的域名与当前页面域名完全一致) 时才发送 Cookie。跨站请求 (Cross-Site Request) 永远不会发送 Cookie。可以有效防御 CSRF 攻击,但可能会影响一些合法的跨站场景 (例如第三方登录、跨站资源共享)。
▮▮▮▮▮▮▮▮▮▮▮▮ Lax: 相对宽松的模式。在以下两种情况下发送 Cookie:
▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮
同站请求 (Same-Site Request)
▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮ 安全跨站请求 (Secure Cross-Site Request):指由 <a> 标签 GET 请求发起的跨站请求,且请求协议为 HTTPS。
▮▮▮▮▮▮▮▮▮▮▮▮
None: 最宽松的模式。在所有请求 (包括同站和跨站请求) 中都发送 Cookie。如果设置 SameSite=None必须同时设置 Secure 属性,否则 Cookie 将被浏览器拒绝。SameSite=None; Secure 适用于需要跨站共享 Cookie 的场景 (例如第三方登录、广告跟踪)。

▮▮▮▮▮▮▮▮▮▮▮▮推荐的 SameSite 策略:

▮▮▮▮▮▮▮▮▮▮▮▮ 优先使用 SameSite=Strict: 如果应用没有跨站请求 Cookie 的需求,强烈建议使用 SameSite=Strict,可以最大程度地防御 CSRF 攻击。
▮▮▮▮▮▮▮▮▮▮▮▮
考虑使用 SameSite=Lax: 如果应用有部分跨站请求 Cookie 的需求 (例如 <a> 标签 GET 请求),可以使用 SameSite=Lax,在安全性和用户体验之间取得平衡。
▮▮▮▮▮▮▮▮▮▮▮▮ 谨慎使用 SameSite=None; Secure*: 只有在明确需要跨站共享 Cookie,并且充分了解其安全风险的情况下,才谨慎使用 SameSite=None; Secure。需要同时设置 Secure 属性,并确保跨站场景的安全性。

▮▮▮▮ React 应用 Cookie 安全实践: 在 React 应用中,通常由后端服务器设置 Cookie。后端服务器应该默认设置 HttpOnlySecure 属性*,并根据应用需求配置合适的 SameSite 属性。

依赖安全审计:

▮▮▮▮ 目的: 减少前端代码中引入的安全漏洞,例如 XSS 漏洞、DOM-based XSS 漏洞、前端依赖库漏洞等。
▮▮▮▮
方法:
▮▮▮▮▮▮▮▮ 代码审查 (Code Review):进行代码审查,检查前端代码是否存在潜在的安全漏洞,例如不安全的 DOM 操作、不正确的输入处理、不安全的依赖库使用等。
▮▮▮▮▮▮▮▮
静态代码分析工具: 使用静态代码分析工具 (例如 SonarQube, ESLint 插件) 自动检测前端代码中的安全漏洞和潜在风险。
▮▮▮▮▮▮▮▮ 依赖安全扫描*: 使用依赖安全扫描工具 (例如 npm audit, yarn audit, Snyk, OWASP Dependency-Check) 扫描前端项目依赖的第三方库,检查是否存在已知的安全漏洞。及时更新和修复存在漏洞的依赖库。

20.2.2 后端安全防范 (与 React 应用配合)

后端输入验证与数据清洗: 后端服务器必须对所有接收到的用户输入进行严格的验证和清洗,防止注入攻击。

▮▮▮▮ 输入验证: 验证输入数据的格式、类型、长度、范围等,只接受合法输入。
▮▮▮▮
数据清洗: 对输入数据进行清洗和过滤,移除或转义可能包含恶意代码的特殊字符。例如,对 HTML 内容进行 HTML 转义,对 SQL 查询参数进行参数化查询或预编译语句,对命令执行参数进行命令注入防护。

输出编码 (Output Encoding)

▮▮▮▮ 目的: 防止后端输出的数据在前端页面被解析为恶意代码,导致 XSS 攻击。
▮▮▮▮
原理: 在后端服务器将数据输出到 HTML 页面之前,对数据进行输出编码,将特殊字符转换为 HTML 实体编码或 JavaScript 字符串转义序列。例如:
▮▮▮▮▮▮▮▮ HTML 实体编码: 将 HTML 特殊字符转换为 HTML 实体编码 (与前端 HTML 转义类似)。适用于将数据输出到 HTML 内容中。
▮▮▮▮▮▮▮▮
JavaScript 字符串转义: 将 JavaScript 字符串中的特殊字符进行转义 (例如 \, ', ", 换行符, 回车符 等)。适用于将数据输出到 JavaScript 代码中 (例如内联 JavaScript 代码, JSON 数据)。

▮▮▮▮ 后端输出编码实践: 后端服务器应该根据输出数据的上下文 (HTML, JavaScript, CSS, URL 等),选择合适的输出编码方式。大多数后端 Web 框架都提供了内置的输出编码功能,例如:
▮▮▮▮▮▮▮▮
Node.js (Express): 可以使用 escape-html 库进行 HTML 实体编码。
▮▮▮▮▮▮▮▮ Java (Spring): Spring MVC 默认对 HTML 内容进行 HTML 实体编码。
▮▮▮▮▮▮▮▮
Python (Django): Django 模板引擎默认对 HTML 内容进行 HTML 实体编码。
▮▮▮▮▮▮▮▮ PHP (Laravel)*: Laravel Blade 模板引擎默认对 HTML 内容进行 HTML 实体编码。

CSRF 防护 (CSRF Protection)

▮▮▮▮ 目的: 防御 CSRF 攻击。
▮▮▮▮
常用 CSRF 防护措施:
▮▮▮▮▮▮▮▮ CSRF Token (同步令牌)
▮▮▮▮▮▮▮▮▮▮▮▮
原理: 服务器端在用户会话中生成一个随机的、唯一的 CSRF Token,并在用户请求页面时,将 CSRF Token 嵌入到 HTML 页面中 (通常以 hidden input 形式)。
▮▮▮▮▮▮▮▮▮▮▮▮ 当用户提交表单或发起 POST/PUT/DELETE 等修改数据的请求时,前端需要将 CSRF Token 作为请求参数或请求头 一起发送给服务器。
▮▮▮▮▮▮▮▮▮▮▮▮
服务器端接收到请求后,验证请求中的 CSRF Token 是否与会话中存储的 CSRF Token 一致。如果一致,则认为是合法请求;否则,拒绝请求,并可能记录异常日志。
▮▮▮▮▮▮▮▮▮▮▮▮ CSRF Token 具有一次性与用户会话绑定的特点,可以有效防止 CSRF 攻击。攻击者无法在不知情的情况下获取到合法的 CSRF Token,也无法伪造有效的 CSRF Token。
▮▮▮▮▮▮▮▮▮▮▮▮
React 应用 CSRF Token 实践: 在 React 应用中,需要在每次发起 POST/PUT/DELETE 等修改数据的请求时,从 HTML 页面中获取 CSRF Token (例如从 meta 标签或 hidden input 中获取),并将其添加到请求头 (例如 X-CSRF-TokenX-XSRF-TOKEN) 或请求体中。可以使用 HTTP 客户端库 (例如 axios, fetch) 的请求拦截器 (interceptor) 自动添加 CSRF Token。

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 ```javascript
2 import axios from 'axios';
3
4 // 从 HTML meta 标签中获取 CSRF Token
5 const csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content');
6
7 // 配置 axios 请求拦截器,自动添加 CSRF Token 到请求头
8 axios.interceptors.request.use(config => {
9 config.headers['X-CSRF-Token'] = csrfToken;
10 return config;
11 });
12
13 // 发起 POST 请求
14 axios.post('/api/data', { data: '...' })
15 .then(response => {
16 // ...
17 })
18 .catch(error => {
19 // ...
20 });
21 ```

▮▮▮▮▮▮▮▮▮▮▮▮ 后端 CSRF Token 实践: 后端服务器需要实现 CSRF Token 的生成、存储、验证和同步逻辑。大多数后端 Web 框架都提供了内置的 CSRF 防护中间件或库,例如:
▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮
Node.js (Express): 可以使用 csurf 中间件。
▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮ Java (Spring): Spring Security 提供了 CSRF 防护功能。
▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮
Python (Django): Django 默认启用 CSRF 防护。
▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮ PHP (Laravel)*: Laravel 默认启用 CSRF 防护。

▮▮▮▮▮▮▮▮ SameSite Cookie (同站 Cookie)
▮▮▮▮▮▮▮▮▮▮▮▮
前面已经介绍过 SameSite Cookie 属性可以用于防御 CSRF 攻击。推荐在后端服务器端默认设置 SameSite=StrictSameSite=Lax Cookie 属性,增强 CSRF 防御能力。

HTTPS 强制使用:

▮▮▮▮ 目的: 保障数据传输安全,防止数据在传输过程中被窃听和篡改 (中间人攻击)。
▮▮▮▮
方法: 强制使用 HTTPS 协议,将网站从 HTTP 升级到 HTTPS。HTTPS 使用 TLS/SSL 协议对 HTTP 请求和响应数据进行加密传输,保障数据传输的机密性和完整性。
▮▮▮▮ HTTPS 配置*: 需要在服务器端配置 SSL/TLS 证书,并将网站配置为强制跳转到 HTTPS。可以使用 Let's Encrypt 等免费 SSL 证书服务。

安全审计与日志:

▮▮▮▮ 目的: 及时发现和响应安全事件,追踪安全漏洞和攻击行为。
▮▮▮▮
方法:
▮▮▮▮▮▮▮▮ 安全审计: 定期进行安全审计,评估应用的安全风险,检查安全措施的有效性,发现潜在的安全漏洞。可以使用安全扫描工具 (例如 OWASP ZAP, Nessus) 进行自动化安全扫描。
▮▮▮▮▮▮▮▮
安全日志: 记录详细的安全日志,包括用户登录日志、操作日志、错误日志、安全事件日志等。安全日志应该包含足够的信息,例如时间戳、用户 IP 地址、请求 URL、请求参数、操作类型、事件详情等。
▮▮▮▮▮▮▮▮ 日志监控与报警*: 对安全日志进行实时监控和分析,及时发现异常行为和安全事件。可以配置日志报警规则,当检测到可疑行为或安全事件时,自动发送报警通知 (例如邮件、短信、Webhook)。

总结

本章深入探讨了 Web 应用安全的重要性,介绍了常见的 Web 安全漏洞 (XSS, CSRF),并详细讲解了在 React 应用开发中需要采取的前端和后端安全防范措施和最佳实践。包括 HTML 转义、内容安全策略 (CSP)、输入验证、Cookie 安全属性、依赖安全审计、后端输入验证与数据清洗、输出编码、CSRF 防护、HTTPS 强制使用、安全审计与日志等。遵循本章介绍的安全最佳实践,可以帮助开发者构建更安全的 React 应用,保护用户数据和应用安全。在接下来的章节中,我们将继续深入学习 React 生态系统的其他重要技术和最佳实践。


本章关键词:

  • Web 安全 (Web Security)
  • 跨站脚本攻击 (Cross-Site Scripting, XSS)
  • 反射型 XSS (Reflected XSS)
  • 存储型 XSS (Stored XSS)
  • DOM-based XSS (DOM 型 XSS)
  • 跨站请求伪造 (Cross-Site Request Forgery, CSRF)
  • HTML 转义 (HTML Escaping)
  • 内容安全策略 (Content Security Policy, CSP)
  • 输入验证 (Input Validation)
  • Cookie 安全属性 (Cookie Security Attributes)
  • HttpOnly Cookie
  • Secure Cookie
  • SameSite Cookie
  • CSRF Token (同步令牌)
  • 输出编码 (Output Encoding)
  • HTTPS (Hypertext Transfer Protocol Secure)
  • 安全审计 (Security Audit)
  • 安全日志 (Security Log)
  • 依赖安全审计 (Dependency Security Audit)

练习与思考:

① 分析一个你之前创建的 React 应用,是否存在潜在的 XSS 和 CSRF 漏洞?
② 为该应用配置内容安全策略 (CSP),限制 JavaScript 和 CSS 资源的来源,并测试 CSP 是否生效。
③ 在表单组件中实现前端输入验证,使用表单验证库 (例如 react-hook-form, yup) 验证用户输入,并显示错误提示信息。
④ 思考如何在后端服务器端实现 CSRF 防护,并与 React 前端应用配合使用 CSRF Token 进行 CSRF 防御。
⑤ 查阅 OWASP Top 10 列表,了解更多常见的 Web 安全漏洞类型,并思考如何在 React 应用中防范这些漏洞。

希望本章内容能够帮助你深入理解 React 应用安全,并在实际项目中应用安全最佳实践,构建更安全的 Web 应用。 🚀📚 Happy secure coding! 🎉


(本章完)

perform step 5.

21. chapter 21:React 生态系统与未来展望

React 作为一个成熟且流行的前端框架,拥有庞大而活跃的生态系统。丰富的社区资源、强大的工具链、不断涌现的新技术,共同构成了 React 生态的繁荣景象。同时,React 也在不断进化和发展,涌现出许多令人期待的新特性和技术趋势。本章将 overview React 生态系统的主要组成部分,包括社区资源、常用工具库、以及 React 的未来发展趋势,帮助读者更全面地了解 React 生态,把握 React 的未来脉搏。

21.1 React 社区与资源

React 的成功离不开其庞大而活跃的社区支持。丰富的社区资源为开发者提供了强大的后盾,无论是学习 React、解决问题、还是获取灵感,都能在社区中找到答案。

21.1.1 官方资源

React 官方网站 (reactjs.org): 🌐 reactjs.org
▮▮▮▮ 文档 (Documentation):React 官方文档是学习 React 最权威、最全面的资源。文档内容详尽、结构清晰、示例丰富,覆盖了 React 的核心概念、API、Hooks、高级特性、最佳实践等各个方面。官方文档不断更新,紧跟 React 最新版本和技术发展。
▮▮▮▮
教程 (Tutorial):官方网站提供互动式教程,引导初学者从零开始学习 React,构建第一个 React 应用。
▮▮▮▮ 博客 (Blog):React 官方博客发布 React 团队的最新动态、技术文章、更新日志、社区活动等信息。
▮▮▮▮
社区 (Community):链接到 React 社区的主要资源,例如 Stack Overflow, Reactiflux Discord, Dev.to, GitHub Discussions 等。

React GitHub 仓库 (facebook/react): 🐙 github.com/facebook/react
▮▮▮▮ 源代码 (Source Code):可以查看 React 框架的源代码,深入了解 React 的内部实现机制。
▮▮▮▮
Issue Tracker (Issue 追踪):可以查看和提交 React 的 bug 报告、功能请求、性能问题等。
▮▮▮▮ Pull Requests (Pull 请求):可以查看和参与 React 的代码贡献,提交 Pull Requests 贡献代码。
▮▮▮▮
Discussions (讨论):GitHub Discussions 提供了 React 社区的讨论平台,可以进行技术交流、问题解答、经验分享等。

React 官方示例 (react.dev/examples): 🖥️ react.dev/examples
▮▮▮▮ 官方示例代码*: React 官方网站提供了大量示例代码,涵盖了各种 React 应用场景和技术实践,例如 Hooks 示例、组件示例、路由示例、状态管理示例、性能优化示例等。这些示例代码是学习 React 实战技巧的宝贵资源。

21.1.2 社区平台与论坛

Stack Overflow (stackoverflow.com): ❓ stackoverflow.com/questions/tagged/reactjs
▮▮▮▮ 技术问答平台: Stack Overflow 是全球最大的程序员问答社区。在 Stack Overflow 上搜索或提问 React 相关的问题,可以快速获得来自全球 React 开发者的解答和帮助。
▮▮▮▮
React 标签 (reactjs):Stack Overflow 上 React 相关的问题都使用 reactjs 标签进行标记,方便查找和浏览。

Reactiflux Discord (reactiflux.com): 💬 reactiflux.com
▮▮▮▮ 实时聊天社区: Reactiflux 是一个非常活跃的 React Discord 社区,拥有数万名 React 开发者。在 Reactiflux Discord 中可以进行实时技术交流、问题讨论、求职招聘、社区活动等。
▮▮▮▮
不同频道: Reactiflux Discord 分为多个频道,例如 #beginner, #advanced, #help, #react-native, #graphql, #jobs 等,可以根据自己的需求选择合适的频道参与讨论。

Dev.to (dev.to/t/react): ✍️ dev.to/t/react
▮▮▮▮ 技术博客平台: Dev.to 是一个面向开发者的技术博客平台。在 Dev.to 上可以阅读和发布 React 相关的技术文章、教程、经验分享、最佳实践等。
▮▮▮▮
React 标签 (react):Dev.to 上 React 相关的文章都使用 react 标签进行标记,方便查找和浏览。

Reddit (reddit.com/r/reactjs): 📰 reddit.com/r/reactjs
▮▮▮▮ 新闻与讨论论坛*: Reddit 的 r/reactjs 子版块是 React 社区的新闻和讨论论坛。可以在 r/reactjs 上获取 React 最新动态、技术文章、工具推荐、招聘信息等。

Hashnode (hashnode.com/n/reactjs): 📝 hashnode.com/n/reactjs
▮▮▮▮ 技术博客平台: Hashnode 也是一个面向开发者的技术博客平台,类似于 Dev.to。在 Hashnode 上可以阅读和发布 React 相关的技术文章。
▮▮▮▮
React 社区 (Reactjs):Hashnode 上 React 社区 (Reactjs) 汇集了大量的 React 技术文章和教程。

21.1.3 在线学习资源

Codecademy (codecademy.com): 🎓 codecademy.com/learn/react-101
▮▮▮▮ 互动式在线课程*: Codecademy 提供了互动式 React 在线课程,通过实践练习和项目实战,帮助初学者快速入门 React。

Udemy (udemy.com): 👨‍🏫 udemy.com/courses/development/web-development/react
▮▮▮▮ 在线视频课程平台*: Udemy 上有大量的 React 在线视频课程,涵盖了从入门到进阶的各个阶段,可以根据自己的需求选择合适的课程学习。

Coursera (coursera.org): 🏛️ coursera.org/courses?query=react
▮▮▮▮ 在线大学课程平台*: Coursera 上一些大学和教育机构提供了 React 相关的在线课程,例如香港科技大学的 "Full-Stack Web Development with React Specialization"。

Egghead.io (egghead.io): 🥚 egghead.io/browse/frameworks/react
▮▮▮▮ 高质量技术视频课程*: Egghead.io 提供了大量高质量的 React 技术视频课程,内容深入、讲解精炼,适合有一定基础的开发者进阶学习。

Scrimba (scrimba.com): 🧑‍💻 scrimba.com/learn/learnreact
▮▮▮▮ 互动式代码学习平台*: Scrimba 提供了互动式 React 代码学习平台,可以在浏览器中直接编辑和运行代码,并与其他学习者互动交流。

21.1.4 组件库与 UI 框架

React 生态系统中有许多优秀的组件库和 UI 框架,可以帮助开发者快速构建美观、易用、功能丰富的 React 应用。

Material UI (mui.com): 🎨 mui.com
▮▮▮▮ Material Design 组件库*: Material UI 是基于 Google Material Design 设计规范的 React 组件库。提供了丰富的 UI 组件,例如按钮、输入框、表格、导航栏、对话框、图标等,风格现代、美观、易用。Material UI 社区活跃、文档完善、定制性强,是构建企业级 React 应用的常用选择。

Ant Design (ant.design): 🐜 ant.design
▮▮▮▮ 企业级 UI 设计语言和 React 组件库*: Ant Design 是由蚂蚁集团开源的企业级 UI 设计语言和 React 组件库。提供了丰富的、高质量的 UI 组件和设计模式,适用于构建复杂的企业级应用和后台管理系统。Ant Design 组件功能强大、定制性强、国际化支持完善。

Chakra UI (chakra-ui.com): 🧘 chakra-ui.com
▮▮▮▮ 简单、模块化 React 组件库*: Chakra UI 是一个简单、模块化、易于定制的 React 组件库。Chakra UI 基于 Styled System 构建,提供了灵活的样式定制和主题配置能力。Chakra UI 组件库 API 简洁、易于上手、无障碍性良好。

React Bootstrap (react-bootstrap.github.io): 🎽 react-bootstrap.github.io
▮▮▮▮ Bootstrap 风格 React 组件库*: React Bootstrap 是基于 Bootstrap CSS 框架的 React 组件库。提供了 Bootstrap 风格的 UI 组件,例如按钮、表单、导航栏、网格系统等。如果项目已经使用了 Bootstrap 风格,React Bootstrap 可以快速集成。

Semantic UI React (react.semantic-ui.com): 💎 react.semantic-ui.com
▮▮▮▮ Semantic UI 风格 React 组件库*: Semantic UI React 是基于 Semantic UI CSS 框架的 React 组件库。提供了 Semantic UI 风格的 UI 组件,特点是语义化、可读性强、定制性高。

更多组件库: 除了以上几个流行的组件库,React 生态系统中还有许多其他优秀的组件库,例如:
▮▮▮▮ Blueprint (blueprintjs.com): 用于构建数据密集型 Web 应用的 React UI 工具包。
▮▮▮▮
Rebass (rebassjs.org): 基于 Styled System 构建的小型、可扩展的 React 组件库。
▮▮▮▮ Evergreen (evergreen.segment.com): Segment 公司开源的 React UI 框架,用于构建企业级 Web 应用。
▮▮▮▮
Fluent UI (developer.microsoft.com/en-us/fluentui): 微软 Fluent Design System 的 React 实现。
▮▮▮▮ PrimeReact (primereact.org)*: 功能丰富的 React 组件库,提供 90+ UI 组件。

21.1.5 开发工具与辅助库

Create React App (create-react-app.dev): 🛠️ create-react-app.dev
▮▮▮▮ 官方脚手架工具*: Create React App (CRA) 是 React 官方提供的脚手架工具,用于快速创建 React 项目。CRA 零配置、开箱即用,集成了 Webpack, Babel, ESLint, Jest 等常用工具,是 React 初学者和快速原型开发的理想选择。

Vite (vitejs.dev): 🚀 vitejs.dev
▮▮▮▮ 新一代前端构建工具*: Vite 是新一代前端构建工具,基于 ES modules 开发,具有极速冷启动、即时热更新、按需编译等特点。Vite 对 React 项目提供了良好的支持,可以作为 Create React App 的替代方案,提供更快的开发体验。

Next.js (nextjs.org): 🌐 nextjs.org
▮▮▮▮ React 服务端渲染框架*: Next.js 是基于 React 的服务端渲染 (SSR) 框架,提供了开箱即用的 SSR、静态站点生成 (SSG)、API 路由、文件系统路由、代码分割、优化等功能,适用于构建 SEO 友好、高性能的 React 应用,特别是内容型网站、电商网站、企业官网等。

Gatsby (gatsbyjs.com): ⚡ gatsbyjs.com
▮▮▮▮ React 静态站点生成器*: Gatsby 是基于 React 的静态站点生成器 (SSG),使用 GraphQL 作为数据层,可以将 Markdown, MDX, CMS 等数据源转换为静态 HTML 文件。Gatsby 适用于构建博客、文档站点、产品官网等静态内容为主的网站,具有高性能、SEO 友好、安全性高等优点。

Redux (redux.js.org): 🧰 redux.js.org
▮▮▮▮ JavaScript 状态管理库*: Redux 是一个流行的 JavaScript 状态管理库,可以用于管理 React 应用的复杂状态。Redux 采用单向数据流、可预测的状态变化、中间件等机制,适用于大型应用的状态管理。Redux Toolkit 是 Redux 官方推荐的工具集,简化 Redux 开发。

Zustand (github.com/pmndrs/zustand): 📦 github.com/pmndrs/zustand
▮▮▮▮ 小型、快速、可扩展状态管理库*: Zustand 是一个小型、快速、可扩展的 React 状态管理库。Zustand API 简洁、易于上手、性能优秀,适用于中小型应用的状态管理,也可以用于大型应用的局部状态管理。

Recoil (recoiljs.org): ⚛️ recoiljs.org
▮▮▮▮ Facebook 官方状态管理库*: Recoil 是 Facebook 官方推出的 React 状态管理库,与 React Concurrent Mode 和 Suspense 深度集成。Recoil 采用原子 (atoms) 和选择器 (selectors) 的概念,提供了更细粒度、更灵活的状态管理方案。

测试库: React 生态系统中常用的测试库包括:
▮▮▮▮ Jest (jestjs.io): 流行的 JavaScript 测试框架,由 Facebook 开源,与 React 完美集成。
▮▮▮▮
React Testing Library (testing-library.com/react): React 官方推荐的测试库,以用户行为为导向进行组件测试。
▮▮▮▮ Cypress (cypress.io)*: 端到端 (E2E) 测试框架,用于进行 React 应用的 E2E 测试。

其他工具库: React 生态系统中还有许多其他有用的工具库,例如:
▮▮▮▮ React Router (reactrouter.com): React 官方推荐的路由库,用于管理 React 应用的路由。
▮▮▮▮
Axios (axios-http.com): 流行的 HTTP 客户端库,用于发起 AJAX 请求。
▮▮▮▮ Formik (formik.org): 表单处理库,简化 React 表单开发。
▮▮▮▮
Yup (github.com/jquense/yup): Schema 验证库,用于数据验证。
▮▮▮▮ Immer (immerjs.github.io/immer/): Immutability 工具库,简化 JavaScript 不可变数据操作。
▮▮▮▮
Styled Components (styled-components.com): CSS-in-JS 库,将 CSS 样式写在 JavaScript 代码中。
▮▮▮▮ Emotion (emotion.sh): 另一个流行的 CSS-in-JS 库。
▮▮▮▮
Storybook (storybook.js.org): UI 组件开发和展示工具,用于构建组件库和 UI 文档。

21.2 React 未来展望

React 作为一个持续发展和创新的框架,未来发展前景广阔。React 团队不断推出新特性和优化,引领前端技术发展趋势。

21.2.1 Server Components (服务端组件)

Server Components 是 React 团队正在积极推进的一项重要新特性。Server Components 允许开发者在服务器端渲染 React 组件,与传统的客户端组件 (Client Components) 形成互补。

Server Components 的特点与优势:

服务器端渲染: Server Components 在服务器端渲染,生成 HTML 字符串,直接返回给客户端。客户端无需执行 JavaScript 代码即可渲染初始页面,提升首屏加载速度,改善用户体验。
零客户端 JavaScript: Server Components 的渲染逻辑完全在服务器端执行,客户端无需下载和执行 Server Components 的 JavaScript 代码。显著减小客户端 JavaScript Bundle 大小,提升应用性能。
直接访问后端数据源: Server Components 可以直接访问后端数据源 (数据库、API 接口),无需通过 API 请求在客户端获取数据。简化数据获取流程,提高数据获取效率。
更好的安全性: Server Components 的后端逻辑和敏感数据不会暴露在客户端,提升应用安全性
渐进式增强: Server Components 可以与 Client Components 无缝集成,实现渐进式增强的 Web 应用架构。可以使用 Server Components 渲染静态内容和初始页面,使用 Client Components 实现交互功能和动态更新。

Server Components 的应用场景:

内容型网站: 博客、新闻网站、文档站点、电商网站的商品详情页等,这些网站对首屏加载速度和 SEO 有较高要求,Server Components 可以显著提升性能。
静态内容为主的页面: 例如 Landing Page, 营销活动页面, 关于我们页面等,可以使用 Server Components 渲染静态内容,无需客户端 JavaScript。
需要访问后端数据源的组件: 例如用户列表、商品列表、评论列表等,可以使用 Server Components 直接从后端获取数据并渲染,简化数据获取流程。

Server Components 的挑战与限制:

交互性限制: Server Components 不具备交互能力,无法使用 useState, useEffect 等 Hooks,无法处理用户事件。交互功能需要使用 Client Components 实现。
服务器端环境依赖: Server Components 依赖服务器端环境运行,无法在纯客户端环境中使用
学习成本: Server Components 引入了新的编程模型和心智模型,开发者需要学习和适应。

Next.js 对 Server Components 的支持: Next.js 13 版本已经开始实验性地支持 Server Components,并计划在后续版本中逐步完善和推广 Server Components。

21.2.2 React Server Actions (服务端 Actions)

React Server Actions 是与 Server Components 配套使用的新特性,用于简化客户端与服务器端的数据交互。Server Actions 允许开发者在 Server Components 中定义服务器端函数,并在 Client Components 中直接调用这些函数,实现客户端与服务器端的无缝数据交互。

Server Actions 的特点与优势:

简化数据交互: Server Actions 提供了一种简洁、声明式的方式,实现客户端与服务器端的数据交互,无需编写传统的 API 接口和客户端数据请求代码
类型安全: Server Actions 基于 TypeScript 构建,提供端到端的类型安全,减少数据交互过程中的类型错误
渐进式增强: Server Actions 可以与 Server Components 和 Client Components 协同工作,实现渐进式增强的数据交互。可以使用 Server Actions 处理表单提交、数据更新、服务端操作等。
优化用户体验: Server Actions 可以与 React Suspense 和 Transitions 特性结合使用,提供更流畅的用户体验,例如表单提交时的乐观更新、加载状态提示等。

Server Actions 的应用场景:

表单处理: 使用 Server Actions 处理表单提交,简化表单数据的服务器端处理逻辑
数据更新: 使用 Server Actions 更新服务器端数据,例如点赞、收藏、评论等操作。
服务端操作: 执行服务端特定的操作,例如用户注册、登录、支付等。

Next.js 对 Server Actions 的支持: Next.js 13 版本已经开始实验性地支持 Server Actions,并计划在后续版本中逐步完善和推广 Server Actions。

21.2.3 Asset Streaming (资源流式加载)

Asset Streaming 是一种优化 Web 应用资源加载性能的技术。传统的 Web 应用资源加载方式是瀑布式加载,即浏览器需要等待 HTML 文档解析完成,才能开始下载和解析 JavaScript, CSS, 图片等资源。Asset Streaming 允许浏览器在解析 HTML 文档的同时,就开始流式加载和解析资源,无需等待整个 HTML 文档下载完成。

Asset Streaming 的优势:

提升页面加载性能: Asset Streaming 可以显著减少页面首次渲染时间 (TTFB, FCP, LCP),提升页面加载速度和用户体验。
优化资源加载优先级: Asset Streaming 可以优先加载关键资源 (例如首屏渲染所需的 CSS 和 JavaScript),延迟加载非关键资源,优化资源加载优先级。
与 Server Components 协同工作: Asset Streaming 可以与 Server Components 协同工作,进一步提升服务端渲染应用的性能。Server Components 在服务器端渲染 HTML 的同时,可以流式输出资源加载指令,让浏览器尽早开始加载资源。

Next.js 对 Asset Streaming 的支持: Next.js 13 版本已经开始实验性地支持 Asset Streaming,并计划在后续版本中逐步完善和推广 Asset Streaming。

21.2.4 Offscreen Rendering (离屏渲染)

Offscreen Rendering 是一种优化 React 应用渲染性能的技术。Offscreen Rendering 允许 React 在屏幕外 (Offscreen) 预先渲染组件,当组件需要显示在屏幕上时,可以直接使用预先渲染的结果,避免重复渲染,提升渲染性能。

Offscreen Rendering 的优势:

提升渲染性能: Offscreen Rendering 可以减少组件的实际渲染次数,避免不必要的重复渲染,提升渲染性能和用户体验。
优化组件切换性能: 对于需要频繁切换显示的组件 (例如 TabPanel, Accordion, Carousel),Offscreen Rendering 可以预先渲染隐藏的组件,当组件切换显示时,可以快速展示,提升组件切换的流畅度。
与 React Concurrent Mode 和 Suspense 协同工作: Offscreen Rendering 可以与 React Concurrent Mode 和 Suspense 特性结合使用,实现更流畅的异步渲染和组件切换效果。

Offscreen Rendering 的应用场景:

TabPanel 组件: 可以使用 Offscreen Rendering 预先渲染未激活的 TabPanel 内容,提升 Tab 切换速度。
Accordion 组件: 可以使用 Offscreen Rendering 预先渲染折叠的 Accordion 内容,提升 Accordion 展开速度。
Carousel 组件: 可以使用 Offscreen Rendering 预先渲染 Carousel 的下一张图片,提升图片切换流畅度。
虚拟列表 (Virtual List):可以使用 Offscreen Rendering 预先渲染可视区域外的列表项,提升虚拟列表的滚动性能。

React 对 Offscreen Rendering 的支持: React 团队正在积极研究和开发 Offscreen Rendering 技术,并计划在未来版本中逐步引入 Offscreen Rendering API。

21.2.5 Transition API (过渡 API)

Transition API 是 React 团队正在推进的一项新 API,用于优化 React 应用的过渡效果和用户体验。Transition API 允许开发者将一些状态更新标记为 "transitions",React 会将 transitions 状态更新的优先级降低,避免 transitions 状态更新阻塞高优先级的更新 (例如用户输入、交互事件),从而提升应用的响应性和流畅度。

Transition API 的优势:

提升用户体验: Transition API 可以优化过渡效果的流畅度,避免过渡动画卡顿,提升用户体验。
优化应用响应性: Transition API 可以保证高优先级更新的及时响应,例如用户输入、交互事件,避免应用卡顿。
与 React Concurrent Mode 和 Suspense 协同工作: Transition API 可以与 React Concurrent Mode 和 Suspense 特性结合使用,实现更流畅的异步过渡效果。

Transition API 的应用场景:

UI 过渡动画: 例如页面切换过渡动画、组件显示/隐藏过渡动画、元素属性过渡动画等,可以使用 Transition API 优化过渡动画的流畅度。
状态更新较慢的操作: 例如大型列表的滚动、复杂组件的渲染、数据请求和加载等,可以使用 Transition API 降低这些操作的状态更新优先级,避免阻塞用户交互。

React 对 Transition API 的支持: React 18 版本已经引入了 useTransition Hook 和 startTransition API,开始支持 Transition API。React 团队计划在后续版本中继续完善和推广 Transition API。

21.2.6 渐进式增强 (Progressive Enhancement)

渐进式增强 (Progressive Enhancement, PE) 是一种 Web 开发理念,强调先构建网站的基本功能和内容,保证所有用户 (包括低端浏览器、禁用 JavaScript 的用户) 都能访问和使用网站的基本功能。然后再在此基础上,为支持更高级特性的浏览器和用户,逐步添加更丰富的交互效果、动画效果、用户体验优化等增强功能。

渐进式增强的优点:

保证基本可访问性: 渐进式增强确保所有用户都能访问和使用网站的基本功能和内容,即使在低端浏览器或禁用 JavaScript 的情况下。
提升用户体验: 对于支持高级特性的浏览器和用户,渐进式增强可以提供更丰富的交互效果和用户体验。
SEO 友好: 渐进式增强的网站通常具有更好的 SEO 效果,因为搜索引擎爬虫可以更容易地抓取和索引网站的基本内容。
降低开发和维护成本: 渐进式增强鼓励优先构建核心功能,逐步添加增强功能,可以降低开发复杂度和维护成本。

React 应用渐进式增强实践:

服务端渲染 (SSR) 或静态站点生成 (SSG):使用 Next.js 或 Gatsby 等框架进行服务端渲染或静态站点生成,提供可访问的 HTML 内容,即使在 JavaScript 加载失败或被禁用的情况下,用户仍然可以访问到页面的基本内容。
语义化 HTML: 编写语义化的 HTML 代码,使用合适的 HTML 标签表达内容的结构和含义,提高可访问性和 SEO
无障碍性 (Accessibility):遵循 Web 无障碍指南 (WCAG),构建无障碍的 React 应用,确保残疾人士也能平等地访问和使用网站
逐步加载 JavaScript: 将 JavaScript 代码分割成更小的 bundle,按需加载 JavaScript 代码,避免一次性加载过大的 JavaScript bundle 导致页面加载缓慢。可以使用 React.lazySuspense 进行懒加载组件,使用 import() 动态 import 代码分割。
特性检测 (Feature Detection):使用特性检测 (例如 Modernizr, if ('IntersectionObserver' in window)) 判断浏览器是否支持某些高级特性,只在支持的浏览器上应用增强功能,对于不支持的浏览器,提供降级方案或基本功能。

总结

本章 overview 了 React 生态系统的主要组成部分和未来发展趋势。React 拥有庞大而活跃的社区,提供了丰富的官方资源、社区平台、在线学习资源、组件库、UI 框架、开发工具和辅助库。React 的未来发展趋势包括 Server Components, Server Actions, Asset Streaming, Offscreen Rendering, Transition API 等新特性,以及渐进式增强的开发理念。掌握 React 生态系统和未来发展趋势,可以帮助开发者更好地学习和应用 React,构建更高效、更强大、更具竞争力的 React 应用。在接下来的章节中,我们将通过实战项目案例分析,进一步巩固和应用本书所学的 React 知识和技能。


本章关键词:

  • React 生态系统 (React Ecosystem)
  • React 社区 (React Community)
  • React 官方网站 (reactjs.org)
  • React GitHub 仓库 (facebook/react)
  • Stack Overflow
  • Reactiflux Discord
  • Dev.to
  • Reddit
  • Hashnode
  • Codecademy
  • Udemy
  • Coursera
  • Egghead.io
  • Scrimba
  • Material UI
  • Ant Design
  • Chakra UI
  • React Bootstrap
  • Semantic UI React
  • Create React App (CRA)
  • Vite
  • Next.js
  • Gatsby
  • Redux
  • Zustand
  • Recoil
  • Server Components (服务端组件)
  • React Server Actions (服务端 Actions)
  • Asset Streaming (资源流式加载)
  • Offscreen Rendering (离屏渲染)
  • Transition API (过渡 API)
  • 渐进式增强 (Progressive Enhancement, PE)

练习与思考:

① 访问 React 官方网站 (reactjs.org),浏览官方文档、教程、博客等资源,深入了解 React 的最新动态和技术发展。
② 选择一个 React 组件库 (例如 Material UI, Ant Design, Chakra UI),学习其基本用法和组件 API,尝试在你的 React 项目中使用该组件库。
③ 了解 React Server Components 和 React Server Actions 的概念和优势,思考在哪些场景下可以使用 Server Components 和 Server Actions 优化 React 应用。
④ 学习渐进式增强的 Web 开发理念,思考如何在 React 应用中实践渐进式增强,提升应用的可访问性和用户体验。
⑤ 关注 React 社区的最新动态和技术趋势,例如 React Conf, React Working Groups, React RFCs 等,保持对 React 技术发展的前沿了解。

希望本章内容能够帮助你更全面地了解 React 生态系统,把握 React 的未来发展趋势,并在实际项目中更好地应用 React 技术。 🚀📚 Happy exploring React ecosystem! 🎉


(本章完)

perform step 5.

22. chapter 22:实战项目案例分析

理论学习与实践应用相结合是掌握 React Web 开发的关键。前序章节系统地介绍了 React 的核心概念、高级特性和最佳实践,本章将通过两个典型的实战项目案例,帮助读者将所学知识应用于实际开发中,提升问题解决能力和项目经验。我们将分析电商网站前端开发和仪表盘应用开发这两个案例,深入探讨项目需求、技术选型、架构设计、核心功能实现、以及性能优化策略,为读者提供可借鉴的实战经验。

22.1 案例一:电商网站前端开发

22.1.1 项目背景与需求分析

项目背景:构建一个面向用户的在线电商平台的前端部分,专注于商品展示、用户浏览、购物流程等核心功能。

核心需求

商品展示
▮▮▮▮ 商品列表页:展示商品列表,支持分页、排序、筛选 (按分类、价格、品牌等)。
▮▮▮▮
商品详情页:展示商品详细信息 (图片、标题、价格、描述、规格参数、评价等)。
▮▮▮▮ 商品图片轮播:在商品详情页展示商品多张图片,支持轮播切换。
▮▮▮▮
商品搜索:支持关键词搜索商品。
▮▮▮▮* 商品分类导航:展示商品分类,方便用户浏览不同类别的商品。

用户互动
▮▮▮▮ 加入购物车:用户可以将商品加入购物车。
▮▮▮▮
购物车管理:展示购物车商品列表,支持修改商品数量、删除商品、计算总价。
▮▮▮▮ 收藏商品:用户可以将商品添加到收藏夹。
▮▮▮▮
用户评价:用户可以对已购买商品进行评价。

用户账户
▮▮▮▮ 用户注册与登录:支持用户注册新账号、使用已有账号登录。
▮▮▮▮
用户信息管理:用户可以查看和修改个人信息 (地址、收货信息等)。

购物流程
▮▮▮▮ 订单确认:用户在购物车结算时,确认订单信息 (商品列表、收货地址、支付方式等)。
▮▮▮▮
支付功能 (模拟):模拟支付流程,展示支付成功页面。
▮▮▮▮* 订单列表:用户可以查看历史订单列表和订单详情。

页面性能与体验
▮▮▮▮ 首屏加载速度优化:提升页面首次加载速度,优化用户体验。
▮▮▮▮
页面交互流畅性:保证页面操作和交互的流畅性。
▮▮▮▮* 响应式布局:支持在不同设备 (PC、移动端) 上良好展示。

22.1.2 技术选型与架构设计

技术选型

前端框架: React (核心技术)。
状态管理: Zustand (轻量级状态管理库,用于管理全局购物车状态、用户状态等)。Context API (用于组件间共享主题、语言等配置)。
路由管理: React Router (处理页面路由和导航)。
UI 组件库: Chakra UI (提供美观、易用的 UI 组件,加速开发)。
HTTP 客户端: Axios (进行 API 请求)。
构建工具: Vite (快速构建和开发)。
代码风格: ESLint + Prettier (保证代码质量和风格一致性)。
TypeScript: TypeScript (增强代码类型安全性和可维护性)。

架构设计

采用前后端分离架构,前端 React 应用负责 UI 渲染和用户交互,后端 API 提供数据接口。

目录结构 (按功能模块组织)

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 src/
2 ├── components/ (通用组件和 UI 组件)
3 ├── common/ (通用 UI 组件例如 Button, Input, Card, ImageWithFallback)
4 ├── product/ (商品模块组件例如 ProductCard, ProductList, ProductDetail)
5 ├── cart/ (购物车模块组件例如 CartItem, CartSummary, CartIcon)
6 ├── user/ (用户模块组件例如 LoginModal, RegisterModal, UserProfile)
7 └── ...
8 ├── containers/ (容器组件负责数据获取和业务逻辑)
9 ├── product/ (商品模块容器组件例如 ProductListContainer, ProductDetailContainer)
10 ├── cart/ (购物车模块容器组件例如 CartContainer)
11 ├── user/ (用户模块容器组件例如 UserProfileContainer)
12 └── ...
13 ├── pages/ (页面组件路由入口)
14 ├── HomePage.tsx
15 ├── ProductListPage.tsx
16 ├── ProductDetailPage/[productId].tsx
17 ├── CartPage.tsx
18 ├── OrderListPage.tsx
19 └── ...
20 ├── services/ (API 服务封装)
21 ├── productService.ts
22 ├── cartService.ts
23 ├── userService.ts
24 └── ...
25 ├── stores/ (Zustand 状态管理 Store)
26 ├── cartStore.ts
27 ├── userStore.ts
28 └── ...
29 ├── hooks/ (自定义 Hooks)
30 ├── useDebounce.ts
31 ├── useLocalStorage.ts
32 └── ...
33 ├── utils/ (工具函数)
34 ├── format.ts
35 ├── validation.ts
36 └── ...
37 ├── styles/ (全局样式和主题)
38 ├── assets/ (静态资源)
39 ├── App.tsx (根组件)
40 ├── main.tsx (入口文件)
41 └── ...

组件通信
▮▮▮▮ Props:父子组件数据传递。
▮▮▮▮
Zustand:全局状态共享 (购物车状态、用户状态)。
▮▮▮▮ Context API:跨组件共享配置信息 (主题、语言)。
▮▮▮▮
回调函数:子组件向父组件传递事件和数据。

数据流
▮▮▮▮ 单向数据流:React 组件数据流从父组件到子组件单向流动。
▮▮▮▮
API 数据获取:容器组件通过 services 调用 API 获取数据。
▮▮▮▮* 状态管理:Zustand 管理全局状态,组件通过 Zustand 订阅状态变化。

22.1.3 核心功能实现

① 商品列表页 (ProductListPage.tsxProductListContainer.tsx, ProductList.tsx)

  • 使用 useState 管理分页、排序、筛选条件。
  • 使用 useEffect + productService.getProductList 获取商品列表数据。
  • 使用 Chakra UI 的 Grid, Card, Image, Text, Button 等组件构建 UI。
  • 实现分页组件,支持切换页码。
  • 实现排序组件,支持按价格、销量等排序。
  • 实现筛选组件,支持按分类、品牌、价格区间等筛选。

② 商品详情页 (ProductDetailPage/[productId].tsxProductDetailContainer.tsx, ProductDetail.tsx)

  • 使用 useParams (React Router Hook) 获取商品 ID。
  • 使用 useEffect + productService.getProductDetail(productId) 获取商品详情数据。
  • 使用 Chakra UI 的 Box, Heading, Text, Image, Slider, Tabs, Carousel 等组件构建 UI。
  • 实现商品图片轮播组件 (可以使用 Chakra UI 的 Carousel 或自定义实现)。
  • 展示商品详细信息 (标题、价格、描述、规格参数、评价)。
  • 实现加入购物车按钮,点击后更新购物车状态 (Zustand)。

③ 购物车功能 (CartPage.tsxCartContainer.tsx, CartList.tsx, CartItem.tsx, CartSummary.tsx)

  • 使用 Zustand 管理购物车商品列表和总价状态 (cartStore.ts)。
  • 使用 useEffect + cartService.getCartItems 获取购物车商品数据 (或从 Zustand 中获取)。
  • 使用 Chakra UI 的 Table, Thead, Tbody, Tr, Th, Td, NumberInput, Button 等组件构建 UI。
  • 展示购物车商品列表,包括商品图片、标题、数量、单价、小计。
  • 实现修改商品数量功能 (使用 NumberInput 组件,更新 Zustand 购物车状态)。
  • 实现删除商品功能 (点击删除按钮,更新 Zustand 购物车状态)。
  • 实现购物车总价计算和展示 (CartSummary 组件)。
  • 实现去结算按钮,跳转到订单确认页 (OrderConfirmPage.tsx) (简易实现,本案例不深入订单流程)。

④ 用户账户功能 (LoginModal.tsx, RegisterModal.tsx, UserProfile.tsx, UserProfileContainer.tsx)

  • 使用 Zustand 管理用户登录状态和用户信息 (userStore.ts)。
  • 使用 Chakra UI 的 Modal, FormControl, FormLabel, Input, Button 等组件构建登录和注册模态框。
  • 实现用户注册功能 (userService.register),注册成功后更新用户状态 (Zustand)。
  • 实现用户登录功能 (userService.login),登录成功后更新用户状态 (Zustand),存储 token (LocalStorage 或 Cookie)。
  • 实现用户退出登录功能 (清除用户状态和 token)。
  • 实现用户信息展示和修改功能 (UserProfile 组件)。

⑤ 商品搜索功能 (HomePage.tsx, SearchInput.tsx, ProductListContainer.tsx)

  • 在首页 (HomePage.tsx) 添加搜索输入框 (SearchInput.tsx)。
  • 使用 useState 管理搜索关键词。
  • 使用 useDebounce Hook 对搜索关键词进行防抖处理,减少 API 请求频率。
  • ProductListContainer.tsx 中,根据搜索关键词过滤商品列表数据。
  • 或者,后端 API 支持关键词搜索,前端将搜索关键词作为参数传递给 productService.getProductList API。

⑥ 响应式布局

  • 使用 Chakra UI 提供的响应式样式 props (例如 display={{ base: 'none', md: 'grid' }}) 和 breakpoints (例如 base, sm, md, lg, xl),实现响应式布局。
  • 使用 CSS Media Queries 进行更精细的响应式样式调整 (可选)。

22.1.4 性能优化策略

代码分割 (Code Splitting):使用 React.lazy 和 Suspense 进行路由级别的代码分割,将不同页面的组件代码分割成独立的 bundle,按需加载,减少首屏加载时间。

图片优化 (Image Optimization)
▮▮▮▮ 使用 WebP 等现代图片格式,减小图片体积。
▮▮▮▮
使用 CDN 加速图片加载。
▮▮▮▮ 使用 <img> 标签的 loading="lazy" 属性实现图片懒加载。
▮▮▮▮
使用 Chakra UI 的 Image 组件,可以方便地进行图片优化和响应式处理。

组件 Memoization: 使用 React.memo, useMemo, useCallback 等 Memoization 技术,避免不必要的组件重新渲染,提升组件渲染性能。特别是对于商品列表组件 (ProductList, ProductCard),可以使用 React.memo 优化渲染性能。

列表虚拟化 (List Virtualization):对于商品列表页,如果商品数量非常庞大,可以考虑使用虚拟列表技术 (例如 react-window, react-virtualized),只渲染可视区域内的商品项,提升长列表渲染性能。

服务端渲染 (SSR) 或静态站点生成 (SSG) (可选):对于电商网站首页、商品列表页、商品详情页等 SEO 敏感页面,可以考虑使用 Next.js 进行服务端渲染或静态站点生成,提升首屏加载速度和 SEO 效果 (本案例未深入 SSR/SSG)。

22.1.5 案例总结与扩展

本电商网站前端开发案例,涵盖了电商平台常见的核心功能模块,例如商品展示、用户互动、用户账户、购物流程等。通过本案例的实践,读者可以掌握 React 组件化开发、状态管理、路由管理、UI 组件库使用、API 数据交互、性能优化等关键技术,为构建更复杂的电商应用打下基础。

案例扩展方向

  • 实现完整的购物流程:订单确认、支付功能 (对接支付 API)、物流查询、售后服务等。
  • 实现用户中心功能:订单管理、收藏夹管理、评价管理、优惠券管理、积分管理等。
  • 实现后台管理系统:商品管理、订单管理、用户管理、促销管理、数据统计等。
  • 接入 CMS 系统:方便运营人员管理商品信息、内容信息、页面布局等。
  • 使用 Next.js 进行服务端渲染或静态站点生成,提升 SEO 和首屏加载速度。
  • 进行更深入的性能优化,例如代码优化、HTTP 优化、浏览器缓存优化等。

22.2 案例二:仪表盘应用开发

22.2.1 项目背景与需求分析

项目背景:构建一个数据可视化仪表盘应用,用于展示各种业务指标和数据分析结果,帮助用户监控业务运营状况,进行数据分析和决策。

核心需求

数据可视化图表
▮▮▮▮ 折线图 (Line Chart):展示时间序列数据趋势。
▮▮▮▮
柱状图 (Bar Chart):展示分类数据对比。
▮▮▮▮ 饼图 (Pie Chart):展示占比数据。
▮▮▮▮
散点图 (Scatter Chart):展示数据分布和关联性。
▮▮▮▮ 地图 (Map Chart):展示地理位置数据。
▮▮▮▮
表格 (Table):展示结构化数据。
▮▮▮▮ 仪表盘组件 (Gauge Chart):展示单一指标的进度和状态。
▮▮▮▮
更多图表类型 (可选):雷达图、漏斗图、热力图、树图等。

数据展示与交互
▮▮▮▮ 数据面板 (Statistic Cards):展示关键指标的汇总数据 (例如总销售额、用户增长率等)。
▮▮▮▮
数据表格 (Data Table):展示详细的数据表格,支持排序、筛选、搜索、分页。
▮▮▮▮ 图表联动 (Chart Linking):实现多个图表之间的数据联动,例如点击柱状图的某个柱子,联动更新折线图的数据。
▮▮▮▮
图表交互 (Chart Interaction):支持图表交互操作,例如缩放、平移、tooltip 提示、数据钻取 (drill-down) 等。
▮▮▮▮ 数据筛选与过滤:支持用户根据时间范围、维度、指标等条件筛选和过滤数据。
▮▮▮▮
数据导出:支持将图表数据导出为 CSV, Excel, PDF 等格式 (可选)。

仪表盘布局与定制
▮▮▮▮ 仪表盘布局:提供灵活的仪表盘布局方式,例如网格布局、自由布局。
▮▮▮▮
组件拖拽与调整大小:用户可以拖拽和调整仪表盘组件的位置和大小。
▮▮▮▮ 仪表盘主题切换:支持切换不同的仪表盘主题 (例如亮色主题、暗色主题)。
▮▮▮▮
仪表盘保存与加载:用户可以保存和加载自定义的仪表盘布局和配置。

数据源接入
▮▮▮▮ 多数据源支持:支持接入多种数据源,例如 REST API, GraphQL API, WebSocket, CSV 文件, JSON 文件, 数据库 (通过后端 API 中转)。
▮▮▮▮
实时数据更新:支持实时数据更新 (例如 WebSocket 推送、轮询)。

用户权限与安全
▮▮▮▮ 用户认证与授权:保护仪表盘数据安全,只允许授权用户访问。
▮▮▮▮
数据权限控制:根据用户角色和权限,控制用户可以查看的数据范围。

页面性能与体验
▮▮▮▮ 图表渲染性能优化:保证大量数据图表的流畅渲染性能。
▮▮▮▮
页面加载速度优化:提升仪表盘页面加载速度。
▮▮▮▮* 响应式布局:支持在不同设备上良好展示。

22.2.2 技术选型与架构设计

技术选型

前端框架: React (核心技术)。
状态管理: Recoil (Facebook 官方状态管理库,更细粒度、更灵活的状态管理,适用于复杂仪表盘应用)。Context API (用于主题、布局配置等共享)。
路由管理: React Router (处理仪表盘页面路由和导航)。
图表库: ECharts (百度开源的强大的数据可视化图表库,功能丰富、性能优秀、社区活跃)。或者 Chart.js, Recharts, Nivo 等其他图表库 (根据需求选择)。
UI 组件库: Ant Design (提供企业级 UI 组件,包括布局组件、表单组件、操作按钮等,与仪表盘风格统一)。Chakra UI (轻量级 UI 组件库,可选)。
HTTP 客户端: Axios (进行 API 请求,获取数据)。WebSocket (处理实时数据推送)。
构建工具: Vite (快速构建和开发)。
布局库: React Grid Layout (实现仪表盘组件拖拽和布局)。或者 react-GridLayout, react-reflex 等其他布局库。
主题库: @ant-design/theme (Ant Design 主题定制和切换)。或者 Chakra UI Theme Provider (Chakra UI 主题定制)。
代码风格: ESLint + Prettier (保证代码质量和风格一致性)。
TypeScript: TypeScript (增强代码类型安全性和可维护性)。

架构设计

采用前后端分离架构,前端 React 应用负责仪表盘 UI 渲染和用户交互,后端 API 提供数据接口和数据处理服务。

目录结构 (按功能模块组织)

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 src/
2 ├── components/ (通用组件和 UI 组件)
3 │ ├── common/ (通用 UI 组件,例如 Button, Select, DatePicker, Card, Icon)
4 │ ├── chart/ (图表组件,例如 LineChart, BarChart, PieChart, MapChart, DataTable)
5 │ ├── dashboard/ (仪表盘模块组件,例如 DashboardLayout, DashboardHeader, DashboardSidebar, DashboardItem)
6 │ ├── widget/ (仪表盘 Widget 组件,例如 StatisticCardWidget, ChartWidget, TableWidget)
7 │ └── ...
8 ├── containers/ (容器组件,负责数据获取和业务逻辑)
9 │ ├── dashboard/ (仪表盘容器组件,例如 DashboardContainer)
10 │ ├── widget/ (Widget 容器组件,例如 ChartWidgetContainer, TableWidgetContainer)
11 │ └── ...
12 ├── pages/ (页面组件,路由入口)
13 │ ├── DashboardPage.tsx
14 │ └── ...
15 ├── services/ (数据服务封装)
16 │ ├── dashboardService.ts
17 │ ├── chartService.ts
18 │ └── ...
19 ├── stores/ (Recoil 状态管理 Atoms 和 Selectors)
20 │ ├── dashboardState.ts
21 │ ├── chartState.ts
22 │ └── ...
23 ├── hooks/ (自定义 Hooks)
24 │ ├── useChartData.ts
25 │ ├── useDashboardLayout.ts
26 │ └── ...
27 ├── utils/ (工具函数)
28 │ ├── format.ts
29 │ ├── dataTransform.ts
30 │ └── ...
31 ├── themes/ (仪表盘主题配置)
32 ├── layouts/ (仪表盘布局配置)
33 ├── assets/ (静态资源)
34 ├── App.tsx (根组件)
35 ├── main.tsx (入口文件)
36 └── ...

组件通信
▮▮▮▮ Recoil Atoms 和 Selectors:全局状态共享 (仪表盘配置、数据状态、主题状态等)。
▮▮▮▮
Context API:跨组件共享主题、布局配置等。
▮▮▮▮ Props:父子组件数据传递。
▮▮▮▮
回调函数:子组件向父组件传递事件和数据。

数据流
▮▮▮▮ Recoil 状态管理:使用 Recoil Atoms 和 Selectors 管理仪表盘状态,组件通过 Recoil 订阅状态变化。
▮▮▮▮
数据驱动视图:仪表盘 UI 组件根据 Recoil 状态动态渲染。
▮▮▮▮ API 数据获取:Widget 容器组件通过 services 调用 API 获取数据。
▮▮▮▮
WebSocket 实时数据:使用 WebSocket 接收后端推送的实时数据,更新 Recoil 状态。

22.2.3 核心功能实现

① 数据可视化图表组件 (components/chart/ 目录下,例如 LineChart.tsx, BarChart.tsx, PieChart.tsx, MapChart.tsx, DataTable.tsx)

  • 封装 ECharts 图表组件 (例如 LineChart 组件封装 ECharts 折线图,BarChart 组件封装 ECharts 柱状图)。
  • 使用 useEffect 在组件挂载后初始化 ECharts 实例,在组件更新时更新 ECharts 配置项 (setOption)。
  • 接收 props 传递图表数据 (data), 图表配置项 (options), 图表类型 (type) 等。
  • 暴露图表交互事件 (例如 onChartClick, onChartHover),方便父组件处理图表交互逻辑。
  • 根据不同图表类型选择合适的 ECharts 图表类型,并配置相应的 ECharts options。

② 数据面板组件 (components/widget/StatisticCardWidget.tsx)

  • 使用 Chakra UI 的 Card, Heading, Text, Icon 等组件构建数据面板 UI。
  • 接收 props 传递指标名称 (title), 指标数值 (value), 指标图标 (icon), 指标趋势 (trend) 等。
  • 根据指标数值和趋势,动态展示指标状态 (例如数值颜色、趋势箭头)。

③ 仪表盘布局与拖拽 (components/dashboard/DashboardLayout.tsx, components/dashboard/DashboardItem.tsx, containers/dashboard/DashboardContainer.tsx)

  • 使用 react-grid-layout 库实现仪表盘布局和组件拖拽功能 (DashboardLayout 组件)。
  • 使用 Recoil 管理仪表盘布局配置状态 (dashboardState.ts),包括 Widget 组件的位置、大小、顺序等。
  • 使用 DashboardItem 组件包裹每个仪表盘 Widget 组件,实现 Widget 组件的拖拽和调整大小功能。
  • 实现仪表盘布局保存和加载功能 (LocalStorage 或后端 API)。

④ 数据源接入与实时更新 (services/chartService.tsx, hooks/useChartData.tsx)

  • 封装数据服务 (chartService.tsx),提供 API 请求方法 (例如 chartService.getChartData(chartId, params)) 和 WebSocket 连接方法 (chartService.connectRealtimeData(chartId, callback))。
  • 创建自定义 Hook (useChartData.tsx),用于封装数据获取逻辑。
    ▮▮▮▮ 在 Hook 中使用 useState 或 Recoil Atoms 管理图表数据加载状态 (loading, error, data)。
    ▮▮▮▮
    使用 useEffect 在 Hook 内部调用 chartService.getChartDatachartService.connectRealtimeData 获取数据。
    ▮▮▮▮* 返回数据加载状态和数据。
  • 在 Widget 容器组件 (containers/widget/ChartWidgetContainer.tsx, containers/widget/TableWidgetContainer.tsx) 中使用 useChartData Hook 获取数据,并将数据传递给图表组件或数据表格组件。

⑤ 仪表盘主题切换 (themes/, App.tsx)

  • 创建不同的仪表盘主题配置 (themes/lightTheme.ts, themes/darkTheme.ts),包括颜色、字体、背景色等。
  • 使用 Context API (ThemeContext.tsx) 共享当前主题状态。
  • 创建主题切换组件 (components/dashboard/ThemeSwitcher.tsx),允许用户切换主题。
  • App.tsx 中使用 ThemeProvider (Ant Design 或 Chakra UI 提供) 包裹应用,根据当前主题状态动态应用主题配置。

22.2.4 性能优化策略

图表渲染优化: ECharts 图表库本身具有良好的渲染性能。在 React 应用中,需要注意避免频繁更新 ECharts 配置项,只在数据变化时才更新。可以使用 useMemo Hook 缓存 ECharts options,减少不必要的配置项更新。

数据采样与聚合: 对于大数据量图表,可以进行数据采样和聚合,减少图表渲染的数据点数量,提升渲染性能。后端 API 可以提供数据采样和聚合接口。

按需加载图表库: 如果应用中只使用部分图表类型,可以使用 Webpack 或 Vite 的 Tree Shaking 功能,按需加载 ECharts 图表库,减小 bundle size。

组件 Memoization: 对于仪表盘组件和 Widget 组件,可以使用 React.memo 进行 Memoization 优化,避免不必要的组件重新渲染。

虚拟化列表 (List Virtualization):对于数据表格组件,如果数据量非常大,可以使用虚拟列表技术 (例如 react-window, react-virtualized),只渲染可视区域内的数据行,提升长表格渲染性能。

代码分割 (Code Splitting):使用 React.lazy 和 Suspense 进行路由级别的代码分割,将不同页面的组件代码分割成独立的 bundle,按需加载,减少首屏加载时间。

22.2.5 案例总结与扩展

本仪表盘应用开发案例,涵盖了数据可视化仪表盘应用常见的核心功能模块,例如数据图表展示、数据交互、仪表盘布局、数据源接入、主题定制等。通过本案例的实践,读者可以掌握 React 组件化开发、状态管理 (Recoil)、路由管理、UI 组件库和图表库使用、数据可视化技术、实时数据处理、性能优化等关键技术,为构建更复杂的数据可视化应用和企业级仪表盘系统打下基础。

案例扩展方向

  • 扩展图表类型:添加更多类型的图表组件,例如地图、雷达图、漏斗图、树图等。
  • 增强数据分析功能:添加数据钻取 (drill-down)、数据联动、数据告警、数据预测等高级数据分析功能。
  • 实现仪表盘分享与协作:支持仪表盘分享给其他用户、多人协作编辑仪表盘。
  • 集成 BI (Business Intelligence) 工具:与后端 BI 工具 (例如 Superset, Metabase) 集成,对接更丰富的数据源和数据分析能力。
  • 优化用户权限管理:实现更细粒度的数据权限控制和角色管理。
  • 探索更先进的可视化技术:例如 WebGL 加速渲染、3D 可视化、交互式数据叙事 (data storytelling) 等。

总结

本章通过电商网站前端开发和仪表盘应用开发两个实战项目案例,详细分析了项目需求、技术选型、架构设计、核心功能实现、以及性能优化策略。希望通过这两个案例的实践分析,读者能够将前序章节所学的 React 理论知识与实际开发相结合,提升项目实战能力,为未来更复杂的 React Web 应用开发奠定坚实的基础。在最后一章,我们将对本书内容进行总结与回顾,并展望 React 技术的未来发展方向和进阶学习路径。


本章关键词:

  • 实战项目案例 (Project Case Study)
  • 电商网站前端开发 (E-commerce Website Frontend Development)
  • 仪表盘应用开发 (Dashboard Application Development)
  • 技术选型 (Technology Selection)
  • 架构设计 (Architecture Design)
  • 核心功能实现 (Core Feature Implementation)
  • 性能优化策略 (Performance Optimization Strategy)
  • 组件化开发 (Component-Based Development)
  • 状态管理 (State Management)
  • 路由管理 (Routing Management)
  • UI 组件库 (UI Component Library)
  • 图表库 (Chart Library)
  • 前后端分离 (Frontend-Backend Separation)
  • 响应式布局 (Responsive Layout)
  • 代码分割 (Code Splitting)
  • 图片优化 (Image Optimization)
  • 组件 Memoization
  • 列表虚拟化 (List Virtualization)
  • 服务端渲染 (SSR)
  • 静态站点生成 (SSG)
  • 数据可视化 (Data Visualization)
  • 实时数据 (Realtime Data)
  • 数据驱动视图 (Data-Driven View)

练习与思考:

① 选择电商网站前端开发或仪表盘应用开发案例,从头开始创建一个 React 项目,并逐步实现案例的核心功能。
② 在项目开发过程中,尝试应用本章介绍的技术选型、架构设计和性能优化策略。
③ 根据案例的扩展方向,选择一个或多个扩展功能进行实现,例如电商网站的订单支付功能、仪表盘的数据联动功能。
④ 对比电商网站和仪表盘应用案例的技术选型和架构设计,思考不同类型应用的最佳实践和差异性。
⑤ 总结你在项目实践过程中遇到的问题和挑战,并思考如何解决这些问题,提升 React 项目开发能力。

希望本章内容能够帮助你将 React 知识应用于实际项目开发,提升实战技能,并为未来的职业发展做好准备。 🚀📚 Happy project building! 🎉


(本章完)

perform step 5.

23. chapter 23:总结与进阶学习

至此,《React Web 开发权威指南》即将 завершение (complete)。本书系统地介绍了 React Web 开发的各个方面,从基础概念到高级特性,从核心 API 到最佳实践,从生态系统到未来展望,力求为读者打造一本全面、深入、实用的 React 开发指南。本章作为本书的 заключение (conclusion),将对全书内容进行总结与回顾,并为读者规划 React 进阶学习路线图,推荐持续学习资源,助力读者在 React Web 开发领域不断精进,更上一层楼。

23.1 React 学习路线图

React 学习是一个循序渐进的过程。从入门到精通,需要系统学习、实践练习、持续探索和不断积累。以下是一个 React 学习路线图,供读者参考:

① 基础入门阶段

  • JavaScript 基础: 扎实的 JavaScript 基础是学习 React 的前提。重点掌握 JavaScript 语法、数据类型、对象、函数、DOM 操作、ES6+ 新特性 (例如箭头函数、Promise、Class、模块化) 等。
  • HTML & CSS 基础: 了解 HTML 结构、常用 HTML 标签、CSS 样式、CSS 布局 (Flexbox, Grid) 等,能够编写基本的 HTML 页面和 CSS 样式。
  • React 核心概念: 理解 React 的核心概念,例如组件 (Component)、JSX 语法、Props、State、生命周期 (Class Component) 或 Hooks (Function Component)、事件处理、条件渲染、列表渲染、Keys 等。
  • 搭建 React 开发环境: 掌握 Node.js, npm (或 yarn, pnpm) 的安装和配置,学会使用 Create React App 或 Vite 快速搭建 React 项目。
  • 编写基础 React 组件: 通过编写一些简单的 React 组件 (例如计数器、待办事项列表),巩固 React 基础知识,熟悉组件开发流程。

② 进阶提升阶段

  • 深入理解 Hooks: 系统学习 React Hooks API (useState, useEffect, useContext, useReducer, useRef, useCallback, useMemo, useImperativeHandle, useLayoutEffect, useDebugValue, useTransition, useDeferredValue, useId, useInsertionEffect),掌握 Hooks 的使用场景和最佳实践,学会使用自定义 Hooks 复用组件逻辑。
  • 状态管理: 学习 React Context API 和常用的状态管理库 (例如 Redux, Zustand, Recoil),掌握状态管理的基本原理和使用方法,能够根据项目需求选择合适的状态管理方案。
  • 路由管理: 学习 React Router 或其他 React 路由库,掌握路由配置、路由参数、导航、路由守卫等,能够构建单页面应用 (SPA) 的路由系统。
  • 表单处理: 学习 React 表单处理,掌握受控组件和非受控组件的区别和应用场景,学会使用表单验证库 (例如 react-hook-form, formik) 处理表单验证和提交。
  • 样式处理: 学习 React 样式处理方案,例如 CSS Modules, Styled Components, Tailwind CSS 等,掌握不同样式方案的优缺点和适用场景,能够灵活选择合适的样式方案。
  • 组件设计原则: 学习组件设计原则 (单一职责原则, 关注点分离原则),掌握 Presentational Components 和 Container Components 的概念,能够设计可复用、可维护的 React 组件。
  • 性能优化: 学习 React 性能优化技术,例如代码分割、懒加载、Memoization、列表虚拟化等,掌握性能分析工具 (React DevTools Profiler, Profiler API) 的使用方法,能够识别和优化 React 应用的性能瓶颈。
  • 测试 (Testing):学习 React 测试,掌握单元测试 (Jest, React Testing Library)、集成测试、端到端测试 (Cypress) 的方法和工具,能够编写高质量的 React 测试用例,保证代码质量和稳定性。
  • TypeScript 与 React: 学习 TypeScript 基础知识,掌握在 React 项目中配置和使用 TypeScript 的方法,能够使用 TypeScript 增强 React 组件的类型安全性和可维护性。
  • 无障碍性 (Accessibility):学习 Web 无障碍 (WCAG) 标准,掌握在 React 应用中实现无障碍的方法 (ARIA 属性, 语义化 HTML),构建更包容、更友好的 React 应用。
  • 国际化 (i18n) 与本地化 (l10n):学习国际化和本地化的概念和流程,掌握在 React 应用中实现国际化的方案 (react-intl, i18next),构建多语言 React 应用。
  • 安全 (Security):学习 Web 安全基础知识,了解常见的 Web 安全漏洞 (XSS, CSRF),掌握在 React 应用中防范安全漏洞的最佳实践。

③ 高级进阶阶段

  • 深入 React 源码: 阅读 React 源码,深入理解 React 的内部实现机制,例如 Virtual DOM, Reconciliation 算法, Fiber 架构, Concurrent Mode 等。
  • React 新特性: 持续关注 React 官方博客和社区动态,学习 React 的最新特性和技术趋势,例如 Server Components, Server Actions, Asset Streaming, Offscreen Rendering, Transition API 等。
  • Next.js 或 Gatsby: 深入学习 Next.js 或 Gatsby 等 React 框架,掌握服务端渲染 (SSR)、静态站点生成 (SSG)、API 路由、性能优化等高级技术,能够构建企业级 React 应用和高性能网站。
  • React Native: 学习 React Native,掌握使用 React 构建跨平台移动应用 (iOS, Android) 的技术。
  • GraphQL: 学习 GraphQL 技术,掌握在 React 应用中使用 GraphQL 查询数据的方法 (例如 Apollo Client, Relay),构建高效的数据驱动型应用。
  • Serverless Functions: 学习 Serverless Functions (例如 AWS Lambda, Vercel Functions, Netlify Functions),掌握在 React 应用中集成 Serverless Functions 的方法,构建 Serverless Full-Stack 应用。
  • 微前端 (Micro-frontends):学习微前端架构,掌握在大型项目中应用微前端技术,实现前端应用的模块化和独立部署。
  • React 设计模式与架构: 学习 React 设计模式和架构模式 (例如 Compound Components, Render Props, Higher-Order Components, Hooks Composition, Atomic Design, Flux, MVC, MVVM, Clean Architecture),提升 React 应用架构设计能力。
  • 性能调优与监控: 深入学习 React 应用性能调优和监控技术,掌握性能分析工具 (Profiler, Performance API) 的高级用法,能够定位和解决复杂的性能问题,构建高性能 React 应用。
  • 参与 React 社区: 积极参与 React 社区,例如参与开源项目贡献、撰写技术博客、参与技术交流、分享经验,提升个人影响力,与社区共同成长。

23.2 持续学习资源推荐

React 技术栈更新迭代速度快,持续学习是保持技术竞争力的关键。以下是一些 React 持续学习资源推荐:

① 官方资源

  • React 官方网站 (reactjs.org): 🌐 reactjs.org:官方文档、教程、博客,第一手权威资料。
  • React GitHub 仓库 (facebook/react): 🐙 github.com/facebook/react:关注源码、Issue、Pull Requests、Discussions,了解 React 最新动态。
  • React 官方示例 (react.dev/examples): 🖥️ react.dev/examples:学习官方示例代码,掌握实战技巧。
  • React 官方博客 (reactjs.org/blog): 📰 reactjs.org/blog:关注官方博客,获取 React 团队的最新动态和技术文章。

② 社区平台与论坛

  • Stack Overflow (stackoverflow.com): ❓ stackoverflow.com/questions/tagged/reactjs:解决技术问题,参与问答交流。
  • Reactiflux Discord (reactiflux.com): 💬 reactiflux.com:实时技术交流,参与社区讨论。
  • Dev.to (dev.to/t/react): ✍️ dev.to/t/react:阅读技术博客,分享经验心得。
  • Reddit (reddit.com/r/reactjs): 📰 reddit.com/r/reactjs:获取社区新闻,参与话题讨论。
  • Hashnode (hashnode.com/n/reactjs): 📝 hashnode.com/n/reactjs:阅读技术博客,关注社区动态。
  • Twitter (twitter.com): 🐦 关注 React 核心团队成员、React 社区知名人士,获取技术动态和行业资讯。

③ 技术博客与资讯

  • Overreacted (overreacted.io): ✍️ overreacted.io:Dan Abramov (React 核心团队成员) 的个人博客,深入剖析 React 技术原理和设计思想。
  • Kent C. Dodds Blog (kentcdodds.com/blog): ✍️ kentcdodds.com/blog:Kent C. Dodds (React 社区知名专家) 的博客,分享 React 测试、最佳实践、工具技巧等。
  • Josh W Comeau Blog (joshwcomeau.com): ✍️ joshwcomeau.com:Josh W Comeau 的个人博客,内容涵盖 React, CSS, Web 开发等,文风生动有趣,深入浅出。
  • CSS-Tricks (css-tricks.com): 🎨 css-tricks.com:CSS-Tricks 网站,关注 CSS, JavaScript, React 等前端技术,提供大量实用技巧和教程。
  • Smashing Magazine (smashingmagazine.com): 📰 smashingmagazine.com:Smashing Magazine 杂志,涵盖 Web 设计、Web 开发、UI/UX 等领域,提供高质量的文章和案例分析。
  • Frontend Focus (frontendfoc.us): 📰 frontendfoc.us:Frontend Focus 周刊,每周精选前端技术资讯、文章、工具、教程等。
  • React Newsletter (reactjsnews.com): 📰 reactjsnews.com:React Newsletter 周刊,每周精选 React 社区最新动态、文章、库、工具等。

④ 视频课程与教程

⑤ 开源项目与示例

  • GitHub Explore (github.com/explore): 🐙 github.com/explore:在 GitHub 上搜索和浏览优秀的 React 开源项目,学习项目代码、架构设计、最佳实践。
  • React Awesome Components (github.com/brillout/awesome-react-components): 🛠️ github.com/brillout/awesome-react-components:Awesome React Components 列表,收集了各种优秀的 React 组件库和示例。
  • React Examples (github.com/markerikson/react-redux-links): 🔗 github.com/markerikson/react-redux-links:React Redux Links 仓库,收集了大量的 React 和 Redux 示例代码和学习资源。
  • React Patterns (reactpatterns.com): 💡 reactpatterns.com:React Patterns 网站,总结和分享 React 设计模式和最佳实践。

⑥ 技术书籍

总结

《React Web 开发权威指南》至此 завершение (complete)。希望本书能够帮助读者系统掌握 React Web 开发技术,从入门到进阶,构建高质量、高性能、可维护的 React 应用。React 生态系统充满活力,技术发展日新月异,持续学习是提升 React 开发能力的关键。希望读者以本书为起点,不断探索、实践、精进,在 React Web 开发领域取得更大的成就!

祝大家 React 开发之旅愉快! 🚀📚 Happy React coding! 🎉


(本书完)