React-面试指南-全-
React 面试指南(全)
原文:
zh.annas-archive.org/md5/ad14d622e1b86c2f653ec95db37a2432译者:飞龙
前言
你好!《完整的 ReactJS 面试指南》 是一本帮助开发者为 React 面试做准备,以便找到工作的书籍。在这本书中,你将发现许多策略和想法,帮助你顺利通过即将到来的 React 面试。
本书将涵盖准备 React 面试的所有不同步骤,从准备阶段开始,一直到最后构建一些真实世界的项目,在这些项目中,你将获得帮助你在最后几章完成编程作业的见解。
React 生态系统相当庞大,因此将在整个书中讨论许多主题,以便你获得对许多最热门的与 React 相关的功能和概念的广泛理解。我们将涉及的一些主题包括状态、组件、Hooks 和测试。
本书面向对象
有志于成为网页开发者、程序员和 React 开发者的朋友们,可以通过学习获得在 React 面试中表现优异的实用知识。
这本书的目标受众主要有以下三个主要角色:
-
有志于成为网页开发者:对于编程新手,他们希望从头开始学习 React,并希望通过 React 面试
-
程序员:任何希望通过学习 React 以及如何在面试中表现良好来扩展知识和技能的程序员
-
React 开发者:他们将通过提升现有的 React 技能,进一步作为 React 开发者实现职业发展
本书涵盖内容
第一章,为面试做好准备,介绍了如何为即将到来的基于 React 的面试做好最佳准备。它将涵盖准备简历和求职信,以及如何创建 GitHub 个人资料或网站的建议。本章还将讨论如何在招聘网站和 LinkedIn 上寻找工作,以及如何通过聚会和推荐来帮助求职。还推荐了一些额外的面试技巧。
第二章,理解 ReactJS 基础及其主要功能,概述了 ReactJS 的一些核心基础,涵盖了包括 JSX 语言、状态和属性、以及类和函数组件在内的许多主要主题。本章还将解释事件处理、虚拟 DOM、数据流、上下文 API 以及如何进行服务器端渲染。
第三章,Hooks:将状态和其他功能引入函数组件,概述了在 ReactJS 中可用的各种 hooks 及其用例。本章结束时,你将学习如何创建自己的自定义 hooks。
第四章, 处理路由和国际化,概述了如何处理路由和国际化。本章主要涵盖了 React Router 库以及如何在 React 应用程序中进行页面路由。涵盖了多个主题,包括路由、链接、参数、翻译以及如何传递参数和占位符。
第五章, ReactJS 的高级概念,概述了 ReactJS 中的高级概念。本章详细介绍了如何使用错误边界、端口、使用 Profiler API 进行调试、严格模式和并发渲染。本章还涵盖了代码拆分,以及在使用 React 时在移动环境中的使用。
第六章, Redux:最佳状态管理解决方案,概述了我们如何在 React 应用程序中使用 Redux。本章讨论了 Flux 模式和 Redux,介绍了它们的核心原则以及如何使用它们来管理应用程序中的状态。借助本章,你还将了解 Redux 中间件、Saga、Thunk、DevTools 和测试。
第七章, 在 ReactJS 中应用 CSS 的不同方法,概述了我们可以如何将 CSS 整合到我们的 React 应用程序中。有几种不同的实现方式,例如使用处理器、CSS Modules、CSS-in-JS 和 styled-components,所有这些都将被讨论。
第八章, 测试和调试 React 应用程序,概述了在 React 应用程序中进行测试和调试的概念。本章将详细介绍使用 React 测试助手以及执行设置和拆卸的步骤。设置和拆卸是在编写测试时进行的,因为我们经常需要在测试运行之前进行配置,以及在测试运行结束后。
除了数据获取和模拟,你还将了解如何创建事件和定时器,以及我们如何使用 React DevTools 进行调试和分析。
第九章, 使用 Next.js、Gatsby 和 Remix 框架进行快速开发,概述了如何使用 React 构建全栈应用程序。本章旨在深入了解使用 Next.js、Remix 和 Gatsby 等框架来开发他们的 React 应用程序。你还将学习如何进行静态站点生成、服务器端渲染以及添加页面元数据。
第十章,破解任何现实世界编程任务,概述了在面试过程中如何准备即将到来的编程带回家作业或代码挑战。您将学习如何设置开发环境,以及项目所需的正确工具和模板。了解选择正确架构的好处、良好代码测试的原因以及如何在 GitHub 上分享项目也同样重要,这些内容将在本节中介绍。
第十一章,基于 React、Redux、Styled Components 和 Firebase 后端构建应用程序,概述了如何构建连接到 Firebase 数据库的 React 应用程序。本章将详细介绍如何最佳规划应用程序的架构,如何创建业务逻辑和表示层,以及如何设置测试层。当一切完成时,您将学习如何将应用程序部署到 GitHub,以便公开在线查看。
第十二章,基于 NextJS 工具包、身份验证、SWR、GraphQL 和部署构建应用程序,概述了如何构建具有身份验证层的 React 应用程序。本章将详细介绍如何最佳规划应用程序的架构,在本例中使用 SWR 和 GraphQL,然后是创建业务逻辑和表示层,以及设置测试层。当一切完成时,您将学习如何将应用程序部署到 GitHub,以便公开在线查看。
为了充分利用本书
读者应了解 JavaScript 或任何类似的编程语言、框架或库。对 React 的了解是受欢迎的,但不是必需的。建议具备基本的编程概念和方法论的理解。
| 本书涵盖的软件/硬件 | 操作系统要求 |
|---|---|
| React 18 | Windows、macOS 或 Linux |
| TypeScript 3.7 | |
| ECMAScript 11 |
读者应具备一个包含集成开发环境(IDE)、命令行界面(CLI)应用程序以及安装的任何工具、框架、库或包(如 NodeJS、npm 和 Next.js)的编程设置。
如果您正在使用本书的数字版,我们建议您亲自输入代码或从本书的 GitHub 仓库(下一节中提供链接)获取代码。这样做将帮助您避免与代码复制粘贴相关的任何潜在错误。
下载示例代码文件
您可以从 GitHub 下载本书的示例代码文件,网址为github.com/PacktPublishing/React-Interview-Guide。如果代码有更新,它将在 GitHub 仓库中更新。
我们还有其他来自我们丰富的图书和视频目录的代码包,可在github.com/PacktPublishing/找到。查看它们吧!
使用的约定
本书使用了多种文本约定。
文本中的代码: 表示文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 昵称。以下是一个示例:“子组件可以使用useContext钩子来消费上下文。”
代码块设置如下:
import { useContext } from 'react';
import { UserContext } from './context';
function MyChildComponent() {
const currentUser = useContext(UserContext);
return <span>{currentUser}</span>;
任何命令行输入或输出都按以下方式编写:
git status
git add .
git commit -m "vercel graphql endpoint for uri"
git push
粗体: 表示新术语、重要单词或您在屏幕上看到的单词。例如,菜单或对话框中的单词以粗体显示。以下是一个示例:“同样,当 API 由于服务不可用而抛出错误时,可以通过DebugValue标签跟踪相应的根本原因。”
小贴士或重要注意事项
它看起来像这样。
联系我们
我们始终欢迎读者的反馈。
一般反馈: 如果您对本书的任何方面有疑问,请通过 customercare@packtpub.com 给我们发电子邮件,并在邮件主题中提及书名。
勘误: 尽管我们已经尽最大努力确保内容的准确性,但错误仍然可能发生。如果您在这本书中发现了错误,如果您能向我们报告,我们将不胜感激。请访问www.packtpub.com/support/errata并填写表格。
盗版: 如果您在互联网上以任何形式发现我们作品的非法副本,如果您能提供位置地址或网站名称,我们将不胜感激。请通过 copyright@packt.com 与我们联系,并提供材料的链接。
如果您有兴趣成为作者: 如果您在某个主题上具有专业知识,并且您有兴趣撰写或为书籍做出贡献,请访问authors.packtpub.com。
分享您的想法
一旦您阅读了React 面试指南,我们很乐意听到您的想法!请点击此处直接访问此书的亚马逊评论页面并分享您的反馈。
您的评论对我们和科技社区都很重要,并将帮助我们确保我们提供高质量的内容。
下载本书的免费 PDF 副本
感谢您购买本书!
您喜欢在旅途中阅读,但又无法携带您的印刷书籍到处走吗?
您的电子书购买是否与您选择的设备不兼容?
不要担心,现在,您每购买一本 Packt 书籍,都可以免费获得该书的 DRM 免费 PDF 版本。
在任何地方、任何时间、任何设备上阅读。直接从您最喜欢的技术书籍中搜索、复制和粘贴代码到您的应用程序中。
优惠远不止于此,您还可以获得独家折扣、时事通讯和每日免费内容的每日电子邮件。
按照以下简单步骤获取好处:
- 扫描二维码或访问以下链接

packt.link/free-ebook/9781803241517
-
提交你的购买证明
-
就这样!我们将直接将免费的 PDF 和其他好处发送到你的邮箱
第一部分:为面试做准备
在这部分,你将学习如何为 React.js 面试做好准备。本章将涵盖求职者应该如何准备他们的简历和求职信,以及拥有 GitHub 账户和作品集网站的重要性。然后,我们将了解一些关于聚会和推荐的信息以及它们如何帮助找到工作。最后,我们将学习一些额外的面试技巧。
本部分包含以下章节:
- 第一章,为面试准备做好准备
第一章:为面试准备做好准备
在今天这个不断变化的就业环境中,找到适合你能力、兴趣和目标的工作可能很困难。如果你想从人群中脱颖而出并获得理想的工作,就必须投入时间和精力来完善面试技巧。本章将为你提供完整的路线图,通过提供有用的指导和每个面试阶段的必要见解,帮助你自信地导航到下一个职业机会。在接下来的章节中,我们将探讨针对 React 开发者的常见面试问题,以便我们为在这个行业作为熟练使用 React 框架的 JavaScript 开发者找到工作做好准备。
当我们开始实际求职时,我们会查看许多资源来寻找机会,包括 LinkedIn、会议和推荐。我们通过利用你的专业网络、增强你的互联网个人资料和进入未开发的就业市场来了解方法,这样你就有必要了解和资源来找到各种机会并建立持久的关系。在本章中,我们将从讨论你的简历和求职信开始,这是你求职的基础。这些关键文件向潜在雇主提供了你资格的概述,并让你了解你的专业态度。为了吸引招聘经理的注意,我们详细介绍了创建吸引人的内容、根据工作的需求定制你的简历和求职信,以及提高它们的整体展示效果。
由于我们将申请 React 职位,因此我们适当地调整简历非常重要,这样我们就有机会让招聘经理看到我们的简历。这可以通过包括相关的热门词汇,如React、Redux和Next.js来实现,这些都是与 React 生态系统相关的,进一步强化了我们的个人资料,因为很明显,我们是一个值得检查的候选人,因为我们使用他们在职位描述中最可能拥有的工具和技术。
接下来的策略应该是建立一个能够提升我们的求职过程并使我们更具可雇佣性的流程。这可以包括以下事项:
-
拥有 GitHub 个人资料
-
创建个人网站作品集
这在创意和技术领域尤为重要,雇主经常需要你技能的具体证据。我们将在以下方面提供建议:
-
选择项目
-
完善你的个人资料
-
建立一个有趣的个人作品集
通过这些信息,我们可以展示我们的技术专长,同时捕捉到我们个性的精髓,这对于突出我们最好的工作,使我们成为一个突出的候选人非常有帮助。
最后,我们提供无价的面试建议,帮助你成功。我们涵盖了所有内容,不留任何石块未翻,以确保你为面试做好了充分的准备,从准备行为和技术问题到掌握沟通和谈判的艺术。
开始这段旅程可能会既令人兴奋又充满挑战,但如果你持有正确的态度并遵循本章中的建议,你就可以拥抱这个过程并取得成功。所以,做好准备,让我们开始你的面试准备之路。
在本章中,我们将涵盖以下主题:
-
准备你的简历和求职信
-
构建你的 GitHub 个人资料或网站作品集
-
寻找可以申请的工作
-
理解聚会和推荐的作用
-
探索面试技巧
准备你的简历和求职信
在本节中,我们将介绍如何创建一份优秀的简历和求职信的过程,这对于你作为 React 开发者在求职过程中想要在面试阶段取得最佳效果至关重要。这是一个可以实质性地影响你是否会发现自己处于一个不断有工作机会涌入的位置的领域。拥有一份优秀的简历和同样优秀的求职信是准备全面的第一步——换句话说,就是为你可能遇到的任何挑战性结果做好准备。第一步始终是说服招聘经理你是一个值得列入候选人名单的候选人。
区分简历和求职信
现在,我们将讨论简历和求职信之间的区别,让我们开始吧。本质上,简历是一种 Word 或 PDF 文档,基本上展示了一个人的工作历史。这就是你将展示你截至最近日期的所有职业经验的地方。简历可以涵盖你的兴趣领域,如教育、工作经验、成就和技能。拥有简历的初衷主要是向试图雇佣你的公司展示你的知识和资格,希望你的个人资料足够强大,能让你获得面试机会。第一步始终是说服招聘经理你是一个值得列入候选人名单的候选人。
另一方面,求职信是篇幅不超过一页的文件。它们的主要目的是伴随简历使用,并且通常与简历同时提交以申请某个职位。求职信不如简历正式,这意味着你可以在这里用词更加表达自己。这是你向公司介绍自己的机会,并解释为什么你是最值得他们认真考虑雇佣的候选人。我们应该利用求职信来突出我们的最佳技能和经验,目标是最终说服招聘经理和公司,我们的简历和个性使我们成为潜在的文化契合者,并且我们有资格获得面试机会,至少这样我们可以证明自己。你可以在图 1.1中找到一个优秀的求职信示例:

图 1.1:求职信
优秀简历和求职信的重要性
现在我们来讨论拥有良好简历和求职信的重要性,以及为什么它们对于求职者来说是必不可少的。相信你只需一个通用的简历就能应付一切,认为这不是一个高优先级的事项,而且你的时间应该花在求职过程的其它领域,比如做工作申请,这实在是太容易了。我们将进一步分析,以便突出拥有突出简历和求职信如何对你有利。
为了做到这一点,我们将探讨留下良好第一印象的重要性,学习如何通过筛选流程,了解哪些定制化需要关注,学习如何推销自己的品牌,以及学习如何训练自己以自信的姿态进入面试。
如何留下良好第一印象
当人们第一次见到我们时,他们实际上对我们知之甚少。这本质上给了我们一块空白画布,我们可以在上面真正地向新人推销自己,并说服他们我们是有价值且值得了解的。简历和求职信基本上是类似向新人打招呼或与想要了解的新人握手的那种介绍性文件。通过撰写良好的文件,我们能够将自己与其他候选人区分开来,并希望将自己置于候选名单的顶端。
根据我的经验,你需要尽可能地友好,带着魅力和尽可能多的信念与积极性去参加面试。有时候,如果你因为任何原因在那天感到情绪低落或不好,那可能会很困难,但你必须找到克服它的方法。我曾经在感觉不是最好的时候去参加面试,这反而影响了面试。同样,我也曾因为那天心态正确,知道要说什么,而面试进行得很顺利。对我来说,一个有效的方法是在面试前听音乐或冥想。感到放松或充满挑战的热情真的会给你提供在那天展示最佳自我的力量。
通过筛选流程
这是我们的机会,向潜在的新雇主展示我们有多么优秀。简历和求职信被用来筛选新候选人,看看哪些人值得面试。如果我们提交的内容写得不好、过时、虚假,或者有太多的语法、拼写和其他不准确之处,那么我们几乎不可能进入面试阶段。在这些情况下,可能会遇到拒绝甚至被“幽灵”的情况,即你从未收到他们的回复。当然,这种情况可能有很多原因;我们只是刚刚讨论了几种情况。
我记得有一次,我的简历上有些拼写错误,我甚至没有注意到,也没有人指出。你可能认为 Word 的拼写检查器会显示所有错误,但事实并非如此。我有一些单词全部是大写字母,拼写检查器没有发现这些错误。这就是为什么你双倍和三倍检查你所写的内容是至关重要的。甚至让其他人帮你校对也是一个好选择。他们可能会发现你遗漏的东西。
定制你的简历和求职信
现在是我们学习定制化的时候了,了解定制简历和求职信如何能进一步增加你的就业机会。如果你为每个申请的职位都创建一个定制的简历和求职信,那么你可以展示你已经花时间阅读了他们的职位描述,并且能够展示你的当前技术栈与工作完美匹配。一个定制的个人资料总是会比通用的个人资料更突出。当然——为每个申请定制内容可能需要更长的时间,但如果这能带来几个面试机会,那么显然这是值得的。
这些年,我在这方面取得了很大的成功,我定制的简历比我在各个地方都用的通用简历带来了更多的面试机会。所以,假设我正在申请一个 React 角色。为了提高成功的可能性,我会尝试突出我所有的 React 经验。所以,我会添加链接到我在 GitHub 或一个活网站中创建的 React 项目。我也是一个技术作家,添加链接到我已经发布的关于 React 的文章将帮助我更加突出。
我还可以更进一步,对于每个相关角色,我都可以提及一些关于 React 的内容,例如——比如——“集成 Redux 存储并优化了应用程序的性能,使用户的加载时间更快。”加入诸如Next.js、Vercel、Netlify、AWS和其他技术的关键词将进一步提升我们的形象,因为它表明我们精通现代技术栈。
销售你的品牌
我们是品牌。这是一个在我们生活中行走时应该意识到的重要概念。每次我们遇到新的人,我们都有机会建立新的联系,这可能会为我们打开更多生活体验的大门。我们使用简历和求职信进行营销,因为我们本质上是在向新的人销售自己和我们的理念。这给了我们一个机会来突出我们的经验、生活成就和技术能力,这些在常规的求职申请中很难衡量。
这种认识让我将这种心态应用到生活的方方面面。无论是我在社交媒体上创建的内容,还是我发送给客户的电子邮件,都关乎拥有这种品牌和商业专业精神。如果你能证明自己是一个可信的人,人们更有可能相信你。
带着自信进入
当我们投入时间和精力制作一份精美的简历和求职信时,它对我们的自信大有裨益。你一开始就能展现出的自信越多,你在面试中表现良好的可能性就越高。当我们充满自信地进入某个场合,而不是感到消极和沮丧时,这确实会带来很大的不同。我们吸引我们向世界输出的东西。我们输出的正能量越多,反馈回来的正能量就越多。
我很难想起我心情低落、缺乏自信时在那里进行的成功面试。人们真的能感受到这种能量,这就是为什么在面试前保持正确的心理状态如此重要的原因。有时候,在经历了多次拒绝和被冷落的经验之后,我会感到情绪低落,这种情况我们每个人都会遇到。在这种情况下,最好暂时休息一下面试,这是我强迫自己做的。自信会随着时间的推移而回归。如果你像我一样感到疲惫不堪,那就休息一下。
在下一节中,我们将学习我们可以创建的不同类型的简历。每份工作都是不同的,因此了解我们可用的各种简历类型以及哪些最适合每项工作非常重要。
探索简历类型
选择一种最能突出你的才能和经验,并且与你所寻找的职位相关的简历格式至关重要。
简历有多种类型,如按时间顺序、功能性、组合、针对性、创意等。每一种都有其不同的用途。我们将学习它们之间的区别,以便在创建自己的简历时考虑这一点。
按时间顺序的简历
最受欢迎的简历格式是按时间顺序的简历。我们可以在图 1.2中看到一个此类简历的例子:

图 1.2:按时间顺序的简历
你从最近的工作开始,然后往回追溯;它以倒序时间顺序展示你的工作历史。这种方法展示了你的职业发展,并展示了你的才能是如何随着时间的推移而发展的。这种风格受到求职者的喜爱,也是我个人偏好的风格,因为它提供了一个简洁且易于理解的就业历史描述,使招聘经理能够快速评估资历和潜力。
一个普通的按时间顺序的简历将从候选人的联系信息开始,然后是专业概述或目标陈述,接着是详细的工作历史列表。每个职位都将记录标题、雇主、雇佣日期、主要职责和成就,以及在该职位期间获得的任何相关技能或认证。
对于有稳定工作历史和明确职业轨迹的候选人来说,按时间顺序的简历最有益,因为他们可以展示他们的发展和进步。然而,这种风格可能不适合那些几乎没有工作经验、长期失业或试图改变职业或角色的人。
功能性简历
与强调你的工作经验不同,功能性简历专注于你的才能和能力。我们可以在图 1.3中看到一个此类简历的例子:

图 1.3:功能性简历
它展示了你的成功和才能,那些在就业历史中存在空缺的人经常选择使用这种类型。
功能性简历仍然包括申请人的工作历史,但它以简化的风格呈现,不包含日期或职位名称。相反,重点在于与申请职位最相关的成就和才能。通常,这些能力会被分类成组,例如沟通、领导力、问题解决和技术能力。
在功能型简历的开头,可能包括一个简短的概述或目标陈述,强调申请人的顶级资质。简历的最后部分也可能包括有关教育和其他相关资历的信息。尽管功能型简历可以有效地突出候选人的能力和成就,但一些招聘经理可能会对此风格持谨慎态度,因为它可能被视为试图掩盖缺乏经验或就业空缺的努力。因此,确保功能型简历针对具体的职位需求进行定制,并以开放和诚实的的方式进行展示至关重要。
组合简历
这种简历格式结合了功能型和按时间顺序简历的方面。简历包括您的工作历史部分,同时突出您的成就和才能。对于拥有多样化工作历史的求职者,这种风格特别有帮助,因为它同时突出了他们的先前经验和才能。
我们可以在图 1.4中看到这种简历的例子:

图 1.4:组合简历
在组合简历中,候选人的就业历史以倒序时间顺序列出,但用对最显著成就和职责的总结代替了每个职位的详细描述。这使得申请人能够在不过度详细的情况下展示他们的资质。
当候选人在特定技能类别中概述他们的主要才能和成就时,工作历史部分之后会包含一个技能部分。技术能力、语言能力、项目管理技能和其他才能可以包含在这个与申请职位相关的区域中。
组合简历也可能以一个总结或目标陈述开始,强调申请人的显著成就和职业目标,并且它可能以申请人的教育和其他相关资历信息结束。
目标简历
目标简历是一种针对特定职位或行业定制的简历。它强调与您申请的职位最相关的知识和专业技能。
我们可以在图 1.5中看到这种简历的例子:

图 1.5:目标简历
对于目标职位申请人来说,通常在根据职位的独特标准定制简历之前,会仔细阅读职位描述。为了突出申请人的相关经验和才能,并展示他们如何满足特定的职位标准,有必要对简历的内容和关键词进行修改。
一种有针对性的简历风格可以从概述或目标陈述开始,突出申请人的强大教育背景、培训经验和成就,以及证明他们适合该职位的成就。尽管候选人的工作历史和教育背景也将被包括在内,但重点将放在突出他们最相关的资格,这些资格与职位描述相匹配。
通常,有针对性的简历是一种突出候选人技能并提高他们获得面试机会的好方法。它显示了申请人对职位的兴趣以及他们根据工作要求定制申请的能力,这对于招聘经理来说可能是一个积极的信号。
创意简历
使用创意简历的优势在于它旨在突出你的个性和创新性。为了脱颖而出,它可能使用非传统形式或结合视觉设计元素。创意简历往往是简历中最具表现力的形式,你可以利用你的设计技能真正让你与众不同。这些可以使用像 Photoshop、Illustrator、Figma 和 Canva 这样的设计工具创建。还有许多在线和模板网站可以让我们创建这些类型的创意简历。
在创意简历中,可以以各种格式包含信息图表、图表、照片和其他设计元素。候选人经常在需要视觉作品集的创意职业中使用它们,如平面设计、市场营销或广告。
除了常规简历外,还可以提交一份创意简历,以更全面和美观地总结申请人的经验和资历。最终,创意简历可以成为展示候选人设计能力和原创性的强大工具,同时给潜在雇主留下深刻印象。但确保简历仍然正式、易于阅读,并且准确简洁地传达候选人的资历和专长是至关重要的。
接下来,在下一节中,让我们找出优秀简历的关键要素。知道如何制作简历是一回事,但如果我们真的想让简历脱颖而出,那么让它满足我们的需求是非常重要的。
优秀简历的关键要素
没有一份简历是同等重要的,尽管简历相似是很常见的情况,但我们完全有能力制作一份能够给我们带来优势的简历。现在我们将学习一些关键元素,这些元素可以帮助我们使简历更加个性化。
首先,我们的简历格式良好且易于阅读非常重要。显然,你不应该有任何拼写或语法错误,因为这可能会损害你的形象和信誉。它必须看起来专业。项目符号通常是描述每个工作角色的最有效方式。只需将它们分解成要点,并描述你在该角色中所做的工作以及它如何对业务产生积极影响。避免写段落,因为它们会使你的简历过长,而且招聘经理更有可能避免它们,因为它们会减慢他们的筛选过程。这是我在与许多招聘经理和招聘人员交谈后得到的建议。总的来说,在这种情况下,三到四个项目符号通常表现最好。
在大多数情况下,我们将按倒序时间顺序列出我们的工作经验。因此,最近的角色在顶部,过去的角色在下面。保持我们的简历更新,并尽可能减少空缺,这将有助于我们长期发展。为了展示我们的才能和能力,给出我们成功的特定例子是一个好主意。为了衡量我们的成就,一个可能的解决方案是使用数字和百分比来展示我们在那里工作期间所取得的成果。在内容中加入与 React 相关的内容是至关重要的,因为毕竟我们是在申请 React 职位。所以,你能在经验部分放入更多与 React 相关的经验就越好。在一个例子中,你可以展示你的经验是如何进步的。比如说,你在一个使用旧有类语法的代码库上工作,然后你将其更新为使用最新的 Hooks 语法。这表明你能够处理旧代码库,并且熟悉最新的语法,这些是程序员拥有的优秀特质:解决问题的能力和适应不同情况的能力。
在教育部分,应该展示我们的资格。因此,我们应该在那里列出我们持有的任何大学或学院学位。提及任何认证、课程和培训可以进一步增加我们的个人资料分量。当谈到技能部分时,最好包括硬技能和软技能。硬技能包括与编程相关的领域,如 JavaScript、React 和 Node.js。另一方面,软技能包括沟通、团队合作、主动性等领域。
最后,确保你的简历能够有效地针对该职位,并使用关键词来突出你的技能和经验,这些技能和经验与你要申请的职位相关。许多公司使用应聘者跟踪系统(ATS)简历扫描工具来寻找候选人。所以,如果你能加入像 HTML、CSS 和 JavaScript 这样的关键词,这些关键词也可能出现在他们的职位描述中,那么你就有机会收到招聘经理的消息。我尽量在我的求职信中使用尽可能多的关键词,因为永远不知道——这可能会带来巨大的差异,并决定你是否是那些被列入候选人名单的人之一。
因此,我们刚刚学习了优秀简历的关键要素。下一步将是同样为求职信做同样的事情。
一封优秀求职信的关键要素
求职信让我们能够以一种更加非正式的方式表达自己,这在简历中通常是不可能的,因为简历需要更加正式。让我们探讨一些撰写优秀求职信的领域。这里的目的是要有良好的互动,所以要有强有力的有趣的开场白,简要地说明你是谁,你能做什么,以及为什么你想申请那家公司的职位。别忘了提到你在职位描述中看到的公司名称和职位名称。
开门见山,突出你的最佳技能和经验。尽可能多地使用魅力来吸引阅读你求职信的人,并尽量以轻松的方式写作。所有这些都将表明你非常适合这个职位。利用这个时间让它尽可能个性化。通过这种方式,我们可以通过展示大量的热情和动力来展示我们对公司的深入了解。
在解决所有这些问题之后,我们应该准备好通过请求面试或表明你愿意分享更多细节来结束求职信。有了这些要点,我们可以通过添加这些重要方面来撰写一封强大且成功的求职信。当我写求职信时,我的目标是使它们引人入胜、吸引人并具有社交性。我努力尝试吸引读者,让他们能够想象与我一起工作的情景。你可能在世界上拥有最好的编程技能,但迟早你将不得不与人互动,因此这也是一个需要改进的领域。硬技能和软技能的良好结合是最理想的。
与简历相比,求职信给了我们更多的创意自由,简历在结构上要严格得多。总的来说,它就像写一封信或一封电子邮件,向潜在的新公司推销我们的技能、品牌和个性。
通常,我们可以涵盖以下话题:
-
一段介绍
-
我们如何使我们的技能与职位描述相匹配
-
我们从自己的研究中了解到的公司信息
-
一个行动号召(CTA),表明我们渴望接受面试并期待回复
这些是在撰写求职信时应包含的一些内容示例。快速进行一次谷歌搜索以获取求职信模板,可以找到许多示例和不同的撰写方式。本质上,你所做的一切就是写一封信来说明你为什么是这个职位的合适人选,这通常不需要模板;我们基本上可以用一篇好的写作来应付。然而,如果你想使用模板,那么如果你认为它对你有效,那么请随意使用。
我们取得了非常好的进展,所以让我们继续我们的旅程。我们可以进一步改进我们的求职过程,而这只需要我们深入查看我们看到的职位描述。在下一节中,我们将了解为什么彻底检查职位描述是一个好主意。
检查职位描述
我们在阅读职位描述方面越擅长,我们申请最合适职位的可能性就越大。在本节中,我们将介绍一些关键步骤,这些步骤将使我们在寻找下一份工作时生活变得更加容易。
当你搜索并找到一份职位描述时,请花些时间从头到尾仔细阅读。职位描述就像一个项目的概要。仔细阅读以确保你确切地知道需要什么,不要遗漏任何内容。寻找提及资格、教育、技能、经验等方面的区域。你并不总是需要满足所有要求。事实上,人们即使不具备所有技能和经验也能被雇佣,这对我来说已经发生了好几次。职位描述就像指南;很多时候,获胜的候选人并不具备所有要求,但他们完全有能力在工作中或空闲时间学习。留意那些 ATS 关键词,因为找到的越多,你得到面试的机会就越大。
我们还需要密切关注的其他领域是工作、地点和薪酬。这些因素与工作与生活的平衡有关,如果我们想要保持健康的心理状态,那么它们需要认真考虑。这些因素越好,我们能够在工作中表现良好的可能性就越高。你可以通过仔细审查职位描述并调整你的申请以适应个别职位和组织来最大化你接到面试电话并被雇佣的机会。
我们必须利用我们所能利用的每一项资产来寻找工作,这就是为什么我们必须强调识别我们的关键技能和成就。现在让我们看看这如何成为提升我们形象的一个好方法。
识别关键技能和成就
我们应该在“工作历史”、“工作经验”或类似标题的章节下创建一份我们曾经担任的工作列表。这个章节应包括完成的志愿工作和我们获得的学位。目标是确定我们在每个职位上应用的能力和产生的影响。我们可以问自己一些问题,比如我们如何提高生产率或业绩,以及我们是否按时并在预算内完成了项目。在我们的简历上列出这些成就以及我们能够想到的其他成就至关重要。任何能让我们显得可信和成为理想候选人的事情都值得提及。在我早期,我列出了我能想到的所有经历,因为这可能与我所申请的工作有些关联,例如——比如——如果你在零售业担任客户服务职位,这可能与你现在申请的职位完全不同,但它证明了你可以谈判并与客户交谈。
制作简历并不困难;然而,很容易犯一些你可能甚至没有意识到的错误,除非有人指出。现在我们将了解一些常见的简历错误以及如何避免它们。
避免常见的简历错误
我们必须注意的可能是拼写和语法错误。一定要多次校对你的简历,因为多次审阅可以提高你不会错过重要内容的几率。使用拼写检查器,如果你能使用像 Grammarly 这样的工具那就更好了,这是一个非常有效和专业的文案工具。
删除非必要信息也会对我们有利,例如介绍段落,解释我们是谁以及我们在寻找什么。这部分应该放在求职信中,而不是简历里。任何关于我们个人爱好章节也可以删除,因为它对于这份工作不是必需的。如果他们想知道你在业余时间做什么,他们可以在面试时询问,在这种情况下,这样做更为合适,尤其是如果这是一次文化适应性面试。
我知道这可能是显而易见的,但仍然,有些人会跳过这一步或者根本不重视。我指的是研究你发送简历的公司。我们喜欢专注于回答面试问题,因为,说实话,我们可以做数百份工作申请,没有人会了解每一家公司。我们的主要目标是找到一份工作,这就是为什么我们把简历发送到各个地方。尽管如此,如果那家公司邀请我们面试,至少,我们应该对他们有所了解。
在下一节中,我们将看看为什么这是至关重要的。
研究公司
去公司的网站进行调研是必须的。不要犯只带着通用的面试问题和手头知识去面试的错误。面试官询问你对公司的了解是很常见的,所以不要期望他们会问与你相关的所有 React 编码问题,因为你永远不知道那天会发生什么。
了解他们所做的工作、他们的客户、他们的技术栈和他们的历史——基本上,所有这些——并且你可以使用搜索引擎,如 Google,来了解更多关于他们的信息。我多年来犯过很多这样的错误。我记得有一次面试,其中一位面试官问我关于公司的了解,我试图避免回答,因为我一无所知。我对于任何编程相关的问题都准备得很充分,因为那是我想要改正的弱点。我没有认为花时间去深入了解公司是值得的,因为——让我们说实话——第一阶段面试可能不会有什么结果,然后你会觉得自己浪费了时间。然而,你必须做好万无一失的准备,以防万一你被问到,而你无法给出答案,这会让你从候选名单中被淘汰。
我们已经讨论了简历中的错误,所以现在,我们将对求职信也进行同样的分析,因为这些同样重要。那么,接下来是下一部分。
需要避免的常见求职信错误
编写一封出色的求职信是提升我们的职位描述到另一个层次的好方法,然而,这也是许多人往往因为不花时间做好而让自己失望的领域。它不应被视为简历的附加选项,而应被视为应与简历一起提交的文件。它们就像是同一枚硬币的两面,相辅相成。
我们可能犯的最大错误之一就是使用通用的模板。使用通用的求职信模板可能会给人留下我们没有花时间针对特定职位定制申请的印象。相反,调整你的求职信以适应你正在申请的职位;这就是应该始终这样做的方式。
另一个需要注意的领域是重复。我们的求职信应该补充,而不是重复我们的简历。利用你的求职信来展示你独特的例子和才能,这些才能让你成为该职位的最佳候选人。强调你能为公司带来的好处,而不是你想要得到什么。成为一个给予者远比成为一个索取者要好,因为给予者提供价值,这是值得拥有的品质。确定在写作时使用哪种语调可能也是一个挑战。努力匹配公司的风格;在正式和非正式之间找到合适的平衡是很重要的。
我们正在学习很多关于简历和求职信的知识,这将在我们申请那些 React 职位时大有裨益。现在是时候解决 GitHub 和作品集的问题了。我们的简历和求职信可以让我们出现在招聘经理面前,但最终,我们拥有的项目和技能很可能使我们获得面试机会。在接下来的部分,我们将了解 GitHub 个人资料和作品集,以及为什么我们应该利用它们。
建立你的 GitHub 个人资料或网站作品集
我们将探讨建立 GitHub 个人资料的感觉以及这如何使你在面试筛选过程中脱颖而出。同样,拥有一个网站作品集同样重要,因为这确实是一个让你能够让自己独特并成为值得雇佣的突出候选人的领域。你需要意识到,就业市场竞争非常激烈,这意味着我们必须尽一切可能来提高我们成为公司认为最适合该职位的候选人的机会。
这些年,我成功仅凭我的作品集网站获得了面试机会,这得益于其设计、内容和多年来我所完成的工作细节。在招聘经理看过十几份简历和求职信之后,我们的简历和求职信在他们眼中可能看起来几乎一模一样。然而,拥有一个定制的作品集和 GitHub 主页可以成为亮点。这对我的成功起到了作用,同样也可以对你产生同样的效果。
建立 GitHub 个人资料或网站作品集寻找工作的好处
创建网站作品集或 GitHub 个人资料可能帮助你找到工作,并在招聘过程中脱颖而出。这些不仅突出了你的能力,还显示了你可以与他人合作,有效使用工具,并处理挑战性情况。此外,它们为潜在雇主提供了一个更全面的关于你的编码经验和技能的概述。制作一个引人注目的个人资料或作品集可能在区分竞争者方面至关重要。
如何让你的 GitHub 个人资料或网站作品集脱颖而出
在申请工作时,制作网站作品集或 GitHub 个人资料是一种很好的方法,可以让自己在竞争中脱颖而出。虽然这可能需要一些时间,但最终的努力是值得的。你可以通过投入时间来扩展你的技能集,分享你正在工作的项目,并与其他开发者建立联系,向潜在雇主展示你对加入团队的认真态度。此外,你可以利用这些网站来突出你的最佳作品,并给出令人信服的理由,说明为什么他们应该雇佣你。向他们展示你的实力,因为在当今的就业市场中,网络存在感有着重要的影响。
我们学习了一些关于如何让你的 GitHub 个人资料和网站作品集脱颖而出的技巧,现在,下一个主题将帮助我们深入了解我们应该在上面放置的内容类型。
你应该在作品集中包含哪些内容
在寻找工作时,你可以做的最重要的事情之一就是建立一个作品集。但我们应该包含哪些材料呢?首先,包括你的资格和任何使你成为最佳候选人的先前工作经验。添加任何你创建的项目或网站,以展示你的技术专长和对该行业的理解。更进一步,提供每个项目的概述,以便未来的雇主可以立即看到你对项目完成的贡献。例如,你是否创建了任何图形或 JavaScript 应用?展示你对某些工具和技术熟悉性的文档也是一个好主意,比如软件许可证或证书。将这些项目包含在你的个人资料和网站上。
创新作品集作品的例子以及它们如何帮助你获得工作
一个坚实的 GitHub 个人资料和网站作品集可能是吸引面试官并尝试获得理想工作的绝佳方法。所以,为什么不通过一个创新的个人资料让你的申请从竞争中脱颖而出呢?我们可以通过提供我们如何通过代码突破常规的例子来与竞争对手区分开来。面试官可以看到,即使是从被放弃或从未使用的倡议中,我们也有独特的主意。我们不应该低估展示非常规解决方案的影响,即使它们没有被使用。雇主会对我们的勤奋和创造力印象深刻。
例如,众所周知,每个人在尝试展示自己的技能时都会尝试构建一个待办事项应用。因为它没有让我们彼此之间有任何区别,所以它已经变得非常普遍。当候选人几乎都是彼此的复制品时,很难在他们之间做出选择。在我看来,一个更好的解决方案是在你的 GitHub 或作品集中展示你的项目进展。
因此,你可以从一个基本的 React 应用开始,这个应用可能包含一些简单的业务逻辑——比如,可能是一个计算器——然后,你可以创建一个更高级的 React 应用,它可以拥有多个功能,比如拥有一些 CRUD 功能,或者如果我们正在构建电子商务应用,可以有一个商店。之后,我们可以更进一步,也许添加一个带有登录和登出的认证层,GraphQL,以及各种微服务相互交互。这显示了随着你的技能不断发展和项目变得更加复杂,你的技能进步是清晰的。
现在,带着这些理解,让我们来看看使用第三方工具构建网站与 DIY 方法相比的一些优缺点。
使用第三方作品集构建网站与 DIY 的优缺点
在创建在线作品集时,在自行创建内容和使用第三方网站之间做出选择可能会很具挑战性。无需担心编码或设计能力即可展示你的作品是使用第三方网站的一个好处。Wix、Squarespace 和 WordPress 等几个第三方网站还允许用户展示他们的技能和专业知识,同时为他们提供如何优化个人资料的指导。然而,由于用户可以完全控制他们网站的布局和功能,选择 DIY 路线在构建作品集时给我们提供了更多的自由。在创建作品集时权衡你的选择时,最终应仔细考虑时尚或可定制性。
让我们现在看看优缺点的总结,看看有什么区别。
总结优缺点
首先,我们将探讨第三方构建工具的优缺点。让我们开始吧。
这里是优点:
-
提供预制的自定义模板
-
无代码工具需要很少的技术知识
-
不需要设计经验
-
快速部署,因为它是现成的
-
快速设置
-
由于使用内容管理系统(CMS),易于更新
-
技术支持和服务
这里是缺点:
-
缺乏定制
-
可能需要为托管和服务付费
-
使用第三方工具意味着你无法展示你的编程技能
现在,我们将探讨 DIY 自定义方法的优缺点。让我们开始吧。
这里是优点:
-
完全定制
-
能够展示你的编程技能
-
免费或付费托管选项
这里是缺点:
-
如果没有设计背景,可能看起来不像你希望的那样好
-
根据复杂程度,构建可能需要更长的时间
-
没有技术支持——你负责一切
好吧——我们学到了很多关于 GitHub 和创建作品集的知识。在接下来的这一节中,我们将学习更多关于寻找申请的工作。有无数种方法可以做到这一点,所以让我们继续阅读,看看我们如何做到这一点。
寻找申请的工作
在本节中,我们将详细介绍在招聘板上寻找工作的最佳方法以及如何使用 LinkedIn 等网络进行操作。你会意识到我们面前已经有了所有需要的工具,我们需要学习如何最好地将它们应用到我们的求职过程中。
理解你的职业目标并针对特定的职位发布
找工作并不是唯一需要考虑的因素。通过了解你的职业目标和针对特定的职位空缺进行申请,你可以缩小你的搜索范围,找到最适合你的工作。LinkedIn 是一个极好的资源,可以帮助你了解当前有哪些机会,以及如何将这些机会与你的抱负相匹配。在这方面,当提供就业详情时,你将完全清楚这份工作与你的整体职业目标有多契合。根据我的经验,在平台上保持活跃并与求职者和招聘经理互动,可以带来更多的成功,这也使得更多的人联系到我。
利用职位板搜索相关机会
在在线职位板上寻找职位空缺是开始求职过程的最佳方式。由于有大量的职位发布,从在线实习到任何领域的初级职位都有,我们可以利用这些板来发现相关的机会,这些机会将帮助我们更接近我们的职业目标。在深入申请和招聘世界之前,确保你了解如何使用职位板及其所有服务;这将有助于确保在探索职位机会并专注于你想要的东西时,追求特定的就业市场不会过于令人不知所措。我喜欢为职位搜索参数设置电子邮件提醒,这样我就能在我的收件箱中收到最新的职位信息。
线上线下建立人脉
好的一点是,你既有线下也有线上的网络选择,这在找工作时至关重要。参加招聘会和研讨会或与行业内的人士交流都是线下网络建立的方式。通过使用如 LinkedIn 这样的社交媒体网站,我们也可以进行线上网络建立。加入职业网络是与你所在领域的招聘人员和可能的雇主建立联系的好方法。通过有效地使用这些工具,你可以与合适的人建立联系,并获取你所在行业的最新职位信息。为了提高成功的机会,不要害怕寻找新的联系并扩大你的职业网络。我参加活动来扩大我的网络,现在有如此多的聚会可以去。我发现,对线上线下机会持开放态度会带来最大的益处。
职业博览会和专业聚会是开始你的求职之旅或了解更多可能适合你的职业类型的好地方。通过利用这些机会,你可以结识招聘经理、有影响力的人以及其他可以帮助你有效建立人脉的人。你可以了解各种职业道路,扩大你在所选领域的专业知识,它们还为你提供了了解公司和行业趋势最新信息的途径。此外,参加职业博览会和研讨会可能为你提供类似会议的经历,在那里你可以学到实用的技能,使你更具竞争力——这是一个脱颖而出的绝佳方式。利用所有机会参加附近的职业博览会或专业聚会。
研究公司和了解他们的当前职位空缺
寻找可能的就业机会需要研究潜在的雇主。一旦你列出了一份潜在公司的清单,你可以进一步研究有关公司文化、职位空缺和其他因素的信息。LinkedIn 是进行公司研究的一个绝佳工具;除了找到全面的公司信息外,你还可以与该行业的招聘经理和现有员工建立联系。通过进行研究,你可以在提交职位申请时选择你想要集中时间和精力的雇主。
使用 Indeed 等在线资源研究薪资及其他信息
寻找工作不一定是猜谜游戏。借助像 Glassdoor 这样的职位板,你可以获取有关潜在就业机会的关键信息,例如该职位的典型工资,以及关键的企业信息。通过了解你所在领域的类似薪资,你可以获得谈判优势并确保你得到有竞争力的薪酬。通过研究公司的历史和前员工的评价,你可以获得关于公司好、坏以及介于两者之间的真实看法。一旦你获得了信息,就没有理由你不能得到你想要的职位或薪酬。
在本节中,我们了解了网络的重要性。在下一节中,我们将进一步探讨,尝试理解聚会和推荐的作用以及它们如何帮助我们。
理解聚会和推荐的作用
我们将探讨聚会和推荐的话题,以及了解正确的人如何帮助你进入新角色。你的力量与你的网络一样强大,因此,尽可能与人建立联系和关系非常重要,因为有一天,他们可能会以你无法想象的方式帮助你。与同行开发者、招聘经理、首席执行官、求职者、会计师和招聘人员建立联系。所有这些人都是进入该行业的潜在途径。现在,让我们更深入地了解这个主题。首先,让我们从聚会开始。
聚会是什么以及为什么对求职者来说很重要
聚会是有计划的集会,将具有相似兴趣的人聚集在一起,如求职者、企业家或学者。这些聚会为参与者提供了极好的机会来建立联系、相互学习、分享概念和策略。它们还提供了与该领域的知识渊博的专家建立有益联系和获得见解的无价机会。当我们寻找可能引导我们到下一个职位的推荐时,聚会对我们来说是一个无价的资源。尽管有这种即时的优势,但聚会提供了一种磨练专业技能、增强自信和获得通过传统求职渠道无法获得的见解的方法。
如何找到适合您的聚会团体的小贴士
要开始寻找理想职业,首先要找到适合您的聚会团体。通过事先进行研究和规划,您可以节省时间和压力。首先了解您所在地区哪些俱乐部专注于您感兴趣的话题或您希望提高的技能。确保该团体的目标与您的目标一致,任何来自前参与者的反馈也可以帮助简化决策。您不希望陷入在活动中间无法自拔的困境。努力向朋友或熟人寻求推荐,以便您能加入最适合您需求的聚会。
参加聚会的好处
找工作可能很困难,但通过参加聚会和使用参考资料,你可以使它变得更简单。聚会为你提供了与领域内的专家建立联系和建立可能带来工作机会推荐的有意义联系的优秀机会。这不仅有助于建立持久的关系,也扩大了你的个人网络,提高了你找到理想工作的机会。通过会议认识与你兴趣或爱好相同的企业人士是一种快速简单的方法,可以在求职中给你带来优势。
在我的职业生涯中帮助我的另一个部分实际上是参加这些聚会。假设你在 React 的一个部分很擅长;如果你能参与聚会甚至教授他人,这有助于提升你的形象。潜在的招聘人员和未来的同事可能就在聚会上。
如何从聚会中获得最大收益
在寻找职业时,聚会可能是一个进入门槛的好方法。你可能会遇到可以介绍你认识其他人或为你提供雇主接触的人。采取主动,积极发展人际关系,以充分利用每一次相遇。进行有意义的对话,展现一个充满活力、热情的群体成员形象。此外,不要害怕在社交媒体上与喜欢的行业领袖互动;这样做可能会让你了解他们可能招聘的职位类型以及你可以如何帮助他们。
我已经无数次这样做过,而且是无意的。我所做的就是与志同道合的人在社交媒体上互动,正是这种坚持不懈让我在 X(以前称为 Twitter)上收到了来自想要面试我担任角色的公司 CEO 的私信。在职业网络方面,永远不要浪费时间;利用任何出现的机遇。
现在我们已经足够了解聚会及其重要性,让我们来看看推荐。
获得工作中推荐的重要性
在现代世界中,推荐是寻找就业的最重要资源之一。这是因为来自现有员工或朋友的推荐可以让你了解公司的内部运作,包括新的职位列表和职业发展机会。通过参加会议和其他网络活动,你可以确保你的名字被列入任何内部和外部行业的机会。这让你在那些没有那么多赞美他们的证词的竞争对手之上,给你更多的机会。当谈到掌控自己的职业轨迹时,利用推荐应该是一个首要任务。
获得推荐的建议
获得理想工作的一个绝佳策略是请求推荐。尽管这可能看起来令人害怕,但并不需要这样。从你现有的网络中的人开始,比如朋友或熟人。选择一个了解你的资格和网络的至关重要的人。然后,询问他们是否知道任何符合你资格的空缺或职位。考虑参加当地的专业会议,这也是另一个网络和了解当地就业机会的机会。在这些活动中,抓住机会结识一些终身有意义的联系,因为你永远不知道你可能会遇到谁。最重要的是,在请求推荐时要坚持不懈,保持专注,保持热情。
好的——我们做得很好,到目前为止我们所学的所有东西都提高了我们作为 React 开发者的形象。在下一节中,我想介绍一些非常有用的面试技巧,这些技巧可以进一步提升我们。继续阅读,看看我们还能做些什么来增强我们的存在感。
探索面试技巧
在本节中,我们将讨论一些优秀的面试技巧以及如何将它们融入我们的求职日常中。提前做好良好的规划至关重要,这样我们才能充分利用那些最终会降临在我们身上的工作机会。
申请工作可能是一件令人紧张的事情,尤其是如果你不知道期望什么。确保成功最好的方法是以正确的态度和理解最佳面试方法做好准备。让我们通过一些有助于我们通过下一次工作面试的要点来探讨。
准备可能的技术面试
毫无疑问,这是最重要的一步,可能是你接近工作机会或被拒绝并从下一轮面试的潜在候选人名单中被移除的区别。技术面试阶段是你最终向新潜在雇主展示你能做什么以及你是否是一位优秀的程序员的时候。通常,技术面试可以有不同的形式。
例如,它们可能是以下任何一种:
-
你与其他开发者就编程话题的对话
-
与其他开发者一起进行配对编程代码练习
-
需要在几天内完成的带回家做的代码作业
-
数据结构和算法在线考试
-
编程的多选题在线问卷
在我的职业生涯中,我遇到过所有这些情况。如果你非常幸运,你可能不需要做任何非常技术性的工作,因为可能雇佣你的公司不相信进行这些类型的代码评估。这是一个可能性,我也面试过认为这样做的公司,但这并不总是有保证。所以,无论怎样,都要做好充分的准备。在准备方面,我会尽我所能地工作在我的技术栈上——也就是说,尽可能多地编写代码并构建应用程序。
例如,你可能会申请前端或全栈工作。所以,在这些情况下,我所做的是仅仅练习构建创建、读取、更新、删除(CRUD)应用程序,这些应用程序连接到具有 React 前端的数据库,然后更进一步,集成第三方库,如 Day.js,用于日期转换,或图表库如 recharts.js,用于数据可视化。这些是实际项目中使用的工具,所以现在学习它们会让你为可能需要使用它们的带回家做的代码挑战做好准备。
完成这一步将增强你对技术栈编程技能的信心。你还需要努力提升的另一个领域是数据结构和算法。你可以在 Udemy 上找到相关课程,或者你也可以在其他地方学习这个主题。接下来,你需要找到时间在网上通过像 LeetCode 这样的编码平台进行实践。这可能会很枯燥,但不幸的是,我们需要为任何情况做好准备。在一些招聘网站上,我看到过要求你填写你为代码测试做了多少准备的表格,这是你在提交简历时申请过程中的一个环节。我可以想象,如果你说你准备不足,那么你可能甚至都不会进入短名单。所以,这个故事的意义在于:让自己达到一个自信的程度,至少尝试去做一个代码测试。由于有很多不同的变量需要考虑,所以不可能做到 100%的准备。至少,我们应该做好充分的准备,以便能够尽力而为。
研究公司
在我们的面试之前,对我们要寻找的机构和职位进行研究至关重要。浏览公司的网站,阅读任何相关的新闻发布或文章。提前准备一份与您发现的相关材料相关的问题清单。这可以帮助潜在的雇主看到我们对行业的兴趣和专业知识。
作为求职者,我们可以通过研究我们所寻求的机构和职位来表现出对其的真正兴趣。这表明他们认真对待面试,并愿意投入时间和精力来准备。我发现,通过研究公司,我可以调整我的回答以适应个别职位和商业文化。这使我能够理解公司的信念、目标和使命,并据此调整我的回答。
我们应该关注的另一个重要领域是了解我们想要工作的公司整体表现以及其员工对公司的总体看法。我相信我们都在职业生涯的某个时刻经历过在一家有毒的工作环境中工作的糟糕公司的感受。并不是所有公司都会给你带来那种梦想般的体验,这就是为什么花时间进行研究是值得的。在研究公司时使用像 Glassdoor 这样的网站是正常的,这样我们可以看到反馈和评价是什么样的。阅读积极的评价时,你总是充满希望和兴奋;然而,对于负面的评价则不然。尽管我们都想找到那份完美的职位,有时甚至愿意为了得到一份工作而妥协,但我们都有权在工作中感到快乐。所以,事先做好研究。
练习你的问题和答案
一个优秀的面试技巧是在面试前练习回答问题,这样你在面试时回答问题时会感到更加自在和自信。考虑可能的问题,并和你的朋友或家人练习你的回答。如今,视频面试和电话面试都很常见。所以,也要找到一种方法来练习这一点。如果你没有人可以练习,你总是可以录下自己,回看视频,并努力改进。
我参加了一个实习课程,它教会了我如何更好地沟通。我们被教导要避免使用填充词和其他可能分散我们信息注意力的语言错误,如嗯和啊。更多的练习给我们提供了识别我们可能存在的任何潜在弱点的机会。
适当的着装
当谈到面试着装时,整洁、专业和良好的仪容至关重要。确保你的衣服干净、熨烫,并且适合你正在寻找的工作。即使是视频面试也是如此。即使角色是远程的,你也应该像专业人士一样着装。至少,为了保险起见,也要穿着得体。
适当的着装可以给人一种专业的印象,这可能会帮助我们建立良好的第一印象。以我为例,至少在面试前我会刮胡子或修剪胡须,以免看起来不整洁。理发或修剪头发也是推荐的。如今,我们很幸运,因为如果是视频面试或电话,那也还过得去。你可以稍微邋遢一点。不过,我个人不会冒这个风险去面对面的面试;可以喷一点香水或须后水,但不要太多以至于让人感到压抑。
保持自信
在面试中,态度至关重要。我努力保持对所寻求职位的良好态度和热情。在回应潜在雇主的查询时,确保创造良好的眼神交流,经常微笑,并给出清晰简洁的回答。这表明我们自信且有能力。自信可能会积极影响面试官,并提高我们获得工作的机会。此外,这也是改善面试中对话的绝佳方法。当我感到自信和准备充分时,我的面试往往会有更好的对话。虽然这并不总是立刻发生,但自信可以在面试中增强。我发现,当我能够正确回答问题并对其他问题给出良好回答时,这一点尤其正确。
面试后的跟进
记得在面试结束后向面试官表示感谢。我确保尽快这样做,并发送一封电子邮件表达我的感谢并重申我对该职位的兴趣。这可以帮助您与其他可能的候选人区分开来,并展示您对该职位的承诺。这表明我们作为候选人对该职位感兴趣,并准备好努力与雇主取得联系以跟进面试。这将给公司留下深刻印象,并使我们与其他可能没有跟进的申请人区分开来。
这也给了我们机会来突出我们的资格和对于该职位的热情。我们可以重申我们的资格和经验,并提醒雇主我们可能为公司带来的潜在利益。通过跟进,我们可以使就业过程的下一步更加清晰。这使我们能够展示我们对该职位的持续兴趣,并询问决策的时间表。
摘要
在本章中,我们讨论了面试准备的重要话题,以帮助您为工作面试做好准备。我们首先讨论了创建一份展示您经验和才能的坚实简历和求职信的重要性。然后,我们讨论了开发您的网站作品集或 GitHub 页面以突出您的工作和技术专长的重要性。之后,我们探讨了寻找工作的几种方法,例如利用 LinkedIn、参加会议和寻求推荐。
最后,我们提供了一些关于如何通过面试的建议,包括研究业务、准备对典型问题的回答,以及准备好提出自己的问题。通过花时间并遵循这些步骤来准备,你将大大提高获得梦想工作的机会。
好的——我们做得很好!让我们继续前进。在接下来的章节中,我们将学习理解 ReactJS 基础及其主要特性。因此,我们可以期待获得关于 JSX、虚拟 DOM、状态、类和函数组件以及许多与 React API 相关的更多概念。稍作休息,准备好学习更多高级主题。
第二部分:精通核心 React 技术面试
在这部分,您将学习 React.js 的核心概念和特性,以在 React.js 基础中建立坚实的基础。我们还将探讨如何使用 Hooks 在函数组件中使用状态和其他 React.js 特性。然后,我们将处理路由以在应用程序的屏幕之间导航,以及国际化以支持区域内的各种地区。最后,我们将学习一些高级概念,例如 portals、错误边界和并发渲染,以及它们如何在 React 应用程序中使用。
本部分包含以下章节:
-
第二章, 理解 React.js 的基本原理及其特性
-
第三章, 钩子 - 将状态和其他功能引入函数组件
-
第四章, 处理路由和国际化
-
第五章, React.js 的高级概念
第二章:理解 ReactJS 基础及其特性
网络开发是当今现代企业生成长期客户关系的关键要求,因为它提供了一个与客户互动的平台。然而,对于拥有大量用户交互的大型企业来说,构建可扩展和优化的网络应用具有挑战性。
JavaScript 是一种流行的网络开发编程语言。在过去几年中,它变得越来越受欢迎,并被用于创建各种应用,包括基于网络的、移动的、桌面和游戏应用。作为 JavaScript 进化的一个部分,一些库在考虑其可重用的用户界面(UI)的同时被创建出来,以加快开发速度,但它们未能通过重新渲染整个 UI(即使是任何小的变化)来提高其性能。这种情况通过引入基于可组合组件的 ReactJS 库得到了克服,该库仅在屏幕更新需要的地方重新渲染应用的具体部分。在我们深入 ReactJS 的特性之前,了解 React 的基础知识非常重要,这样您可以在面试中自信地面对,并为 React 开发者角色打下坚实的基础。
本章将为您提供 React 基础知识的完整知识,如类组件、函数组件、状态、props 和 JSX 作为起点。您必须理解重要特性,如虚拟 DOM 单向数据流、refs、上下文 API 和服务器端渲染(SSR),以便能够回答面试官提出的大部分问题。
在本章中,我们将涵盖以下主要主题:
-
ReactJS 的先决条件
-
ReactJS 简介
-
JSX 简介
-
使用元素和组件构建视图
-
使用 props 和 state 控制组件数据
-
理解 key prop 的重要性
-
学习事件处理
-
理解虚拟 DOM
-
单向数据流与双向数据流的区别
-
在 React 中访问 DOM 元素
-
使用上下文 API 全局管理状态
-
理解服务器端渲染
ReactJS 的先决条件
在学习 ReactJS 的基础和特性之前,您需要了解以下网络技术和主题。这些技术将作为学习 ReactJS 的强大基础:
-
HTML、CSS 和 JavaScript 的基本知识
-
ES6 特性的基础,如
let、const、箭头函数、类、导入和导出、扩展运算符、Promise 和析构赋值等 -
对包管理器如 npm 的基本理解
通过理解这些先决条件,您将清楚地了解网络开发的核心理念和 React 生态系统中常用的 ECMAScript 特性。
ReactJS 简介
ReactJS 在 JavaScript 社区中的流行源于其构建大规模应用程序的能力,这些应用程序的数据随时间不断变化。在面试过程的开始阶段,面试官期望您讨论 ReactJS 的简介、其特性、您选择它进行编程的原因、JSX 及其在 React 开发中的作用,以及更多内容。因此,让我们回答一些入门问题。
什么是 React?
React(也称为React.js或ReactJS)是一个开源的前端 JavaScript 库,用于构建 UI,特别是用于单页和移动应用程序。它用于处理网络和移动应用程序的视图层。
React 是由 Facebook 的软件工程师 Jordan Walke 创建的,后来由 Facebook 团队维护。React 首次于 2011 年在 Facebook 的新闻源上部署,并于 2012 年在 Instagram 上部署。
选择 React 进行编程的原因有哪些?
React 是当今前端库中流行的选择之一,用于网络开发,并且有多个原因。以下是一些最显著的原因:
-
快速:React 可以在保持快速和响应式 UI 的同时处理复杂的更新。
-
声明式:它遵循声明式方法,您只需编写所需的全部代码。然后,React 将负责执行所有 JavaScript/DOM 步骤以生成所需的输出。
-
高性能:由于使用了虚拟 DOM 策略,React 在 DOM 操作调用上成本较低,因此与其它 JavaScript 语言相比,性能更优。
-
SEO 友好型:React 允许您通过 SSR(服务器端渲染)功能创建对搜索引擎优化(SEO)友好的网络应用程序。此功能使应用程序加载速度更快,页面加载时间更短,渲染时间更快,从而与客户端渲染(CSR)相比,带来更好的搜索引擎排名。
-
跨平台:该库支持使用 React Native 构建网络应用程序、跨平台桌面应用程序,甚至移动应用程序。
-
易于测试:该库提供了一套全面的测试工具,通过在测试中模拟用户行为来轻松测试组件。
-
强大的社区支持:它拥有非常强大的社区支持,全球有数百万开发者可以访问或共享资源,如教程、文章、博客和 YouTube 视频,并在各种论坛和社区中讨论它们。
上述原因解释了为什么与其它框架或库相比,ReactJS 是一个流行的库。ReactJS 还有一些显著特性,为商业带来了诸多好处。
React 的主要特性有哪些?
React 具有一些卓越的特性,使其在现代前端技术中成为更优的选择。以下是一些主要特性:
-
它使用JavaScript XML(JSX)语法,这是 JavaScript 的一种语法扩展,允许开发者在 JavaScript 代码中编写 HTML,这使得代码易于理解和阅读。
-
它使用虚拟 DOM 而不是真实 DOM,因为真实 DOM 操作成本较高。传统的 JavaScript 框架会一次性更新整个 DOM,这使得 Web 应用变得相当缓慢。
-
它支持 SSR(服务器端渲染),与 CSR(客户端渲染)相比,它提供了快速的初始页面加载时间和 SEO 友好的应用。
-
它遵循单向数据流或单向数据绑定,其中数据从父级流向子级,但反之则不然。这有助于使应用更少出错,更容易调试,并提供了对数据的更多控制。
-
它使用可重用或可组合的 UI 组件以快速的速度开发视图,并遵循DRY(不要重复自己)原则,该原则指出逻辑重复应该被消除。
React 可以用JSX或 JavaScript 代码编写,但大多数开发者由于学习曲线的收益,在代码中使用 JSX。下一节将提供您在面试开始时最常被问到的问题的答案。
理解 JSX
建议您使用 JSX 与 React 来描述 Web 应用中 UI 应该呈现的样子。尽管在 React 中使用 JSX 不是强制性的,但它带来了许多优势,所有这些优势我们都会在这里介绍。
JSX 是什么?
JSX 是 JavaScript 语言的一种类似于 XML 的语法扩展,基于 ES6,这意味着您可以使用 HTML 之类的语法来结构化组件渲染。这意味着它只是语法糖,允许您在 JavaScript 内部编写 HTML 并将其放置在 DOM 中,而无需使用任何createElement()或appendChild()方法。
让我们通过一个简单的 JSX 代码片段的例子来更好地理解它是如何工作的:
const myElement = <h1>This is my first JSX code</h1>
ReactDOM.render(myElement, document.getElementById
('root'));
在渲染代码后,React 将<h1>标签内的内容输出到您的 DOM 中。
没有前导 JSX 代码的纯 JavaScript 代码片段如下所示:
const myElement = React.createElement('h1', {},
'This is my React element without JSX code!');
ReactDOM.render(myElement, document.getElementById
('root'));
注意
JSX 比 HTML 更严格,因此所有元素都应该有闭合标签。
为什么浏览器不能理解 JSX?
Web 浏览器只能读取 JavaScript 对象,但不能读取 JSX,因为没有浏览器引擎内置的实现来读取和理解该语法。要使用 JSX,您需要使用转译器或编译器,如 Babel,在运行时将 JSX 代码转换为相应的纯 JavaScript 代码:

图 2.1:JSX 转译
上述图表解释了 JSX 代码中的转译是如何工作的。
JSX 的优势是什么?
JSX 不是编写 React 应用的强制要求,但它提供了许多好处:
-
JSX 使得仅通过查看代码中的布局就能更容易地阅读和理解组件的结构。
-
大多数开发者发现,在编写 UI 模板代码时,将其作为视觉辅助工具非常有帮助。
-
它允许您创建可重复使用的组件,这些组件可以在整个应用程序中重复使用
-
它可以在 React 应用程序中显示有用的错误和警告消息
-
JSX 在将代码转换为常规 JavaScript 时进行优化,因此与编写常规 JavaScript 相比,它提高了性能
一旦您熟悉 JSX 代码,使用元素和组件构建视图就变得容易。
使用元素和组件构建视图
元素和组件是创建 React 视图或应用程序的基本构建块。使用隔离的、可定制的和可重复使用的组件来构建 UI 非常容易。以下问题和它们涵盖的内容将帮助您了解 ReactJS 的基础知识,并将作为本书下一组问题的基石。
什么是组件?
组件是一个独立的、可重复使用的代码块,它将 UI 分割成更小的部分。例如,在构建使用 React 库的 Web 应用程序的 UI 部分时,您可以将它的 UI 分割成小块以提高可重复使用性,如下图中用蓝色方框突出显示:

图 2.2:React 组件
每个部分都可以被视为一个组件,并且它们分别在一个单独的文件中表示,而不是在单个文件中构建 UI。这些组件可以在应用程序的任何适用位置重复使用。
注意
每个组件返回一些 HTML 代码,但您只能返回一个 HTML 元素。否则,JSX 将抛出一个错误,表示JSX 表达式必须有一个 父元素。
有哪些创建组件的不同方式?
在 ReactJS 中创建组件有两种可能的方式。让我们来看看它们:
-
将
props对象作为第一个参数,并返回 React 元素以渲染输出:function User({ message }) { return <h1>{`Hello, ${message}`}</h1>; } export default User;
注意
在引入 React Hooks 功能之后,也可以在函数组件中使用本地状态。
-
React.Component。上述函数组件可以写成以下形式:
class User extends React.Component { render() { return <h1>{`Hello, ${this.props.message}`} </h1>; } } export default User;上述代码中的
render()方法是每个类组件必须实现的唯一强制方法。
元素和组件之间的区别是什么?
元素是一个描述组件实例或 DOM 节点及其所需属性以在某个时间点表示 UI 的普通对象。它包含有关组件类型、其属性以及其下的任何子元素的信息。与 DOM 元素相比,创建 React 元素的成本更低。
任何 React 元素的语法表示如下:
React.createElement(type, {props}, children);
这里是一个创建简单的SignOut React 元素的示例,其中元素包含另一个元素:
const element = React.createElement("div",
{ id: "signout-container" },
React.createElement("button", {id: "signout-btn"},
"Sign Out")
);
上述React.createElement方法返回一个对象,如下所示:
{
type: 'div',
props: {
children: {
type: 'button',
props: {
children: 'SignOut'
id: 'signout-btn'
}
},
id: 'signout-container'
}
}
最后,它使用ReactDOM.render()渲染到 DOM 中。
相同的 React 元素可以通过简化的方式在 JSX 中以短格式表示:
const signoutElement = <div id="signout-container">
<button id="signout-btn">SignOut</button>
<div>;
注意
React 元素是不可变的——也就是说,一旦你创建了一个元素,你就不能再进一步修改其子元素或属性。
另一方面,一个组分由 React 元素组成。换句话说,一个组分是一个创建元素的工厂。这个组分可以是两种类型之一——一个类或一个可选输入并返回元素树的函数类型。
让我们通过一个例子来理解这个概念,这个例子使用了前面的 SignOut React 元素来创建一个 React 组分:
const SignOut = ({handleSignOut}) => (
<div id="signout-container">
<button id="signout-btn" onClick={ handleSignOut}>
SignOut
</button>
<div>
)
以下图表示了元素和组分在 React 视图中的结构:

图 2.3:组分与元素
这些组分可以进一步分为纯组分、高阶组分(HOCs)等,以及它们的特定功能。你将在接下来的问题中了解更多关于它们的信息。
什么是纯组分?
纯组分是指对于相同的 state 和 props,渲染相同输出的组分。在函数组件中,借助 Memoize API(即 React.memo()),可以在组件周围包裹以实现纯功能。这个 API 主要用于性能优化。
Memoize API 通过对 props 进行浅比较来防止不必要的更新渲染。这意味着它不会比较前一个状态与新状态。这是因为函数组件默认情况下会阻止重新渲染,当你再次设置(即使用设置函数)相同的状态时。
让我们通过一个例子来理解这个记忆化概念。首先,创建一个父 UserEnquiryForm 组分来输入用户输入,然后创建另一个显示相同信息的组分,称为 UserProfile;这是子组分。子组分被包裹在 Memoize API 中,以防止相同的 prop 细节从父组件传递下来:
import { memo, useState } from 'react';
const UserProfile = memo(function UserProfile
({ name, age }) {
return (<>
<p>Name:{name}</p>
<p>Age: {age}</p>
</>);
});
export default function UserEnquiryForm() {
const [name, setName] = useState('');
const [age, setAge] = useState(0);
const [email, setEmail] = useState('');
return (
<>
<label>
Name: <input value={name} onChange=
{e => setName(e.target.value)} />
</label>
<label>
Age: <input value={age} onChange=
{e => setAge(e.target.value)} />
</label>
<label>
Email: <input value={email} onChange=
{e => setEmail(e.target.value)} />
</label>
<hr/>
<UserProfile name={name} age={age}/>
</>
);
}
在前面的代码中,email 属性没有被传递给子组件。因此,对于 email 输入的变化,不会进行任何重新渲染。
然而,在类组件中,由于我们用 PureComponent 扩展了它而不是 Component,组件将变成一个纯组件。内部,纯组件实现了 shouldComponentUpdate() 生命周期方法,并使用浅比较。
注意
建议所有 React 组分在处理 props 时都像纯函数一样。这个指南有助于提高性能,因为它防止了不必要的重新渲染,并避免了应用中意外的错误。
除了纯组分之外,在 React 生态系统中还创建了一种特殊的组分类型,作为一种模式。它作为一个具有零副作用(类似于纯组件的行为)的纯函数工作。
什么是高阶组分?
HOC(高阶组件)是一个函数,它接受一个组件并返回一个新的组件。HOC 不是 React API 的一部分,但它们是重用组件逻辑的高级技术。你可以使用 HOC 在组件之间共享 props 和 states。
HOC 可以用以下语法表示:
const withHigherOrderComponent = (OriginalComponent) => (props) => <OriginalComponent {...props} />;
基于 HOC 概念创建了第三方库,如 Redux 的connect和 Relay 的createFragmentContainer。
什么是片段以及你会在哪里使用它们?
React 片段是一种语法,允许你将一组子元素包装或分组到一个 React 组件中,而无需在 DOM 中添加额外的节点。你可以使用<Fragment>或带有空标签的更短语法(<></>)。
例如,让我们以一个Author组件来表示一个发布了多篇博客文章的作者。该组件遍历作者的博客文章并显示它们,而不添加额外的 DOM 节点,如<div>或<span>:
function Author() {
return posts.map(post =>
<Fragment key={ post.id}>
<Post title={post.text} body={post.body} />
<Date date= {post.date} />
</Fragment>
);
}
在前面的代码片段中,使用<Fragment>标签来支持在迭代列表项时的key prop。替代的<></>语法不支持 key 属性。因此,如果你正在迭代列表项,则首选使用<Fragment>而不是<></>。
使用片段还有更多好处:
-
在非常大的或深层的 DOM 树中,片段更快且占用更少的内存
-
CSS 框架或库,如
<div>元素,会干扰期望的布局 -
DOM 检查器不那么拥挤,因为它不包含任何额外的 DOM 节点
现在你已经了解了组件的各种构建块,你可以轻松地学习如何控制组件中的数据或组件之间的数据。数据管理对于任何 Web 应用都是至关重要的。
使用 props 和 state 控制组件数据
组件内的数据由 props 和 state 控制。所有类型的 React 应用都基于这两个概念,它们是创建高效和健壮应用的重要核心主题。这些主题不仅在面试中很有用,你也会在日常工作中遇到它们的用法。
React 中的 props 是什么?
Props,代表属性,是以单个值或包含一组值的对象的形式出现的参数,它们被传递到组件中。它们的命名规范类似于 HTML 属性。它们有助于将数据从父组件传递到子组件,并用于在屏幕上动态渲染子组件。这意味着 props 充当组件在组件树内部通信的通道。
React 中 props 的主要目的是实现以下组件功能:
-
根据业务需求将自定义数据传递到你的组件中。
-
根据 prop 的值在组件中触发状态变化。
-
通过在组件的渲染代码中使用
props.propName(对于函数组件)或this.props.reactProp(对于类组件)来访问 props。这些数据对于条件渲染 UI 很有帮助。
下面是一个从父组件向子组件传递道具的例子(在这种情况下,Employee):
import React from 'react';
function App() {
return (
< >
<Employee name="John" age="30" department="Manufacturing"></
Employee>
<Employee name="Malcolm" age="35" department="Engineering" ></
Employee>
<Employee name="Luther" department="Finance"></Employee>
</ >
);
}
function Employee(props) {
return (
< >
<span>Name: {props.name} </span>
<span>Age: {props.age} </span>
<span>Department: {props.department} </span>
</ >
)
}
Employee.defaultProps = {
name: "Jack",
age: "45",
department: "HR"
}
export default App;
在前面的代码中,使用了defaultProps属性来分配道具的默认值。如果没有传递显式道具,将使用这些值。
访问道具对象的属性的更好方法是使用 ES6(ECMAScript 2015)中的解构。
通过解构的帮助,前面的子组件可以重写如下:
function Employee({name, age, department}) {
return (
<>
<span>Name: {name} </span>
<span>Age: {age} </span>
<span>Department: {department} </span>
</>
)
}
重要提示
道具在本质上是不可变的(只读),尝试修改这些值将引发错误。如果你仍然需要在组件中修改数据,那么状态是管理你的数据的正确选择。
你能描述一下 React 中的状态吗?
状态是一个内置的 JavaScript 对象,其中存储属于组件的属性值。换句话说,状态是私有的,完全由组件控制。状态的关键部分是,每当状态对象发生变化时,组件都会重新渲染。
状态的作用域始终在组件内部,如下面的图所示:

图 2.4:组件内部状态的作用域
下面是一个User函数组件的例子,其中包含欢迎信息作为状态,以解释其用法:
import React, { useState } from "react";
function User() {
const [message, setMessage] = useState
("Welcome to React world");
return (
<>
<h1>{message}</h1>
</>
);
}
在前面的代码中,使用了useState钩子向User组件添加状态。它返回一个数组,包含当前状态和用于更新状态的 setter 函数。
与函数组件相比,类组件将状态属性存储在内部状态对象中,并在User组件内部通过this.state.message访问它:
class User extends React.Component {
constructor(props) {
super(props);
this.state = {
message: "Welcome to React world",
};
}
render() {
return (
<>
<h1>{this.state.message}</h1>
</>
);
}
}
内置的setState函数用于更新前一个类组件的状态。
注意
为了提高可读性,建议将状态变量保持尽可能简单,并尽量减少有状态的组件以实现单一数据源。
道具和状态之间主要有哪些区别?
道具和状态都是用于管理组件数据的纯 JavaScript 对象,但它们以不同的方式使用,具有不同的特性:
| 道具 | 状态 |
|---|---|
| 只读和不可变 | 可变和异步更改 |
| 道具是从父组件传递给子组件 | 由组件本身管理 |
| 由子组件访问 | 不能由子组件访问 |
| 道具使组件可重用 | 状态不能使组件可重用 |
| 用于组件间的通信 | 用于渲染动态变化 |
表 2.1:道具与状态对比
与道具不同,状态可以通过不同的方式更新。让我们更深入地了解批量更新多个状态。
React 是如何批量更新多个状态的?
React 防止组件在每次状态更新时重新渲染。React 通过使用内置的 Hooks 在事件处理器内进行分组更新来优化应用程序性能,从而实现这一点。整个过程被称为批处理。React 版本 17 仅支持浏览器事件的批处理,而 React 版本 18 支持批处理的改进版本,称为自动批处理。
自动批处理支持从任何位置调用的状态更新,而不仅仅是浏览器事件。换句话说,它支持原生事件处理器、异步操作、超时和间隔。
让我们看看一个展示自动批处理的例子:
import { useState } from 'react';
export default function MultiState() {
const [count, setCount] = useState(0);
const [message, setMessage] = useState('Batching');
console.log('Application re-rendered');
const handleAsyncFetch = () => {
fetch(https://jsonplaceholder.typicode.com/todos/1
).then(() => {
// trigger only one(1) re-render due to
Automatic Batching
setCount(count +1);
setMessage('Automatic batching');
});
}
return (
<>
<h1>{count}</h1>
<button onClick={handleAsyncFetch}>
Click Me!</button>
</>
)
}
即使前面的代码使用事件处理器更新了两个状态变量,React 也会默认自动批处理它们,组件只会重新渲染一次。
你能防止自动批处理吗?
自动批处理是 React 库的一个优秀特性,它优化了渲染性能。然而,在某些情况下,你可能需要为每个状态更新重新渲染你的组件,或者根据另一个更新的状态值更新一个状态。React 通过 ReactDOM 引入了flushSync API 函数,以便在必要时随时停止自动批处理。此函数对于需要立即将更新刷新到 DOM 的情况也很有用,尤其是在与第三方代码(如浏览器 API 和 UI 库)集成时。
例如,假设你想在向简单的待办事项应用中添加一个新的待办事项或任务后更新网页的滚动位置。如果你希望直接关注新的待办事项内容,这种行为是有帮助的。在这种情况下,最新的待办事项状态需要立即更新,以便你获得正确的滚动位置:
const handleAddTodo = (todoName) => {
flushSync(() => {
setTodos([...todos, { id: uuid(), task: todoName }]);
});
todoListRef.current.scrollTop = todoListRef.
current.scrollHeight;
};
这个函数的使用并不常见,使用它可能会严重影响应用程序的性能。
你如何更新状态中的对象?
你不应该直接更新 React 的状态对象。相反,你需要创建一个新的对象或复制现有的对象,然后使用新创建的对象设置状态。因此,在更新状态对象时,你始终需要将状态中的对象视为只读的,尽管 JavaScript 对象是可变的。
让我们看看这个比较的实际效果,并展示这两个更新过程的结果。例如,已经创建了WeatherReport组件及其属性,包括temperature和city。之后,组件属性在事件处理器内部被直接修改:
function WeatherReport() {
const [weather, setWeather] = useState({
temperature: 26,
city: "Singapore",
});
const handleWeatherChange = (e) => {
if (e.target.name === "temperature") {
weather.temperature = e.target.value;
}
if (e.target.name === "city") {
weather.city = e.target.value;
}
};
return (
<>
<label>
Temparature:
<input value={weather.temperature} onChange=
{handleWeatherChange} />
</label>
<label>
City:
<input value={weather.city} onChange={handleWeatherChange} />
</label>
<div>
Report:
<span>{weather.temperature}</span>
<span>{weather.city}</span>
</div>
</>
);
}
一旦前面的代码被部署,尝试在 UI 中输入新的天气详情。你会发现输入字段不会更新。
你可以通过创建weather对象的新副本然后设置状态来修复这个问题。为此,让我们更新前面的事件处理器:
handleWeatherChange(e) {
setWeather({
...weather,
[e.target.name]: e.target.value
})
}
前面的代码按预期更新了 UI 中的天气详情。
有时候,只需要修改状态对象中的一个字段。在这种情况下,建议使用对象展开语法来利用重用先前状态对象属性值的优势,而不是在状态中设置每个字段。
如何更新嵌套状态对象?
使用展开语法更新顶层状态对象相当容易。但是,当涉及到嵌套状态对象时,你不能直接更新所需的嵌套属性,就像你在常规 JavaScript 对象中做的那样。
这可以通过一个例子进一步解释:我们将使用嵌套状态对象并修改其内部的嵌套属性。为了演示这种行为,让我们考虑具有嵌套地址详情属性的用户User状态对象:
{
name: 'Tom',
age: 35,
address: {
country: 'United States',
state: 'Texas',
postalCode: 73301
}
}
现在,你可以在 React 组件中使用类似于在纯 JavaScript 中进行的表达式user.address.postalCode = 75015来尝试更新postalCodeproperty,但屏幕不会更新为最新值。
如果无法通过作为解决方案将嵌套的User对象展平,那么有两条可能的途径来正确更新状态:
-
使用展开语法创建一个新的顶层对象
updatedUser,它指向新创建的嵌套对象:const updatedAddress = { ...user.address, postalCode: 75015 }; const updatedUser = { ...user, address: updatedAddress }; setUser(updatedUser); -
通过单个 函数调用 更新嵌套对象:
也可以通过在单个状态设置函数中使用展开语法来一起更新嵌套对象和顶层对象:
setUser({ ...user, address: { ...user.address, postalCode: 75015 } });
如果你想要超越传统方法,第三方库如Immer提供了一种方便的方式来更新深层嵌套的状态对象。这个库允许直接更新嵌套对象,就像常规 JavaScript 更新属性一样。
使用 Immer 修改嵌套对象看起来是这样的:
setUser(user => {
user.address.postalCode = 75015;
});
组件的状态也可以是一个项的数组,而不仅仅是原始值或对象类型。然而,你需要提供额外的 key 属性,这将在后续问题中详细说明。
理解 key 属性的重要性
Keys 帮助你控制组件或元素实例。它们在决定元素是否需要在元素列表中重新渲染中起着重要作用。因此,每个开发者都应该了解 key 属性的重要性。
key 属性是什么,它的用途是什么?
关键属性是在创建组件中元素列表时需要包含的一个特殊属性。这个 key 属性帮助 React 识别哪些元素已更改、已添加或已删除。换句话说,它帮助你在元素被修改后仍然保留元素的唯一标识或列表中兄弟元素的唯一标识。
即使你没有提供 key 属性,列表也可以成功地将内容渲染到浏览器中,但会在控制台记录一条警告消息,如下所示:
Warning: Each child in an array or iterator
should have a unique "key" prop
建议使用数据中的唯一 ID 值作为 key 属性。这个值可以是字符串或数字。
让我们通过实现程序员的待办事项列表来更好地理解这个关键的属性概念。在这里,你是在迭代列表时将键分配给TodoItem组件,而不是分配给提取的<li>标签:
import React from "react";
import ReactDOM from "react-dom";
function TodoItem(props) {
const { item: todo } = props;
return <li>{todo.id}: {todo.message}</li>;
}
function TodoList(props) {
const { todos: list } = props;
const updatedTodos = list.map((todo) => {
return <TodoItem key={todo.id} item={todo} />;
});
return <ul>{updatedTodos}</ul>;
}
const devTodoItems = [{id: 1, message:"Write a component"}, {id:2, message:"Test it"}, {id:3, message:"Publish the component"}];
ReactDOM.render(
<TodoList todos={ devTodoItems } />,
document.getElementById("root")
);
不建议将索引(从元素列表迭代中接收)作为键分配,因为如果列表的元素在未来被重新排序,那么元素的键也会改变。
了解事件处理
事件处理对于在应用程序中与网页交互至关重要。React 有自己的事件处理生态系统。事件处理器决定了每当特定事件被触发时必须采取什么操作。本节将为您提供一个关于 React 库中事件处理的良好理解。
什么是合成事件?
合成事件是浏览器实际原生事件对象的跨浏览器包装器。它提供了一个统一的 API,防止浏览器不一致性,并确保事件在多个平台上工作。
如果你在这两种事件中使用相同的preventDefault和stopPropagation方法,合成事件和原生事件之间有一些相似之处。你也可以通过在syntheticEvent实例上使用nativeEvent属性直接访问原生事件。例如,以下搜索组件仍然能够访问原生输入事件和其他在处理器内部的其他属性或方法:
function Search() {
handleInputChange(e) {
// 'e' represents synthetic event
const nativeEvent = e.nativeEvent;
e.stopPropogation();
e.preventDefault();
// Code goes here..
}
return <input onChange={handleInputChange} />
}
与 HTML 事件处理相比,React 事件处理有一些不同之处,我们将在下一节中讨论。
你如何区分 React 事件处理和 HTML 事件处理?
React 事件处理类似于 HTML 中的 DOM 元素事件处理。然而,它们之间有一些显著的区别:
-
命名约定:在 HTML 中,事件名称按照惯例以小写表示:
<button onclick="handleSingUp()">SingUp</button> <button onClick={handleSignUp}>SingUp</button> -
事件处理器中的
false。例如,让我们以一个简单的登录表单为例,该表单包含用户名和密码字段以输入数据。在提交表单后,你需要限制
onsubmit事件的默认行为,并且包含表单必须不被刷新:<form onsubmit="console.log('The form has been submitted.'); return false"> <input type="text" name="name" /> <input type="password" name="password" /> <button type="submit">Login</button> </form>然而,在 React 中,可以通过调用
event.preventDefault()方法来防止默认行为:function Login() { function handleSubmit(e) { e.preventDefault(); console.log('You submitted Login form.'); } return ( <form onsubmit="handleSubmit"> <input type="text" name="name" /> <input type="password" name="password" /> <button type="submit">Login</button> </form> ) } -
在函数名称后使用
()或使用addEventListener()来附加事件和监听器。以下示例显示了一个按钮的
onclick事件和调用相应的处理器。这可以通过两种方式完成。对于第一种方法,你可以在
handleSiginUp函数名称后插入括号:<button onclick="handleSingUp()"> However, in React, you just need to specify the method name inside the curly braces (`{}`) of the event attribute's value.
你如何在类组件中绑定事件处理器?
在 React 类组件中绑定事件处理器有几种方法:
-
构造函数中的
this关键字。例如,让我们在构造函数中添加一个
handleUserDetails()绑定:class User extends Component { constructor(props) { super(props); this.handleUserDetails = this.handleUserDetails. bind(this); } handleUserDetails() { console.log("Show User details"); } render() { return <button onClick={this.handleUserDetails}>Profile</ button>; } }如果你忘记绑定处理器,当函数被调用时
this关键字将是未定义的。 -
公共类 字段语法:
如果你对在构造函数中绑定处理程序不感兴趣,那么公共类字段方法在可读性和易用性方面要优越得多。这种语法在 Create React App (CRA) 工具中默认启用。
之前的绑定方法可以使用公共类字段语法重写和简化,如下所示:
class User extends Component { handleUserDetails = () => { console.log("Show User details"); } render() { return <button onClick={this.handleUserDetails}>Profile</ button>; } } -
显式使用
this关键字。箭头函数被传递到
User组件的回调中,如下所示:handleUserDetails() { console.log("Show User details"); } render() { return <button onClick={() => this.handleUserDetails()}> Profile</button>; }这种箭头函数方法的主要问题在于,每次组件渲染时都会创建不同的回调,如果将回调作为属性传递给子组件,子组件可能会进行额外的重新渲染。因此,建议使用构造函数中的绑定或公共类字段语法方法。
React 库背后的魔法,包括同步组件数据与 UI、DOM 更新等,都由虚拟 DOM 来处理。在下一节中,你将了解虚拟 DOM、其重要性以及其背后的工作流程。
理解虚拟 DOM
DOM 代表 Document Object Model,它以树形数据格式表示网页(HTML)的整个 UI。虚拟 DOM 不是由 React 发明的,但它将其作为核心功能。其主要目的是在重新渲染 UI 时最小化 DOM 操作的数量。React 使用此功能来提高其性能。在面试中,你很可能会有关于这个话题的问题。
什么是虚拟 DOM?
虚拟 DOM 是由 React 组件生成的 Real DOM 的内存中轻量级虚拟表示。UI 的虚拟表示存储在内存中,并与 Real DOM 同步以与最新的状态更新保持一致。这是通过名为 ReactDOM 的库实现的,这一步骤发生在调用渲染函数和元素显示在屏幕之间。整个过程被称为 reconciliation。
虚拟 DOM 是如何工作的?
React 和 Vue.js 技术在底层使用虚拟 DOM 来抽象开发者的手动 DOM 操作。这种编程机制在四个主要步骤中工作。
例如,让我们以一个简单的搜索表单为例,该表单包含在 CitySearch 组件 中的输入字段,看看虚拟 DOM 是如何工作的。我们将提供图表以方便理解:
function CitySearch() {
return (
<div>
<h2>Find city:</h2>
<form>
<span>
City:
</span>
<input onChange={handleCitySearch} />
</form>
</div>
)
}
之前的城市搜索在内部使用以下虚拟 DOM 机制的步骤:
- 当应用程序首次渲染时,React 创建一个代表 UI 的虚拟 DOM 并将其存储在内存中:

图 2.5:初始虚拟 DOM
- 当底层状态发生变化时,它将自动为更新创建一个新的虚拟 DOM。由于虚拟 DOM 只是一个代表 UI 的对象,因此在这一点上 UI 不会有任何变化(例如重绘):

图 2.6:更新后的虚拟 DOM
- 一旦创建了新的虚拟 DOM,React 就将其与预更新的虚拟 DOM 版本或快照进行比较。React 使用 diffing 算法来比较变化;这个过程被称为 协调:

图 2.7:比较虚拟 DOM 快照
- 在协调过程之后,React 使用像 ReactDOM 这样的渲染库,它接受变化并更新 Real DOM:

图 2.8:更新后的 Real DOM
什么是 Shadow DOM?
Shadow DOM 是一种主要设计用于作用域变量和 CSS 的 Web 组件技术。当父组件中定义的 CSS 样式不会影响或应用于子组件时,这非常有用。
Real DOM、虚拟 DOM 和 Shadow DOM 之间的区别是什么?
在现代 Web 开发中,虚拟 DOM 和 Shadow DOM 作为真实 DOM 的附加功能被引入,以提高性能和封装。
这些三个 DOM 之间的显著差异如下列出:
| Real DOM | Virtual DOM | Shadow DOM |
|---|---|---|
| 它为整个屏幕创建一个单一的 DOM | 它在内存中创建整个真实 DOM 的副本并跟踪变化 | 它创建具有独立作用域(即,作用域 CSS 样式和 JavaScript)的真实 DOM 的小副本 |
| 任何变化都将涉及整个屏幕的重绘 | 状态变化将涉及页面特定部分的重绘 | 变更适用于其自身的 Web 组件 |
| 通过像 Solid.js 这样的库在 Web 浏览器中实现 | 通过像 React、Vue 等这样的 JavaScript 库实现 | 通过像 Lit 和 Vaadin 这样的 Web 组件库实现 |
| 适用于小到中等规模的应用程序,没有复杂的交互性 | 适用于具有复杂交互性的大型应用程序 | 适用于小到中等规模的应用程序,交互性相对简单 |
| 相比虚拟 DOM,使用更少的 CPU 和内存 | 相比 Real DOM,使用更多的 CPU 和内存 | 相比虚拟 DOM,使用更少的 CPU 和内存 |
| 不支持封装,因为组件可以在外部被修改 | 支持封装,因为组件不能在外部被修改 | 支持封装,因为组件不能在外部被修改 |
表 2.2:Real DOM 与 irtual DOM 与 Shadow DOM 的比较
什么是 React Fiber?
Fiber 是 React 版本 16 中引入的一种新的协调引擎,它能够实现虚拟 DOM 的内置调度和增量渲染。增量渲染意味着能够将渲染工作分成块,并在多个帧上分散。因此,借助增量渲染,Fiber 在动画、布局和手势等方面提高了应用程序的性能。
这个协调器是对一个名为 stack reconciler 的旧协调算法的完全重写。
React 应用程序中的数据流和通信
React 的单向数据流特性使得 UI 在从小到大规模的应用程序中数据变化时简单且可预测。了解数据流和组件之间的通信的好处对于更好地理解相关的 React 概念非常重要。
你能描述一下 React 中的单向数据流吗?
单向数据流也称为单向数据绑定,是指数据在应用程序的不同部分之间传输时只能单向流动。这种技术或特性已经在函数式响应式编程中存在。
React 遵循单向数据流,其中数据通过 props 从父组件传递到子组件,而不是相反。此外,子组件不能更新来自父组件的数据。React 不鼓励双向或双向绑定,以确保你遵循清晰的数据流架构。
下图更清晰地展示了 React 中的数据流:

图 2.9:单向与双向数据流
更新父组件中的数据的唯一方法是通过从子组件触发事件。
注意
与 React 不同,Vue.js 在组件之间遵循双向数据绑定或双向数据流。
单向数据流有哪些优点?
单向数据流的主要优势是拥有数据的一个单一真相来源。单向数据流特性的其他优点也很多。以下列出了一些关键好处:
-
调试:由于开发者知道数据从何而来以及去向何方,因此调试问题要容易得多
-
更少错误:数据单向流动,这使得程序更少出错,并使开发者有更好的控制
-
效率:在应用中,单向数据流的已知边界不会浪费额外资源
你不能总是依赖状态、属性和数据流概念来控制视图层。无论 JavaScript 库如何,你可能需要访问 DOM 元素,而通过传统的document.getElementById等方式访问元素变得繁琐。在下一节中,你将找到更好的替代方法来访问 DOM。
你如何访问 DOM?
React 库在底层处理 DOM 操作,无需任何手动 DOM 更新。但有时,你可能会遇到需要由 React 管理 DOM 元素访问的情况(如聚焦、滚动到特定元素等)。为了解决这些用例,引入了 refs 来访问 DOM 节点。
React 面试官可能期望你对 DOM 访问和可能的用例有很好的了解,这两者都不能通过传统的声明式方法处理。
什么是 refs?如何创建 refs?
Ref 的 current 属性。换句话说,refs 是具有附加 current 属性的纯 JavaScript 对象。为了更好地理解这个 ref 概念,让我们看看使用 ref 实现输入元素自动聚焦行为的示例。为此,我们将遵循以下说明:
-
从 React 库中导入
useRefHook。 -
在
SignUpForm组件内部声明inputRef,并使用 Hook 的返回值。 -
将
inputRef传递给<input>元素,这会将输入的 DOM 节点连接到inputRef.current属性。 -
在加载组件实例时,通过在
useEffectHook 中的 DOM 节点上调用focus()来程序化地将焦点应用于输入元素。
在遵循所有前面的步骤之后,使用 refs 的最终组件应该看起来像这样:
import {useRef, useEffimport {useRef, useEffect} from 'react';
export default function SignUpForm() {
const inputRef = useRef(null);
useEffect(() => {
inputRef.current.focus();
}, [])
return (
<>
<input type="email" ref={inputRef} />
<button>
Verify Email
</button>
</>
);
}
与在函数组件中使用 useRef Hook 类似,createRef 通常用于在类组件中创建 ref。
Refs 的主要用途是什么?
Refs 的主要目的是在典型的单向数据流之外强制修改子组件。这里讨论了一些可以通过 refs 作为逃生口的常见用例:
-
对于第一个用例,让我们谈谈如何通过某些事件或操作修改 UI 元素。这个用例类别包括管理输入字段焦点、文本选择和媒体控制(或播放)等场景。
作为例子,让我们看看通过外部按钮点击控制的上述文本选择和音频场景:
//Text selection const hasInputText = inputRef.current.value.length > 0; if (hasInputText) { inputRef.current.select(); } // Audio controls const playAudio = () => { audioRef.current.play(); }; const pauseAudio = () => { audioRef.current.pause(); }; -
对于第二个用例,让我们谈谈在不通过 CSS 或 JavaScript 编写任何显式动画的情况下在 UI 中触发命令式动画。
这里是一个示例,展示如何通过滚动事件访问
div元素并找到其位置,以在布局中执行某些操作:function handleScroll() { const block = blockRef.current; const { y } = block.getBoundingClientRect(); const blockBackgroundColor = y <= 0 ? 'white' : 'black'; setBackground(blockBackgroundColor); } -
对于第三个用例,让我们讨论在 React 应用程序中与第三方 DOM 库的集成。
使用 refs 很容易将现有的应用程序与一些 JavaScript 插件或库集成。例如,第三方插件如
DataTable.js和select2可以轻松地在 React 应用程序内部访问,而无需从头开始重新创建。
注意
如果可以声明式地实现任务,建议避免使用 refs。
你如何比较 refs 和状态?
Refs 和状态都用于在渲染之间持久化组件数据。然而,它们之间有很多不同之处:
| Ref | State |
|---|---|
由 useRef(initialValue) Hook 创建,它返回 {current: initialValue} JavaScript 对象 |
由 useState(initialValue) Hook 创建,它返回当前状态值和更新状态的设置函数–即 [value, setValue] |
| 不会触发对其所做的任何更改的重新渲染 | 触发状态中的任何更改的重新渲染 |
| 它通常用于与外部 API 通信 | 它经常在组件内部使用以改变其外观 |
可以在渲染过程之外更新 current 属性值 – 即,引用是可变的 |
你不应该直接更新状态变量;应该使用设置函数来修改值 – 即,状态是不可变的 |
| React 无法跟踪引用变化 | React 可以跟踪状态变化 |
| 在渲染过程中你不应该读取或写入引用 | 你可以在任何时候读取状态 |
除非基于状态的声明性视图无法实现,否则应避免使用引用,因为 DOM 结构的变化可能与 React 的 diff 和 update 方法冲突 |
始终推荐使用状态进行 UI 更新 |
表 2.3:引用与状态
尽管引用(在可变方面)不如状态严格,但大多数时候,你将使用状态而不是引用,因为引用是访问 DOM 的逃生门,而你不需要那么频繁地这样做。
注意
如果你是在屏幕渲染之外存储信息,那么你应该使用引用。否则,你总是需要使用状态来存储数据。
前向引用是什么?
现在,组件更加复杂,几乎不直接使用纯 HTML 元素。这导致了一个父组件和子组件的层次结构,具有可组合的视图。如果你只是尝试将引用作为属性传递给子组件,子组件将不会接收到需要访问的实际 DOM 元素。相反,它返回 { current: null}。
React 的 forwardRef 是一种方法,允许父组件将其引用传递给其子组件。在下面的示例中,子组件通过使用 forwardRef 方法从其父组件接收一个引用,然后将其转发到 <button/> DOM 元素:
import { forwardRef } from 'react';
const MySignInButton = forwardRef(function MySignInButton(props, ref) {
const { label, ...otherProps } = props;
return (
<label>
{label}
<button {...otherProps} ref={ref} />
</label>
);
});
前面的子组件接收一个 ref 作为第二个参数,而第一个参数指的是 props。
注意
你可以通过使用 userImperativeHandle 钩子来限制你关于 DOM 节点的信息暴露,而不是传递整个 DOM 节点。你将在 第三章 中了解更多关于这个钩子的内容。
使用上下文 API 全局管理状态
上下文可以帮助你与子组件共享全局数据,即使这些组件在组件树中存在更深的层级。你可以通过使用上下文 API 和钩子来执行大型应用的状态管理。有很大可能性你会在面试中遇到与上下文实现相关的问题,以解决有关在 React 应用程序中全局维护数据的常见用例。
什么是开孔钻探?
传参钻取指的是通过中间的几个组件(这些组件不需要数据,但仅帮助传递数据)将属性从组件树中的高级组件发送到低级组件的过程。这个“传参钻取”术语在 ReactJS 中官方并不存在,但它经常被用来表示这种情况。
你能描述一下上下文吗?
上下文用于解决传参钻取问题。它提供了一种通过在中央位置存储数据,而不需要在组件树的每一级手动传递数据的方式来将数据从父级组件传递到子级组件。
您可以通过以下三个简单步骤在 React 中使用上下文:
-
来自 React 库的
createContext(defaultValue)工厂函数用于创建上下文对象。它只接受一个参数来提供默认值。让我们创建一个带有默认用户名的用户上下文:// userContext.js import { createContext } from 'react'; export const UserContext = createContext('Jonathan'); -
Context.Provider组件需要应用于带有上下文更改的父组件,这些更改已提供给其子组件。此组件上的value属性用于设置上下文值。在这一步,
username字段将更新为当前用户详情,作为提供者组件的上下文:import { UserContext } from './userContext'; function App() { const value = 'Michael'; return ( <Context.Provider value={value}> <MyParentComponent /> </Context.Provider> ); }需要消费上下文的子组件应该被包裹在提供者组件内部。
-
useContext钩子。这个钩子返回上下文的值:import { useContext } from 'react'; import { UserContext } from './context'; function MyChildComponent() { const currentUser = useContext(UserContext); return <span>{currentUser}</span>; }
您将在第三章中了解更多关于useContext钩子的内容。
这些步骤已经用图表表示,以帮助您理解实际操作中的上下文:

图 2.10:上下文在行动
上下文的作用是什么?
上下文的主要目的是允许您的组件访问全局数据,而不会遇到任何传参钻取问题,并且当全局数据发生变化时,不需要重新渲染组件。
这里是一些上下文的常见用例:
-
应用程序主题信息,用于将品牌应用于整个应用程序
-
认证用户配置文件信息
-
用户设置
-
偏好语言设置
-
应用程序配置
到目前为止,我们已经学习了如何在客户端渲染 React 应用程序。在下一节中,您将学习如何通过服务器端渲染 Web 内容来提高页面加载速度,以及一些其他好处。
理解服务器端渲染技术
SSR正迅速成为 JavaScript 库和框架中一个更加突出的特性。基于 React 的框架,如 Next.js、Gatsby 等,使得创建 SSR 变得更加容易。如今,面试官期望应聘者对 SSR 有很好的了解,以及常规的CSR。
服务器端渲染是什么?
SSR 是单页应用(SPAs)的一种流行的替代渲染方法。这种技术将在服务器上渲染客户端 SPA,然后将完全渲染的 HTML 页面发送到客户端。这对于 SEO 来说很有帮助,因为搜索引擎可以在将页面发送给用户之前先轻松找到内容。
服务器端渲染和客户端渲染之间主要区别是什么?
CSR 是一种渲染模式,其中浏览器下载一个最小的 HTML 页面,并使用 JavaScript 进行渲染。数据获取、模板化和路由过程由客户端处理——即浏览器处理。
与此相反,SSR 将服务器上的 HTML 文件转换为包含客户端数据的完全渲染的 HTML 页面。
何时需要使用服务器端渲染?
哪种渲染方法被使用取决于业务需求。但在这里,我们提到了一些可以考虑使用 SSR 的情况:
-
如果您优先考虑 SEO 并构建博客网站,建议使用 SSR
-
如果您的网站需要更快的初始页面加载时间
-
如果应用程序具有简单的 UI,与较少的功能和页面交互性较小
-
如果应用程序包含较少的动态数据
-
如果网站的用户流量较少
还有一些其他预渲染技术,例如静态站点生成(SSG)和增量静态再生(ISR),这两者将在第九章中讨论。
我们现在已经回答了关于 React 的许多基本问题。这些知识也作为回答即将到来的章节中下一阶段问题的基础。
摘要
在本章中,我们讨论了许多在 React 面试中经常被问到的基础概念。我们首先介绍了 ReactJS 和 JSX,并讨论了它们在构建健壮的 Web 应用程序中的优势。然后,我们继续讨论 ReactJS 的核心构建块,如元素、组件、属性和状态,以及虚拟 DOM 和单向数据流在幕后是如何工作的。
最后,我们讨论了与通过事件处理与应用程序交互相关的问题,以及如何在 React 生态系统中使用上下文和 SSR 来全局共享数据。在下一章中,我们将考虑与 Hooks 相关的问题,它们的重要性以及 Hooks 如何通过更快、更有效地实现功能来使 React 变得更好。
第三章:Hooks:将状态和其他特性引入函数组件
Hooks 是在函数组件中引入的,以利用 React 功能的好处,而无需编写任何类和生命周期方法。大多数开发者使用 Redux、Recoil、Mobx 和其他第三方库来管理大型应用程序中的全局状态。但是,当您一起使用 React Hooks,如 useContext 和 useReducer 时,它成为外部状态管理的更好替代方案。Hooks 比涉及大量样板代码、重复的文件和文件夹的复杂外部库更容易使用。React 还提供了许多其他内置 Hooks,这些 Hooks 可以用于 React 应用程序中的各种用例。如果没有特定的内置 Hooks 可用于处理您的用例,您可以创建自己的 Hook 来满足您的业务需求。Hooks 的常见用例(或横切关注点)包括身份验证、日志记录、缓存、数据获取和错误处理。
在本章中,您将了解 React Hooks、它们的优点以及各种内置 Hooks,这些 Hooks 将 React 功能添加到函数组件中。将通过示例深入解释内置 Hooks,以了解它们在 React 应用程序中的使用。此外,您将获得知识和信心,以回答与第三方 Hooks 相关的问题、创建自己的 Hooks 以及调试 Hooks。
在本章中,我们将涵盖以下主要主题:
-
Hooks 及其目的简介
-
使用 Hooks 进行本地状态管理
-
使用 Hooks 进行全局状态管理
-
在 React 应用程序中执行副作用
-
使用 Ref Hooks 访问 DOM 节点
-
优化应用程序性能
-
了解流行的第三方 Hooks
-
创建自己的 Hooks
-
调试 Hooks
Hooks 及其目的简介
最初,React 主要与类组件一起使用,但随着时间的推移,组件变得复杂,因为使用了各种模式来重用组件逻辑。随后,Hooks 被引入以简化代码,无需编写任何模式,例如渲染属性和高阶组件(HOCs)。由于 Hooks 在构建 React 应用程序中现在扮演着重要角色,你可以在 React 面试中期待到几个关于 Hooks 的问题。本节将为您提供关于 Hooks 是什么以及它们的目的的详细答案。
什么是 Hooks?
Hooks 是允许组件使用本地状态并执行副作用(或横切关注点)以及其他 React 功能的简单 JavaScript 函数,而无需编写类。Hooks API 是在 React 16.8 中引入的,以将状态逻辑从组件中隔离出来。
简而言之,Hooks 功能是您的函数组件“挂钩”到 React 生命周期和状态的一种方式。
Hooks 背后的动机是什么?
Hooks 可以解决各种各样的问题。
这里有一些例子:
-
在组件之间重用状态逻辑的困难:默认情况下,React 没有提供重用组件逻辑的方法。例如,渲染属性和高阶组件(HOCs)等编程模式试图解决这个问题。但那些模式需要修改组件层次结构,这使得应用程序变得复杂,有多个包装层,并使代码更难跟踪。
因此,引入了 Hooks 来将状态逻辑从组件中分离出来,而不需要修改组件层次结构。
-
理解复杂组件的困难:随着应用程序的增长,组件变得更加复杂,拥有完整的状态逻辑和副作用。生命周期方法被各种无关逻辑占据,例如数据获取、添加事件监听器或在一个地方移除事件监听器。例如,
componentDidMount生命周期方法可以为组件执行数据获取并添加事件监听器。同时,相关的监听器逻辑,如清理,需要在componentWillUnmount生命周期方法中添加。最终,将大型组件拆分成小型组件变得困难,同时测试它们也变得困难。Hooks 可以将大型组件拆分成带有相关代码的小函数,而不是根据生命周期方法来拆分代码。
-
由类引起的混淆:类不是 React 特有的,但它们属于 JavaScript。如果你想要处理类组件,首先你应该清楚地了解称为 this 的关键字行为,它在其他语言中是不同的。此外,如果你不熟悉使用 ES2022 公共类字段语法,你需要记住在构造函数中绑定事件监听器。所有这些概念在开发者中造成了大量关于正确使用的混淆。
Hooks 对于创建 React 功能非常有帮助,无需编写类,并且可以避免开发社区中的混淆。
你能描述 Hook 的使用规则吗?
使用 Hooks 时需要遵循两个主要规则:
-
useState和useEffectHooks。 -
仅从 React 函数中调用 Hooks:你不应该从常规 JavaScript 函数中调用 Hooks。相反,你可以从 React 函数组件或自定义 Hooks 中调用它们。
注意
可以使用名为 eslint-plugin-react-Hooks 的 eslint 插件(www.npmjs.com/package/eslint-plugin-react-hooks)来强制执行在 你能描述 Hook 的使用规则吗 部分中描述的两个规则。
我能否在类组件中使用 Hooks?
你不能在类组件中编写 Hooks。换句话说,Hooks 是为函数组件创建的。然而,你可以在单个组件树中混合类组件和函数组件,而不会引起任何问题。
React 组件通过一个可更新的结构称为 状态 来持有数据和跟踪数据变化。在实际应用中,大多数组件使用状态来处理和显示它们 UI 中的数据。在下一节中,我们将讨论与使用状态 Hooks 进行本地状态管理相关的问题及其答案。
使用 Hooks 进行本地状态管理
在 React 应用程序内部,有两种 Hooks 可以实现本地状态管理。第一个 Hook,名为 useState,可用于简单的状态转换,另一个 Hook,useReducer,用于复杂的状态逻辑。基本上,useState 在内部使用 useReducer。这意味着整个组件状态可以通过 useReducer Hook 本身来管理。由于状态是 React 组件的核心构建块,每个开发者都应该清楚地了解如何使用 Hooks 来管理状态。
什么是 useState Hook?
useState Hook 用于向函数组件添加状态。这是 React 中最常用的内置 Hooks 之一。此 Hook 接收初始状态作为参数,相同的初始状态可以是值或函数类型(即初始化函数)。如果初始状态来自昂贵的计算,建议使用初始化函数,它只会在初始渲染时执行。useState Hook 返回一个包含两个值的数组:状态变量和用于更新状态的设置函数。
useState 的语法表示如下:
const [state, setState] = useState(initialState)
让我们以一个使用 useState Hook 保留计数器状态的计数器组件为例。设置函数用于更新 count 状态变量并重新渲染 UI 以反映任何更改:
import { useState } from "react";
function Counter() {
const [count, setCount] = useState(0);
return (
<>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me</button>
</>
);
}
每次点击计数器按钮时,count 状态变量将增加一,并且相应的 UI 将使用最新的状态变量值进行更新。
注意
状态设置函数不会更新正在执行的代码中的当前状态。它只会在下一次渲染中可用。
是否总是建议使用更新函数?
你可能会听到来自开发者社区的推荐,即始终使用更新函数来更新状态,如果新状态是从先前状态计算得出的。这个规则有助于避免在执行一些状态计算逻辑后出现不可预测的状态。尽管遵循这个规则没有坏处,但并不总是必要的。在大多数情况下,React 会在下一个事件发生之前更新状态变量。也就是说,在事件处理程序开始时,状态不会有过时数据的风险。
但如果你在同一个事件处理程序中执行多个状态更新,那么建议使用更新函数来接收预期的数据结果。在事件处理程序中使用更新函数的编码方式如下:
function handleClick() {
setCounter(a => a + 1);
setCounter(a => a + 1);
setCounter(a => a + 1);
}
在此代码中,a => a + 1 是一个更新函数。React 将您的更新函数放入队列中,因此对同一状态变量的更新被批处理。在下一个渲染过程中,React 将按相同的顺序调用它们。
什么是 useReducer 钩子?如何使用它?
useReducer 钩子是 useState 钩子的替代品。它用于将自定义状态逻辑(例如,添加、更新和删除状态项)与渲染逻辑分离。换句话说,它有助于将状态管理从组件中提取出来。
此钩子接受三个参数。第一个参数是一个 reducer 函数,指定如何更新状态,第二个参数用于初始状态,第三个参数是一个可选的初始化器函数,用于确定初始状态。相比之下,useState 只接受初始状态。
useReducer 钩子返回一个包含两个值的数组,当前状态和 dispatch 函数,用于修改状态并触发重新渲染。
让我们通过一个计数器示例来了解此钩子的用法。在这里,您可以使用 reducer 函数通过增加、减少和重置操作来更新计数器状态值:
ffunction reducer(state, action) {
switch (action.type) {
case "increment":
return { count: state.count + 1 };
case "decrement":
return { count: state.count - 1 };
case "reset":
return { count: action.payload };
default:
throw new Error();
}
}
function init(initialCount) {
return { count: initialCount };
}
function Counter() {
const initialCount = 0;
const [state, dispatch] = useReducer(reducer, initialCount, init);
return (
<>
Count: {state.count}
<button
onClick={() => dispatch({ type: "reset", payload: initialCount })}
>
Reset
</button>
<button onClick={() => dispatch({ type: "decrement"
})}>decrement</button>
<button onClick={() => dispatch({ type: "increment"
})}>increment</button>
</>
);
}
React 一次保存初始状态并忽略后续渲染。因此,如果您通过函数调用推导状态,则需要避免在每个渲染中重新创建初始状态。相反,您可以使用初始化器函数作为 reducer 函数的第三个参数。
在前述代码的第三个参数中,init 函数已被用于根据 Hook 中提到的第二个参数的默认值处理初始状态。
前述代码的重要阶段在以下步骤中描述:
-
当任何按钮点击事件被触发时,相应的事件处理程序将携带一个动作派发到 reducer 函数。
-
此后,reducer 函数将根据要求将状态更新为新状态。
-
状态更新将触发组件重新渲染以更新 UI。
图 3.1 中的流程图以逐步方式描述了 useReducer 钩子的行为:

图 3.1:useReducer 钩子的行为
注意
如果与上一个状态相比状态没有变化,useReducer 钩子不会重新渲染子组件。
应该在什么情况下使用 useReducer 钩子而不是 useState 钩子?
useReducer 和 useState 钩子都有助于管理应用程序状态,但 useReducer 钩子提供了以下原因的受控且强大的状态管理解决方案:
-
当您需要管理复杂的状态逻辑时,首选
useReducer钩子而不是useState。例如,当状态包含多个嵌套值或下一个状态依赖于前一个状态时,useReducer钩子是一个更好的选择。 -
useReducer钩子可以在单个函数中处理多个动作,而不是使用useState钩子为每个动作创建单独的函数。 -
useReducer钩子对于优化触发深度或嵌套更新的组件的性能也非常有帮助,因为你可以通过上下文在任何嵌套级别传递useReducer钩子的 dispatch 函数,而不是将回调函数传递到组件树的每一层。换句话说,它有助于避免在第二章中提到的属性钻取问题。此外,dispatch 函数在重新渲染之间不会改变。
注意
由于 useState 本身是在底层由 useReducer 钩子派生出来的,因此你可以使用 useReducer 覆盖所有状态用例。
如果你需要将组件状态与存在于组件树更深层次的多个子组件共享,则建议使用 React 内置的 context 钩子。此钩子主要用于维护应用程序范围内的数据,这被称为全局状态管理。
使用 Hooks 进行全局状态管理
useContext 钩子通常与 useState 钩子一起用于全局状态管理。useContext 钩子的主要优势是它解决了属性钻取问题。
useContext 钩子的详细常见用法已在第二章中解释。因此,本节将主要关注与使用 useContext 钩子进行全局状态管理相关的特定用例问题。
如何覆盖组件树中特定部分的上下文?
有时,你可能需要用不同的值覆盖组件树中某个部分的上下文。可以通过将这部分包裹在一个具有不同值的 provider 中来覆盖上下文值。
例如,以下代码将蓝色背景应用于除联系页面外的所有页面,而联系页面将使用上下文提供者应用白色背景:
<ColorContext.Provider value="blue">
<About />
<Services />
<Clients />
<ColorContext.Provider value="white">
<Contact />
</ColorContext.Provider>
</ColorContext.Provider>
使用 provider 覆盖上下文时,没有对嵌套级别或覆盖次数的限制。
如果没有匹配的 provider,上下文值会是什么?
如果在 useContext 钩子的调用组件上方没有匹配的 provider,则将返回上下文创建中提到的默认值——即 createContext(defaultValue) 中使用的默认值。
如果你指定了一个默认值,你可以避免在页面中任何缺失的 provider 上出现意外的错误。
在类组件中,根据需求,副作用是在不同的生命周期方法中处理的,例如 componentDidMount、componentDidUpdate 和 componentWillUnmount。另一方面,在函数组件中,effect 钩子简化了基于渲染的单点副作用处理。下一节将涵盖关于在 React 应用程序中执行副作用时经常提出的问题。
在 React 应用程序中执行副作用
效果是 React 编程中的一个逃生门。React 提供了一些效果钩子,用于在函数组件中实现副作用,如数据获取、订阅、计时器、日志记录、DOM 操作等。这些钩子仅应在您与外部系统同步时使用。有三种类型的钩子可用:
-
useEffect:这是一个常用的钩子,用于将组件连接到外部系统。
-
useEffect钩子除了在浏览器重绘屏幕以测量布局之前触发之外,与useEffect钩子相同。 -
useInsertionEffect:此钩子在 React 对 DOM 进行更改之前触发,例如添加动态 CSS。
让我们详细讨论各种效果钩子和它们的特性,以回答面试中提出的问题。
反应依赖项如何影响 useEffect 钩子内部的逻辑?
useEffect 钩子接受一个可选的依赖项参数,该参数接受一个反应值的数组。您不能为您的效果选择依赖项,并且每个反应值都应该声明为依赖项,以避免任何类型的错误。存在不同的场景来传递反应依赖项。
传递依赖项数组
如果您在依赖项数组中传递反应值,则效果应在初始渲染之后以及每次具有更改依赖项的重新渲染后运行逻辑。
以下是一个示例,用于传递名称和状态反应依赖项,以了解使用依赖项数组时的 useEffect 语法:
useEffect(() => {
// Runs after first render and every re-render with
dependency change
}, [name, status]);
传递空依赖项数组
如果您的效果不使用任何反应值,它仅在初始渲染后运行。在这种情况下,效果钩子如下所示:
useEffect(() => {
// Runs after initial render only
}, []);
未传递依赖项数组
如果您跳过传递依赖项数组,则效果将在您的组件每次重新渲染后运行,如下所示:
useEffect(() => {
// Runs after every re-render
});
React 将使用 Object.is 比较比较依赖项数组中的每个反应值与其上一个值,以验证更改。
在 useEffect 钩子内部,设置和清理函数被调用的频率是多少?
在大多数情况下,效果应该有一个清理函数来清除或撤销由其相应的设置代码创建的更改。React 在以下不同的组件生命周期阶段调用设置和清理函数:
-
挂载:设置函数中的逻辑在组件被添加到 DOM 或视图中时运行。
-
重新渲染:在组件及其依赖项更改的每次重新渲染之后,清理(如果已定义)和设置函数将按顺序调用。在这里,清理函数使用旧属性和状态运行,然后设置代码使用最新的属性和状态运行。
-
卸载:在组件从 DOM 或视图中移除后,清理代码将最后一次运行。此清理函数有助于避免不希望的行为,如内存泄漏,并提高性能。
如果在您的 React 应用程序中启用了严格模式,则在第一次实际设置调用之前将有一个额外的仅开发使用的设置和清理周期。这是为了确保清理逻辑与设置逻辑相匹配,以避免与设置代码有任何差异。
你应该在什么时候从依赖中移除一个对象或一个函数?
如果效果依赖于在渲染过程中创建的对象或函数,那么可能存在一种情况,即效果可能比必要的更频繁地重新运行。这是因为为每次渲染创建的对象或函数是不同的。
让我们通过一个例子更好地理解这个概念。以下是一个根据Url和name查询参数依赖关系在useEffect Hook 中获取用户列表的示例。在这里,查询对象是在渲染过程中创建的,用于构建绝对 URL 路径:
const userUrl = "https://jsonplaceholder.typicode.com/users";
export default function Users() {
const [users, setUsers] = useState([]);
const [name, setName] = useState("John");
const [message, setMessage] = useState("");
const userQueryOptions = {
url: userUrl,
name,
};
useEffect(() => {
const userUrl = buildUserURL(userQueryOptions); //buildUserURL is
excluded from code snippet
fetch(userUrl)
.then((res) => res.json())
.then((users) => setUsers(users));
}, [userQueryOptions]);
return (
<>
Users: {message}
<input value={message} onChange={(e) => setMessage(e.target.
value)} />
<input value={name} onChange={(e) => setName(e.target.value)} />
{users &&
users.map((user) => (
<div>
Name: {user.name}
Email: {user.email}
</div>
))}
</>
);
}
在前面的代码中,由于message状态的变化,userQueryOptions对象在每次重新渲染时都被重新创建。此外,这个message数据与效果内部的反应性元素无关。
通过将对象移动到效果内部并使用name字符串替换对象依赖项,可以修复此问题,因为name是效果所依赖的唯一反应性值:
useEffect(() => {
const userOptions = {
url: userUrl,
name,
};
const userUrl = buildUserURL(userOptions);
fetch(userUrl)
.then((res) => res.json())
.then((users) => setUsers(users));
}, [name]);
同样,您可以通过将函数移动到效果 Hook 内部来避免在渲染阶段创建函数。之后,您可以将函数依赖项替换为直接的反应性依赖值。
useLayoutEffect Hook 有什么用?它是如何工作的?
useLayoutEffect Hook 是一种在浏览器重绘屏幕之前调用的特殊类型的 effect Hook。此 Hook 主要用于在状态更新时组件闪烁的场景。想象一下网页上的弹出组件。组件首先需要确定元素在视口中的位置,然后才能正确地在屏幕上渲染。
useLayoutEffect Hook 的主要目的是为组件提供布局信息以进行渲染。它通过三个简单的步骤工作:
-
在没有布局信息的情况下渲染初始内容。
-
在浏览器重绘屏幕之前计算布局大小。
-
使用正确的布局信息重新渲染组件。
警告
组件将被渲染两次,并在屏幕重绘之前阻止浏览器。这会影响应用程序的性能。因此,建议仅在需要时使用useLayoutEffect Hook。
由于效果将在每次渲染后运行,因此提高 React 应用程序性能的主要方法之一是避免不必要的重新渲染。React 中创建了一些内置的 Hooks 来优化性能。让我们在下一节深入探讨这些细节。
优化应用程序性能
性能优化对客户体验有巨大影响。尽管 React 应用程序默认具有非常快的 UI,但当应用程序的大小增加时,可能会有性能问题。本节将重点介绍与性能优化钩子相关的问题,这些问题可能是面试官用来从更广泛的角度评估你的技能。
什么是记忆化?如何在 React 中实现?
记忆化是一种优化技术,通过缓存昂贵的函数调用结果来加速 Web 应用程序。当再次传递相同的输入参数时,它会返回缓存的結果。
在 React 中,这种优化可以通过两个钩子实现:useMemo 和 useCallback。这些钩子通过在相同输入给出时返回缓存的結果来提高性能,跳过不必要的重新渲染。
你能描述一下 useMemo() 钩子吗?
useMemo() 钩子用于在重新渲染之间缓存昂贵的计算结果。这个钩子的语法如下:
const cachedValue = useMemo(calculateValue, dependencies)
此钩子通过接受两个参数来返回昂贵的计算结果。第一个参数是执行昂贵计算的功能,第二个参数是用于计算的依赖项数组,这些依赖项是响应式值。换句话说,当依赖项值没有变化时,将返回缓存的結果(或上一次渲染存储的值)。否则,将再次执行计算。
让我们通过一个例子来理解这个概念。考虑一个数字的阶乘计算函数,并在其周围应用 useMemo 钩子。该组件还执行一个与计算函数无关的增量操作:
import { useState, useMemo } from "react";
function factorial(number) {
if (number <= 0) {
return "Number should be positive value.";
} else if (number === 1) {
return 1;
} else {
return number * factorial(number - 1);
}
}
export default function CounterFactorial() {
const [count, setCount] = useState(0);
const [number, setNumber] = useState(1);
const factorial = useMemo(() => factorial(number), [number]);
return (
<>
<h2>Counter: {count}</h2>
<button onClick={() => setCount(count + 1)}>Increment</button>
<h2>Factorial: {factorial}</h2>
<input
type="number"
value={number}
onClick={() => setNumber(number + 1)}
/>
</>
);
}
在前面的代码中,如果你增加计数器的值,由于相应的响应式数字没有被更新,因此与阶乘函数相关的任何重新渲染都不会发生。也就是说,阶乘函数只有在输入数字发生变化时才会被调用,而不会在计数器值增加时被调用。
useMemo() 钩子的可能用例有哪些?
记忆化有助于优化应用程序性能,一些开发者甚至认为尽可能多地记忆化几乎所有组件都没有害处。然而,这种技术在函数内的简单计算中是不必要的。
有几种常见情况,记忆化非常有用:
-
当在渲染内容时进行昂贵的计算,例如排序、过滤、更改格式等
-
当你将属性传递给
useMemo钩子包装的组件,并且希望在属性没有变化时跳过重新渲染——也就是说,当纯组件可以包装在useMemo中时 -
当传递给包装组件的值被用作其他钩子的依赖项时
React DevTools的分析器部分将有助于识别需要添加记忆化的卡顿组件。
使用useMemo时常见的错误有哪些?你如何纠正它们?
useMemo Hook 的使用相当简单,这个 Hook 可能会被广泛用于优化渲染性能。然而,你需要小心以下一些常见的错误:
-
如果你尝试从
useMemoHook 返回一个对象,那么要么用括号将其包裹,要么写一个显式的返回语句。例如,以下
useMemoHook 返回一个未定义的值,因为开括号({)是箭头函数的一部分,但不是对象的一部分:const findCity = useMemo(() => { country: 'USA', name: name }, [name]);这可以通过为对象添加显式的返回语句来修复,如下所示:
const findCity = useMemo(() => { return { country: "USA", name: name, }; }, [name]); -
如果你忘记指定依赖项,那么计算将在每次渲染时重新执行:
const filterCities = useMemo(() => filteredCities(city, country));用于计算的响应式值应传递到依赖项数组中,以避免不必要的渲染:
const filterCities = useMemo( () => filteredCities(city, country), [city, country] ); -
你不应该在循环中调用
useMemo。相反,将其包裹或提取到一个新组件下:{ products.map((product) => { const revenue = useMemo(() => calculateRevenue(product), [product]); return ( <> <span>Product: {product.name}</span> <span>Revenue: {revenue}</span> </> ); }); }这可以通过在子组件内部提取
useMemo计算来解决:{ products.map((product) => { return <Report product={product} />; }); }
在使用useMemo Hook 时,上述点可以作为最佳实践。
你应该在什么情况下使用useCallback Hook 而不是useMemo Hook?
当顶层组件重新渲染时,默认情况下,React 会递归地重新渲染所有子组件。如果子组件有大量的计算,这种情况将影响应用程序的性能。在这种情况下,需要使用 Memo API 或useMemo Hook 来优化子组件。
然而,如果你将回调函数作为 prop 传递给子组件,React 将始终重新渲染子组件,因为函数定义或箭头函数在每次重新渲染时都被视为新函数。这违背了记忆化的目的。在这种情况下,useCallback有助于优化性能。
useCallback Hook 类似于useMemo Hook,但它缓存的是回调函数而不是值。你仍然可以使用useMemo Hook,但计算函数必须返回另一个函数;也就是说,它需要一个额外的嵌套函数。
让我们通过一个包含TaxCalculation作为父组件和TaxPayer作为子组件的例子来讨论这个概念。在子组件中,你需要考虑在相同的 props 发送和重新渲染缓慢的情况下跳过重新渲染。
为了跳过重新渲染,首先需要使用记忆函数包裹子组件(TaxPayer):
import { memo } from 'react';
const TaxPayer = memo(function TaxPayer({ onSubmit }) {
// ...
});
如果父组件在income prop 发生变化时重新渲染,那么这种变化会导致子组件也重新渲染。当子组件没有进行大量计算且income prop 的变化很小的时候,这不会是一个大问题。
然而,如果你将回调函数作为 prop 传递给子组件,它会在每次传递时创建一个新的函数。无论性能影响如何,都应该始终避免这种情况。
为了避免每次新 prop 导致的重新渲染,让我们为 handleSubmit 回调函数应用 useCallback Hook:
function TaxCalculation({ year, income}) {
const handleSubmit = useCallback((taxPayerDetails) => {
post('/tax/' + year, {
taxPayerDetails,
income
});
}, [year, income]);
return (
<div>
<TaxPayer onSubmit={handleSubmit} />
</div>
);
}
在前面的代码中,回调函数将被缓存,直到或除非依赖的响应式值发生变化。
类组件中存在的 Ref API 类似,一些 Hooks 在函数组件中也被创建,特别是为了与 DOM 节点交互。下一节将讨论使用 Hooks 访问 DOM 节点的重要概念。
使用 ref Hooks 访问 DOM 节点
当你需要与外部系统(或非 React 系统)如内置浏览器 API 交互时,Refs 非常有用。函数组件中有两个内置的 ref Hooks。useRef Hook 用于声明一个可以持有任何类型的值的 ref,但主要用于 DOM 节点。useImperativeHandle 用于仅暴露所需方法的定制 ref。
请注意,关于 ref 的介绍已经在第二章中进行了覆盖。在本节中,我们将超越在第二章中已经讨论的关于 ref 的内容。
你如何避免重新创建 Ref 内容?
useRef Hook 接受初始值(或默认值)作为参数,就像 useState Hook 一样。此 Hook 的声明应放在封装组件的顶部。
React 在第一次渲染时保存此初始值,并在后续渲染中忽略它,但如果你为 ref 的初始值创建了一个昂贵的对象,它可能会在每次渲染中不必要地被调用。这将影响应用程序的性能。
使用以下示例可以更好地解释初始 ref 值的声明以及 ref 内容是如何重新创建的:
function CreateBlogArticle() {
// This is an expensive object to create the article
}
function Blog() {
const articleRef = useRef(new CreateBlogArticle());
//...
}
在前面的代码中,CreateBlogArticle() 函数始终被调用以创建昂贵的对象,即使 React 从第二次渲染开始就忽略了此对象。
可以通过限制 CreateBlogArticle() 函数在后续渲染中的调用来解决此问题,如下所示:
function Blog() {
const articleRef = useRef(null);
if (articleRef.current === null) {
articleRef.current = new CreateBlogArticle();
}
//...
}
现在博客文章对象仅在初始渲染期间计算一次。
在渲染方法中能否访问 ref?
是的,你可以在渲染方法中访问 ref 的当前值,但不建议在渲染过程中读取或写入 ref.current 值。这是因为屏幕上显示的 ref 值可能不会因为知道更改 ref 不会触发重新渲染而更新,这与状态变量不同。
你如何从 ref 实例中公开方法子集?
useImperativeHandle 钩子用于从子组件向父组件暴露自定义方法或现有方法的一个子集。这有助于限制父级 ref 只能访问某些函数或属性,而不提供对整个 ref 的访问。常见的用例是创建一个组件,以便在库下共享,消费者只能访问暴露的 API。
假设你正在计划创建一个对话框组件,并想在某个顶级父组件中共享对话框的一些基本功能。在这种情况下,你可以在子组件内部暴露 open、close 和 reset 方法,而不是提供对整个对话框 DOM 节点的访问:
useImperativeHandle(ref, () => ({
open: () => ref.current.invokeDialog(),
close: () => ref.current.closeDilaog(),
reset: () => ref.current.clearData(),
}));
使用 useImperativeHandle 钩子的组件需要用 forwardRef 包装,并且从 forwardRef 渲染函数的第二个参数接收 ref。
还有几个内置的钩子,如 useId、useDeferred、useTransition 和 useSyncExternalStore,由于它们的使用量很少,所以没有涵盖。让我们快速了解一下它们:
-
useId: 这个钩子用于为 HTML 可访问性属性生成唯一的 ID -
useDeferred: 这个钩子用于将 UI 的一部分更新推迟到最新数据可用时进行 -
useTransition: 这个钩子通过标记一些状态修改为低优先级来帮助提高用户响应性 -
useSyncExternalStore: 这个钩子用于订阅存在于 React 系统之外的外部数据存储
React 提供了几个内置的钩子,但你可以在这些钩子的使用之外,使用 React 社区创建的第三方钩子来覆盖基于你业务需求的具体用例。在下一节中,我们将涵盖与第三方钩子及其相应答案相关的问题。
了解流行的第三方钩子
Hooks API 在开发者社区中非常受欢迎,内置的钩子自 2019 年以来一直存在。开发者尝试创建了许多第三方钩子,如 useImmer、useFetch、useDebounce、useForm、useLocalStorage、Redux Hooks 等,以解决在 Web 开发中观察到的常见用例。如果你想要掌握 Hooks 概念,那么你应该对第三方钩子及其如何解决一些常见问题有一个很好的理解。
useImmer 钩子是什么?它的用途是什么?
useImmer 钩子就像 useState 钩子一样,但在管理具有嵌层数据的复杂状态时提供了优势。它更新状态的方式就像它是直接可变的,类似于常规 JavaScript。这个钩子基于 Immer 库,通过创建一个新的状态副本来进行修改。
这个钩子可以通过 use-immer npm 库安装。像 useState 一样,它返回一个元组。元组的第一个值是当前状态,第二个是更新器函数。
让我们看看UserProfile组件的示例,并直接更新地址详情,如下所示:
import { useImmer } from "use-immer";
function UserProfile() {
const [user, setUser] = useImmer({
name: "Tom",
address: {
country: "United States",
city: "Austin",
postalCode: 73301,
},
});
function updatePostalCode(code) {
setUser((draft) => {
draft.address.postalCode = code;
});
}
return (
<div className="profile">
<h1>
Hello {user.name}, your latest postal code is ({user.address.
postalCode}
)
</h1>
<input
onChange={(e) => {
updatePostalCode(e.target.value);
}}
value={user.address.postalCode}
/>
</div>
);
}
在底层,Immer创建了一个临时草稿对象,并将所有更改应用于它。一旦所有突变完成,Immer将生成下一个状态对象。
如果你的用例无法由内置钩子或任何第三方钩子满足,那么你可以构建自己的钩子来提供满足你需求的解决方案。在下一节之后,你将能够回答与自定义钩子相关的问题。
构建自己的钩子
尽管 React 为常见用例提供了某些内置钩子,如useState、useEffect、useContext等,但你有时可能需要使用钩子来满足无法由内置钩子或第三方库解决的问题。在本节结束时,你将能够回答与自定义钩子及其目的相关的问题,以及如何避免传统方法来共享组件逻辑。
什么是自定义钩子?你如何创建它们?
React 自带了几个内置钩子,但它不会限制你在有限场景中使用钩子。你也可以通过将组件逻辑提取到称为自定义钩子的单独可重用函数中来创建自己的钩子。这些钩子有助于隐藏组件中的复杂逻辑。它们涵盖了广泛的使用场景,如数据获取、表单处理、在线或离线状态订阅、连接到聊天室、动画等。
通过演示实时示例,可以更好地解释自定义钩子的创建和使用。让我们考虑一个博客网站应用程序,其中你列出特定用户的全部帖子,并同时显示特定帖子的评论。在这里,你需要创建两个名为Posts和Comments的组件。这两个组件都需要根据给定的 URL 和可选查询参数从服务器获取数据。一旦收到响应,这两个组件都会在屏幕上显示数据。
为了避免在两个组件中都有与数据获取、加载、错误处理等相关逻辑的重复,可以将代码移动到一个以use为前缀的单独可重用自定义钩子中。可以在useFetchData.js文件中创建名为useFetchData的数据获取钩子,如下所示:
import { useState, useEffect } from "react";
const useFetchData = (url, initialData) => {
const [data, setData] = useState(initialData);
const [loading, setLoading] = useState(false);
useEffect(() => {
setLoading(true);
fetch(url)
.then((res) => res.json())
.then((data) => setData(data))
.catch((err) => console.log(err))
.finally(() => setLoading(false));
}, [url]);
return {data, loading};
};
export default useFetchData;
此后,上述自定义钩子可以在作为Posts.jsx文件和Comments.jsx文件一部分创建的消费者组件中使用。在Posts组件中的使用方式如下:
import useFetchData from './useFetchData.js';
export default function Posts() {
const url = "https://jsonplaceholder.typicode.com/posts?userId=1";
const { data, loading} = useFetchData(url, []);
return (
<>
{loading && <p>Loading posts… </p>}
{data && (
data.map((item) =>
<div key={item?.title}>
<p>
{item?.title}
<br/>
{item?.body}
</p>
</div>
)
)}
</>
);
}
同样,你可以在Comments组件中也重用useFetchData。在这些更改之后,Posts和Comments组件的代码变得更加简单、简洁和易于阅读。
自定义钩子的好处是什么?
使用自定义钩子的主要优势是代码的可重用性,无需在许多组件中编写重复的逻辑。还有其他一些优势也值得考虑自定义钩子:
-
可维护性:使用自定义钩子更容易维护代码。在未来,如果您需要更改钩子的逻辑,您只需更改一个地方,而不会干扰代码的其他部分——即组件或文件。
-
可读性:使用自定义钩子而不是在 UI 中实际展示的组件周围包裹多层 HOCs、提供者和消费者以及渲染属性,可以使应用程序的代码变得更加清晰和易于阅读。此外,通过将特定组件逻辑移动到单独的钩子中,组件代码也变得更加简洁。
-
可测试性:在 React 应用程序中,您需要为测试容器和展示组件编写单独的测试。如果您的容器使用了多个 HOCs,那么集成测试尤其具有挑战性。复杂性增加,因为您需要测试容器和组件一起执行集成测试。使用自定义钩子可以消除这个问题,因为它们允许您将容器和组件组合成一个单独的组件。
此外,与 HOCs 相比,编写单元测试和模拟钩子更容易。
-
社区驱动的钩子:React 社区已经非常流行,并为他们的特定用例创建了多个钩子。建议的做法是在创建自己的钩子之前,首先检查您正在寻找的钩子是否已经被其他人创建。一些社区驱动的钩子可以在
usehooks.com/和github.com/imbhargav5/rooks找到。
列出的优势激励了许多 React 开发者创建自定义钩子,以应对他们在 React 应用程序中遇到的独特功能。如果任何第三方开源库已经提供了覆盖您特定场景的钩子,建议重用相同的钩子,而不是通过构建自定义钩子来重新发明轮子。
你是否仍然应该考虑使用渲染属性和 HOCs?
渲染属性和 HOCs 都是 React 生态系统中用于在组件之间共享组件状态逻辑的传统高级模式。然而,与这两种传统模式相比,Hooks 要简单得多,并且足以覆盖用例。此外,使用 Hooks,您不需要更改现有的组件结构,也不需要添加更多组件,最终得到一个嵌套的树形结构。
你推荐将效果移动到自定义钩子中吗?
当你需要与 React 范围之外的世界交互时,使用 Effect Hooks 来执行应用程序的副作用。一些非 React 系统可以访问 Web API,调用外部 API 等。随着时间的推移,你应该通过实现针对你用例的特定解决方案来减少代码中的副作用数量。标准指南是在没有内置解决方案可用时使用 effect Hooks。这是因为避免副作用可以使你的应用程序更简单、运行更快、更不容易出错。通过将你的副作用移动到自定义 Hook 中,当有解决方案可用时,升级与副作用相关的代码变得更加容易。
随着你的应用程序随着许多自定义 Hook 来满足业务需求而增长,应用程序的复杂性增加,并且有很大可能遇到应用程序中的错误。下一节将介绍如何调试 React 自定义 Hook。
故障排除和调试 Hooks
传统的调试方法,如 IDE 和浏览器 DevTools 中的调试器,对于调试自定义 Hook 并不有效。React 提供了 useDebugValue Hook,允许开发者通过为它们分配自定义格式化的标签来调试自定义 Hook。在本节结束时,你将了解如何调试自定义 Hook。
你如何调试自定义 Hook?
useDebugValue Hook 用于扩展与自定义 Hook 内部逻辑相关的数据在 React DevTools 中的可视化。这些信息出现在 React DevTools 扩展的 Component Inspector 选项卡中。
当前的调试信息仅限于显示我们自定义 Hook 内部使用的内置 Hooks 的信息。对于开发者来说,通过逐行计数输出并识别哪个条目映射对应于代码中调用的 Hooks 来读取信息是很困难的。这种困难可以通过为我们的自定义 Hook 添加额外的条目到 React DevTools 输出中而得到纠正。
让我们用一个例子来更好地理解这一点。所需的详细信息可以通过在自定义 useFetchData Hook 中的 useDebugValue Hook 中记录,正如在 什么是自定义 Hook?如何创建它们? 部分所创建的那样:
const useFetchData = (url, initialData) => {
useDebugValue(url);
const [data, setData] = useState(initialData);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
useDebugValue(error, (err) =>
err ? `fetch is failed with ${err.message}` :
"fetch is successful"
);
useEffect(() => {
setLoading(true);
fetch(url)
.then((res) => res.json())
.then((data) => setData(data))
.catch((err) => setError(err))
.finally(() => setLoading(false));
}, [url]);
useDebugValue(data, (items) =>
items.length > 0 ? items.map((item) => item.title) :
"No posts available"
);
return {data, loading};
};
在前面的代码中,第二次和第三次调试调用使用了一个可选的第二个参数来格式化显示的值。
FetchData 自定义 Hook。例如,如果你悬停在 Posts 组件上,右侧的 Hooks 部分看起来如下:

图 3.2:DevTools 中的 DebugValue 标签
同样,当 API 由于服务不可用而抛出错误时,可以通过 DebugValue 标签追踪相应的根本原因。
到目前为止,我们已经讨论了与内置 Hooks 相关的几个问题,然后是关于第三方 Hooks 和自定义 Hooks 的主题。所有这些主题都按照特定的顺序进行覆盖,以便理解各种类型 Hooks 之间的联系。
摘要
本章全面介绍了 React 应用程序中的 Hooks。我们首先提供了 Hooks 的介绍,包括 Hooks 的动机以及使用 Hooks 时需要遵循的规则。接下来,我们探讨了在组件中使用 useState 和 useReducer Hooks 进行状态管理,以及使用 useContext Hook 在组件之间共享数据的全局状态管理。此后,我们介绍了如何借助 effect Hooks 在应用程序中执行副作用。
除了常用的内置 Hooks 之外,我们还讨论了使用 ref Hooks 访问 DOM 节点、通过 Hooks 进行性能优化、使用第三方 Hooks,以及根据业务需求创建自定义 Hooks。
在下一章中,我们将介绍一个重要的导航库,称为 React Router,用于在页面之间导航,并了解其丰富的功能。那一章还将讨论支持国际化以及通过传递参数创建动态本地化消息的重要主题。
第四章:处理路由和国际化
在软件开发的广阔世界中导航并非没有困难。我们的旅程经常需要熟练地导航各种屏幕和语言设置,以确保提供卓越的用户体验。本章深入探讨了路由和国际化以及任何现代 React 应用程序的虚拟环境和视野。
本章将带我们深入探讨屏幕导航,并介绍 React Router。它是网络应用导航中最重要的工具,因为它在用户在不刷新页面的情况下穿越您的应用时刷新浏览器 URL。我们将介绍许多类型的路由,如基本路由和嵌套路由,学习如何将路由添加到我们的应用中,甚至深入到访问 URL 参数的世界。
但我们的冒险并未结束。随着我们进入国际化与本地化的领域,我们将超越功能性的困难,并在全球范围内规划我们的路线。这些发展方面展示了我们对多样性的承诺,确保我们的应用能够使用用户所在地的语言进行交流。
我们将详细介绍如何将翻译和格式化消息添加到我们的程序中,从而提高其面向全球受众的可用性。我们还将学习如何使用占位符并向这些消息传递参数,以实现动态翻译。将本章视为您通往更用户友好和全球化的应用的路线图。我们将利用这些知识来引导我们穿越软件开发领域,避免任何障碍,并提供无瑕疵的用户体验。在接下来的几节中,准备开始这场迷人的冒险。
在接下来的章节中,我们将涵盖以下主题,以了解如何处理路由以及国际化是如何工作的:
-
导航屏幕和 React Router 简介
-
路由、路由类型和链接
-
添加路由
-
访问 URL 参数
-
嵌套路由
-
介绍国际化与本地化
-
添加翻译和格式化消息
-
传递参数和占位符
技术要求
请确保您已在您的机器上设置并安装了 JavaScript 构建工具 Vite.js。您可以在以下链接找到它:vitejs.dev/。我们将使用它来完成我们的下一个 React 项目。
此外,熟悉一下 React Router 库:reactrouter.com/en/main。
我们现在准备开始。下一节将向我们介绍 React Router。让我们行动起来吧!
导航屏幕和 React Router 简介
理解导航和 React Router 库对于任何程序员来说都是至关重要的。在本节中,我们将探讨使用 React Router 遍历屏幕的基本原理以及为什么它至关重要。本节的目标是概述 React Router,并简单描述通过各种 Web 应用程序进行导航的方式,而不使内容过于技术化。我们将通过提供有用的建议来缩小雇主期望与你的现有技能水平之间的差距,这些建议有助于在利用 React Router 的同时提高工作流程效率。
现在,让我们探讨为什么我们应该首先使用 React Router 库,以及它能帮助我们什么。值得一提的是,Next.js 已经内置了路由功能,但这仍然是一项值得学习的知识。这是因为在不同 React 框架中,路由的方式不同,其核心工作原理仍然有效,并且可以在任何地方使用。
React Router 库的目的是什么?
创建路由逻辑可能会很耗时且困难,这正是 React Router 发挥作用的地方。由于库的广泛功能,它可以极大地减轻我们的路由挑战。React Router 是一个开源的 Web 应用程序路由模块,允许你在不同的页面和组件之间进行导航。它提供了一个易于使用的界面,用于在 Web 项目中实现动态路由。它支持众多 URL,并让你完全控制你应用的路由,从而实现无缝且引人入胜的用户体验。
现在我们已经了解了使用这个库的目的,我们将在下一节学习导航是如何工作的。
在 React Router 中,屏幕之间的导航是如何工作的?
在屏幕之间导航一开始可能看起来很令人畏惧,但使用 React Router 可以使这个过程变得相当容易和实用。使用 React Router,你可以高效地管理你应用中的所有路由,使得在不同屏幕之间切换变得轻而易举。此外,它允许你保持你的 UI 与 URL 保持同步。所以,即使你不是专家,React Router 也能让你轻松地添加所需的功能,使你的应用运行顺畅。
React Router 启用了客户端路由,这基本上是路由启动的方式。本质上,计算机的浏览器从网站的服务器请求一个页面,服务器获取并确定 CSS 和 JavaScript 文件,并在网站上渲染从服务器本身提供的 HTML。当用户点击网站上的链接时,这个过程会为全新的页面重新启动。
Next.js 已经内置了路由解决方案,因此我们将使用另一个流行的 JavaScript 构建工具 Vite.js 来查看本章中的代码。以下是工具的链接:vitejs.dev/。
第一步是构建一个BrowserRouter组件并配置主路由。这为我们的 Web 应用启用了客户端路由。我们的main.jsx文件作为起点,如下所示:
import * as React from 'react';
import * as ReactDOM from 'react-dom/client';
import { createBrowserRouter, RouterProvider } from 'react-router-dom';
import './index.css';
const router = createBrowserRouter([
{
path: '/',
element: <div>Hello world!</div>,
},
]);
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<RouterProvider router={router} />
</React.StrictMode>
);
这段代码定义了我们的初始路由。在这种情况下,它将是我们的根路由。根路由是加载并显示在网站上的第一页,通常称为我们的主页。
在我们进入下一节之前,让我们快速了解一下BrowserRouter及其相关的子主题。首先,BrowserRouter是一个通过使用 HTML5 历史 API 来保持 UI 与 URL 同步的路由解决方案。这个 API 利用了诸如popstate、replacestate和pushstate等事件。我们可以使用BrowserRouter通过使用干净的 URL 和历史记录来存储地址栏中的当前位置。我们还可以用它来跟踪 iframe 中的 URL 变化。
React Router 有许多功能,你可以在其文档中找到。以下是提供的概述:
| 功能 | 描述 |
|---|---|
| 路由器 | 虽然你的应用可能只会使用一个路由器,但根据其运行设置,可以访问多个路由器。其中包含的有些是createBrowserRouter、createMemoryRouter、createHashRouter和createStaticRouter。 |
| 路由器组件 | 你将在你的应用中用于页面路由的路由器组件类型。 |
| 路由 | 用于创建和管理路由的方法。这些可以包括操作、懒加载和加载器。 |
| 组件 | 使用此功能,我们可以使用自定义组件来管理我们的数据。例如,我们可以使用Form组件,该组件模拟浏览器进行客户端路由和数据变更,或者使用Await组件进行自动错误处理。还有用于导航到其他页面的重要Link组件。这些都是我们可用的组件之一。 |
| 钩子 | 这些自定义钩子就像任何 React 钩子一样工作,并为我们提供新的功能。useNavigation、useSearchParams、useOutlet和useLocation都是具有不同目的的钩子。 |
| 获取工具 | 这些用于管理我们从 API 接收到的数据。我们可以获取数据并执行重定向。 |
| 工具 | 我们可以使用工具执行不同的操作。例如,matchPath可以用来匹配路由路径模式并将其与 URL 路径名称进行比较,并返回匹配信息。 |
表 4.1:React Router 功能
这涵盖了 React Router 的大部分主要功能。要了解更多信息,请阅读官方文档:reactrouter.com/en/main。
在下一节中,我们将进一步探讨并学习更多关于路由和链接的内容。
路由、路由类型和链接
路由是 React Router 应用程序中最关键的组件。它们将 URL 段与组件链接,以及执行数据加载和数据修改。通过路由嵌套,复杂的项目布局和信息依赖变得简单,因为路由是提供给路由构建操作的对象,这使得整个过程变得更加容易。
我们可以使用哪些类型的路由?
React Router 给我们提供了访问各种路由形式的能力。以下表格解释了这一点:
| 路由类型 | 描述 |
|---|---|
path |
路径模式将与 URL 进行比较,以查看此路由是否与 URL、链接 href 或表单操作匹配。 |
index |
这决定了路由是否是索引路由。索引路由,如默认子路由,将在其父级的 URL 中渲染到其父级的 Outlet。 |
children |
嵌套路由允许你在单个页面上渲染多个组件,同时保持路由完整性。 |
caseSensitive |
这指定了路由是否应该匹配大小写。 |
loader |
在路由渲染之前,路由加载器被调用,并通过 useLoaderData 为元素提供数据。 |
action |
当一个表单、fetcher 或提交将提交发送到路由时,路由动作会被调用。 |
表 4.2:路由类型
现在我们已经了解了不同类型的路由,是时候学习创建路由和链接的代码了。
如何创建路由和链接?
我们可以通过使用 React Element 和 React Component 来创建路由。
使用 element 的典型语法如下:
<Route path="/about" element={<About />} />
如果你想使用 Component 代替,那么代码将如下所示:
<Route path="/about" Component={About} />
链接的工作方式略有不同。客户端路由允许我们的应用程序在点击链接后调整 URL,而不是从服务器请求另一个文档。相反,应用程序可以迅速显示新的 UI,并使用 fetch 执行数据调用以更新页面内容。由于网络浏览器不需要请求全新的页面或重新访问下一页的 CSS 和 JavaScript 内容,这导致加载时间更快。它还允许增强用户交互,如滚动。
以下示例展示了如何使用链接进行页面导航:
import { createRoot } from 'react-dom/client';
import { createBrowserRouter, RouterProvider, Link } from
'react-router-dom';
const router = createBrowserRouter([
{
path: '/',
element: (
<div>
<h1>Hello World</h1>
<Link to="about">About Us</Link>
</div>
),
},
{
path: 'about',
element: <div>About</div>,
},
]);
createRoot(document.getElementById('root')).render(
<RouterProvider router={router} />
);
这段代码块展示了如何创建一个包含链接的首页,该链接可以导航到关于我们页面。
现在我们已经了解了路由和链接,让我们学习如何添加路由。
添加路由
如果我们使用 <Routes> 组件,路由可以在我们的应用程序中渲染,该组件与我们的文件中的其他子路由相匹配。路由搜索其所有子路由以找到最佳匹配,如果位置发生变化,则渲染该 UI 分支。为了表示嵌套 UI,这也对应于嵌套 URL 路径,<Route> 组件也可以嵌套。通过渲染 <Outlet>,父路由渲染其子路由。
以下代码示例说明了如何向文件中添加路由:
<Routes>
<Route path="/" element={<Menu />}>
<Route
path="messages"
element={<MenuItems />}
/>
<Route path="actions" element={<MenuActions />} />
</Route>
<Route path="about" element={<About />} />
</Routes>
在此文件中,有四个路由:
-
"/" -
"/messages" -
"/actions" -
"/about"
消息和操作的路线是主路线"/"下的嵌套路线。"/about"路线是一个顶级路线,因为它独立且不像前两个那样嵌套。当页面位于其定义的路由时,属性元素内的组件将加载。路线也可以使用 JSX 和createRoutesFromElements声明。
基本知识已经介绍完毕。现在,让我们继续下一个主题,即访问 URL 参数。这将教会我们如何导航到由其 ID 确定的页面。这为我们进行GET请求提供了更多的定制选项,这是在我们学会使用基本路由导航到页面之后的下一步。
访问 URL 参数
我们可以使用 React 中的useParams钩子,它提供了一个与指定路由匹配的当前 URL 动态参数的关键/值对象。所有参数都从父路由继承到子路由。一个工作示例如下:
import * as React from 'react';
import { Routes, Route, useParams } from 'react-router-dom';
function ProfilePage() {
// Get the userId param from the URL.
let { userId } = useParams();
// ...
}
function App() {
return (
<Routes>
<Route path="users">
<Route path=":userId" element={<ProfilePage />} />
<Route path="me" element={...} />
</Route>
</Routes>
);
}
因此,使用这种路由配置,应用程序可以根据 URL 的结构渲染各种组件。首先,users/userId的页面路由渲染ProfilePage组件,并将userId部分作为userId提供给组件。users/me的路由是渲染元素属性中提供的组件的路由。
如您所见,URL 参数功能强大,为我们提供了对路由的另一个层次的定制。在下一节中,我们将探讨嵌套路由,这是我们现在已经学会了如何创建基本路由后的自然发展。有了嵌套路由,我们将在同一页面上渲染多个组件。
嵌套路由
在 2014 年,React Router 中的嵌套路由受到了 Ember.js 路由机制的影响。Ember.js 团队发现,URL 的某些部分通常决定了渲染页面布局的方法以及数据如何与渲染的布局连接。在我们的示例中可以看到创建具有嵌套元素的页面的一个方法:
createBrowserRouter(
createRoutesFromElements(
<Route path="/" element={<Root />}>
<Route path="connect" element={<ConnectPage />} />
<Route
path="admin"
element={<Admin />}
loader={({ request }) =>
fetch("/data/api/admnin.json", {
signal: request.signal,
})
}
/>
<Route element={<AuthLayout />}>
<Route
path="login"
element={<Login />}
loader={redirectIfUser}
/>
<Route path="logout" action={logoutUser} />
</Route>
</Route>
)
);
此代码块用于用户认证的登录流程。如果用户已登录,则加载管理页面。还有一个登录和登出路由。现在,让我们看看动态路由,这是另一个有用的功能。
动态路由
当开发具有多个页面或视图的应用程序时,这些页面或视图具有基本结构但信息或行为不同,动态路由变得方便。与在应用程序中建立预定的路由数量相反,动态路由允许您根据应用程序的整体当前状态现场构建路由。
我们可以在这里看到它的样子:
import { BrowserRouter, Route } from 'react-router-dom';
function App() {
return (
<BrowserRouter>
<Route path="/users/:id" component={Profile} />
</BrowserRouter>
);
}
路由结构中的:id属性表示一个动态值,它可能根据用户提供的输入而变化。当 URL 符合此模式时,React Router 会从 URL 中获取 id 参数并将其提供给 Profile 组件。接下来,我们将查看错误页面,因为当用户遇到损坏的页面时,这是我们需要了解的情况。
错误页面
为了解决用户访问不存在路由或在使用我们的应用程序时遇到困难的情况,我们可以在 React Router 中创建错误页面或找不到页面。当出现问题时,这有助于提供更好的用户体验,防止用户遇到空白页面或不一致的布局。
以下代码示例展示了如何创建错误页面。
首先,我们必须创建必要的组件:
import React from 'react';
const NotFound = () => {
return <div>404 – The page was not found</div>;
};
const ErrorPage = () => {
return <div>An error occurred. :(</div>;
};
export { NotFound, ErrorPage };
此代码创建了两个组件 - 一个用于404错误页面,另一个用于通用错误页面。
现在,让我们设置路由:
import React from 'react';
import { BrowserRouter as Router, Route, Switch } from
'react-router-dom';
import { NotFound, ErrorPage } from './ErrorComponents';
import Home from './Home';
function App() {
return (
<Router>
<Switch>
<Route exact path="/" component={Home} />
<Route path="/error" component={ErrorPage} />
<Route component={NotFound} />
</Switch>
</Router>
);
}
export default App;
此代码创建了一个包含我们应用程序所有页面路由的组件。
就这样,我们已经考虑了错误页面并学习了如何设置一些路由。
接下来,我们将介绍国际化和本地化。了解如何根据不同地区调整您的应用程序非常重要,因为我们所有人生活在不同国家。所以,让我们直接进入正题。
国际化和本地化
国际化和本地化是软件开发中的基本实践,使您能够设计和部署可以针对多种语言和地区定制的系统。让我们学习它们之间的区别。
国际化是什么?
国际化是将您的应用程序创建和准备成可以在多种语言中使用的进程。这通常涉及将应用程序的所有字符串提取到可以翻译成多种语言的单独文件中。它还需要对您的软件进行结构化,以确保它可以正确管理和显示这些翻译。
本地化是什么?
这涉及到将您的本地优化应用程序翻译成特定的本地语言。翻译应用程序的文本只是本地化的一部分。它可能还包括其他区域特有的元素,例如文本方向、数字形式以及日期和时间格式等。
React Router 允许您构建本地化路由。例如,为了管理语言选择,您可能为各种语言(如"/en/about"和"/fr/about")设置多个路由,或者您可以使用上下文或状态。
我们已经学到了很多,我们的知识已经大大增加。接下来,我们将进入本章的倒数第二节,我们将学习如何在我们的 React 应用程序中添加翻译和格式化消息。我们刚刚学习了国际化本地化,这是我们为不同语言准备应用程序的地方。现在,让我们学习如何在编写的代码中实现不同的语言。
添加翻译和格式化消息
将应用程序中的文本内容从一种语言翻译成另一种语言,并按照区域习俗和标准进行翻译的过程被称为翻译和格式化消息。我们可以利用像FormatJS这样的库将翻译和格式化消息添加到 React 应用程序中。React Router 默认不允许翻译或本地化;然而,它可以很容易地与 FormatJS(或类似包)一起使用来构建国际化路由系统。
让我们看看一个使用该库的代码示例,看看它可能是什么样子:
import { IntlProvider, FormattedMessage } from
'react-intl';
import English from './translations/en.json';
import French from './translations/fr.json';
const Home = () => (
<div>
<h2>
<FormattedMessage id="home.title" />
</h2>
<p>
<FormattedMessage id="home.welcome" />
</p>
</div>
);
// We can assume that we are able to get the user's preferred language from somewhere like in user or browser settings...
const userLanguage = 'fr';
// This value can be dynamically created.
const messages = {
en: English,
fr: French,
};
const App = () => (
<IntlProvider locale={userLanguage} messages=
{messages[userLanguage]}>
<Home />
</IntlProvider>
);
export default App;
在这个例子中,应用程序已经设置了英语和法语翻译。默认语言是硬编码为法语;然而,在实际应用中,它可以根据用户在浏览器中设置的某种语言设置动态生成。en和fr语言代码映射到由messages对象导入的翻译文件。
翻译和格式化消息——它们究竟是什么?我们马上就会找到答案。所以,继续阅读。
翻译是什么?
这通常指的是在程序中将文本从一种语言翻译成另一种语言。在软件中,我们通常保留许多包含每种支持语言翻译文本的语言文件(通常在 JSON 或类似格式中)。这使得应用程序能够根据用户的偏好或区域设置动态显示适当的语言。
格式化消息是什么?
许多应用程序需要在翻译的字符串中显示动态内容,而不仅仅是直接的翻译。格式化通信在这方面发挥作用。使用格式化消息,您可以管理复数规则,向您的翻译字符串添加变量,使用区域标准格式化日期和数字,等等。
通过结合翻译和格式化消息,我们可以开发出易于适应各种语言和位置的应用程序,从而提高可访问性和用户体验。
我们的进展非常出色——只剩下最后一部分,我们就会完成这一章。最后一部分将介绍传递参数和占位符。到目前为止,我们已经学会了如何向单页应用添加数据;然而,在实际应用中,我们在单页应用中有多条路由。因此,在下一节中,我们将学习如何传递参数和占位符,以便在我们的应用程序中实现动态路由。
传递参数和占位符
React 和 JavaScript(一般而言),以及 React Router(具体而言),支持传递占位符和参数。然而,对于动态路由和数据在路由之间的传输,当与 React Router 结合使用时,这些方法工作得很好。
我们如何传递参数?
通过使用多种技术,例如 URL 参数、查询参数或 history prop 的状态对象,我们可以将数据发送到由 React Router 显示的组件。通常,这是为了将精确的数据从一个路由传递到另一个路由。
这个示例展示了当我们使用 history prop 的状态对象时传递参数的过程:
import { Link } from "react-router-dom";
<Link
to={{
pathname: "/route",
state: { myData: "Hello, World!" }
}}
>
My Link
</Link>
接收组件揭示了我们可以如何访问我们传递的数据:
import { useLocation } from "react-router-dom";
function MyComponent() {
const location = useLocation();
console.log(location.state.myData);
// Outputs: "Hello, World!"
// ...
}
最后,我们将学习如何使用占位符(URL 参数)。
我们如何使用占位符?
URL 参数,这些参数是 URL 的一部分,可以根据您希望显示的内容而变化,但仍然渲染相同的核心组件,由 React Router 支持。这些通常用于开发动态路由。
这里是一个如何在组件中创建路由并利用 URL 参数的示例:
import { Route, useParams } from "react-router-dom";
function MyComponent() {
let { id } = useParams();
return <h2>My id is: {id}</h2>;
}
function App() {
return (
<Route path="/post/:id">
<MyComponent />
</Route>
);
}
在这个示例中,id占位符可以依赖于任何值。当你访问"/post/123"时,MyComponent组件会渲染,并且useParams()返回一个包含{ id: "123"}的对象。
使用 React Router 构建动态和响应式应用需要参数和占位符。
概述
随着本章的结束,很明显,在 React 应用的路由和国际化环境中进行探索之旅既困难又令人满意。我们考察、探究并揭开了几个关键主题的复杂性,每个主题都对开发全面、交互式和国际化的应用做出了贡献。
我们从学习 React Router 开始,它是我们应用显示的可靠导航器。我们研究了路由,了解了它们的许多类型,并学习了如何在我们的应用中有效地使用它们。我们对获取 URL 参数和分层路由的研究扩大了我们的专业知识,使我们能够在应用内部设计复杂和精细的路径。
然后,我们将焦点转向国际化本地化,拓宽视野以确保世界各地的人们都能与我们的应用互动。我们认识到打破语言障碍的重要性及其对用户体验的巨大影响。学习如何添加翻译和格式化消息使我们能够用用户的母语与他们建立联系,使我们的应用成为全球性的实体。我们还通过学习如何使用占位符和将参数传递给消息来发现创建动态和响应式翻译的强大功能。
这些能力帮助我们成为高效的开发者,能够创建不仅有用而且无处不在的程序。这条道路为我们提供了设计和构建不仅响应性强、弹性好,而且全球化和包容性的在线应用程序所需的工具。随着我们完成这一章,思考一下你学到了什么,但请记住,这仅仅是你的更大旅程中的一个站点。继续探索、学习和,最重要的是,继续你的成长,追随与你共鸣最多的道路。
在下一章中,我们将学习更多高级的 ReactJS 概念,例如错误边框、端口、高阶组件、并发渲染和转发引用。因此,让我们为新的冒险做好准备,随着我们知识的增长。
第五章:ReactJS 的高级概念
每个网页开发者都应该对 React 的基础知识、核心概念、Hooks 和路由导航有深入的了解,以便在 React 技术栈中建立成功的职业生涯。但如果你想要将你的 React 技能提升到下一个层次,你应该能够通过应用高级 React 概念,如 Portals、错误边界、并发渲染特性、分析器等,来构建生产级别的应用。虽然其中一些概念很久以前就被引入,并且随着每个主要版本的更新而得到改进,但其他高级概念只在新版本中引入。
在本章中,您将了解 ReactJS 的高级概念,以便您可以在各种实时用例中使用它们。将讨论错误边界、Portals、并发渲染、Suspense 等高级概念,以及与代码质量和性能优化相关的特性,如严格模式、静态类型检查和分析器,以涵盖中级到高级开发者的面试问题。最后,我们将快速探讨一些与 React Native 相关的问题,这些问题旨在针对 iOS 和 Android 等移动环境。
在本章中,我们将涵盖以下主要主题:
-
探索 Portals
-
理解错误边界
-
使用 Suspense API 管理异步操作
-
使用并发渲染优化渲染性能
-
使用 Profiler API 调试 React 应用程序
-
严格模式
-
静态类型检查
-
移动环境中的 React 及其特性
本章的主要目标是让您对 React 的高级概念有一个清晰的理解,并解决那些用来测试求职者高级技能水平的面试问题。
探索 Portals
现在,在网页上使用模型窗口或弹出窗口来快速吸引用户的注意力是非常常见的。它们有助于通知用户一些重要信息或请求用户输入。但在大型应用中实现这些小部件是具有挑战性的,因为它涉及到编写复杂的 CSS 代码和处理 DOM 层级。幸运的是,React 提供了 Portals 功能来解决这类用例。
Portals 在 2017 年被引入,首次出现在 React 版本 16 中。它们用于在 DOM 层级之外渲染 React 组件。Portals 的使用并不典型,但在某些特定用例中非常有帮助,您将在以下小节中看到。
什么是 Portals?您如何创建它们?
React Portals 允许您将子组件渲染到存在于父 DOM 层级之外的 DOM 节点。尽管您在父组件之外渲染子组件,但组件之间仍然存在父子关系。
可以通过调用从 react-dom 包导入的 createPortal 函数来创建一个 React Portal。此函数接受两个必需参数和一个可选参数:
-
Children: 可以用 React 渲染的任何 JSX 代码。 -
DOMNode: 需要在其中渲染门户内容的 DOM 节点。 -
Key: 用于在组件树中区分门户的唯一标识符。这是可选的。
以下模态窗口的示例显示了如何在根树层次结构之外的特定 DOM 节点处创建门户:
import { createPortal } from 'react-dom';
const ModalWindow =({ description, isOpen, onClose })=> {
if (!isOpen) return null;
return createPortal(
<div className="modal">
<span>{description}</span>
<button onClick={onClose}>Close</button>
</div>
,document.body);
}
在前面的代码中,门户返回一个 React 节点,该节点可以在组件树中的任何位置渲染。在这个例子中,返回的节点将是一个模态小部件。这个模态已经被附加到文档主体上,并且与 HTML 中的root节点处于同一级别。
注意
常规做法是将顶级节点命名为root,因为其中的一切都将由 React 管理。仅使用 React 构建的应用程序通常只有一个根节点。但是,如果您正在将 React 集成到现有应用程序中,您可能有许多独立的根 DOM 节点。
任何 React 组件都可以将前面的门户用作子组件:
function ParentComponent() {
const [open, setOpen] = useState(false);
return (
<div className="container">
<button onClick={() => setOpen(true)}>Open Modal</button>
<Modal
message="This is a portal modal!"
isOpen={open}
onClose={() => setOpen(false)}
/>
</div>
);
}
没有对特定组件或应用程序中可使用的门户数量的限制。使用门户,您还可以将 React 组件渲染到非 React 服务器标记(如静态或服务器端渲染的页面)和非 React DOM 节点中,这些节点由 React 之外管理。
门户的常见用例有哪些?
在可以直观地看到子组件从父容器中分离的应用程序中,门户非常有用。最常见的用例如下列出:
-
Modal windows or dialogue components: 门户可用于创建大型对话框或模态窗口,这些窗口可以浮在网页的其余部分之上,而无需担心父组件。
-
如果使用
overflow:hidden或z-index样式,那么在门户内部创建的工具提示不会从其父容器中切断。 -
Loaders: 当后台任务(如从数据库获取数据)正在进行时,在现代网络中显示加载屏幕是合理的。这有助于阻止用户在后台任务完成之前与应用程序交互。
-
Popovers: Popovers 对于快速提供上下文信息非常有用。例如,可以使用个人资料卡片来显示用户个人资料信息,而无需点击并访问个人资料本身。您只需将鼠标悬停在图标或按钮元素上即可阅读详细信息。
-
Cookie alerts: 可以创建 cookie 警报(或横幅),以便访客在访问网站时选择允许跟踪哪些 cookie。
-
Drop-down menus: 如果下拉菜单显示在具有隐藏溢出样式的父组件内部,则可以将其创建为门户。
注意
通过将子组件移出主组件树,渲染性能将得到优化,因为组件不会在每次状态更新时重新渲染。此外,它提供了抽象的灵活性。
门户内部的事件冒泡是如何工作的?
即使 portal 存在于 DOM 树的某个位置,它通过支持所有组件功能(如访问 props、state、context 和事件传播)来保留其在 React 组件树中的位置。这意味着事件冒泡也适用于 portals。
portals 中事件冒泡的行为类似于 React 子组件在组件树内部触发事件。从 portals 触发的事件将向上传播到包含的 React 树中的祖先元素,即使这些元素在 DOM 树中不是祖先。例如,在以下 HTML 代码中,位于主根(#main-root)下的父组件可以捕获一个未捕获的冒泡事件,该事件来自使用 portals 实现的兄弟节点(#dialog-root):
<html>
<body>
<div id="main-root"></div>
<div id="dialog-root"></div>
</body>
</html>
注意
portals 中的事件冒泡遵循 React 树而不是 DOM 树。
在 portals 中采取了哪些可访问性预防措施?
你需要确保使用 portals 构建的 React 应用程序对残疾人士也是可访问的。例如,当你在不同模态窗口和父网页之间移动焦点时,键盘焦点应该自然工作。作为 portals 部分创建的模态对话框应遵循 WAI-ARIA 模态编写实践(www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/)。
实现键盘可访问性的部分指南如下:
-
当对话框或模态框打开时,焦点会移动到对话框内的一个元素。
-
使用制表符键在可聚焦元素之间循环时,应仅遍历对话框元素。焦点不应跳过已打开的对话框。
-
按下 Esc 键后,对话框应关闭。
如果你打算使用第三方库来创建模态框,你需要确保该库遵循所需的可访问性指南。
在构建应用程序时,你总会遇到意外的错误。这些错误可能以多种方式发生,例如通过网络请求、调用第三方 API、访问不存在的嵌套对象属性等。错误边界主要在 React 应用程序中用于处理这些类型的错误。
理解错误边界
在 React 应用程序中,你可以以两种可能的方式处理错误。第一种方法是在命令式代码块中使用 try..catch 块来处理错误,类似于常规事件处理器。第二种方法是使用 错误边界。这些用于处理将在屏幕上渲染的声明式组件代码。
React 团队在 React 版本 16 中引入了错误边界作为其一部分。React 库中没有创建用于错误边界的官方组件,因此你需要自己创建错误边界组件。
什么是错误边界?
错误边界只是具有特定任务列表的 React 组件。它们用于捕获其子组件树中可能发生的 JavaScript 错误,记录这些特定错误,并将屏幕重定向到回退 UI 以从错误状态中恢复。该组件有助于防止整个组件树因为树中某个地方发生的错误而崩溃。
错误边界在渲染期间、生命周期方法和整个组件树的构造函数中捕获错误。可以通过定义以下生命周期方法之一或两者来使用类组件创建错误边界:
-
static getDerivedStateFromError:此方法用于在抛出错误后渲染回退 UI -
componentDidCatch:此方法用于记录错误信息
可以使用这两种方法之一创建错误边界来保护应用程序免受崩溃的影响。以下是实现方式:
class MyErrorBoundary extends Component {
constructor(props) {
super(props);
this.state = { isErrorThrown: false };
}
static getDerivedStateFromError(error) {
return { isErrorThrown: true };
}
componentDidCatch(error, errorInfo) {
logErrorToReportingService(error, errorInfo);
}
render() {
if (this.state.isErrorThrown) {
return <h1>Oops, the application is unavaialble.</h1>;
}
return this.props.children;
}
}
如果任何生命周期方法在渲染阶段发生错误,将调用getDerivedStateFromError方法。在这个方法中,你可以更新错误状态标志变量的值,以反映下一次渲染中的回退 UI。根据错误状态变量,render方法将在屏幕上更新 UI。同时,可以使用componentDidCatch方法将相同的错误报告给日志服务,用于调试目的。
一旦创建了错误边界,它就可以作为一个常规的 React 组件使用。错误边界组件需要围绕你怀疑可能存在任何类型错误的最顶层 React 组件进行包装。组件的使用方式如下:
<MyErrorBoundary>
<MyComponent />
</MyErrorBoundary>
前面的错误边界捕获MyComponent组件树中抛出的任何错误,并防止应用程序崩溃。
注意
你还可以用不同的一组错误消息包裹单个组件的错误边界,以防止它们破坏页面的其他部分。错误边界的设计决策取决于业务需求和 UX 设计。
如果包含的错误边界未能捕获错误,错误将传播到其周围的下一个最近的错误边界。这种行为类似于catch()块,它将错误传播到下一个最近的捕获块。
像 Jest 这样的流行测试框架可以用来为错误边界编写类似于任何其他 React 组件的单元测试。单元测试应该模拟 React 组件(错误边界所包裹的组件)中的错误,并验证错误边界是否能够捕获错误并正确渲染回退 UI。也可以通过强制选定的组件进入错误(红色按钮)状态来使用 React DevTools 验证错误边界。
是否可以创建一个作为函数组件的错误边界?
在撰写本文时,无法使用最新的 React 版本创建错误边界作为函数组件 - 也就是说,您只能使用类组件创建错误边界。此外,您可以通过重用社区中的react-error-boundary (github.com/bvaughn/react-error-boundary)包来避免编写错误边界类。
何时错误边界不起作用?
在以下场景中,错误边界不会捕获错误:
-
onClick、onChange和其他在渲染阶段不使用,因此不需要错误边界来从错误中恢复 UI -
setTimeout、requestAnimationFrame和其他 -
服务器端渲染:React 不支持在服务器上使用错误边界
-
当错误边界内部有错误时:React 无法捕获在错误边界本身抛出的错误
对于上述情况(除了最后一个情况外),您可能需要选择常规的 JavaScript try..catch语句或promise#catch()块来处理错误,确保在错误边界中没有发生错误。
就像错误边界用于在应用程序中捕获任何错误并显示回退 UI 一样,Suspense API 用于在子组件完成加载之前显示回退 UI。
使用 Suspense API 管理异步操作
Suspense 功能是在 React 版本 16 中引入的,与错误边界同时出现。最初,它仅用于与lazy API 一起进行代码拆分,并且不能用于服务器端渲染。React18 改进了 Suspense API,使其能够支持许多用例,包括服务器端渲染和异步操作,如数据获取。
什么是 Suspense API?如何使用它?
Suspense API 用于显示回退 UI,如加载指示器,直到其子组件准备好渲染。suspense 组件接受一个fallback属性来渲染一个替代 UI,如果其子组件尚未完成渲染。您可以在应用的最顶层或应用的各个部分使用 suspense 组件。
让我们通过以下示例学习如何使用 Suspense 功能。
考虑一个简单的用例,即从特定作者那里加载博客文章。在这里,博客文章组件(即<Posts/>)在获取文章列表时挂起。在内容准备好显示之前,React 切换到最近的 suspense 边界来显示回退加载指示器(即<Loading/>),而不是显示文章列表:
import { Suspense } from "react";
import Posts from "./posts.js";
export default function Author({ author }) {
return (
<>
<h1>{author.name}</h1>
<span>{author.age}</span>
<Suspense fallback={<Loading />}>
<Posts authorId={author.id} />
</Suspense>
</>
);
}
function Loading() {
return <h2>Loading...</h2>;
}
一旦博客文章数据已被获取,React 会切换回显示实际的博客文章数据。
您还可以将更新列表和显示状态的内容延迟到新结果准备就绪。通过传递查询到useDeferredValue Hook,可以实现这种替代 UI 模式:
const deferredAuthorDetails = useDeferredValue(author);
在传统应用程序中,你需要使用 isLoading 数据标志变量来指示数据获取是否完成,并在屏幕上显示相应的内容。然而,如果你使用 Suspense 功能,React 会自动确定是否显示回退 UI 或组件数据,而不依赖于任何额外的标志。
注意
只有启用了 suspense 的框架才与 Suspense 功能集成,以将加载状态传达给 React。
我可以使用 suspense 组件进行任何类型的数据获取吗?
suspense 组件无法检测 effect 或事件处理器内部的数据获取。它只能用于以下启用了 suspense 的数据源:
-
使用启用了 suspense 的有意见框架(如 Relay、Next.js、Remix 和 Hydrogen)进行数据获取
-
使用
lazyAPI 懒加载组件代码
在编写本文时,不支持在没有框架的情况下使用 Suspense 功能。然而,React 团队有一个计划,在未来的版本中提供官方 API 以将数据源与 suspense 组件集成。
你是如何在更新过程中防止不必要的回退的?
如果可见的 UI 被回退替换,将会有闪烁的用户体验。这不是一个好的用户体验。这种情况发生在状态更新导致组件挂起,但最近的 suspense 边界已经向用户显示了一些回退内容时。你可以通过使用 startTransition API 将状态更新标记为非紧急来避免这些不必要的回退。
考虑一个在应用程序中导航页面并应用页面更新过渡以防止不必要的回退的例子:
function navigate(url) {
startTransition(() => {
setPage(url);
});
}
在过渡期间,React 将等待内容加载,而不会重新触发 suspense 回退 UI 来隐藏已经显示的内容。
注意
React 只会防止非紧急更新的不必要的回退。它不会延迟任何紧急更新的渲染。
在过去,React 只能一次处理一个任务,渲染过程是同步的。一旦任务开始,就无法中断。这被称为阻塞渲染。后来,通过引入并发模式解决了这个问题,如果存在另一个紧急任务,并发模式可以中断任务。并发模式最初作为一个实验性功能引入,并在 React 版本 18 中被并发渲染功能取代。
使用并发渲染优化渲染性能
React 18 引入了并发渲染器,这使得渲染过程异步,并确保它可以被中断、暂停、恢复,甚至放弃。因此,React 可以快速响应用户交互,即使它正在进行繁重的渲染任务。
新功能,如 suspense、流式服务器渲染和过渡,都是由并发渲染驱动的。
你如何在 React 中启用并发渲染?
首先,你需要将react和react-dom包更新到版本 18。之后,你需要将已弃用的ReactDOM.render方法替换为ReactDOM.createRoot方法。并发渲染将在你使用并发功能(如 suspense、流式服务器渲染和过渡)的应用程序部分自动启用。
随着应用程序变得复杂,你需要花费大量时间分析应用程序的性能。在向客户交付之前,测量应用程序性能的特征尤为重要。尽管你可以使用浏览器的 User Timing API(Web API)来测量组件的渲染成本,但 React 团队已经创建了更好的替代方案。例如,Profiler API 可以帮助识别 React 应用程序中的性能瓶颈。
使用 Profiler API 调试 React 应用程序
如果你正在基准测试 React 应用程序的性能,那么跟踪你的组件重渲染的次数以及每次重渲染的成本将帮助你识别应用程序中的缺陷区域或部分。React 提供了两种不同的方式来衡量应用程序的渲染性能:React Profiler API和React DevTools的 Profiler 标签。考虑到它支持 Suspense 功能,推荐使用 React Profiler API。
你如何衡量渲染性能?
React 提供了 Profiler API 以编程方式测量组件树的渲染性能。该组件有两个属性:id属性,用于识别正在测量的 UI 部分,以及onRender回调,每次树更新时都会调用。
回调函数接收诸如id、phase、actualDuration、baseDuration、startTime和commitTime等参数,这些参数用于记录渲染时间。
假设你对一个存在于在线书店应用程序中的作者简介组件的渲染性能表示怀疑,并希望对其进行分析。在这种情况下,AuthorBio组件需要被Profiler组件包裹,并附带onRender回调。这看起来如下所示:
<App>
<Profiler id="bio" onRender={onRender}>
<AuthorBio />
</Profiler>
<Posts />
</App>
你还可以使用多个Profiler组件来衡量应用程序的不同部分。
JavaScript 在 ECMAScript5 中提供了严格模式作为一项新特性,以强制执行 JavaScript 的受限版本。此特性在编写代码时带来了更严格的规则,并在违反这些规则时抛出错误。可以通过在文件顶部添加use strict行来启用严格模式。同样,React 提供了StrictMode组件作为开发工具,用于在编写 React 代码时强制执行更严格的警告和检查。
严格模式
React 团队引入了严格模式作为调试工具,用于识别网络应用程序中可能存在的错误或问题。此工具作为 React API 中的 StrictMode 组件提供。它不会渲染任何类似于 Fragment 组件的 UI。此功能仅适用于开发模式 – 它不会影响生产环境中的行为。本节重点介绍重要的严格模式概念和可能在面试中提出的问题。
你如何启用严格模式?
你可以通过将整个应用包裹在根组件周围来为整个应用启用严格模式,如下所示:
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
const root = createRoot(document.getElementById("root"));
root.render(
<StrictMode>
<App />
</StrictMode>
);
你还可以仅在应用程序的某些部分(即不是整个应用程序)中使用严格模式,如果你认为这些部分有很高的错误可能性。考虑将严格模式用于应用程序页面主体。它应该看起来像这样:
<>
<Navigation>
<Details>
<StrictMode>
<Services />
<Support />
</StrictMode>
</Details>
<Footer />
</Navigation>
</>;
大多数时候,React 开发者会遇到渲染部分逻辑不当的问题,以及 effects Hook 内缺少清理代码的问题。这些类型的错误可以通过严格模式轻松识别。
你能描述一下严格模式启用的仅限开发模式的检查列表吗?
严格模式启用了以下仅限开发模式的检查列表,以在早期开发过程中查找常见错误:
-
组件将重新渲染一次以查找由不纯渲染引起的错误
-
组件将重新运行一次效果以查找由效果缺少清理引起的错误
-
组件将被验证是否使用了已弃用的 API,并将通过警告通知用户
这些检查仅适用于开发目的。它们不会对生产构建产生影响。
严格模式的两次渲染过程中调用哪些函数?
严格模式在开发模式下会调用以下列表中的函数两次:
-
函数的组件体,不包括事件处理程序内部的代码
-
传递给 Hook 的函数,如
useState、useReducer和useMemo -
状态更新函数
-
类组件方法,如
constructor、render、shouldComponentUpdate和getDerivedStateFromProps
如果你的函数不纯,在开发模式下运行两次将影响预期的输出。这个结果可以帮助你尽早识别代码中的任何错误。
除了严格模式之外,你还可以在 React 应用程序中使用静态类型检查来避免运行时出现的错误和错误。随着你的应用程序增长,你可以使用类型检查捕获许多错误。
静态类型检查
React 基于 JavaScript,JavaScript 是一种弱类型语言。因此,在 React 中我们没有默认的静态类型检查功能。
在其旧版本(<15.5)中,React 有PropTypes验证器,以便在应用程序中执行简单的类型检查。在那之后,这个库从 React 的核心模块中移出,并作为一个独立的库创建,即prop-types(prop-types 包)。如今,PropTypes在现代 React 应用程序中不常用。尽管在 React 中静态类型检查不是强制性的,但在面试中您可能会遇到一些与静态类型检查相关的问题。
静态类型检查有哪些好处?
在 JavaScript 应用程序中进行静态类型检查有许多好处。其中一些列在这里:
-
可以在运行时之前(即在编译时)识别类型错误
-
可以在早期阶段检测 bug 和错误
-
优化且易于阅读
-
更好的 IDE 支持
-
可以生成文档
如果您尽早发现错误,修复它们会更便宜。
您如何在 React 应用程序中实现静态类型检查?
在 React 中,有多种方式可以实现静态类型检查,但以下两种方式是最好的:
-
TypeScript
-
Flow
这两个静态类型检查器可以帮助您在运行代码之前识别某些类型的错误。由于 TypeScript 稳健且社区支持度最高,让我们看看它如何在 React 中实现。
TypeScript 是由微软创建的,被认为是 JavaScript 的超集。它自带编译器,可以在构建时捕获错误和 bug。它支持 JSX,并且可以无问题地使用 React hooks。如今,TypeScript 可以通过添加各种选项来支持主要框架,如下所示:
-
Next.js:
npx create-next-app@latest --ts -
Remix:
npx create-remix@latest -
Gatsby:
npm init gatsby –ts -
Expo:
npx create-expo-app -t expo-template-blank-typescript
如果您没有使用这些框架,您需要遵循以下手动步骤在 React 应用程序中设置 TypeScript:
-
npm或yarn包管理器:tsc compiler (that is, the TypeScript compiler) so that you can build the application. -
通过以下命令生成
tsconfig.json文件:npx tsc --init常用的选项包括 TypeScript 文件的源目录和输出文件夹的生成 JavaScript 文件。配置看起来像这样:
//tsconfig.json { "compilerOptions": { // ... "rootDir": "src", "outDir": "dist" // ... }, }您可以添加更多配置选项,如这里所述:TypeScript 配置选项。TypeScript 的 React 启动器提供了一个包含良好规则的配置文件。
-
.ts扩展名或.tsx扩展名用于包含 JSX 代码的文件。 -
DefinitelyTyped(DefinitelyTyped 仓库)或创建一个本地的声明文件。
现在,您可以使用 TypeScript 包中的tsc命令构建 TypeScript 项目。
最初,React 主要用于 Web 开发。如今,它也可以用于移动、桌面和 VR 应用。React Native 是一个独立的库,是为了支持移动设备而创建的。它基于与 React 相同的概念,但使用原生组件而不是 Web 组件在屏幕上渲染。
移动环境中的 React 及其特性
当 Facebook 最初决定将其服务扩展到移动设备时,它决定基于 HTML5 运行移动页面,而不是构建当时许多科技巨头更喜欢的原生应用。然而,它最终遇到了用户体验和性能开销的问题。
2013 年,Facebook 团队发现了一种使用 JavaScript 生成 iOS 应用 UI 元素的方法。这个想法对移动应用很成功,后来 React Native 也支持了 Android 设备。
本节将重点关注 React Native,这样我们可以超越 ReactJS 概念,涵盖与架构、导航及其与 ReactJS 的区别相关的重要主题,这些可能在 React 面试中有所期待。
什么是 React Native?
React Native 是一个流行的基于 JavaScript 的移动应用框架,用于为 iOS、Android 和 Windows 构建原生渲染的移动应用。这个库的主要优势是你可以使用一个在多个平台上运行的单一代码库。
Facebook 团队在 2015 年开源了 React Native。仅仅几年后,这个库就成为了移动开发中最受欢迎的解决方案之一,现在被用于 Facebook、Instagram、Skype、Uber 等流行移动应用中。
React 和 React Native 之间的区别是什么?
React Native 基于 React 库,它们共享许多概念。但也有一些主要区别,如下所示:
| React | React Native |
|---|---|
| 它用于开发 Web 应用 | 它用于开发移动应用 |
使用 react-router 库进行页面导航 |
使用内置的导航库进行页面导航 |
| 使用虚拟 DOM 来渲染网页 | 使用原生 API 来渲染页面 |
| React 使用 HTML、CSS 和 JavaScript 来创建用户界面 | React Native 使用原生组件和 API 来构建应用 |
| 它使用 JavaScript 和 CSS 库进行动画 | 它自带内置的动画库 |
表 5.1:React 与 React Native 对比
因此,React Native 是在 React 库之上构建的一个附加库,用于创建原生应用,这个原生库有自己的架构。
你能根据线程模型描述 React Native 的架构吗?
Fabric 是由 Facebook 团队创建的新渲染架构,甚至他们的应用程序也是由这个渲染器支持的。该架构的核心原则是在 C++ 中统一渲染器逻辑,并优化主机平台之间的互操作性。它基于线程模型,类似于旧架构,但功能不同,以优化用户体验,使其优于原生应用程序。
在旧架构中,React Native 桥被用于在 JavaScript 和原生模块之间进行通信。但它有其局限性——例如,通信只能通过异步操作进行,并且需要将数据序列化为 JSON 或反序列化为 JSON。在新架构中,此桥组件被JavaScript 接口(JSI)所取代。
让我们看看在新渲染架构中各种组件是如何进行通信的:

图 5.1:Fabric 渲染架构
在每个 React 应用程序中,无论使用的是旧渲染器还是新渲染器,都会运行三个并行线程:
-
UI 线程或主线程:此线程负责处理 iOS 和 Android 主视图。它处理一些原生交互,例如点击按钮、用户手势事件、滚动等。
-
JS 线程:此线程负责处理你的 React Native 应用程序的所有逻辑。它负责处理代码中编写的所有 DOM 层级操作并执行它们。之后,代码被发送到原生模块线程进行优化。
-
阴影或背景线程:此线程负责布局计算,例如元素的位置、高度和宽度,然后将它们转换为原生元素。
在旧架构中,桥组件用于通过序列化和反序列化数据以异步方式在 JS 线程和 UI 线程之间进行通信。因此,内存管理和应用程序性能变得过载。在新架构中,桥组件已被 JSI 取代,以实现原生代码和 JavaScript 代码之间的有效通信。JSI 是一个轻量级层,其中用 C++ 编写的函数可以被 JavaScript 引擎(如 JavaScript Core(JSC)或 Hermes)直接调用或调用原生代码中的方法。
新架构的工作流程如下:
-
当用户点击移动应用程序的应用图标时,Fabric 渲染系统直接加载原生端而不是打开原生模块。
-
渲染系统在准备好后通知 JS 线程。之后,JS 线程加载名为
main.bundle.js的最终包,其中包含 JavaScript 代码、React 逻辑及其组件。 -
JS 代码通过 ref 原生函数被调用,该函数已通过 JSI API 暴露给 Fabric。
-
阴影线程内的瑜伽引擎执行布局计算,将基于 Flexbox 的样式转换为宿主布局等。
-
最后,组件将在屏幕上渲染。
此外,新架构中增加了两个新组件:Turbo module 和 CodeGen。
Turbo module 是原生模块(存在于旧架构中)的改进版本,通过懒加载模块与 JavaScript 和平台原生代码进行通信,以改善启动性能。CodeGen 静态类型检查器有助于沟通动态 JavaScript 代码和作为静态类型 C++ 编写的 JSI 代码。
如何在 React Native 中执行导航?
React Native 使用 react-navigation 库在原生应用之间进行页面导航。多个屏幕之间的转换由各种类型的导航器管理,例如堆栈导航器、抽屉导航器和标签导航器。在多个屏幕之间导航时,您还可以在它们之间传递数据。
React Navigation 由核心实用工具组成,这些工具被导航器用于在您的应用中创建导航结构。该包可以使用以下命令安装:
npm install @react-navigation/native
React Navigation 中的每个导航器都存在于自己的库中。例如,如果您想使用 native-stack 导航器,应单独使用以下命令进行安装:
npm install @react-navigation/native-stack
堆栈导航器为您的应用提供了在屏幕之间切换和管理导航历史的方法。这种行为类似于网络浏览器处理导航历史的方式。它还提供了在堆栈内导航页面时可能期望在 Android 和 iOS 设备上出现的手势和动画。
下面是一个基于堆栈导航器创建的组织网站导航菜单项的示例:
import * as React from "react";
import { View, Text } from "react-native";
import { NavigationContainer } from "@react-navigation/native";
import { createNativeStackNavigator } from "@react-navigation/native-stack";
import HomeScreen from "components/HomeScreen";
import ServicesScreen from "components/ServicesScreen";
const Stack = createNativeStackNavigator();
function App() {
return (
<NavigationContainer>
<Stack.Navigator>
<Stack.Screen name="Home" component={HomeScreen} />
<Stack.Screen name="Services" component={ServicesScreen} />
</Stack.Navigator>
</NavigationContainer>
);
}
export default App;
在前面的代码中,我们创建了一个堆栈导航菜单,用于将用户重定向到网站的重要屏幕,例如主页和服务页面。
此外,您可以使用 Navigation API 嵌套导航器。
新架构有哪些好处?
React Native 的新架构在用户体验、代码质量、性能和可扩展性方面带来了许多好处。我们在这里列出了一些:
-
更好的互操作性:在旧架构中,当您尝试将 React 视图嵌入宿主视图时,会出现布局跳跃问题。这是因为 React Native 的布局是异步的。新的渲染器通过同步渲染 React 页面提供了改进的互操作性。
-
更好的数据获取行为:通过集成 React 的 Suspense 功能,数据获取用户体验得到了改善。
-
类型安全:代码生成确保了 JS 和平台层之间的类型安全。它使用 JavaScript 组件声明来生成 C++ 结构体以保存属性。从 JS 规范生成的代码必须通过 Flow 或 TypeScript 进行类型化。
-
同步执行:这提高了用户体验。现在,可以同步执行函数,而不是异步执行。
-
并发:JavaScript 可以调用在不同线程上执行的功能。
-
共享的 C++代码:新的渲染器是用 C++实现的。因此,可以编写平台无关的代码,并在不同平台之间共享。
-
性能提升:在新渲染架构中,可以识别特定平台的全部限制,并为 iOS 和 Android 的使用提供了解决方案。最初,视图展平解决方案仅在 Android 上可用,但现在默认适用于两个平台。
-
更快的启动:由于主组件默认是懒加载的,因此启动时间将更快。
-
一致性:由于新的渲染系统是跨平台的,因此组件的行为在各个平台之间是一致的。
-
更少的开销:不再需要在 JavaScript 和 UI 层之间进行序列化和反序列化。
使用旧架构无法实现这些好处。
视图展平是什么?
React API 的声明性和组合特性允许你创建深层次的 React 元素树,其中大多数节点仅影响屏幕的布局,而不是在屏幕上渲染。这些节点被称为仅布局节点。大量的仅布局节点会导致渲染时的性能下降。
渲染器实现了视图展平算法以提升性能。视图展平是一种优化算法,由 React Native 渲染器使用,以避免深层次布局树。这种机制合并或展平这些仅用于布局的节点类型,并减少屏幕上显示的主视图层次结构的深度。
这个过程可以用MyLogoComponent来解释,它包含具有边距和填充样式的视图容器组件:
function MyLogoComponent() {
return (
<View>
<View style={{margin: 10}} >
<View style={{padding: 20}}>
<Image {...} />
<Text {...}>This is a caption</Text>
</View>
</View>
</View>
);
}
在前面的代码中,在容器和组件的实际内容之间添加了两个主视图(即<View style={..}>),以对内部内容应用结构样式。
视图展平算法作为渲染器差异阶段的组成部分集成,并将第二和第三个视图的样式合并到第一个视图中。这样,它避免了创建和渲染两个额外的主视图的需求。
以下图表显示了使用此机制没有深层次布局树的原生屏幕的外观:

图 5.2:合并视图的原生屏幕
应用此视图展平算法后,将不会出现任何可见的变化。
在本节中,我们介绍了一些 React Native 的重要基本概念,这些概念你可能在 React 面试中遇到。你可能会被问到这些问题来测试你对 React 技术栈的了解。本节也标志着本章的结束,其中我们涵盖了 React 生态系统中的广泛高级主题。
摘要
本章介绍了一系列你可能在 ReactJS 面试中遇到的高级概念。我们首先介绍了新特性,如处理模态窗口的 portals、防止应用因错误而崩溃的错误边界,以及 Suspense 特性,它为耗时较长的后台任务显示替代 UI。之后,我们讨论了与并发渲染相关的话题,它支持提高渲染性能的特性,接着是 Profiler API,它可以用来检测应用程序特定部分的渲染成本。
然后,我们讨论了仅用于开发的特性,如严格模式和静态类型,这些特性有助于我们避免在代码中遇到任何可能的错误和 bug。最后,我们介绍了在移动环境中以及 React Native,以及它与 ReactJS 的区别,包括其内部结构和渲染架构。
在本章中,我们帮助你学习了高级概念、它们的重要性以及 React 开发中的最佳实践。因此,这本书将提高你的 React 技能,让你成为专家,并在竞争激烈的就业市场中脱颖而出。
在下一章中,我们将了解 React 中流行的状态管理解决方案。我们将从了解 Flux 模式和 Redux 架构开始,以便你理解 Redux 的基础。之后,我们将讨论核心原则、各种组件、处理异步请求、中间件以及调试 Redux 应用程序等重要话题。
第三部分:超越 React 和高级主题
在本部分中,你将了解流行的 React.js 状态库 Redux,以及与使用本地状态相比,在我们的项目中拥有全局状态存储可以带来极大的好处。我们还将探讨在 React.js 应用程序中使用 CSS 的多种方式以及每种方法的优缺点。
然后,我们将随着对测试的不同方式的学习来进行测试和调试,以使我们的代码更加可靠。最后,我们将了解 React.js 库 Next.js、Gatsby 和 Remix,看看它们如何帮助我们构建 React.js 应用程序。
本部分包含以下章节:
-
第六章,Redux:最佳状态管理解决方案
-
第七章,在 React.js 中应用 CSS 的不同方法
-
第八章,测试和调试 React 应用程序
-
第九章,使用 Next.js、Gatsby 和 Remix 框架进行快速开发
第六章:Redux:最佳状态管理解决方案
随着你的 JavaScript 单页应用程序的需求变得更加复杂,维护应用程序状态将变得具有挑战性。这种应用程序状态可以由服务器或 API 响应、本地组件状态以及 UI 状态(如分页控件、活动路由和选定的标签页)创建。可以通过你应用程序中的直接或间接模型或 UI 交互来更改状态。在某个时候,你可能会失去对何时、为什么以及如何更改状态的控制。这个问题已经被状态管理设计模式和库(如 Flux、Redux、MobX、Recoil、Rematch、Vuex 等)所解决。
选择正确的状态管理解决方案对于任何中型到大型规模的 React 应用程序至关重要。阅读本章后,你将能够流畅地回答有关 Flux 模式和 Redux 架构、核心原则、主要组件、处理异步数据流、中间件如 Saga 和 Thunk 以及 Redux DevTools 用于调试的问题。
在本章中,我们将涵盖以下主要内容:
-
理解 Flux 模式和 Redux
-
Redux 的核心原则、组件和 API
-
Redux 中间件 – Saga 和 Thunk
-
使用 RTK 标准化 Redux 逻辑
-
使用 Redux DevTools 调试应用程序
Redux 最初是为 React 应用程序创建的,并且在所有可用的状态管理库中都非常受欢迎。让我们在下一节中了解更多关于 Flux 模式、Redux 基础和核心概念,以便更好地理解 Redux 库。
理解 Flux 模式和 Redux
Flux 被创建为一种设计模式,用于管理 React 应用程序中的数据流。这是对 观察者 模式的一种轻微修改,该模式定义了一种订阅机制,其中任何对象的状态更改都会通知所有其他对象(en.wikipedia.org/wiki/Observer_pattern)。
2015 年,Redux 库被引入。它受到了 Flux 架构的启发,但实现方式不同。接下来的几个问题将关注 Flux 和 Redux 的核心概念,为 Redux 状态管理库的坚实基础。
什么是 Flux 模式?你能解释数据流吗?
Flux 是一种管理应用程序中单向数据流的模式,并作为传统 MVC 模式的替代品。它既不是框架也不是库,而是一种新的架构,用于解决客户端 Web 应用程序中的状态管理复杂性。它是在与 React 应用程序一起工作时由 Facebook 内部开发和使用的。
Flux 在其数据流中有四个主要组件:Action、Dispatcher、Store 和 View。以下是关于它们的更多内容:
-
Action:这代表一个发送到派发器的 JavaScript 对象,用于触发数据流。
-
分发器:这是一个更新存储的单例回调注册表,并在 Flux 应用程序的数据流中充当中央枢纽。它没有真正的智能,只是简单地从动作分发有效载荷到存储。
-
存储:这是应用程序状态和逻辑维护的地方。
-
视图:它从存储接收数据并重新渲染应用程序。视图将触发动作以响应任何用户交互。
基于 preceding 组件的 Flux 架构的逐步数据流看起来是这样的:
-
如果任何用户执行任何 UI 交互,将生成一个事件,并且视图将向分发器发送动作。
-
分发器将那些动作发送到相应的存储。
-
存储更新状态并通知视图重新渲染。
以下图表描述了基于 Flux 的 Web 应用程序中数据流的发生方式:

图 6.1:Flux 数据流
在大多数应用程序中,我们还会创建 action creators 作为一组辅助方法库,这些方法不仅创建动作对象,还将动作传递给分发器。
Flux 的优点是什么?
Flux 架构具有以下优点,并且有助于在客户端 Web 应用程序中使用:
-
由于其单向数据流,它很容易理解
-
Flux 组件是解耦的,每个组件都有自己的职责
-
它是一个开源架构,而不是框架或库
-
由于其设计,运行时错误将减少
-
它易于维护
Flux 架构有助于将 API 通信、缓存和本地化代码的实现从视图或 UI 层移出。
如何区分 Flux 和 MVC?
模型-视图-控制器(MVC)设计模式于 1976 年在 Smalltalk 编程语言中引入。随着应用程序的增长,这种模式因其多数据流而变得复杂。Facebook 团队通过引入 Flux 架构解决了这个问题。MVC 和 Flux 设计模式之间的主要区别列在以下表格中。
| MVC | Flux |
|---|---|
| 数据流方向是双向的 | 数据流方向是单向的 |
| 控制器处理逻辑 | 存储处理逻辑 |
| MVC 中没有存储概念 | Flux 中可以有多个存储 |
| 它是同步的 | 它是异步的 |
| 由于双向数据流,调试困难 | 使用分发器调试更容易 |
| 它用于客户端和服务器端框架 | 它仅用于客户端框架 |
表 6.1:MVC 与 Flux 对比
Flux 并非完全不同于 MVC 的方法,但它是一个改进的 MVC 模式。如果应用程序复杂且数据模型复杂,最好选择 Flux 而不是 MVC。
什么是 Redux?
Redux 是一个流行的、可预测的状态容器库,旨在编写在客户端、服务器和原生环境中表现一致的 JavaScript 应用,同时易于测试。它受到了 Facebook 的 Flux 架构的启发。它消除了 Flux 模式中存在的无必要复杂性。
当应用包含较少的组件时,使用组件状态相当直接。随着组件数量的增加和应用的变大,维护应用中每个组件的状态将变得具有挑战性。在这种情况下,Redux 通过创建一个全局存储来拯救,所有需要的组件都使用这个全局存储,而不需要从一个组件传递 props 到另一个组件。
注意
Redux 是一个大小约为 2 KB 的轻量级库,包括其依赖项。
Flux 和 Redux 之间有什么区别?
尽管 Redux 受到了 Flux 架构的启发,但有一些主要区别,如下表所示。
| Flux | Redux |
|---|---|
| 这是由 Facebook 开发的 | 这是由 Dan Abramov 和 Andrew Clark 开发的 |
| 它是一种用于管理应用状态的应用架构 | 它是一个用于管理状态的开放源代码 JavaScript 库 |
| Flux 在应用中提供多个存储 | Redux 的目标模式是在应用中只有一个存储 |
| 它由四个主要组件组成:ActionDispatcherStoreView | 它由三个主要组件组成:ActionReducerStore |
| 存储管理处理逻辑 | Reducers 管理处理逻辑 |
| 它有一个单例分发器 | 它不会使用任何分发器 |
| 存储的状态是可变的 | 存储的状态是不可变的 |
表 6.2:Flux 与 Redux 对比
除了上述区别之外,Redux 通过函数组合减少复杂性,而 Flux 使用回调注册。
你何时需要使用 Redux?
Redux 用于维护和更新应用中的数据,为多个组件提供共享状态。但并非所有类型的应用都需要它。它具有较大的学习曲线和需要编写更多代码的需求。
以下是一个 Redux 有用的用例列表:
-
你有大量的应用状态需要由应用中的许多组件共享
-
你需要遵循应用状态的单一真相源
-
应用状态需要频繁更新
-
更新应用状态的逻辑很复杂
-
你需要监控状态更新在一段时间内发生的情况
-
应用代码不是一个小型代码库,许多团队成员需要在其上工作
此外,如果你可以在 React 或其他前端框架内部管理状态,那么你不需要使用 Redux。
Redux 不仅仅是一个小型的库;它还基于核心原则的模式,一个由三个主要组件组成的工作系统,并为 Redux 应用程序提供了多个附加功能和广泛的 API,以覆盖常见的用例。让我们在下一节深入探讨所有这些主题。
Redux 的核心原则、组件和 API
尽管 Redux 受到了 Flux 架构重要品质的启发,但它有自己的基础原则和多种组件,这使得 Redux 系统能够处理大型应用程序的状态管理。作为本节的一部分,你将清楚地了解 Redux 的内部结构和它们的用法,以回答中到高级别的问题。
Redux 的核心原则是什么?
Redux 基于三个核心原则。这些原则有助于更好地理解库:
-
getState()函数如下:console.log(store.getState());这棵单一的树也有助于在开发中持久化状态,以加快开发周期。
注意
Redux 的单存储方法与 Flux 的多存储方法的主要区别之一。
-
状态是只读的:修改状态的唯一可能方式是发出一个动作,该动作是一个对象,描述了发生了什么。这意味着应用程序不能直接更改状态,而是通过传递一个动作来表达更改状态的意图。
cities state by dispatching an action:store.dispatch({ type: 'ADD_CITY', payload: "London" })由于前面的动作是一个普通的 JavaScript 对象,它可以被序列化、存储、记录,并且可以用于调试目的的重放。
-
cities状态变量:function cities(cities = [], action) { switch (action.type) { case 'ADD_CITY': return [ ...cities, { name: action.payload, position: 1 } ] default: return cities; } }
初始时,你的应用程序可以从单个还原器开始。一旦你的应用程序增长,你可以将大型还原器拆分为多个小型还原器,这些还原器管理状态树的具体部分。此外,你还可以控制还原器的调用顺序,传递额外的数据,并在应用程序中使它们可重用于常见任务。
Redux 是如何工作的?Redux 的主要组件有哪些?
Redux 系统通过在中央存储中保留整个应用程序的状态来工作。每个作为 Redux 提供者子组件的 UI 组件都可以访问这个存储状态,而不是从一个组件向另一个组件发送 props。Redux 工作流程的整个过程基于三个主要核心组件:动作、还原器和存储。
在以下代码中,使用简单的待办事项示例解释了 Redux 使用核心组件的工作流程,以更好地理解。在示例中,日常活动如吃饭和跑步被视为待办事项,并使用 Redux 工作流程添加到存储中:
-
type字段表示要执行的动作类型,以及其他用于更改状态的数据字段。它们是向 Redux 存储发送应用程序数据的唯一方式(例如,通过表单数据、用户交互或 API 调用)。所有这些动作都是通过动作创建器创建的,这些动作创建器只是返回动作的函数。addTodo that returns a todo action:function addTodo(todo) { return { type: 'ADD_TODO', payload: todo } }前面的动作还包含 todo 详细信息作为有效负载。它将由
store.dispatch(addTodo)方法执行,该方法将此动作发送到存储。 -
todo,如下代码片段所示:const todoReducer = (state = initialState, action) => { switch (action.type) { case "ADD_TODO": const { name, priority } = action.payload; return [...state.todos, { name, priority }]; default: return state; } };前面的 reducer 将初始状态和动作作为参数。如果 switch case 与
ADD_TODO动作类型匹配,它将复制状态中的现有 todos,使用新的todo值更新 todos,并返回新的 todo 列表。否则,将返回带有未更改 todos 的现有状态。您可以根据可能的动作(如更新、删除和过滤应用程序中的 todos)添加更多功能案例。
注意
它不仅限于使用 switch-case 代码块来决定新状态应该是什么。您还可以使用if/else循环或任何其他编程结构。
-
createStore或configureStore -
dispatch(action) -
getState()
这些辅助方法将被用来创建或更新存储中的 todos 状态,如下所示:
import { createStore } from "redux";
import todoReducer from "reducers/todoReducer";
const store = createStore(todoReducer); // Create a store
const firstTodo = addTodo({ name: "Running", priority: 2 });
console.log(firstTodo);
store.dispatch(firstTodo); // Dispatch a todo
const secondTodo = addTodo({ name: "Eating", priority: 1 });
console.log(secondTodo);
store.dispatch(secondTodo);
console.log(store.getState()); // Returns the todos list
createStore from plain Redux, and you will see the usage of the configureStore method when we introduce the RTK.
注意
随着应用程序的增长,可以使用称为选择器的函数访问存储状态中的特定状态信息部分。reselect 库因其记忆化的选择器函数而广受欢迎。
还可以通过添加 store enhancers 和 middleware 来扩展 store 的功能。这些主题将在本章后续问题中介绍。
我可以使用 Redux 与非 React UI 库一起使用吗?
尽管 Redux 主要用于与 React 和 React Native 库一起使用,但它可以与任何其他 UI 库(即 Redux 作为各种 UI 库的数据存储)一起使用。但您需要使用 UI 绑定库将 Redux 与您的 UI 框架或库集成。例如,React Redux 是将 Redux 与 React 库结合在一起的官方绑定库。还有为 AngularJS、Angular、Vue、Mithril、Ember 和其他许多框架提供的绑定。Redux 提供了一个订阅机制,可以被任何其他代码使用,但它主要用于与通过 React 或其他类似库创建的声明性视图或 UI 集成。
Reducers 遵循哪些规则?
在 Redux 中,reducer 组件应遵循一些特定的规则。这些规则在此列出:
-
Reducers 应该仅根据当前状态和动作参数推导出新的状态值。
-
Reducers 不应该修改现有的状态。然而,他们可以通过复制现有状态并对复制的值进行更改来执行不可变更新。
-
他们不允许执行任何异步逻辑,计算随机值或任何副作用。
遵循上述规则的函数也被称为纯函数。换句话说,reducers 仅仅是纯函数。通过遵循这些规则,reducers 使 Redux 代码和状态可预测,没有任何错误。
mapStateToProps()方法和 mapDispatchToProps()方法之间的区别是什么?
mapStateToProps()方法是一个用于从存储中选择连接组件所需数据部分的实用函数。所选状态将被传递给应用了connect()的组件作为属性。这样,这个方法有助于避免将整个应用程序状态传递给组件。
以下示例将city值作为属性传递给WeatherReport组件以查找天气信息:
const mapStateToProps = (state) => {
return {
city: state.user.address.city,
};
};
connect(mapStateToProps)(WeatherReport);
现在,WeatherReport组件只接受city作为属性。你可以通过解耦 Redux 代码和 React 组件,轻松地在应用程序的任何地方使用此组件:
<WeatherReport city={city} />
这个函数的简写符号是mapState,这个函数会在存储状态改变时被调用。
mapDispatchToProps()方法是一个实用函数,用于指定组件可能需要派发的动作。此函数提供动作派发函数作为属性。
以下函数指定了WeatherReport React 组件所需的动作:
const mapDispatchToProps = (dispatch) => {
return {
changeCity: (city) => {
dispatch(changeCity(city));
},
};
};
前面的代码片段执行了一个城市更改动作。这是通过在组件中直接调用props.changeCity(city)动作来完成的,而不是调用props.dispatch(changeCity(city))这种冗长的表达式。
对于mapDispatchToProps函数,有一个推荐的对象简写符号。在这种方法中,Redux 将其包装在一个看起来像(…args) => dispatch(changeCity(…args))的函数中,并将该包装函数作为属性传递给您的组件。
现在,前面的代码可以简化如下:
const mapDispatchToProps = {
toggleCity
};
总结来说,mapStateToProps函数用于将存储数据渲染到组件中,而mapDispatchToProps用于将动作创建者作为属性提供给组件。
什么是存储增强器?
存储增强器是一个接受存储创建函数(即createStore)并返回一个新的增强存储创建函数的高阶函数。这有助于自定义原始 Redux 存储,并将覆盖存储方法,如dispatch、getState和subscribe。
查看以下代码片段以了解存储增强器实现的外观:
const ourCustomEnhancer =
(createStore) => (reducer, initialState, enhancer) => {
const customReducer = (state, action) => {
// Logic to return new state
};
const store = createStore(customReducer, initialState, enhancer);
//Add enhancer logic
return {
...store,
//Override the some store properties or add new properties
};
};
存储增强器与 React 中的高阶组件(HOC)概念非常相似。因此,你也可以将 HOC 称为组件增强器。
注意
中间件为 Redux 的派发函数提供了额外的功能,增强器为 Redux 存储提供了额外的功能。
实时应用程序包含涉及副作用(如外部 API 调用、生成随机值、保存文件和更新本地存储)的逻辑。默认情况下,Redux 不支持执行这些类型的副作用。然而,Redux 中间件使得拦截派发的动作并注入额外的复杂行为(包括副作用)成为可能。接下来,我们将对此有更好的了解。
Redux 中间件 – Saga 和 Thunk
基本的 Redux 存储只能通过分发一个动作来执行简单的同步状态更新。例如Redux Thunk和Redux Saga这样的中间件通过将异步逻辑写入与存储交互来扩展存储功能。这些中间件有助于避免在我们的动作、动作创建者或组件中直接引起副作用。
什么是 Redux 中间件?如何创建中间件?
Redux 中间件提供了一种第三方扩展,通过修改动作或取消动作来拦截发送给 reducer 的每个动作。这对于日志记录、错误报告、路由和执行异步 API 调用非常有用。尽管 Redux 中间件类似于 Node.js 中间件(例如 Express 和 Koa),但它解决了不同的问题。
在以下示例中,我们将通过逐步说明来演示创建一个名为loggerMiddleware的自定义中间件,以在控制台中记录各种动作:
-
作为第一步,你需要按照以下方式从 Redux 库中导入
applyMiddleware函数:import { applyMiddleware } from "redux"; -
创建一个名为
loggerMiddleware的中间件,用于拦截动作以进行日志记录,其结构化语法如下:const loggerMiddleware = (store) => (next) => (action) => { console.log("action", action); return next(action); }; -
在创建
loggerMiddleware函数之后,需要将其传递给applyMiddleware函数:const middleware = applyMiddleware(loggerMiddleware); -
最后,我们需要将自定义中间件传递给
createStore函数。尽管中间件被分配为存储的第三个参数,但createStore函数会根据类型自动识别中间件:const store = createStore(reducer, middleware);
在动作被分发到存储之前,中间件会执行,并在控制台中记录动作详情。由于在中间件内部调用了下一个函数,因此 reducer 也会执行以更新存储中的状态。
也可以通过将它们传递给applyMiddleware函数来创建多个中间件,如下所示:
const middleware = applyMiddleware(
loggerMiddleware,
firstMiddleware,
secondMiddleware,
thirdMiddleware
);
在前面的代码中,所有这些中间件都是依次执行的。
你如何在 Redux 中处理异步任务?
大多数现代 Web 应用都需要处理异步任务。在 React 中,有两个流行的库可以用来处理这些任务:Redux Thunk和Redux Saga。
Redux Thunk 中间件用于编写一个动作创建者,它返回一个函数而不是一个动作对象。从动作创建者返回的函数被称为 thunk 函数,用于延迟计算。这些函数接受两个参数——dispatch和getState方法:
const thunkFunction = (dispatch, getState) => {
// This is the place where you can write logic to
dispatch other actions or read state
}
store.dispatch(thunkFunction);
所有 thunk 函数都是通过 store 的dispatch方法调用的,而不是从应用代码中调用。这种行为在前面代码中也可以看到。
与生成动作以进行分发的动作创建者类似,你可以使用 Thunk 动作创建者来生成 thunk 函数。例如,可以使用名为getPostsByAuthor的 thunk 动作创建者检索特定用户创建的帖子列表,它生成匿名 thunk 函数:
export const getPostsByAuthor = (authorId) => async (dispatch) => {
const response = await client.get(`/api/posts/${authorId}`);
dispatch(postsLoaded(response.posts));
};
之后,您可以在 UI 组件内部访问动作创建器以处理任何用户交互。以下 AuthorComponent 在懒加载事件中访问帖子列表:
function AuthorComponent({ authorId }) {
//...
const onLazyLoading = () => {
dispatch(getPostsByAuthor(authorId))
}
}
最后一个重要步骤是将 redux-thunk 中间件配置到 Redux 存储中,以分发 thunk 函数。有两种可能的选择。Thunk 中间件需要传递给 applyMiddleware() 方法,以手动将 thunk 中间件添加到存储中。但如果你使用 RTK,configureStore API 在创建存储时会自动添加 thunk 中间件(即不需要任何额外的配置)。
Redux Thunk 的用例有哪些?
Redux Thunk 可以有任意逻辑,并且可以用作多种目的。Redux Thunk 的最常见用例如下:
-
当你试图将复杂逻辑从 React 组件中移除时。
-
当你正在执行异步请求,如 Ajax 调用和其他异步逻辑时。
-
当你需要创建需要连续分发多个不同动作的逻辑时。
-
当你计划编写需要访问
getState或其他状态值以做出决策的逻辑时。
总结来说,Redux Thunk 中间件的主要用途是处理非同步的动作。
Redux Saga 是什么?
Redux Saga 是 Redux Thunk 中间件处理异步副作用的一个流行竞争对手。Redux Saga 使用一个名为 generators 的 ES6 功能,这有助于编写异步代码。这些生成器是可以在执行过程中暂停、恢复、退出并在稍后重新进入的函数。
将使用来自 redux-saga 包的特殊辅助函数生成副作用。以下列出了一些常用函数:
-
Call:一个效果描述,指示中间件在 Saga 中调用其他函数。 -
Put:用于向存储分发动作。 -
Yield:一个内置函数,允许顺序使用生成器函数。 -
takeLatest:一次只调用一次函数处理器,并通过再次运行带有最新数据的任务来取消之前的任务。 -
takeEvery:每当动作触发时,无限并发地调用函数处理器。
Saga 函数监听已分发的动作,并触发你代码中编写的副作用。例如,以下 postsSaga 函数监听 GET_POSTS 动作,并调用 Posts API 获取作者的帖子:
import { takeLatest, put, call } from "redux-saga/effects";
import { GET_POSTS } from "./actionTypes";
import { getPostsSuccess, getPostsFail } from "./actions";
import { getPosts } from "../backend/api/posts ";
function* fetchAuthorPosts() {
try {
const response = yield call(getPosts);
yield put(getPostsSuccess(response));
} catch (error) {
yield put(getPostsFail(error.response));
}
}
function* postsSaga() {
yield takeLatest(GET_POSTS, fetchAuthorPosts);
}
export default postsSaga;
在前面的代码中,无论是成功响应还是失败响应都会分发到存储中。这个响应取决于通过 call 辅助函数发生的 API 调用。
你是如何在 Redux Saga 和 Redux Thunk 之间做出选择的?
无论是 Redux Thunk 还是 Redux Saga 中间件,都有助于允许 Redux 存储异步地与外部 API 调用(或副作用)交互。但选择其中之一完全取决于你的项目需求和个人偏好。如果你是 React 或 Redux 生态系统的初学者,且项目规模较小,Redux Thunk 是一个不错的选择。此外,Redux Thunk 需要的样板代码更少,更容易理解。
另一方面,Redux Saga 适用于需要将逻辑拆分为多个文件的大型项目。然而,Redux Saga 相较于 Redux Thunk 的主要优势是能够编写干净且可读的异步代码测试。
纯粹的 Redux 需要大量的样板代码来满足状态管理需求。开发者需要实现一些常见任务,如设置存储、编写 reducer 和 actions 等。此外,你可能还需要根据需要从其他包中导入 API。这个过程使得开发者学习并实现 Redux 解决方案变得困难。RTK 通过其辅助工具将标准化这个过程并简化它。
使用 RTK 标准化 Redux 逻辑
RTK 包提供了必要的工具来简化 Redux 开发。这个包不仅简化了开发,还防止了常见的错误,提供了建议的最佳实践,以及更多功能。
什么是 RTK?
@reduxjs/toolkit,它围绕核心 redux 包进行包装。总的来说,这个包提供了构建 Redux 应用程序所需的实用工具和常见依赖项。
这个工具有助于覆盖常见的用例,例如设置存储、创建 reducer 和 actions、编写不可变更新逻辑,以及一次性创建整个状态切片。
默认情况下,RTK 自动支持以下官方推荐的工具或库集合,以覆盖大多数常见用例:
-
Redux DevTools
-
Immer
-
Redux Thunk
-
Reselect
RTK 通过 TypeScript 支持,API 提供了出色的类型安全,并减少了代码中使用的类型数量。
RTK 解决了哪些问题?
RTK 有助于加快开发过程并自动应用推荐的最佳实践。它解决了在 Redux 库中发现的以下三个主要问题:
-
配置过于复杂的 Redux 存储
-
这个 Redux 库需要大量的依赖项来构建大型应用程序
-
Redux 需要太多的样板代码,这影响了代码的效率和品质
工具包提供了一些配置全局存储的选项,创建 actions 和 reducers,通过抽象 Redux API 使开发更加简单。
什么是 RTK Query?如何使用它?
RTK Query 是一个强大的数据获取和客户端缓存工具,用于简化 Redux 应用程序中的常见用例。例如,此工具支持在 Web 应用程序中加载数据、避免需要手动编写数据获取和缓存逻辑等用例。如果你正在使用 RTK 包,此查询功能将作为一个可选的附加组件提供。此外,此功能是在 createSlice 和 createAsyncThunk 等 RTK API 方法之上构建的,以实现其实施。
让我们通过 Web 应用程序中的数据获取用例来解释 RTK Query 的用法。
首先,你需要从 RTK Query 包中导入 createAPI 和 fetchBaseQuery API 方法。此 createAPI 方法接受一个对象,该对象包括由 fetchBaseQuery API 创建的 baseQuery 配置以及与服务器交互的 API 端点列表。
在此示例中,将创建两个端点 – 一个用于创建用户,另一个用于列出用户:
import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react";
export const usersServerApi = createApi({
reducerPath: "api",
baseQuery: fetchBaseQuery({
baseUrl: "https://jsonplaceholder.typicode.com/",
}),
endpoints: (builder) => ({
users: builder.query({
query: (page = 1) => `users?page=${page}&limit=10`,
}),
createUser: builder.mutation({
query: (name) => ({
url: "users",
method: "POST",
body: { name },
}),
}),
}),
});
export const { useUsersQuery, useCreateUserMutation } = usersServerApi;
如前述代码所示,RTK Query 为每个可用的端点自动生成 React Hooks,这些 Hooks 可以通过导出声明在函数组件中使用。
接下来,需要通过将 RTK Query 生成的切片还原器映射到根还原器以及处理数据获取的自定义中间件来配置存储。setupListeners API 是一个可选的实用工具,用于启用 refreshOnFocus 和 refreshOnReconnect 行为:
import { configureStore } from "@reduxjs/toolkit";
import { setupListeners } from "@reduxjs/toolkit/query";
import { usersServerApi } from "./services/usersAPI";
export const store = configureStore({
reducer: {
[usersServerApi.reducerPath]: usersServerApi.reducer,
},
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat(usersServerApi.middleware),
});
setupListeners(store.dispatch);
之后,你需要使用 react-redux 包中的 Provider 组件包裹我们的应用程序,将存储作为属性传递给所有子组件,就像任何 Redux 应用程序一样:
const rootElement = document.getElementById("root");
render(
<Provider store={store}>
<App />
</Provider>,
rootElement
);
完成这些操作后,你可以在组件中通过查询进行请求。第二页上的用户列表可以按以下代码片段所示检索:
const { data, error, isLoading } = useUsersQuery(2)
除了用户的 data、error 和 isLoading 字段外,前面的查询还提供了其他布尔实用工具,如 isFetching、isError 和 isSuccess,这些可能根据功能需求而有用。
Redux 是大型应用程序的最佳状态解决方案。然而,调试这类应用程序中出现的错误将具有挑战性。Redux DevTools 通过追踪应用程序状态何时、何地以及如何被更改,使开发和调试体验变得容易。
使用 Redux DevTools 调试应用程序
就像 Chrome DevTools 用于动态操作网页内容一样,Redux DevTools 允许你直接操作 Web 应用程序中的 Redux 操作。如今,这个工具已成为开发任何类型 Redux 应用程序的标准开发工具。
什么是 Redux DevTools?
Redux DevTools 是一个仅用于调试应用程序状态变化的开发工具。它用于执行时间旅行调试和 Redux 的实时编辑,具有热重载、动作历史、撤销和重放功能。如果您不想将 Redux DevTools 作为独立应用程序安装或将其集成为客户应用程序中的 React 组件,它可以用作 Chrome、Firefox 或 Edge 浏览器的浏览器扩展。
以下是一个 DevTools 快照示例,表示获取待办事项、完成和删除待办事项操作的顺序:

图 6.2:Redux DevTools UI
在前面的屏幕截图中,左侧面板表示动作列表,在选择特定动作时具有 跳过 和 跳转 选项,右侧面板描述了当前状态、状态差异和其他有用功能。
注意
RTK 的 configureStore API 会自动设置与 Redux DevTools 的集成。
Redux DevTools 的主要功能有哪些?
下面列出了 Redux DevTools 的一些主要功能:
-
它提供了检查每个状态和动作负载的能力
-
您可以通过取消动作来回到过去
-
一旦 reducer 代码发生变化,每个阶段动作将被重新评估
-
如果 reducer 抛出错误,您可以追踪导致错误的动作以及错误的内容
-
您可以使用
persistState()存储增强器在页面重新加载之间持久化调试会话
使用 Redux DevTools 的 dispatch 选项,可以在不编写任何代码的情况下在应用程序中分发动作。
摘要
本章提供了关于 React 应用程序 Redux 状态管理解决方案的全面知识。我们本章开始以对 Flux 的简要介绍,其架构、与 MVC 模式的差异以及用例,然后是 Redux 基础知识、与 Flux 的差异以及作为状态管理解决方案的优势,接着我们讨论了 Redux 的核心原则、其组件、各种附加组件和数据流。之后,我们了解了异步任务、Redux 中流行的中间件库,如何在 React 应用程序中使用它们以及它们的用例。最后,我们介绍了调试技术以及 Redux DevTools 以跟踪状态变化。
在下一章中,我们将了解在 React 应用程序中应用 CSS 的各种方法。首先,我们将从 React 中的常规 CSS 样式方法开始,使用内联样式和外部样式。然后,我们将介绍一些高级技术,例如使用 CSS Modules 的局部作用域 CSS 和基于 CSS-in-JS 解决方案的 styled-components 库。
第七章:在 ReactJS 中应用 CSS 的不同方法
在现代 Web 开发中,创建美观且用户友好的界面对于建立引人入胜和有效的应用程序至关重要。ReactJS 是一个流行的前端框架,用于创建用户界面,并且有几种实现层叠样式表(CSS)的方法,这是负责在线内容样式的语言。本章试图回答面试者可能对 CSS 主题提出的一些重要问题。通过解释将 CSS 纳入 ReactJS 的各种方法,我们将能够从更广泛的知识体系中受益,这将使我们更好地应对有关此主题的面试问题。
我们将探讨实现 CSS 的五种不同方法:CSS Modules、styled-components和原子 CSS(使用 Tailwind CSS 框架)。这些解决方案各有优缺点,具体取决于项目目标和偏好。通过研究这些选项,您将获得在面试环境中应对这些问题的知识和信心,这在您需要创建 ReactJS 应用程序时尤其有用。通过了解编写和维持干净、可管理和可扩展代码的原则,您会发现面试中提出高质量答案要简单得多。
Sass 和 Less 等预处理器也将在本章中介绍,因为我们旨在涵盖将 CSS 实现到我们的 React 项目中所有相关的用例,并准备好回答这些领域的任何面试问题。
在本章中,我们将详细探讨这些重要的 CSS 相关主题:
-
应用 CSS 的不同方式
-
探索处理器和 CSS Modules
-
CSS-in-JS 方法以及
styled-components及其用法 -
如何在 React 应用程序中使用 styled components
技术要求
确保您的计算机上已安装Node和npm,并且已安装并正常工作用于 Create React App 和 Next.js 的 JavaScript Node 包。使用您喜欢的 IDE 和命令行界面(CLI)来处理这些项目。
Create React App 的包可以在以下位置找到:create-react-app.dev/。
Next.js 的包可以在以下位置找到:nextjs.org/。
应用 CSS 的不同方式
在本节中,我们将探讨在 React 项目中应用 CSS 的不同方式。获得的知识将为我们提供应对这些常见问题的关键面试答案,并且示例可以帮助我们详细解释它们之间的差异以及它们是如何工作的。让我们继续学习,更深入地了解这些 CSS 解决方案。
虽然 ReactJS 是一个用于创建用户界面的 JavaScript 库,CSS 是一种用于描述 HTML 或 XML 文档外观和格式的样式表语言。将 CSS 与 ReactJS 结合使用可以帮助开发者高效地设置组件样式,从而实现美观且一致的界面。使用 CSS 与 ReactJS 有几种方法,我们将在本章中学习。
在接下来的章节中,我们将学习导入 CSS、CSS Modules、CSS 预处理器、Atomic CSS 和内联样式。后者涉及使用 JavaScript 对象直接将样式添加到 React 组件中。虽然内联样式对于小型组件或动态样式很有用,但它们可能导致代码重复和维护性问题,我们将在后面讨论。首先,让我们从导入样式表开始。
我们如何导入外部样式表?
在 React 中利用 CSS 的标准技术涉及创建单独的 CSS 文件,并使用类名来设置组件样式。这种解决方案将样式和逻辑关注点分开,使代码更加结构化和易于管理。官方 React 文档建议开发者在开始新的 React 项目时使用生产级别的 React 框架。这包括 Next.js、Remix、Gatsby 和 Expo(用于原生应用)。现在,这被认为是开发 React 应用程序最现代的方式,你可以在这里了解更多:react.dev/learn/start-a-new-react-project。
我们将查看两个代码示例,一个使用 Next.js,另一个使用 Create React App,以展示两种(旧与新的)构建 React 应用程序的过程之间的对比。Next.js 被认为是构建 ReactJS 应用程序最现代的推荐方式,而 Create React App 现在被视为一个遗留工具。这是因为 Next.js 被视为一个更适用于生产级别的 ReactJS 框架。
我们如何使用 Create React App 构建 React 应用程序?
这是如何在 Create React App 中实现传统方法的示例。
首先,创建一个 React 项目,然后创建一个 CSS 文件。使用 CSS 规则和类名,在名为 App.css 的单独 CSS 文件中指定你的样式,如下所示:
/* App.css */
.container {
text-align: center;
margin: 0 auto;
background-color: #bada55;
padding: 1rem;
}
.title {
font-size: 2rem;
font-weight: bold;
}
现在,将创建的 CSS 文件导入到你的 React 组件文件中,该文件应该是 App.js:
// App.js
import './App.css';
export default function App() {
return (
<div className="container">
<h1 className="title">Hello, World!</h1>
</div>
);
}
注意
className 属性用于 JSX 元素中应用导入样式表中的相应 CSS 类。我们使用 className 属性而不是 class,因为 class 是 JavaScript 中的一个保留字。在编写 CSS 文件时这并不是问题,但在 JavaScript 文件中就是了。此外,JSX 是一种命名约定,这意味着它需要使用 camelCase 命名约定来使用元素属性,如类名。
在你的控制台中启动 npm run start 命令,你的应用程序应该已经启动并运行。
我们如何使用 Next.js 构建 React 应用程序?
Next.js 是一个基于 ReactJS 的知名开源 Web 开发框架。它的目的是让开发者更容易创建服务器端渲染的 React 应用程序,使他们能够创建针对搜索引擎优化(SEO)和提供卓越用户体验的高性能 Web 应用程序。
Next.js 的项目结构略有不同,尽管导入 CSS 样式表的方式仍然相同。幸运的是,当使用 Next.js 中最新的 App Router 功能时,这个过程实际上非常相似。这就是我们在 Next.js 中导入 CSS 样式表的方法。
首先,使用 Next.js 创建一个 React 项目,然后在 app 文件夹内创建一个 Home.css 文件。使用以下所示的 CSS:
/* Home.css */
.container {
margin: 0 auto;
display: flex;
flex-flow: column nowrap;
background-color: #0384c8;
padding: 2rem;
}
.main-content {
display: flex;
flex-flow: row nowrap;
padding: 2rem 0;
}
现在,只需将 app 文件夹中的 page.js 文件内的所有代码替换为这里提供的代码即可:
import './Home.css';
export default function Home() {
return (
<div className="container">
<h1>Heading 1</h1>
<h2>Heading 2</h2>
<h3>Heading 3</h3>
<h4>Heading 4</h4>
<h5>Heading 5</h5>
<section className="main-content">
<p>
Lorem ipsum dolor sit amet, consectetur
adipiscing elit. Nullam eu
mi sit amet velit convallis tincidunt.
</p>
<p>
Lorem ipsum dolor sit amet, consectetur
adipiscing elit. Nullam eu mi sit
amet velit convallis tincidunt.
</p>
</section>
</div>
);
}
使用 npm run dev 命令运行您的应用程序,它应该和之前一样工作。
这是我们从 Create React App 时代起就使用的默认导入样式表的方法。然而,它并不能实现组件级别的隔离,全局类名可能会导致命名冲突和不需要的样式覆盖。例如,CSS Modules 和 CSS-in-JS 框架解决了这些问题,并为装饰 React 组件提供了更广泛的功能。一些流行的 CSS 框架包括 Tailwind CSS、MUI、Chakra UI、Semantic UI、NextUI、React Bootstrap、Ant Design 和 Emotion。实际上,Tailwind CSS 是在您第一次配置 Next.js 应用程序时可以选择的一个选项。
还值得一提的是,在 CSS 网页布局模型中,构建网站结构最流行的两种方式是使用 Flexbox 或 CSS Grid。Flexbox 无疑更受欢迎,尽管根据网站设计和复杂度,通常会选择使用其中一种或两种。我们可以单独使用这些网页布局模型,或者与 CSS 框架一起使用。我们可能还希望研究的一个领域是动画。除了使用常规的 CSS 库创建动画外,我们还可以利用各种第三方库。一些流行的库包括 React Spring、Green Sock、Framer Motion、React Move 以及许多其他库。
我们现在将讨论另一种为我们的 ReactJS 应用程序添加样式的的方法,称为内联样式,这是在常规 HTML 和 ReactJS 应用程序中做样式的一种常见方式。它已经是一段时间内 HTML 的默认样式方法,并且使用 JSX 在 React 中也是可行的。
内联样式也提供了许多优势,这使得它成为为我们的 React 应用程序添加样式的一个非常有吸引力的选项。我们能够根据组件状态或属性使用动态样式,并且有组件隔离,这降低了意外样式覆盖或其他组件不兼容的可能性。更快的开发速度、易用性,以及我们甚至不需要 CSS 类名的事实,都增加了使用这种方法的好处。
我们如何使用内联样式?
在 ReactJS 应用程序中,内联样式允许开发者使用 JavaScript 对象直接对特定元素或组件应用样式,而不是在单独的样式表或类中指定 CSS 样式。它们被指定为包含键值对的字面量对象。基本上,它们是位于 JSX 花括号内的对象,看起来像这样{{ backgroundColor: blue }}。在 JSX 花括号内,我们将使用 CSS 属性及其值。对象的键是 CSS 属性名称,值是相关的属性值。
让我们来看一个例子,以便我们可以看到在实际代码中它看起来是什么样子。只需将page.js文件中的所有代码替换,将其转换为一个现在使用内联样式而不是外部样式的应用程序:
const container = {
display: 'flex',
flexFlow: 'column nowrap',
backgroundColor: '#7e7dd6',
padding: '2rem',
};
const mainContent = {
display: 'flex',
flexFlow: 'row nowrap',
padding: '2rem 0',
};
export default function Home() {
return (
<>
<div style={container}>
<h1>Heading 1</h1>
<h2>Heading 2</h2>
<h3>Heading 3</h3>
<h4>Heading 4</h4>
<h5>Heading 5</h5>
<section style={mainContent}>
<p>
Lorem ipsum dolor sit amet, consectetur
adipiscing elit. Nullam eu
mi sit amet velit convallis tincidunt.
</p>
<p>
Lorem ipsum dolor sit amet, consectetur
adipiscing elit. Nullam eu
mi sit amet velit convallis tincidunt.
</p>
</section>
</div>
</>
);
}
当我们需要根据组件的状态或属性应用动态样式时,React 内联样式可能会有所帮助。例如,基于用户交互或其他事件,我们可能将样式对象声明为组件状态的属性,并动态地更改它。
我们还可以通过将其分配给状态来在内联样式中使用变量,这看起来是这样的:
'use client';
import { useState } from 'react';
export default function Home() {
const [h1color, setH1Color] = useState('blue');
return (
<div>
<h1 style={{ color: h1color }}>Hello World</h1>
</div>
);
}
然而,内联样式有一些缺点,包括无法在组件之间重用样式,对于大型系统来说,它们不如外部样式表有效,并且如果不小心使用,可能会影响可读性。
解决这些问题的方法之一是使用 CSS 模块。CSS 模块是一种为组件编写模块化、作用域化的 CSS 的方法。它有助于解决典型的 CSS 问题,如全局作用域和名称冲突。
在 React 中实现 CSS 的另一个好策略是使用styled-components。styled-components是一个知名的 CSS-in-JS 包,用于为 React 组件添加样式。它允许你使用标签模板字面量直接在你的 JavaScript 代码中编写 CSS。styled-components生成唯一的类名并将样式注入 DOM 中,将它们限制为单个组件。这种方法提高了开发者的体验和组件分离。
我们在项目中使用 CSS 的另一种方法是使用原子 CSS。原子 CSS,也称为函数式 CSS,是一种关注开发小型、单一用途 CSS 类的样式技术。每个类提供一种类型的规则或一组紧密相关的规则,并且它们通常以定义其目的或它们应用的属性的方式标记。
这里的优势是开发速度快,因为通过简单地混合现有的原子类,你可以快速原型化和构建组件。遵循一个通用主题或模板,以确保每个开发者都使用相同的文档和类集。这使得调试非常简单,并且入职过程很快,因为每个人都使用相同的过程。
我们如何使用原子 CSS?
原子 CSS 是一种构建 CSS 代码的策略,强调使用简短、专业的类,这些类可以组合起来产生复杂的样式。目标是把设计分解成可管理的、可重用的部分,能够以多种方式合并以产生所需的设计。
原子 CSS 技术由许多知名的 CSS 库实现,包括 Tailwind CSS、Bootstrap CSS 和 Bulma 等。这些库提供了预定义的原子类集合,可以快速生成复杂的样式。我们现在将使用 Tailwind CSS 库在我们的 Next.js 应用中进行一些基本的样式设计,因为其在社区中的流行度以及 Tailwind CSS 集成到 Create Next App 的事实,而 Create Next App 是构建 Next.js 应用的官方框架。当你理解了基础知识时,你可以使用任何 CSS 库。
安装 Tailwind CSS 相对简单;你只需遵循这里的设置指南:tailwindcss.com/docs/guides/nextjs。
完成这些后,我们可以看到这个例子中的语法是什么样的:
export default function Home() {
return (
<>
<div class="flex flex-row">
<div class="basis-1/4 bg-teal-600">01</div>
<div class="basis-1/4 bg-teal-700">02</div>
<div class="basis-1/2 bg-teal-800">03</div>
</div>
</>
);
}
接下来,让我们了解预处理器和 CSS 模块。CSS 预处理器是一个程序,允许我们使用预处理器的语法来构建 CSS。在 CSS 模块中,每个类名和动画名现在,根据定义,都是本地分配的。为了提高效率和安全性,CSS 模块允许你在 CSS 文件中创建样式;然而,你需要将样式作为 JavaScript 对象来使用。
探索处理器和 CSS 模块
我们可以用两种不同的方式用 CSS 构建网站——通过使用 CSS 处理器和 CSS 模块。CSS 处理器已经存在很长时间了,并且被设计成是传统 CSS 的改进。它们让我们能够嵌套 CSS 代码,并将代码编译成常规 CSS。另一方面,CSS 模块为我们提供了文件中的作用域 CSS 代码,这有助于避免名称冲突。现在让我们了解它们,从 CSS 处理器开始。
CSS 处理器是什么?
CSS 处理器,通常被称为 CSS 预处理器,是添加额外功能到 CSS 的工具,例如变量、混合和嵌套规则。它们使你能够以更少重复和模块化的方式编写,更容易维护。Sass(也称为 SCSS)、Less 和 Stylus 是最广泛使用的三种 CSS 预处理器。为了将改进的 CSS 语法转换为网络浏览器可以理解的常规 CSS,这些预处理器需要一个构建步骤。当使用 Webpack 等构建工具时,你可以将这个构建阶段纳入你的开发流程中。
我们如何使用 CSS 处理器?
Next.js 原生支持 Sass,利用 .scss 和 .sass 扩展名。通过 CSS 模块和 .module.scss 或 .module.sass 扩展名,你可以应用组件级别的 Sass。首先,使用 npm install –save-dev sass 命令安装 Sass。然后,在一个新的 .scss 文件中用 Sass 语法编写你的样式。通过在 React 组件文件中引用 .scss 文件来导入生成的 CSS,如下所示:
import './styles.scss';
const MyComponent = () => {
return <div className="myComponent">Hello, World!</div>;
};
export default MyComponent;
导入 .scss 文件与导入正常的 .css 文件完全相同。
CSS 模块是什么?
在模块化方法中局部作用域 CSS 的方法是 CSS 模块。通过为每个组件自动创建独特的类名,它通过确保样式不会扩散到程序的其他区域来帮助防止全局样式之间的冲突。将 CSS 样式写入单独的文件,通常带有 module.css 扩展名,并将它们导入到 JavaScript 文件中是 CSS 模块的工作方式。导入的样式被处理为一个对象,其中产生的唯一类名作为值对,键作为主要类名。
我们如何使用 CSS 模块?
我们可以通过使用 CSS 模块在我们的组件中利用局部作用域的 CSS。使用 CSS 模块时,类名默认是局部作用域的,这可以防止任何命名冲突。这也是 Next.js 应用程序中使用的默认样式方法。我们可以在下面的代码片段中看到它的样子。
这是 Home.module.css 文件的 CSS:
/* Home.module.css */
.main {
display: flex;
padding: 2rem;
color: #ffffff;
}
.box {
background-color: rgb(241, 255, 240);
color: #000;
padding: 1rem;
margin: 1rem;
}
这是 page.js 文件的 JavaScript:
// page.js
import styles from './Home.module.css';
export default function Home() {
return (
<>
<div className={styles.main}>
<h1>Hello World!</h1>
</div>
<div className={styles.box}>
<p>
Lorem ipsum dolor sit amet, consectetur
adipiscing elit. Etiam convallis, nulla non
laoreet condimentum, turpis felis finibus
metus,ut molestie risus enim id neque. Integer
tristique purus non gravida sodales. Maecenas
ultricies feugiat dolor lobortis commodo. Sed
maximus vitae neque quis mollis.
</p>
</div>
<div className={styles.box}>
<p>
Lorem ipsum dolor sit amet, consectetur
adipiscing elit. Etiam convallis, nulla non
laoreet condimentum, turpis felis finibus
metus,ut molestie risus enim id neque. Integer
tristique purus non gravida sodales. Maecenas
ultricies feugiat dolor lobortis commodo. Sed
maximus vitae neque quis mollis.
</p>
</div>
</>
);
}
如你所见,它类似于使用内联样式;然而,我们仍然有一个外部样式表,所以这是两者的最佳结合。还有另一种实现 CSS 的方法,即使用 styled-components 和 CSS-in-JS 方法。这为我们提供了另一种设置项目的方式,并且与其他方法相比可以提供许多优势。现在让我们更深入地了解这种实现方式。
CSS-in-JS 方法、样式组件及其用法
这是我们学习的一个基本领域,因为 CSS-in-JS 方法在整个 React 框架中适用。我们将了解这种方法,以及我们如何使用第三方库,如 styled-components,作为我们之前学到的其他 CSS 技术的替代方案。
CSS-in-JS 是什么?
CSS-in-JS 是一种创新的 Web 开发样式解决方案,它将 CSS 集成到 JavaScript 代码中。这种方法不是使用单独的 CSS 文件,而是允许开发者在他们的 JavaScript 或 TypeScript 脚本中直接定义和监督组件的样式。CSS-in-JS 允许改进组件封装、作用域样式和更简单的动态样式。它还允许你在样式中使用 JavaScript 的全部功能,包括根据组件的状态动态应用样式或使用 JavaScript 变量计算样式值。
样式组件是什么,以及它们如何在 React 项目中使用?
在 React 的一个流行第三方工具 styled-components 的帮助下,程序员可以在 JavaScript 中指定组件样式,而不是外部 CSS 文件。它提供了一种针对特定组件的 CSS 代码的编写方法,简化了在整个应用程序中管理样式和重用样式的过程。
样式组件采用 CSS-in-JS 方法,这意味着 JavaScript 函数和变量被用来定义组件的 CSS 样式。这使得程序员能够通过利用 JavaScript 的所有功能,如函数、变量和其他语言结构,来创建动态样式。当使用 styled-components 时,我们有 服务器端渲染(SSR),这保证了我们的样式在服务器上得到适当的渲染。与内联样式等其他 CSS 相比,它有一个优势,因为它不需要额外的努力来确保良好的 SSR 支持。由于样式组件在大多数代码编辑器中包含语法高亮、代码检查和自动完成支持,开发者的体验也得到了进一步的提升。
这使得开发体验更加积极,并提高了生产力,因为它们还允许你将样式从组件的 JSX 中分离出来,从而产生更干净、更易于维护的代码。我们还可以使用样式组件为每个组件生成唯一的类名,确保样式被限制在适当的组件中,避免意外的样式泄漏或冲突。
我们获得的一个额外好处是通过使用 styled-components 的 React 上下文 API 来内置主题支持。这使得我们在整个应用程序中构建和管理统一主题变得简单,这是其他 CSS 技术所无法实现的。支持所有 CSS 功能,如伪选择器、媒体查询和关键帧,是一个巨大的优势。
如何在 React 应用程序中使用样式组件
为了加强这一学习,让我们通过一个示例来看看语法是什么样的。我们将快速查看一个简单、易于理解的简单设置,应该会使这一点非常清楚。
我们如何使用样式组件?
基本上,创建样式组件可以通过四个简单的步骤完成。首先,我们必须安装 styled-components 库的包,该包可以在以下链接找到:styled-components.com/。接下来,我们将包导入到文件的顶部。然后,我们为我们的 HTML 创建一个具有 CSS 样式的 JavaScript 类型的对象。我们使用 styled 方法后跟我们要使用的 HTML 元素,例如 div、section 或 p 标签等。
最后,我们在代码中返回对象以在屏幕上渲染它。以下代码片段展示了工作示例:
import styled from 'styled-components';
const ContainerDiv = styled.div`
color: blue;
font-size: 30px;
`;
export default function Home() {
return <ContainerDiv>Hello World!</ContainerDiv>;
}
我们成功地完成了这一部分,并学习了关于许多不同的 CSS 相关面试问题,这将使我们在这个主题领域在面试中处于有利位置。
摘要
我们探讨了在 ReactJS 应用程序中利用 CSS 的几种方法,强调了设计和样式在创建美观用户界面中的重要性。导入外部样式表、内联 CSS 样式、CSS 模块、styled-components 和像 Tailwind CSS 这样的原子 CSS 框架是探索的五种主要选项。
我们讨论了如何将外部 CSS 文件连接和导入到 React 组件中,从而实现集中管理和分离样式逻辑与组件逻辑的关注点。这种方法非常适合在 React 应用程序中使用经典 CSS。
我们还研究了原子 CSS 和其以实用工具为首要的方法,特别关注了流行的 Tailwind CSS 框架。通过提供一大套可用于构建定制设计的实用类,这种技术减少了自定义 CSS 的需求。
在 CSS 模块的主题上,我们探讨了 CSS 模块如何以模块化的方式处理特定组件的样式。CSS 模块通过使用本地作用域的类名来消除全局样式冲突,并鼓励组件的可重用性。我们还讨论了流行的 styled-components 包,它允许你使用标签模板字面量创建样式化组件。这种方法鼓励组件封装、主题支持和基于属性的动态样式。
通过了解并利用这些不同的 CSS 方法在你的 ReactJS 应用程序中,你可以轻松地设计和样式化你的应用程序组件,同时保持代码库的整洁、可管理和可扩展。
在下一章中,我们将学习如何测试和调试我们的 ReactJS 应用程序。
第八章:测试和调试 React 应用
React 已经成为网络开发领域最受欢迎的前端库,使程序员能够构建有效、可扩展且易于维护的应用程序。为了确保您的应用程序的稳定性和可靠性,随着项目的规模和复杂性不断增加,全面测试和高效调试变得越来越重要。本章将详细探讨掌握测试和调试 React 应用所需的工具和方法,为您在提高技能的过程中打下基础。
我们将首先讨论 React 测试助手,它有助于测试并提高生产力。然后,我们将回顾目前 JavaScript 和 React 生态系统中最受欢迎和最灵活的测试工具,包括 Enzyme、Jest 和React 测试库。通过这样做,您将能够根据您独特的需求和需求选择合适的工具。接下来,我们将详细讨论测试生命周期的设置和拆除阶段。
我们已经专门用一节来讨论解决测试中数据获取和模拟问题的最佳实践,因为它们是应用程序的关键组成部分。我们将深入了解测试用户事件、控制定时器和模拟现实世界交互的细节,为您提供确认应用程序响应性和性能所需的工具。
最后,我们将介绍 React DevTools,这是调试和评估您的 React 应用的必备工具。
到本章结束时,您将具备成功测试和调试您的 React 应用所需的知识、能力和自信。如果您对可用的工具和方法有牢固的掌握,您将能够构建在不断变化的环境中既可靠又健壮的应用程序。因此,让我们踏上成为 React 应用测试和调试专家的旅程,以便您的项目能够经受时间的考验。
在本章中,我们将从软件的角度深入探讨测试和调试的主题,学习测试我们的 React 应用的基本知识、理念和概念。以下内容将涵盖:
-
介绍 React 测试助手
-
测试我们的软件
-
在我们的应用中管理数据
-
使用事件和定时器执行代码
-
使用 React DevTools 进行调试和分析
技术要求
您可以在此处找到本章的项目和代码:github.com/PacktPublishing/React-Interview-Guide/tree/main/Chapter08
介绍 React 测试助手
在本节中,我们将学习 RTL(寄存器传输级)的基础知识。但首先,让我们尝试理解在编程中测试的含义,以便我们可以了解核心概念和方法论。
软件开发中的测试是什么?
在软件开发中,审查软件程序或系统以验证其满足其功能和非功能标准,并认证其整体质量、性能和可靠性,被称为测试。它包括在受控环境下运行程序以找出错误、缺陷或可能的问题,在产品交付给最终客户之前。测试通常在多个级别进行,从单个组件级别到完全集成的系统,它是软件开发生命周期的一个关键部分。
根据项目情况,可能会有任何数量的测试阶段。让我们看看这些测试级别的阶段可能是什么样子:
| 测试阶段 | 描述 |
|---|---|
| 单元测试 | 测试独立的代码或其部分被称为单元测试。它保证每个单元都按照其规范操作并按预期行事。 |
| 集成测试 | 测试各种软件单元、模块或组件之间的集成和关系称为集成测试。它保证组件之间能够有效沟通,并且组合的系统作为一个整体运行。 |
| 回归测试 | 回归测试是为了确保新的代码修改或改进不会对已存在的功能产生负面影响。它涉及在软件修改后重新运行早期测试。 |
| 安全测试 | 在安全测试期间,评估产品的安全特性和弱点。它指出了可能的安全问题,如数据泄露、未经授权的访问和编码缺陷。 |
| 功能测试 | 通过功能测试,将软件的功能与声明的需求进行比较。它包括测试多个功能、用例和场景,从最终用户的角度评估程序。 |
| 阿尔法和贝塔测试 | 在将程序分发给选定的小组外部用户之前,内部测试员在受限环境中进行阿尔法测试。贝塔测试涉及将程序提供给更多外部用户,以获取实际用户的反馈并发现任何可能的问题。 |
| 性能测试 | 在性能测试期间,评估软件在各种负载水平下的适应性、速度、可扩展性和稳定性。这包括测试响应速度、资源使用和系统中的限制等变量。 |
表 8.1:软件开发测试阶段
如您所见,在项目生命周期中,我们可以执行许多不同类型的测试。接下来,让我们学习如何在 React 应用程序中进行测试。
我们如何在 React 应用程序中进行测试?
在 React 中,测试是确认和验证每个组件以及整个应用程序是否按预期工作并符合设定的标准的方法。这通常涉及测试每个单独的 React 组件、用户交互以及应用程序状态的任何潜在变化。在 React 应用程序中,我们可以以几种方式执行测试,这些通常是单元测试、集成测试和端到端(E2E)测试。
你如何为 React 应用程序设置测试环境?
为了使你的 React 应用程序可靠、可维护且质量最高,你必须设置一个测试环境。如果你的测试环境设置正确,你可以在一个受控、隔离的环境中执行测试,这个环境与生产环境非常相似。这有助于在这些问题影响最终客户之前找到并解决它们。所有测试环境都需要我们开发者为它们编写测试,这被称为测试驱动开发(TDD)。
下图描述了软件开发工作流程中的 TDD 周期。在这个编程方法中,编码、测试和设计紧密相连。虽然有很多变体,但基本原理保持不变:

图 8.1:软件开发中的 TDD 周期
现在我们已经了解了软件开发中的 TDD 周期,让我们继续探讨测试框架/库,看看我们如何在我们的应用程序中最好地使用它们。
你如何选择一个测试框架或库?
当构建 React 应用程序时,考虑一个好的测试库是个好主意。拥有良好的测试结构意味着我们的软件应该按预期运行并满足用户的期望。因此,让我们看看目前可用的几个流行的测试库:
-
React Testing Library(RTL):轻量级的 RTL 专注于测试组件的功能。与其他测试框架相比,它提供了一个更直接的 API。
-
Jest:流行的测试框架 Jest 已经设置为与 React 一起工作。它具有内置的测试 React 应用程序的功能,例如模拟和快照测试。
-
Enzyme:浅渲染、完整 DOM 渲染和快照测试只是 Enzyme 这个强大的测试框架为 React 提供的几个测试工具之一。
-
Vite:前端构建工具 Vite 有一个名为Vitest的单元测试框架。它是一个具有众多现代特性的优秀单元测试框架,包括对 TypeScript、JSX 和 React 组件测试的支持。
-
Cypress:Cypress 是一个基于 JavaScript 的 E2E 高级网页测试自动化解决方案。前端开发人员和 QA 工程师可以使用这个工具构建自动化网页测试,该工具专为开发者设计,并直接在浏览器中运行
在测试方面,我们可以在 React 项目中设置多种方式。每个开发者都有自己的偏好。有些人选择有一个专门的文件夹,其中包含所有测试文件,并且与主要组件分开。其他人更喜欢将测试文件放在与组件相同的文件夹中,在两种情况下,测试文件都遵循与组件相同的命名约定——例如,index.js 和 index.test.js。
下一个图显示了这两个用例的示例。这是一个 Next.js 项目,它为 Jest 和 RTL 项目设置了默认配置。有一个名为 __tests__ 的文件夹,其中包含一个名为 index.test.tsx 的测试。在 pages 文件夹中,紧挨着 index.tsx 组件,还有一个 index.test.tsx 文件。这两个测试都可以使用 npm test 命令运行:

图 8.2:React 项目测试文件结构
现在我们已经对一般的测试约定有了些了解,接下来我们将讨论 RTL 的基本原理。
React 测试库的基本原理是什么?
在受欢迎的测试工具 RTL 的帮助下,开发人员被敦促以与消费者如何与应用程序交互的方式相似的方法测试他们的组件。RTL 鼓励根据个人观察和执行的内容来测试组件,而不是基于实现的具体细节,确保程序保持可访问性、可管理性和用户友好性。RTL 是一系列包,它可以在 React 和 React Native 项目中使用。因此,了解我们可以使用相同的包来测试我们的网页和移动应用是很好的。
RTL 有许多不同的核心原则,我们应该熟悉它们:
-
fireEvent方法,它允许你启动各种 DOM 事件,如点击、更改或提交,以模仿用户交互。这使你能够通过测试组件对用户交互的反应来验证预期的行为是否显示。 -
GetByText、GetByRole和GetByTestId是一些常用的查询。 -
自定义渲染:有一个默认的渲染函数,你可以用它来渲染你的组件,但你也可以设计自己的渲染函数,将你的组件包裹在特定的上下文或提供者中。当你的组件依赖于独特的上下文设置,如主题或本地化时,这非常有帮助。
-
使用
screen可以输出一个对象,它让你能够轻松访问显示的部分和查询方法,无需手动分解。通过使用screen,你可以使你的考试更加简洁,并使其更容易阅读。 -
当与下载数据或依赖于异步活动的组件一起工作时,使用
waitFor、waitForElementToBeRemoved和find*搜索。通过确保在继续之前,您的测试等待必要的组件或操作,这些方法有助于管理组件的异步操作。 -
除了
fireEvent之外,还有@testing-library/user-event包。这个包中高级的事件模拟功能更接近用户行为,比基本的fireEvent方法更接近。
因此,现在我们已经掌握了使用 React 测试助手来设置强大测试环境的概念,让我们将所学知识应用到实际中,看看我们如何最好地使用这些工具来设置我们的测试环境。这还将是一个查看一些示例测试用例的机会。
测试我们的软件
现在,让我们专注于学习如何设置和清理我们的项目和代码库,以隔离测试的影响——也就是说,设置和拆卸。设置和拆卸是在编程的上下文中在每个测试或一系列测试之前和之后采取的操作,尤其是在软件测试中。这样做可以确保我们有良好的测试覆盖率,并且我们的测试是可靠的。在设置和拆卸测试时遵循一种系统的方法至关重要,这保证了测试之间是独立的,不会相互影响,产生精确和可靠的发现。
在自动化测试中,设置和拆卸步骤对于分离特定测试的影响至关重要。在每个测试之前,设置过程有助于建立一致的状态。这一阶段可能包括生成所需对象、连接到数据库或初始化特定设置等任务。通过在每个测试之前执行这些程序,我们保证每个测试都是从相同的起点开始的,无论之前的测试结果如何。这意味着测试的行为不受先前测试的副作用的影响,这对于准确、可靠的测试至关重要。
在测试期间进行的任何修改都可以在拆卸阶段撤销。这可能需要诸如切断数据库访问、删除测试数据或擦除测试期间创建的对象等操作。如果我们每次测试后都进行清理,我们不必担心一个测试期间所做的更改会影响后续的测试。如果没有拆卸步骤,测试最终可能会留下可能影响后续测试行为的某些修改。
由于设置和拆除阶段的存在,每个测试都在相同的起始环境中运行,并且不会对其他测试的环境产生影响,这些阶段共同确保每个测试都是隔离和可重复的。自动化测试的一个指导原则是确保测试是可信的,并且发现的任何缺陷都是归因于正在测试的代码,而不是测试配置或跨测试交互。
我们可以遵循一些规则来帮助我们生成有效的测试计划。让我们逐一了解它们,看看遵循它们如何为我们提供良好的策略:
-
设置测试环境:确保所有测试的测试环境相同。这包括测试执行所需的任何先决条件,如软件、设备和网络设置。
-
版本控制:使用版本控制工具,如 Git 和 GitHub,来跟踪代码和测试的更改,以便您可以查看新代码或测试可能引起的问题。
-
创建良好的测试:选择您希望运行的精确测试,然后列出每个测试的变量和测试条件。
-
利用测试隔离:创建测试,使它们不依赖于其他测试。这意味着每个测试都必须有自己的设置和拆除,不能依赖于任何其他测试的结果或状态。
-
使用监控:为了收集测试结果并发现测试数据中的任何异常或趋势,请使用日志和监控。
-
持续改进:始终根据每个测试周期的发现和建议增强测试和测试环境。
-
使用方法:实施每个测试前后执行的设置和拆除程序。这些技术可以用来构建和删除测试所需的资源,例如临时文件或数据库连接。
-
并行或顺序测试:按顺序运行测试以确保它们之间没有冲突,或者根据测试类型并行运行以加快过程。
-
模拟外部函数:一种将正在评估的代码单元与其依赖项(如外部库、服务或函数)隔离的测试方法称为模拟外部函数。通常,这是为了提供可预测和可控的测试条件。对于各种测试场景,模拟允许您在实际调用之前模仿外部依赖的行为。
现在我们已经学习了为测试设置项目的一些基础知识,是时候更进一步,学习如何为我们的 React.js 项目编写测试了。
我们如何为组件、属性和事件编写测试?
一旦你选择了测试框架和库,你就可以开始为你的 React 应用程序开发测试。你将创建各种测试,每个测试都有不同的目的和范围。我们可以编写几种类型的测试,包括组件测试、单元测试、集成测试、事件测试和端到端测试。目标是尽可能多地覆盖所有测试,以设定基准并给你提供信心和信心,即你的应用程序已经实施了彻底的测试。
组件测试是什么?
React 组件测试是一种单元测试形式,专门用于单独测试 React 组件。React 组件是 React 应用程序的构建块,定义了 UI、封装了功能并管理了应用程序的状态。测试 React 组件确保它们的行为正确,并满足预期的功能和标准。
在这个代码示例中,我们可以看到名为Counter.tsx的组件的组件测试看起来是什么样子。我们有一个配套的Counter.test.tsx文件,用于测试按钮的递增和递减。
这里是Counter.tsx文件的代码:
import { useState } from 'react';
const Counter = () => {
const [count, setCount] = useState(0);
const increment = () => setCount(count + 1);
const decrement = () => setCount(count - 1);
return (
<div>
<h1>Counter: {count}</h1>
<button onClick={increment}>Increment</button>
<button onClick={decrement}>Decrement</button>
</div>
);
};
export default Counter;
这是我们的测试文件Counter.test.tsx的代码:
import { render, screen, fireEvent } from '@testing-library/react';
import '@testing-library/jest-dom/extend-expect';
import Counter from './Counter';
describe('Counter component', () => {
test('renders Counter component', () => {
render(<Counter />);
expect(screen.getByText(/Counter:/i)).toBeInTheDocument();
});
test('increases the count when the Increment button is clicked', () => {
render(<Counter />);
fireEvent.click(screen.getByText(/Increment/i));
expect(screen.getByText(/Counter: 1/i)).toBeInTheDocument();
});
test('decreases the count when the Decrement button is clicked', () => {
render(<Counter />);
fireEvent.click(screen.getByText(/Increment/i));
fireEvent.click(screen.getByText(/Decrement/i));
expect(screen.getByText(/Counter: 0/i)).toBeInTheDocument();
});
});
我们现在已经学习了组件和组件测试文件的基础知识。
什么是单元测试?
React 单元测试是一种测试方法,专注于单个 React 组件。它们的目的确保每个组件的行为适当,遵循预期的功能和要求,并测试组件的逻辑和输出。单元测试是测试过程中的一个重要方面,因为它们帮助开发者识别和解决最细粒度级别的问题,确保应用程序的每个组件都正常工作。
我们在我们的组件测试示例中看到了单元测试的样子。
什么是集成测试?
React 集成测试是一种测试类型,用于验证多个 React 组件之间的正确交互和行为,或者 React 组件与其他系统组件(如 API 或外部服务)之间的交互。与单元测试不同,集成测试分析组件在程序内部如何相互作用,确保总体功能正确且数据在不同系统区域之间流畅流动。
集成测试是在describe()函数块作用域内运行的多个测试,正如我们之前组件测试示例中所示。
什么是事件测试?
React 事件测试是一种测试形式,专注于确认 React 组件事件处理器的行为和功能。触发 React 应用程序内部指定动作的用户交互或系统事件被称为事件。按钮点击、表单提交、鼠标移动和键盘输入都是事件的例子。通过测试事件处理器,你确保应用程序能够适当地响应用户交互,并在事件触发时采取必要的行动。
什么是快照回归测试?
在 React 中,我们可以使用快照测试作为一种确认我们的 UI 没有改变,并且保持与之前相同的方法。这有助于我们检查是否有意外改变可能会影响我们的设计在屏幕上的渲染方式。使用快照测试时,通常会对我们的代码库进行快照,然后与一个包含测试的参考快照文件进行比较。如果快照不相同,测试就会失败,这就是我们如何确保 UI 没有发生变化的。我们可以随时更新快照到最新版本以匹配我们对 UI 所做的任何更改。
端到端测试是什么?
端到端测试是一种尝试验证整个程序功能性的测试形式,从 UI 到后端服务和数据库。端到端测试用于模拟现实世界的用户情况,并确保整体结构按计划工作,提供无缝的用户体验和准确的功能。
Cypress 是一个流行的端到端测试库,它不与 React 项目捆绑在一起,但可以作为单独的包安装。您可以从文档中了解更多信息:www.cypress.io/。
我们可以使用我们之前的 Counter 项目示例来查看使用 Cypress 进行端到端测试时的代码样子。它与 Jest 和 RTL 非常相似,这三个包可以无缝协同工作。
让我们来看看我们修改过的 Counter 文件:
import { useState } from 'react';
import './App.css';
function App() {
const [count, setCount] = useState(0);
return (
<div className="App">
<h1>Counter App</h1>
<h2 data-testid="counter-display">Count: {count}</h2>
<button onClick={() => setCount(count + 1)}
data-testid="increment-button">
Increment
</button>
<button onClick={() => setCount(count - 1)}
data-testid="decrement-button">
Decrement
</button>
</div>
);
}
export default App;
这是我们的 Counter 测试文件:
describe('Counter App', () => {
beforeEach(() => {
cy.visit('/');
});
it('increases the counter', () => {
cy.get('[data-testid="increment-button"]').click();
cy.get('[data-testid="counter-display"]').
contains('Count: 1');
});
it('decreases the counter', () => {
cy.get('[data-testid="decrement-button"]').click();
cy.get('[data-testid="counter-display"]').
contains('Count: -1');
});
it('increases and decreases the counter', () => {
cy.get('[data-testid="increment-button"]').
click().click();
cy.get('[data-testid="decrement-button"]').click();
cy.get('[data-testid="counter-display"]').
contains('Count: 1');
});
});
这些示例为我们提供了端到端测试和组件测试之间的比较;相似之处非常明显。
管理我们应用程序中的数据
现在,我们将学习如何管理我们应用程序中的数据。这也被称为数据获取和模拟,这是两个需要掌握的重要概念。在进行这个领域的测试时,有必要了解数据获取 API 的工作方式和如何模拟它们的数据。这种知识对于几个原因都是必要的,包括开发效率、独立测试、与外部系统的集成和交互,以及成本和速率限制。
在开发者效率方面,开发者可以通过模拟 API 响应来分离应用程序的部分进行测试和开发。这意味着即使一个功能的后端部分尚未完成,前端开发者仍然可以通过模拟 API 响应来工作。至于独立测试,程序员可以通过模拟 API 提供的数据来确认他们的测试不受其他系统状态或行为的影响,从而产生更可靠和一致的结果。
当我们使用外部系统,如 API 时,我们可以在各种软件系统之间进行通信和交换数据。为了从数据库获取数据、与其他应用程序通信或向用户提供服务,许多当前的应用程序都是建立在 API 之上的。这就是为什么创建、维护和增强这些应用程序需要对这些 API 如何工作的功能性理解。
当我们考虑成本和速率限制时,许多 API 包含使用限制或额外费用。为了防止达到这些限制或浪费不必要的金钱,我们可以在开发和测试期间模拟 API 响应。
要在应用程序或系统中使用数据,必须从数据源(如数据库、API 或文件系统)获取数据。在在线应用程序和其他软件系统中,数据获取通常用于显示、分析或更改数据。它通常涉及向本地存储位置或远程服务器发送查询,处理答案,然后在应用程序中使用这些数据。
在测试、开发或设计流程时,模拟数据指的是创建虚构或模拟数据来复制实际数据的行为。当为系统构建功能、测试代码或设计用户界面时,模拟数据可以用作真实数据的替代品。它使程序员能够在不依赖可能私有的、不可靠的或不可达的外部数据源或实时数据的情况下测试他们的程序和应用程序。
我们如何为测试模拟数据?
在测试您的 React 应用程序时,很可能会需要模拟数据来模仿现实世界的情境。这对于测试依赖于 API 或第三方服务的组件特别有帮助。有多个库可用于模拟数据:
-
Axios Mock Adapter:Axios Mock Adapter 库拦截 Axios 请求并返回模拟数据
-
Nock:Nock 是一个 HTTP 请求拦截器,它返回伪造的数据
-
JSON Server:JSON Server 是一个使用 JSON 数据来模拟 REST API 的包
为什么我们应该在测试中使用模拟数据?
有许多原因说明为什么在测试中使用模拟数据而不是真实数据是一个好主意。我们可以使用模拟数据来分离我们系统的各个部分,这使得找到问题并测试特定组件变得更加简单,而不会受到其他依赖项的影响。受控的模拟数据确保测试可以重复进行并产生一致的结果,这是另一个优点。开发者也可以通过快速生成模拟数据来验证他们的代码和应用程序,而无需等待访问实际数据。此外,在开发和测试期间,敏感或私人数据可能会被暴露,这对组织来说可能是一个大问题。使用虚拟数据有助于防止这种情况发生。
在下一节中,我们将学习事件和计时器,这是至关重要的学习内容,因为它与编程中的异步或时间依赖性动作相关。异步编程是一种技术,允许你的程序在开始一个可能长时间运行的操作的同时,对其他事件保持响应,而不是需要等待该工作完成。
当那个任务完成时,结果将在你的程序中显示。像 JavaScript 这样的极其灵活的异步和并发编程语言非常强大,因为它与同步一样是单线程的,但与异步不同,它也不会阻塞代码执行,这对我们的 React 应用来说是非常好的。
使用事件和计时器的代码执行
现在,让我们继续学习关于事件和计时器的主题。在软件开发中,事件和计时器被实现来跟踪程序外部发生某事的精确时间点。事件和计时器是编程中的关键概念,尤其是在处理异步或时间依赖性动作时。它们也在测试此类系统中发挥着至关重要的作用。让我们更深入地探讨每个主题,以加深这些概念的理解。
事件是什么?
事件是在程序执行期间发生的活动或事件,通常由用户的输入、系统变化或其他来源触发。在事件驱动编程中,系统组件通过执行称为事件处理程序或回调的指定例程来对这些事件做出响应。
在测试中模拟事件至关重要,以确保当事件发生时,应用程序能够按预期响应。你可能希望测试你的 Web 应用程序如何响应用户活动,如按钮点击、表单提交或导航事件。通过在测试中模拟这些事件,你可以确保你的应用程序的事件处理程序正常工作,并按计划处理各种情况。
计时器是什么?
计时器在编程中发挥作用,因为它们在经过一定时间后或在固定间隔内计划执行某些函数或代码片段。在 JavaScript 中,常见的计时器函数是setTimeout和setInterval,它们允许你在延迟后立即运行一个函数或在预定义的间隔内定期运行。
计时器可能会使测试变得复杂,因为它们需要异步活动,这可能导致意外的行为或竞争条件。竞争条件,也称为竞争风险,是一种情况,其中软件或其他系统的实质性行为依赖于其他不可控事件的发生顺序或时间。当其中一个或多个替代行为是不希望出现的时候,它就构成了一个错误。
在测试依赖于计时器的代码时,适当地处理计时器至关重要,以确保产生准确和可靠的测试结果。既然我们已经了解了计时器,下一节将在此基础上进一步探讨调试以及如何充分利用我们对计时器的了解,这些知识可以协同使用。
使用 React DevTools 进行调试和分析
React DevTools 是一个浏览器插件,提供了各种工具来测试您的 React 应用程序。它允许您调查组件层次结构,查看 React 组件树,并验证组件的 props 和 state。我们将深入了解我们可用的各种调试技术,以及如何使用这些技术来增强我们对所编写代码的信心。
React DevTools 可以在以下图中看到。它可在 Chrome 网络商店中找到:

图 8.3:React DevTools
通过这样,我们已经了解了 React DevTools。接下来,我们将学习如何为我们的自动化测试配置 CI/CD 管道,这是我们调试工具箱中的另一个有用工具。
我们如何配置 CI/CD 管道来自动化测试?
为了确保我们的测试在每次代码更改时都运行,我们可以配置一个持续集成/持续部署(CI/CD)管道,该管道会自动运行测试。这使我们能够尽早发现问题,并确保我们的代码符合预期的标准。使用 CI/CD 管道自动测试 React 应用程序具有多个优点,包括更高的代码质量、更快的反馈、更大的协作以及更高效的部署流程。这些优势使团队能够更快、更一致地创建高质量的软件,使 CI/CD 管道成为现代软件开发的重要工具。
使用诸如 GitHub、GitLab 或 Bitbucket 之类的代码托管平台,并结合诸如 GitHub Actions、Jenkins、Docker、Kubernetes 或 CircleCI 之类的 CI/CD 测试平台是一种常见的做法。
我们如何调试 React 应用程序?
调试 React 应用程序可能很困难,但对于任何 React 开发者来说,这是一项必要的技能。在本小节中,我们将介绍一些调试 React 应用程序的基本策略和技巧。
我们如何利用 IDE/代码编辑器内的调试工具?
如 Visual Studio Code 等流行的代码编辑器包括 JavaScript 和 React 应用程序的调试功能。您可以通过配置启动配置立即在编辑器中调试 React 应用程序,这允许您创建断点、逐步执行代码并检查变量。
我们如何使用 DevTools 设置断点?
调试 React 应用程序始于使用断点,这会在特定时间点中断代码的执行。您可以使用浏览器内置的开发者工具设置断点、分析变量并逐行遍历代码。使用 DevTools 并浏览 源 选项卡,在程序中设置断点。定位必要的文件,滚动到您希望设置断点的行,然后单击行号即可。
如果您在设置断点后重新加载页面,代码将在断点处停止。
我们如何使用日志记录跟踪应用程序行为?
另一个用于故障排除 React 应用程序的关键工具是日志记录。您可以使用 console.log() 命令输出变量值、跟踪代码流程以及解决问题。
只需在代码中将 console.log() 后跟您希望记录的值,即可添加 console.log() 语句。
我们如何创建错误边界?
错误边界是 React 组件,可以在组件层次结构的任何位置检测 JavaScript 问题,报告它们,并用回退 UI 替换崩溃的组件。如果单个组件中的未处理错误被错误边界组件包裹,您可以防止应用程序崩溃。
我们如何理解 JavaScript 错误代码?
React 应用程序可能会遇到各种问题,从语法错误到运行时错误。了解这些问题及其相关的错误代码对于有效的故障排除至关重要。例如,React 开发者常见的一个典型问题是 TypeError: Cannot read property 'propName' of undefined'。当您尝试访问一个未定义对象的属性时,此错误会发生。
如果您理解错误代码及其相关问题,您可以更快地定位问题并进行修复。
我们如何安装调试器扩展?
浏览器调试器插件也可以帮助您调试 React 应用程序。例如,React DevTools 扩展包含专为调试 React 应用程序开发的工具,例如探索组件层次结构、检查属性和状态以及突出显示浏览器中的选定组件。同样,我们可以使用 Redux DevTools 扩展来调试应用程序的状态变化。使用 Redux 更适用于我们正在处理更复杂的应用程序,该应用程序需要全局状态。
我们如何使用 React 的 ESLint 插件?
ESLint 是一个流行的 JavaScript 代码检查工具,可以帮助您找到并纠正语法错误、可能的错误和代码质量问题。React 的 ESLint 插件添加了针对 React 应用程序定制的额外代码检查规则,帮助您检测常见错误和最佳实践违规。
错误监控工具是什么?
被称为错误监控工具的是用于跟踪、识别和报告在开发、测试或部署过程中在应用程序中出现的错误和异常的工具。这些工具帮助程序员定位问题、确定其根本原因并迅速解决它们。为了开发者能够开发出更好的软件,错误监控系统通常包括实时错误跟踪、警报和详尽的错误报告等功能。
可用的错误监控工具相当多,其中一些突出的包括 LogRocket、Sentry 和 Rollbar。
我们已经到达了本节和本章的结尾。我们对测试和调试的了解在面试中将是至关重要的,因为这是许多公司期望开发者擅长的领域。
摘要
本章为我们提供了对测试和调试 React 应用程序关键部分的深入理解。我们首先讨论了测试在软件开发中的重要性以及针对 React 应用程序的强大测试环境的必要性。接下来,我们探讨了各种测试框架和库,突出了它们的独特特性以及选择最佳工具时需要考虑的标准。我们还讨论了设置和清理的重要性。
我们在本章中涵盖了为组件、属性和事件构建测试的内容,强调了创建广泛的测试套件以确保我们的 React 应用程序的可靠性和可维护性的必要性。为了将测试过程进一步深化,我们讨论了为测试模拟数据,这使我们能够在不依赖外部依赖的情况下模拟真实世界场景。在测试过程中理解事件和时间也是我们讨论的另一个热点话题。
我们还介绍了 React DevTools,它帮助开发者评估和理解在测试阶段以及 CI/CD 管道中他们的应用程序的内部结构和行为。本章我们还讨论了调试 React 应用程序和使用错误监控工具。对于希望构建高质量、持久应用程序的 React 开发者来说,理解测试和调试的艺术是至关重要的,因为这些技能将使我们成为更好的开发者。在编程世界中,能够解决问题是一种非常受欢迎的品质。
在下一章中,我们将有机会了解一些最现代的 React.js 构建工具。Next.js、Gatsby 和 Remix 是 React.js 开发的三个流行选择,因此让我们进一步扩展我们的知识并掌握这些令人惊叹的库。
第九章:使用 Next.js、Gatsby 和 Remix 框架进行快速开发
开发者持续寻找工具和框架,可以在不牺牲他们在快速数字环境中应用程序的灵活性和弹性的情况下加快开发过程。像Next.js、Gatsby和Remix这样的全栈 React 框架已成为当代 Web 开发环境中的关键参与者,因为对无缝用户体验和动态、数据驱动应用程序的需求不断上升。本章将彻底研究这三个强大的框架,重点关注它们的独特功能、优势和用例。
我们将探讨每个框架如何处理不同的技术,例如服务器端渲染(SSR)、SEO 以及创建静态站点。这些框架可以用来创建快速、可扩展且性能极高的应用程序,因此我们将了解它们是如何做到这一点的。了解流行 React 构建工具之间的区别将帮助我们解释在面试中被问到时为何选择使用它们。那么,让我们继续学习,看看 Next.js、Gatsby 和 Remix 如何革新我们的 Web 开发方法,帮助我们实现数字目标。到本章结束时,我们应该在这个领域拥有更深入的知识,以帮助我们给出出色的面试答案。
本章我们将讨论以下主题:
-
将 React 用作全栈框架
-
静态站点生成
-
服务器端渲染
-
添加页面元数据
-
SEO 最佳实践
将 React 用作全栈框架
让我们通过学习 Next.js、Gatsby 和 Remix 开始我们的旅程。如果你是一位现代开发者,那么你应该使用这三种构建工具之一来开发你的 React 应用程序。它们在官方 React 文档中被推荐,了解它们的工作原理以及何时选择其中一个而不是另一个对于准备出色的面试答案至关重要。首先,我们将看看 Next.js 能为我们提供什么。
什么是 Next.js?
Next.js 是一个开源框架,借助 React 创建现代、可扩展和高性能的在线应用程序。由Vercel开发和维护的 Next.js 提供了一套完整的功能和优化,使其成为静态站点生成(SSG)和 SSR 的绝佳选择。
选择 Next.js 作为其 React 项目开发者的开发者将获得一个功能丰富的多样化工具。这使得 Next.js 成为构建 React 项目的最佳框架,并取代了Create React App成为默认选择。以下表格展示了其一些突出特点:
| 功能 | 说明 |
|---|---|
| 代码拆分 | 它可以自动将 JavaScript 代码拆分成更小的块,确保用户只加载给定页面所需的代码。这提高了 Web 应用程序的整体速度和加载时间。 |
| 动态导入 | Next.js 允许开发者利用动态导入来按需加载 JavaScript 模块和组件,从而减少初始包大小并提高速度。 |
| API 路由 | 包含构建无服务器 API 端点的支持,因此简单地在您的在线应用程序内构建RESTful或GraphQLAPI。 |
| 文件路由 | 它使用基于文件的路由方法,使得只需向pages文件夹添加新文件即可轻松添加新路由和页面。 |
| 内置 TypeScript | 提供 TypeScript 支持,允许开发者创建类型安全的代码,同时还能受益于改进的工具和重构。 |
| 混合渲染 | 使用 Next.js,我们可以启用静态站点创建以及 SSR,允许开发者根据特定情况选择最佳解决方案,甚至可以在单个应用程序内结合两种方式。 |
| 热重载 | 热重载为 React 应用程序的前端 UI 添加了动态功能。这意味着我们对应用程序代码所做的任何更改都会立即反映在用户实时看到的 Web 应用程序前端上。 |
| 内置 CSS 支持 | Next.js 与许多 CSS 库无缝工作,并在设置时可以选择配置Tailwind CSS。 |
表 9.1:Next.js 特性
总体而言,Next.js 是一个强大且灵活的框架,可以处理各种 Web 开发需求,从 SSG 到静态网站的构建,使其成为创建高效在线应用程序的程序员的一个有吸引力的选择。
现在,让我们看看 Gatsby,这是另一个流行的选择。
Gatsby 是什么?
Gatsby 是一个基于 React 的免费开源静态站点生成器,用于创建现代、高性能的 Web 应用和静态网站。Gatsby 使用 GraphQL,一种 API 查询语言,从多个来源检索数据,并在构建过程中将其与 React 组件集成,以生成静态 HTML、CSS 和 JavaScript 文件。最终结果是网站速度快、SEO 友好且优化良好。
它与 Next.js 有一些相似之处。然而,它在一些关键领域有所不同。首先,它由 GraphQL 驱动,也可以用作无头内容管理系统(CMS)。无头 CMS 将展示层(内容展示的地方)与后端(内容维护的地方)分开。它区分了知识和展示。这允许在不同数字媒体平台上重用和重新排列材料。基本上,它相当于开发一个 WordPress 网站,但在这个案例中,它是为 JavaScript 开发者构建的,因此我们可以访问完整的特性集。
我们可以查看一些独特的特性,这些特性使 Gatsby 成为一个出色的 React 构建工具:
| 特性 | 说明 |
|---|---|
| GraphQL 集成 | Gatsby 利用 GraphQL 构建一个统一和灵活的数据层,允许开发者从 Markdown 文件、CMS、API 和数据库等多种来源检索数据,并通过他们的 React 组件使用它。 |
| 插件库 | 它拥有一个广泛的插件库,让程序员能够自定义其功能,将其与多种服务集成,并根据自身需求调整开发过程。 |
| 渐进式网络 应用(PWA) | 通过 Gatsby 生成的网站可以轻松创建 PWA,使其能够在手机和平板电脑上实现离线访问、快速启动和类似应用程序的性能。 |
| 性能提升 | 为了保证快速加载时间和无障碍的用户体验,Gatsby 自动执行多种速度优化,例如代码拆分、内联关键 CSS 和图片的懒加载。 |
| 多才多艺的 托管选择 | 该框架与各种托管系统配合良好,提供持续部署,确保网站设置和维护简单。 |
表 9.2:Gatsby 特性
Gatsby 是一个强大且灵活的框架,用于创建快速、SEO 友好和彻底优化的 React 和 GraphQL 网络应用程序和静态网页。由于其注重速度、开发者满意度和灵活性,它成为程序员和企业的首选选项。
最后,让我们看看我们的最终选择,Remix 框架,它能做什么,以及其功能和特性如何与前两种选项相匹配。
什么是 Remix?
Remix 是一个使用 React 的尖端网络框架。由React Router的创建者构建的 Remix,旨在为开发者提供出色的用户体验,同时强调网络基础和最佳实践的重要性。就像 Next.js 和 Gatsby 一样,Remix 框架也拥有众多特性,使其成为考虑 React 项目起点时的优秀选择。Remix 具有许多功能,包括以下表格中突出显示的功能:
| 特性 | 说明 |
|---|---|
| 灵活性 | Remix 旨在与各种后端技术、服务器系统和信息数据源交互,这使得它成为在多种应用程序上工作的程序员的理想选择。 |
| 嵌套路由 | 它是一个易于使用且强大的嵌套路由框架,允许你在浏览过程中保持状态和滚动位置的同时,设计复杂的多级结构。 |
| 渲染输出 | 该框架具有 SSR、SSG 和客户端渲染(CSR)功能,允许程序员为特定用例选择最佳的渲染技术,甚至在一个应用程序内混合多种方式。 |
| Web 标准 | 由于其依赖于互联网标准,并强调利用浏览器原生技术的意义,它在使用Fetch API、其他 HTML 方法以及浏览器导航等特性时,提高了速度和易用性。 |
| 数据获取改进 | 用户可以访问 Remix 的“loader”方法,这些方法提供了一种独特的信息获取策略,使程序员能够从服务器或客户端获取信息,确保数据的快速处理和快速页面转换。 |
表 9.3:Remix 特性
简而言之,Remix 是一个强大且灵活的 Web 框架,用于创建高性能、功能丰富的 React 应用。它通过专注于 Web 原则、最佳实践和开发者体验,与其他框架区分开来,成为想要构建现代在线应用的开发者的一个有吸引力的替代品。这些特性使其对开发者友好,文档也易于理解。
现在,让我们进入下一节,我们将学习关于 SSG 的内容,这是一个流行的工具,可以帮助网络开发者快速有效地设计网站。我们讨论的所有框架都使用它来进行构建。
静态站点生成
现在是我们学习 SSG 以及为什么它如此重要的时候了。我们将概述静态站点创建的过程、它的工作原理以及为什么它在全球数百万开发者中变得如此受欢迎。有了这项技术,你将能够快速开发令人惊叹的网站,而无需担心安装复杂的服务器设置或处理烦人的后端操作。让我们行动起来吧。
你为什么应该关注 SSG?
这是一种流行的网站开发方法,原因很好。不是依赖于服务器在用户每次请求页面时即时生成站点内容,静态站点创建预先构建所有必需的文件,并在用户请求页面时通过用户的计算机发送。这导致加载时间更快,整体性能更好。静态站点也更容易且成本更低,因为没有动态内容需要处理。因此,无论是试图简化工作流程的开发者,还是希望提高网站性能和可访问性的公司所有者,静态站点创建绝对是一件值得考虑的事情。
使用静态站点生成器的优势有哪些?
对于旨在简化并改进其流程的开发者来说,静态站点生成器是变革性的。不依赖于数据库或服务器端脚本的静态站点生成器,即使在巨大流量下也往往很可靠,这使得它们成为企业和个人都极佳的选择。此外,凭借大量主题的灵活性,开发者可以在享受静态站点性能的同时,完全控制其网站的美观。凭借性能、安全性和个性化等优势,静态站点生成器越来越受欢迎也就不足为奇了。
使用静态站点生成器可以带来更多优势并改善维护。现在让我们了解这些如何使我们的应用程序更安全、更健壮。
为什么静态网站的速度和性能如此之好?
静态站点生成器是帮助通过将源文件(如 Markdown 或模板)转换为静态 HTML、CSS 和 JavaScript 文件来创建静态网站的工具。它们因在速度和性能方面优于动态网站而闻名,以下是我们将要了解的许多原因:
-
按需页面内容:与动态网站不同,动态网站根据每个请求即时构建页面,而静态站点生成器在整个构建过程中预先构建所有页面。当用户请求页面时,网站的服务器只需发送已创建的 HTML 文件,这使得加载更快。
-
缓存静态文件:内容分发网络(CDNs)和网站可以轻松缓存静态文件。CDNs 可以在全球各地的主机上保留静态文件的副本,使用户能够从附近的位置查看内容。这减少了延迟并加快了加载时间。
-
数据压缩:静态站点生成器通常包含用于最小化和压缩资产(如 HTML、CSS 和 JavaScript)的工具。这减少了文件大小,从而加快了传输和加载时间。
-
减少网页加载时间:由于网页是预先构建的,运行它们的计算机不需要花费时间执行服务器端代码或搜索数据库。这减少了系统压力,并使计算机能够同时处理更多请求。
-
强大的安全设置:由于静态网页没有数据库或服务器端编程,它们不太容易受到如SQL 注入和跨站脚本(XSS)等攻击。这可以通过降低网站被黑客攻击和被犯罪活动拖垮的机会来立即提高性能。
因此,基本上,静态站点生成器为网站开发者提供了各种优势,包括更快的性能、改进的安全性、以及更简单的维护和升级。了解市场上最常见的静态站点生成器以及安装静态站点生成器的逐步说明至关重要。
了解配置静态站点生成器的最佳实践也有助于确保您的品牌网站从一开始就能正常工作。最后,就像所有计算机用户一样,定期跟踪您的静态网站对于保持持续的最佳性能和检测任何可能的危险至关重要。对于想要将他们的网站构建技能提升到下一个层次的人来说,使用静态站点生成器是一个不错的选择。
在这个领域我们的知识得到提升后,让我们继续学习 SSR,这样当我们被问到时,也能深入讨论这个话题。
服务器端渲染
在本节中,我们将探讨创建快速加载网页最有效和最成功的方法之一:SSR。使用 SSR 技术确保网站快速加载且在各种设备上看起来都很好。我们将讨论它意味着什么,它是如何工作的,为什么它很重要,以及采用这种策略而不是其他策略的好处。
然后,根据上述信息,我们将学习如何使提高我们网站的用户体验和更好地控制加载时间变得容易得多,这样我们的用户就可以更快且不间断地访问材料。当我们完成这一章时,我们将拥有解释 SSR 所需的所有知识。现在,让我们回答一些关于 SSR 的最大问题。
什么是 SSR 以及为什么它很重要?
网站必须尽可能快地加载。这就是 SSR 登场的地方。在将网页发送到客户端浏览器之前,在服务器端生成网页的过程称为 SSR。这大大加快了过程,因为服务器可以向浏览器提供一个预先填充的 HTML 文件,而不是等待浏览器请求必要的资源并自己构建 HTML 文件。
这不仅提升了用户体验,还提供了主要的 SEO 优势,因为搜索引擎可以比客户端生成的内容更容易地抓取服务器端渲染的内容。简而言之,SSR 是确保更快加载时间和提高搜索引擎曝光率的重要策略。
SSR 是如何工作的?SSR 页面加载的基本原理
SSR 是一种可以显著提升网站速度和用户体验的方法。SSR 是一种在将网页传输到客户端(浏览器)之前在服务器上渲染网页的方法。当用户请求一个页面时,服务器创建 HTML,填充基本数据,并将其作为预渲染的页面返回给客户端。这种策略大大减少了加载网站所需的时间,并使得 CSR(客户端渲染)更快。在基本术语中,SSR 通过在服务器上生成带有初步内容的页面,提供了一个更快、更高效、更直观的体验。
SSR 有哪些优点?
目前网站最重要的一个要素就是它们的加载速度。没有人喜欢等待网站加载,这就是 SSR 发挥作用的地方。SSR 允许服务器在将 HTML 代码传输到浏览器之前构建网站,从而实现更快的加载时间、增强的 SEO 和更高的用户可访问性。此外,SSR 还可以帮助预防一些常见问题,如内容在准备好之前加载(可能导致设计损坏)和内容在屏幕上移动。这导致了更流畅的用户体验。总的来说,SSR 的好处使其成为成功和高效网站开发的一个关键组成部分。
当在 Web 应用程序中使用时,SSR(服务器端渲染)提供了各种好处。在这些许多优点中,我们可以找到许多原因来解释为什么它现在如此广泛地被使用。让我们深入了解:
-
强大的 SEO(搜索引擎优化):因为最终编程文件是在传输到客户端的 Web 浏览器之前在服务器上创建的,所以搜索引擎可以简单地扫描和索引服务器端渲染的内容。这有助于搜索引擎理解并评估你的网站,从而提高其在搜索结果中的突出度。
-
动态数据:SSR 适用于需要定期更新或需要客户端查看个性化内容的 App。由于内容是在服务器上创建的,它可以根据用户的输入、cookies 或其他因素轻松更改或定制,而无需重建整个网站。
-
快速加载时间:与 CSR 相比,SSR 可以提供更快的初始页面加载时间,因为浏览器从服务器获得完全渲染的 HTML 内容,无需等待 JavaScript 加载和运行后才在屏幕上显示内容。这可以提高用户对网站性能的感知。
-
Web 浏览器兼容性:使用 SSR,我们可以改善对过时浏览器和具有受限 JavaScript 功能的设备的支持。由于数据是在服务器上显示的,无论某些 JavaScript 功能是否可用,这些浏览器都可以查看它们。
使用 SSR,我们获得了许多好处,但我们也需要考虑 SSR 如何对我们的应用程序产生不利影响。接下来,我们将看到这些缺点,并学习它们是否是决定性的。
SSR 有哪些缺点?
虽然 SSR 具有增加 SEO 和加快初始加载时间等优势,但它也有一些缺点。让我们来看看其中的一些缺点:
-
应用服务器需求:为了显示和传输内容,SSR 依赖于服务器。如果服务器出现延迟或宕机,这可能会导致问题,因为整个网站可能会变得无法访问或加载缓慢。
-
更复杂的架构:实施 SSR 时,应用架构变得更加复杂,因为需要服务器端编程和管理。因此,调试和维护可能会变得更加困难,而开发时间可能会增加。
-
缓存问题:与静态文件相比,SSR 产生的动态内容通常更难以充分缓存。因此,缓存性能的优势可能会降低,服务器的负载可能会增加。
-
服务器效率降低:SSR 会增加服务器负载和 CPU 使用,尤其是在流量大的网站上,因为它需要服务器为每个请求渲染页面。因此,可能需要更强大且昂贵的服务器基础设施,以及更长的周转时间。
尽管有这些缺点,但在某些情况下,SSR 仍然是一个有用的选择,例如,如果 SEO 是必需的,或者必须快速显示动态内容。在选择渲染策略之前,分析利弊并考虑项目的具体需求至关重要。
保持主题,现在让我们转向与页面元数据相关的 SEO 主题,这将为我们提供关于如何改进应用程序和网站的 SEO 的面试问题的答案。
添加页面元数据
为了增强 SEO 而添加页面信息并不一定具有挑战性。一旦我们通过了这一章节,我们应该能够掌握并讨论如何通过使用 SSG 和其他技术来提高网站可见性的基础知识。一旦我们掌握了这些原则,开发页面就会变得容易得多,这为我们提供了在现实世界中提出关于如何创建更好的 SEO 问题的答案所需的经验。
页面元数据是什么?为什么它对 SEO 很重要?
为了增加网站流量,SEO 是必不可少的。页面元数据可能是良好 SEO 策略中最关键的部分之一,但还有更多。标题标签、元描述和关键词是元数据的例子,这是描述网页内容的信息。通过优化元数据,可以使搜索引擎更好地理解您网站上的内容,并使潜在访客更加感兴趣。简单来说,页面元数据既是访客也是搜索引擎的路线图,因此它是任何 SEO 计划的关键部分。
对于 SSG,可以使用哪些类型的页面元数据?
理解页面信息的重要性对于创建静态网站至关重要。原则上,元数据是关于信息的信息,它帮助搜索引擎理解您网站的目的。标题标签、元描述和 alt 文本只是经常在网站上使用的几种信息类型。显示在您的网络浏览器标签中的文字被称为标题标签,元描述则给出对页面内容的简短概述。当图片包含描述性的 alt 文本时,搜索引擎通常会准确地对其进行分类。通过包含这些不同类型的元数据,您可以提高您网站的搜索引擎排名,并确保您发布的内容被正确索引。
为了清楚地了解元数据可以产生多大的影响,我们将学习一些流行的类型。现在我们将学习元标题、元描述、元视口、元机器人、元语言和开放图标签。
什么是元标题?
任何网站的 SEO 计划都必须包括标题标签。确保它们适当地代表每个页面的内容,因为当搜索引擎如谷歌爬取您的网站时,它们首先注意到的是标题标签。除了关键词之外,标题标签应该是清晰、易懂且有趣的。一个写得好的标题标签可以提高您的搜索引擎排名,并增加点击量。因此,无论您是在开发新网站还是优化现有网站,都要特别注意您的标题标签,并确保它们能够成功地吸引人们对您网站的注意。
我们可以在一个例子中看到它的样子:
<title>Home page – Programming content</title>
标题标签基本上描述了一个网站页面。
什么是元描述?
元描述现在是 SEO(搜索引擎优化)领域中不断变化的网站优化的重要组成部分。这些文本摘录为说服潜在客户访问您的网站提供了理想的机会。通过创建一个引人入胜的元描述,使用您选择的关键词,可以脱颖而出。元描述可以帮助提高您网站的整体搜索引擎排名,所以它不仅仅是关于获得点击量。花些时间创建一个清晰、直接的元描述,适当地总结您的内容,吸引那些寻找您能提供的东西的访客。
这里是一个实际应用的例子:
<meta name="description" content="This is a website about programming">
这个标签的内容给出了网页内容的摘要。
什么是元视口?
元视口是一种元数据元素,用于网页的 HTML 代码中,以管理网站内容在多个设备上的布局和缩放,尤其是具有各种屏幕尺寸和分辨率的移动设备。元视口标签对于在手机、平板电脑和其他移动设备上渲染响应式和易于使用的网站至关重要。
这里是一个元视口的例子:
<meta name="viewport" content="width=device-width, initial-scale=1">
这段代码告诉浏览器如何控制页面的宽度,并给它一个 1 的比例尺,这在移动设备上查看时很有用。它有助于页面在移动设备上显示得更好,这些选项使网站能够调整并具有响应性。
元机器人是什么?
元机器人是一种元数据元素,用于网页的 HTML 代码中,以指导搜索引擎爬虫(也称为机器人、蜘蛛或爬虫)如何索引或跟踪网站上的链接。网站管理员可以通过使用元机器人标签来调节搜索引擎爬虫在查看其网站页面时的行为,这有助于优化索引并避免潜在的 SEO 问题。
让我们看看一个例子:
<meta name="robots" content="noindex, nofollow">
在这个例子中,noindex值指示爬虫不要索引此页面,这意味着它不会出现在搜索结果中,而nofollow值指示爬虫不要跟踪此页面上的任何链接。
元作者是什么?
元作者标签是一种元数据元素,用于网页的 HTML 代码中,以表示网站材料的作者或创作者。这个标签并不直接与搜索引擎优化(SEO)相关联,但它可以向用户或搜索引擎提供有关管理网站页面上书面材料的个人或组织的有用细节。
让我们看看一个例子,看看它是什么样子:
<meta name="author" content="Sarah Thomas">
这告诉所有查看 HTML 的人,Sarah Thomas 创建了该网页。
元语言是什么?
元语言标签,通常被称为内容语言元标签,是一种元数据元素,用于在 HTML 代码中指定主页信息的主体语言。此标签有助于搜索引擎、网络浏览器和其他在线服务理解内容语言,这对于搜索、解释和可访问性可能很重要。
我们可以在以下代码示例中看到它的样子:
<meta http-equiv="Content-Language" content="en-us">
在这个例子中,HTML 元素使用lang="en"属性来指定整个 HTML 页面是用英语编写的。《meta http-equiv="Content-Language" content="en-us">`标签进一步说明内容是用美式英语编写的。
开放图标签是什么?
开放图标签负责使社交媒体平台上的链接比其他链接更具视觉吸引力。您可以通过使用开放图标签来决定您的网站链接在 Facebook、Twitter/X、LinkedIn 等社交网络上的显示方式,这些开放图标签是 HTML 代码片段。您可以通过在网站上使用开放图标签来定制当有人分享链接时出现的图片、标题和描述,从而提供更有趣和更具美感的链接预览,这最终可能导致互动和点击率的增加。所以,如果您想让您的网站在社交网络上脱颖而出,请考虑使用它。
我们可以在代码中看到一个开放图标签的例子:
<!-- Twitter Example -->
<meta property="twitter:card" content=
"summary_large_image" />
<meta property="twitter:url" content=
"https://www.yoursite.com/page" />
<meta property="twitter:title" content=
"Your Website Title" />
<meta
property="twitter:description"
content="A description of your website."
/>
<meta
property="twitter:image"
content="https://www.yoursite.com/image.jpg"
/>
<!-- Facebook Example -->
<meta property="og:type" content="website" />
<meta property="og:url" content=
"https://www.yoursite.com/page" />
<meta property="og:title" content=
"Your Website Title" />
<meta property="og:description" content=
"A description of your website." />
<meta property="og:image" content="https://www.yoursite.com/image.jpg" />
此代码演示了使用 Twitter/X 和 Facebook 的示例来使用开放图标签。
现在我们对这些标签的用法有了更好的理解,另一个我们应该注意的重要领域是网站审计。这就是我们基本上在我们的网页上运行测试,以测试其使用我们所学到的元标签的 SEO 能力,这将给我们带来更高的网站评分。Lighthouse Chrome 扩展是网站审计的一个非常受欢迎的选择。现在让我们来了解一下它。
我们如何使用 Lighthouse 扩展来审计我们的网站?
Lighthouse 是一个开源的自动化工具,用于提升 Web 应用的性能、质量和准确性。Lighthouse 通过对该页面运行一系列测试,然后生成一份关于页面如何有效执行的报告来审计页面。根据结果,你可以利用失败的测试来确定你需要做什么来提升你的应用。
在下一节中,我们将学习 SEO 最佳实践,我们将获得更多有价值的知识,这些知识在部署在线实际应用时将非常有用。
SEO 最佳实践
SEO 是通过优化网站的内容、结构和其它方面来提高网站在搜索引擎结果中的排名的过程。因此,我们应该实施可以帮助我们的网站在搜索引擎结果中排名更好的 SEO 最佳实践。通过这样做,它增加了我们吸引更多有机访问者和提高其整体在线可见性的机会。我们可以通过许多方式实现这一点,其中一些方法如下列所示:
| 策略 | 描述 |
|---|---|
| 元标签 | 为每个页面创建独特且吸引人的元标题和描述。因为这些是出现在搜索引擎结果中的片段,它们应该是内容的准确描述。 |
| 页面 加载速度 | 优化你网站的加载速度,以创造更好的用户体验并改善搜索引擎结果。压缩图片、减少代码并利用浏览器缓存。 |
| URL 结构 | 使用描述性、易于阅读的 URL,以表明项目的主题。避免使用长、复杂的 URL,其中包含多余的参数。 |
| 优质内容 | 创建相关、有洞察力和娱乐性的内容,以满足目标受众的需求。写作应正确无误、易于理解,并与主题或关键词相关。 |
| 移动优化 | 网站往往从移动用户那里获得高流量,甚至比桌面用户还高。由于移动兼容性是搜索引擎的排名标准之一,请确保网站是响应式和移动友好的。优秀的移动体验会增加用户兴趣并促使他们再次访问。 |
| 优秀的 UI 和 UX | 通过简化导航、使行动号召明显以及合理组织材料,专注于提供愉悦的用户体验。 |
| 安全证书 | 在您的网站上安装 SSL 证书以确保数据加密并提高搜索引擎排名。HTTPS 是 Google 的排名指标。 |
表 9.4:SEO 最佳实践
我们已经成功完成了本章,并学到了很多关于不同元数据类型差异的知识。现在,我们将在我们的 React 项目中以及面试条件下应用所学到的知识,因为这种知识始终是有用的。
摘要
本章对 Next.js、Gatsby 和 Remix 这三个强大的全栈 React 框架进行了全面考察。我们更深入地了解了这些技术如何通过探索其特殊功能和用例来加速网络开发过程,并轻松构建动态、数据驱动的应用程序。我们在路上涵盖了当代网络开发的关键主题,包括创建静态网站、SSR 以及为 SEO 添加页面信息。通过分析每个框架的优缺点,我们为选择特定项目的最佳工具铺平了道路。
Next.js、Gatsby 和 Remix 为我们提供了开发 React 应用程序的不同选择。我们已经探讨了这三者的区别,这为我们提供了良好的面试答案,因为我们能够比较这些区别,并给出为什么在项目中会选择其中一个而不是另一个的有效理由。
在本章中,我们涵盖了有用的信息,这些信息鼓励我们更多地了解这些框架及其优势,以便在竞争激烈的面试市场中脱颖而出。掌握 Next.js、Gatsby 和 Remix 的道路才刚刚开始,有无数的发展和创新机会。这些技术的最终潜力在于你的想象力、创造力和决心,在你继续学习和探索的过程中,推动网络开发领域的可行性极限。
在下一章中,我们将学习如何破解现实世界的编程任务。
第四部分:编程任务实践
在本部分,你将学习如何在规定的时间内以高级别破解面试过程中的编码任务。在深入项目之前,我们还将获得有用的提示和建议。然后,我们将构建两个 React 应用程序来展示你的编程技能——一个应用程序基于 Redux、styled-components 以及 Firebase 后端,另一个应用程序基于 Next.js 工具包、GraphQL 和 SWR,使用 REST API。
本部分包含以下章节:
-
第十章, 破解任何现实世界的编程任务
-
第十一章, 基于 React、Redux、Styled Components 和 Firebase 后端的 App 构建
-
第十二章,基于 Next.js 工具包构建应用、身份验证、SWR、GraphQL 和部署
第十章:破解任何现实世界编程任务
在今天快速发展的技术环境中,为了有效地完成现实世界的编程作业,程序员必须管理越来越多的障碍和复杂程度。在本章中,我们将探讨基本阶段、工具和最佳实践,这将使你能够自信地处理任何编程项目,并生成高质量、可维护的软件。当我们能够自信地谈论这个主题并提供现实世界的例子时,我们的面试准备就会变得更好。随着时间的推移,我们将能够提供更多我们自己的个人经验,这使我们成为面试过程中更好的候选人。
本章将为你提供克服任何摆在你面前的编程作业所需的知识和信心,从设置你的开发环境到组织你的代码库和分享你的工作。我们将介绍如何选择正确的脚手架工具和模板,以帮助你构建一个出色的应用程序基础,同时节省你的时间和精力。我们还将探讨满足你项目需求的最优应用架构,在多功能性和易用性之间取得平衡。
随着我们继续前进,你会意识到测试你的代码以确保其可靠性、可扩展性和安全性是多么重要。我们将提供实际指导,教你如何在工作流程中包含测试策略,并展示如何使用测试驱动开发(TDD)从一开始就生成高质量的代码。最后,我们将引导你完成设置和管理你的 Git 仓库、编写清晰且信息丰富的 README 以及与世界分享你的工作的步骤。这不仅会增加你工作的曝光度和影响力,还会激发合作和持续发展。
到本章结束时,你将获得征服任何现实世界编程工作的必要能力和方法,你将朝着成为软件开发领域宝贵资产的道路迈进。
在本章中,我们将探讨与解决编程作业相关的主题,给你在这个编程领域所需的信心。这些基本技能对于在工作中以及面试中表现良好至关重要,因为你的问题解决能力决定了你是一名多么优秀的程序员。作为开发者,我们经常被问到我们的开发环境和技术栈,如果我们要在公司的另一个团队中有效工作,那么我们必须了解我们将要使用的工具。因此,在面试时对这些问题的正确回答可以表明我们知道自己在说什么,并且可以融入任何团队。
这些是我们将在本章中讨论的主题:
-
准备你的开发环境
-
选择正确的脚手架工具或模板
-
确定应用程序架构
-
测试你的代码
-
创建带有 README 的 Git 仓库并分享
技术要求
在你的机器上,请确保你已经下载并安装了最新版本的 Node.js 和npm,你可以在这里找到:nodejs.org/en。同时,确保你已经安装了一个代码编辑器,例如 Visual Studio Code,你可以从这里下载:code.visualstudio.com/。
准备你的开发环境
现在我们来学习如何制作优秀的 React 应用程序。最困难的步骤是设置你的开发环境,但这并不一定如此。借助工作流程背景知识和这些设置成功的 JavaScript 开发环境的基本指南,你将更接近你的 React 开发者梦想工作。本节将带你了解整个过程的每个阶段,从为你的项目选择最佳的脚手架工具到调试和修复出现的任何困难。每个开发者都应该了解设置开发环境并让框架运行的基本知识。在面试环境中,我们可能会被要求进行技术测试,因此能够找到正确的工具、安装它们并让你的代码库运行是作为开发者所需的所有知识。在面试时能够做到这一点是至关重要的。所以,让我们开始吧。
你为什么需要一个好的开发环境?
开发环境是开发者软件开发过程中的一个重要组成部分。开发环境提供了一个隔离的环境,在其中你可以生成和测试你的代码,而不用担心会干扰到实际的生产环境。这减少了最终用户或客户中断的风险,并允许你尝试和测试不同的编码方式。开发环境还允许你测试你的源代码针对多个平台、浏览器和设备,以验证你的应用程序对所有用户都能完美运行。如果你利用开发环境,你可以更有效地编写代码,更早地识别错误和缺陷,并最终向消费者提供高质量的产品。
开发环境要求我们拥有一个集成开发环境(IDE)或一个文本编辑器/代码编辑器。但它们之间有什么区别呢?让我们来看看。
IDE 和文本编辑器/代码编辑器有什么区别?
我们有许多方法可以设置我们的开发环境。开发者,尤其是那些使用 JavaScript 和 React 的开发者,可以根据个人需求选择各种工具。IDE 是一个流行的选择,因为它包括用于开发、调试和测试的全套工具。文本编辑器,作为一种基本的工具,能够有效地简化编码,也是另一个选择。另一方面,对于喜欢直接在浏览器中工作的开发者来说,在线代码编辑器可能是最佳选择。这些环境允许快速轻松地进行测试和部署,无需在您的桌面或笔记本电脑上安装任何软件。
现在我们已经了解了这些工具,是时候学习设置现代 React 开发环境的基本步骤了。
您如何设置 React 开发环境?
设置 React 开发环境可能看起来很困难,但有了适当的步骤,任何人都可以完成它。首先,您必须熟悉 JavaScript 和命令行。然后,在此处下载并安装 Node.js 和npm:nodejs.org/en,这两者都是 React 开发所必需的。选择一个编码编辑器,例如 Visual Studio Code,您可以从此处下载:code.visualstudio.com/。随后,建议使用 Next.js、Remix 或 Gatsby 命令来构建一个新的 React 项目。
您可以在此处了解设置过程:
react.dev/learn/start-a-new-react-project
安装软件需要我们使用包管理器,它基本上是一组捆绑在一起的工具,可以自动化一个过程。这使得我们能够从计算机上安装、升级、配置和删除软件。npm是 JavaScript 生态系统中最受欢迎的包管理器。其他选择包括yarn和pnpm,它们都有自己的优缺点。还有npx,它是npm包运行器的缩写,允许开发者运行npm注册表上可访问的任何 JavaScript 包,而无需安装它。
开发者通常会根据他们使用的工具的文档使用npm或npx。无论如何,我们将在接下来的示例中使用npm和npx。设置 React 项目的命令相当简单。让我们看看每个 React 框架的语法,如下所示:
-
Next.js
运行此代码将构建一个 Next.js 入门项目:
npx create-next-app这很简单,当您完成设置后,您就可以开始使用了。
-
Remix
此处的代码是用于 Remix 的,将为我们创建一个新的项目:
npx create-remix再次强调,设置过程很容易跟随,因此创建一个 Remix 项目不会花费很长时间。
-
Gatsby
使用此代码为我们设置一个 Gatsby 项目:
npx create-gatsby -
Vite.js
使用此命令,我们可以设置一个 Vite.js 项目:
npm create vite@latest
Gatsby 的设置与 Next.js 和 Remix 类似,因此在这三者之间切换相当容易。
最后,启动开发服务器,看看你的项目是如何焕发生机的。虽然设置过程可能看起来既耗时又繁琐,但最终产品绝对物有所值。当你拥有一个完全工作的开发环境时,你将能够开始开发动态且有趣的网络应用程序。
开发环境的好坏取决于我们用来创建它的工具。让我们更多地了解 React 脚手架工具,看看它们如何使我们的工作变得更加容易。
我们可以使用哪些工具来搭建 React 项目?
我们在探索网络开发的世界时,肯定会遇到一些新颖且引人入胜的工具和框架。有几款脚手架工具在众多工具中脱颖而出。webpack 是一个开源的打包运行器,它简化了代码打包部署的过程。Next.js、Remix 和 Gatsby 都使用了 webpack。然后是 Babel,这个工具允许我们使用最新的 JavaScript 版本,并将其编译成大多数浏览器都支持的格式。这些技术协同工作,为我们提供无缝且快速的脚手架体验,从而让我们有更多时间专注于提供最佳的用户体验。
脚手架工具对于拥有易于维护的项目至关重要,我们将发现为什么这是如此。
为什么脚手架工具对我们项目的成功如此重要?
对于任何职业来说,拥有正确的工具都是至关重要的,项目管理也不例外。拥有正确的工具,从看板到协作软件,都可能使项目成功或失败。这些工具改善了沟通,提高了生产力,并简化了操作。没有它们,项目可能会变得混乱且难以管理。正确的工具可以帮助团队保持进度,按时完成任务,并最终取得成功。投资正确的项目管理工具可能看起来像是一项不必要的开支,但从长远来看,它可能会节省时间、金钱,并让你的精神得到放松。同样,检查你是否安装了最新版本的 Node.js 或与之兼容的版本也非常重要。这可以帮助你避免破坏性更改,这些更改可能会影响你正在使用的软件,因为可能存在兼容性问题。
设置环境既令人兴奋又令人畏惧。在这个过程中遇到技术问题可能会让人感到沮丧,因此我们将学习如何克服其中的一些问题。
你如何在设置环境的过程中解决常见的故障?
为了使这个过程不那么令人不愉快,你可以解决典型的困难。首先,请确保你拥有所有必需的工具和软件。然后,确保你的互联网连接可靠且稳定。如果你遇到问题或丢失文件,尝试刷新你的浏览器或清除你的缓存。仔细遵循任何指示,并在必要时寻求帮助。记住,一点耐心和努力将有助于解决在整个设置过程中可能出现的任何问题。
总结来说,拥有一个有效的 React 开发环境对任何 Web 开发者来说都极其有益。它不仅允许你成功开发和测试你的项目,而且允许你直接进入编码任务,而不是与集成问题作斗争。之前讨论的工具和框架已被证明在创建所需环境并提供出色的脚手架方面至关重要。对这些组件的深入了解将使我们能够无错误或技术问题地部署我们的应用程序。在开发 React 应用程序时,正确的环境和结构对于成功至关重要。
现在我们更深入地探讨脚手架工具,并学习如何为我们的编程任务选择一个好的架构。
选择合适的脚手架工具或模板
脚手架工具、模板、程序和其他资源都可供你使用,帮助你实现你的想法。然而,为了充分利用你的项目,你必须花时间正确评估这些选项。在本节中,我们将探讨选择适合 React 应用程序的可行架构的许多组成部分以及如何有效地使用它们。
编程中的脚手架是什么?
脚手架是创建一个结构化框架、布局或模板的过程,以便为后续开发提供一个稳定的基座。脚手架通过自动化重复操作并为项目建立有效的结构,为开发者节省时间和精力。对于各种编程语言和技术,存在脚手架工具和框架来帮助开发者以最少的手动设置启动他们的项目。这些工具通常包括最佳实践和推荐模式,这可能导致易于维护和扩展的代码库。
脚手架通常包括以下内容:
-
文件结构:建立一个一致、可持续的系统来组织文件和文件夹,使得导航简单,并能够理解项目的设置
-
配置和设置文件:为工具和库(如 linters、bundlers 和 transpilers)创建配置文件,以支持维护标准化的开发环境并简化构建过程
-
代码示例:提供如何利用框架、库或项目组件的简单示例,可以帮助用户快速启动并掌握推荐的最佳实践
-
样板启动套件:生成可快速定制用于特定用例的可重用代码片段或组件,包括建立构建系统、设置 Web 服务器或开发标准用户界面(UI)元素
除了脚手架之外,我们还需要考虑项目的规模、复杂性和完成 React 编程任务所需的技术。在下一节中,我们将讨论并学习更多关于项目方面的内容。
您如何在创建项目时确定要考虑哪些项目因素?
在创建项目时,有各种方法来分析我们应该考虑哪些项目因素。这可以包括以下内容:
-
每个项目的规模
-
复杂度
-
需要的技术
通过正确考虑这些变量,您可以开发出满足组织需求的优秀前端设计。对于较小的项目,可能需要更简单的代码和测试,而对于较大的项目,可能需要更复杂的框架来实现 React 组件。了解完成手头工作可能需要哪些技术至关重要。通过全面了解所有这些因素,您可以确保您的工作质量,满足客户需求,并提供流畅的用户体验。
我们现在将学习如何评估工具和模板的功能,因为这对于项目设置和规范是必需的。
您如何评估每个工具或模板的功能,并确定哪个最适合您的需求?
在选择最适合您需求的工具或模板之前,仔细比较每个工具或模板的功能。通过花时间彻底分析每个工具的功能和能力,您可以确保选择最适合您特定需求的工具。无论您是在寻找具有广泛定制可能性的工具,还是寻找开箱即用的简单模板,都有许多解决方案可以满足任何需求。最终,您的决定将受到许多变量的影响,例如您的财务状况、您的专业知识程度以及您希望通过所选工具或模板实现的具体目标。
分析项目的属性是我们自己在处理项目以及为客户工作时考虑的因素。让我们找出为什么这样做如此重要。
您如何分析这些适应性、兼容性、可扩展性和安全性功能?
在今天这个快节奏的环境中,企业需要能够跟上他们不断变化需求的软件。在选择程序时,分析软件的可适应性、兼容性、可扩展性和安全性方面是至关重要的。软件需要能够随着企业的发展而发展,因此可适应性是必不可少的。可扩展性使软件能够根据不断变化的公司需求进行扩展,而兼容性则保证了它能够无缝地与其他重要内部系统集成。在可扩展性的问题上,我们有多种方法可以使我们的应用程序可扩展。例如,如果我们选择使用像 Next.js 这样的现代框架,那么我们就可以保证我们能够获得最新的工具和功能。当我们有使用一些流行的编码标准和方法的思维模式来保持我们的项目稳健时,这可以进一步增强。
这里列出了一些实现方式:
-
为此需要
.nvmrc和.npmrc文件。 -
ESLint 配置:用于 JavaScript 代码分析和查找问题模式。
-
Prettier 配置:以确保每个开发者的代码遵循相同的格式。
-
提交代码审查:一种审查我们的提交信息并确保它们遵循一系列标准的方法。
-
框架定制和插件:可以通过启用文档中概述的功能和定制,或者通过安装第三方包和工具来进一步扩展框架。
最后但同样重要的是,安全性是任何软件的支柱,它保护私人信息并确保程序足够强大,能够抵御攻击者。通过检查这些基本品质,企业可以确保他们拥有在当今竞争激烈的环境中取得成功的软件。
从项目开始就选择正确的工具可以节省你大量的时间,并增加你对代码库的信心。现在让我们来看看这如何影响我们长远的发展。
你如何选择满足你所有要求的正确工具或模板,以确保成功的结果?
如果你想要得到一个良好的结论,你必须使用满足你所有需求的适当工具或模板。在如此多的可能性面前,选择正确的工具或模板可能会很困难。然而,如果你花时间进行研究和选择最适合你需求的工具,你将为自己设定成功的条件。找到适合你项目的理想匹配最终可以节省你的时间和精力,无论是软件程序、项目管理模板还是应用程序。避免选择仅仅“足够好”的工具;相反,更努力地寻找能够帮助你实现目标的工具。
对于 React 应用程序,选择合适的脚手架工具或模板至关重要。在考虑其他任何事情之前,务必理解什么是脚手架以及它的意义。然后,应考虑完成工作所需的大小、复杂性和技术。接下来,研究所有可用的工具或模板以完成这些任务。通过比较其功能与其他工具,确定哪个工具或模板最适合您的需求。在选择一个最适合您需求的选项之前,考虑每个选项的适应性、兼容性、可扩展性和安全性特征。遵循这些步骤,您将能够为项目选择一个坚实的框架,并最终简化工作。
保持话题一致,我们现在将过渡到应用程序架构的主题,这与脚手架有关。
决定应用程序架构
在为 React 项目选择应用程序架构时,必须考虑许多方面和因素。良好的架构鼓励可维护性、可扩展性和重用。架构应该具有适应性,并随着项目需求的变化而变化。随着您对 React 的熟练度和熟悉度提高,您将发现自己能够更好地做出明智的架构设计决策。当项目扩展和出现新的需求时,愿意重构和重新考虑您的决策。
选择应用程序架构时,我们需要考虑哪些因素?
每个项目都是不同的,因此需要不同的配置。我们必须在开始工作之前考虑许多不同的因素。没有正确或错误答案,因为最终这可能会归结为我们的个人偏好、团队对技术栈的熟悉度,甚至是客户的要求。
例如,如果我们正在构建一个需要处理支付的网站,那么需要考虑许多因素,例如安全地管理和收取支付的方式,以及用户注册和创建账户的方式。因此,弄清楚为什么我们会选择一个状态管理解决方案而不是另一个,或者为什么我们会选择像 Stripe 这样的支付网关而不是其他支付网关,都是我们应该能够讨论的选项。
再次强调,这里没有正确或错误答案;主要的是我们能够解释和证明我们的选择。
为了为我们的 React 项目选择最佳的应用程序架构,我们应该考虑以下特征:
-
状态管理:考虑到组件间的交互级别和应用程序状态的详细程度,确定您是否需要像 Redux 这样的状态管理工具。
-
路由:如果您的应用程序需要在各种视图或页面之间切换,请配置您的框架以进行路由或使用如 React Router 的外部路由框架。
-
识别项目规范:首先理解项目的需求、规模和范围。确定主要特性、性能标准和不同的部署平台。
-
未来兼容性:为了提供未来的扩展和修改,在设计组件、状态管理和 API 设计时,应考虑模块化和适应性。
-
文件夹结构:确保你的文件和文件夹结构是逻辑的、可扩展的,并且维护良好。按类别分组,例如组件、媒体和测试,或者按功能集分组。
-
应用程序编程接口 (API):确定完成任务所需的外部库或 API,例如用于数据分析、授权或其他专用功能的 API。
-
静态站点生成 (SSG) 与服务器端渲染 (SSR):为了提高性能和搜索引擎优化(SEO),使用 SSR 或 SSG 解决方案,如 Next.js、Gatsby 或 Remix,可以根据项目需求进行选择。
-
应用编程最佳实践和标准:使用 ESLint 等 linters、Prettier 等代码格式化工具以及如 Airbnb JavaScript 风格指南等文档化规则,在整个项目中强制执行代码标准和最佳实践,以确保每个开发者的代码和项目设置保持一致。
-
styled-components、Sass 或纯 CSS。
现在,我们将学习在项目中测试代码的各个方面。
测试你的代码
所有程序员都必须能够利用版本控制系统(VCSs)并对他们的代码进行充分的测试。通过测试你的代码,你可以确保它按预期运行,并且在各种潜在输入使用时没有错误。高效的版本控制将使团队协作更加顺畅,未来的工作部署将能够更有效地管理源文件。在本节中,我们将探讨这些关键主题,以便你具备成功所需的知识。
测试与版本控制相结合时效果显著。让我们在下一节中了解它如何帮助我们。
为什么在测试代码时要使用版本控制?
有时候我会忘记我在代码中做了什么更改,或者不小心重写了某个部分,忘记了它原来的样子。版本控制解决了这些问题。它是一种机制,可以跟踪你随时间在代码中实施的更改,并在必要时让你回到早期版本。在测试代码时,设置一个 VCS 是至关重要的。它不仅使你能够跟踪更改,而且还能使有组织和安全的团队合作成为可能。版本控制可以加快测试过程,减少错误,并使整个过程更加有效。
在本节中,我们学习了一些版本控制的使用案例。现在,我们将探讨在下一节中跟踪代码库变化的方法。
您应该为您的代码库使用哪些测试和变更跟踪工具?
测试和跟踪代码库中的变化可能很困难,但正确的工具确实能提供很大帮助。在众多替代方案中,选择最适合您特定需求的工具至关重要。Cypress 和 Selenium 是两个受欢迎的测试解决方案,它们能够跨多个浏览器实现自动化测试。版本管理工具如 Git 和 Subversion (SVN) 也可以使跟踪变化变得更加简单。在出现错误的情况下,这些技术有助于促进合作和简单的回滚。寻找能够优化您的流程并帮助您实现目标的工具需要花费时间进行研究和测试各种解决方案。
最后,让我们通过学习如何创建 Git 仓库,来了解创建具有良好文档的代码库的重要性。
创建带有 README 的 Git 仓库并进行共享
由于许多不同的原因,构建具有优秀文档的代码库至关重要。让我们继续阅读,以便了解其中的原因。
为什么创建具有良好文档的代码库至关重要?
开发者借助清晰的文档可以更容易地理解代码库的功能、目的和设计。因此,他们可以更成功地使用、贡献或修改代码。一个文档良好的代码库使得团队成员更容易协作,因为每个人都了解代码的结构和预期用途。这使得讨论变更、解决问题和监控进展变得更加容易。
从本质上讲,入职是指将新团队成员融入项目的过程。由于清晰的文档,他们可以迅速熟悉代码库,这降低了学习曲线,减少了提问或寻找解决方案所需的努力。质量控制还有另一个好处。测试用例、预期结果和协助项目的说明通常包含在文档中。这保证了更新符合项目目标并保持代码质量。
我们按照代码生成的顺序来解释我们的代码,这通常会在长期内节省时间。开发者不需要筛选代码或寻求其他开发者的帮助;他们可以直接查阅文档以获取指导和澄清。当所有上述建议都整合在一起时,维护文档的能力会变得相当简单。每当文档清晰时,随着时间的推移,保留代码库也会变得简单,因为它为开发者提供了在添加更新或解决错误时使用的完整指南。这减少了创建新问题或损害当前功能的可能性。
这些增强功能可能表明开发者的专业知识和自信。存储库反映了参与者的良好表现,拥有良好文档的代码。它展示了致力于最佳实践、细致入微和对其他开发者需求的考虑。一个良好的代码库增加了其他人认为它有价值并在此基础上进行扩展的可能性。这可能导致更广泛的开发者社区接受、合作和创新。为了确保理解、可维护性和合作,拥有一个具有清晰文档的代码存储库至关重要。它提高了项目的长期性和可行性,并有利于展示项目的开发者。
你如何创建一个 Git 存储库?
创建一个新的 Git 存储库是一个非常简单的过程。如何操作的详细信息可以在这里找到:github.com/new。
实际上,你所要做的就是创建一个新的存储库,然后在你的本地项目文件夹中运行设置代码。代码的示例可以在这里看到:
echo "# myapp" >> README.md
git init
git add README.md
git commit -m "first commit"
git branch -M main
git remote add origin https://github.com/yourusername/myapp.git
git push -u origin main
每次我们创建一个新的存储库时,代码都会自动生成,并且已经为我们的项目配置好了,所以我们只需要将代码复制粘贴到我们的命令行中,就可以设置我们的项目并使其在 Git 上受版本控制。
摘要
当我们到达本章的结尾,关于解决任何现实世界编程问题的章节时,很明显,一个坚实的基础和系统化的方法将大大加快软件开发过程。我们通过仔细配置我们的开发环境,选择合适的脚手架工具和模板,以及选择可接受的应用程序架构,为成功项目奠定了基础。
通过我们所学的所有知识,我们现在有了在有人询问我们设置开发环境时可以使用的信息,以及选择正确项目架构的一些很好的理由。这些往往是常见的面试问题,或者至少是我们可以展示我们在创建 React 项目方面的经验和思考方式的谈话话题。
测试的重要性也不容忽视,我们能够讨论为什么它是 React 开发者应该认真对待并在开发过程中使用的领域。
在本章中,我们强调了实施严格测试计划的必要性,以确保我们的代码在整个开发过程中的稳定性、可扩展性和安全性。通过采用 TDD(测试驱动开发)和添加严格的测试框架,你可以在开发早期发现并解决问题,从而提高产品的整体质量。
此外,我们还强调了构建一个结构良好、README 清晰且信息丰富的 Git 仓库的必要性。这不仅为你和其他未来的开发者提供了一个极好的参考,而且还能营造一种促进持续发展的工作氛围。
最终,掌握现实编程的艺术需要技术知识、战略规划和良好沟通的融合。在磨炼这些能力并采纳本章中提到的最佳实践之后,你现在更有信心和技巧地应对任何编程挑战。记住,软件工程师的职业是一个持续的学习过程,随着你不断成熟和发展,你将发现自己能够克服软件开发领域中的任何复杂障碍。
在下一章中,我们将基于 React Hooks/Redux、styled-components和 Firebase 构建一个应用程序。这将使我们能够基于到目前为止所获得的知识,进一步推进我们的 React 和面试技能。
第十一章:基于 React、Redux、Styled Components 和 Firebase 后端构建应用程序
随着我们继续阅读本书,我们将涵盖与 React 生态系统、流行库以及构建现代 Web 应用时代稳健 React 应用程序的最佳实践相关的许多概念和技能。本章致力于利用前几章学到的知识创建一个全栈 React 应用程序。作为面试过程的一部分,面试官可能会要求你基于某些功能和技术要求构建一个完整的 React 项目,或者分配一个快速编码挑战来评估你在特定领域的技能。本章将指导你如何应对编码轮次,通过从头开始创建一个 React 项目,遵循标准指南,并对实施的项目提出面试问题。在本章中,我们将通过实现各种 UI 组件、应用样式、通过身份验证机制验证注册用户的身份、集成 Firebase 后端以及部署应用程序使其可供公众使用来构建一个电子商务应用程序。
首先,我们将使用官方的react-intl库来支持国际化,以及 Firebase 包来实现后端,开始搭建项目的流程。然后,我们将快速介绍 Firebase 后端及其服务,将 Firebase 集成到我们的 React 项目中,并实现身份验证功能。
此后,我们将按照 Redux 标准构建业务逻辑,以处理从 UI 触发的各种动作。为了增加来自不同地区的用户基础,我们将通过国际化过程增强应用程序。然后,我们将编写几个单元测试,以确保我们的代码无瑕疵并符合业务要求。在本章结束时,我们将把我们的 Git 仓库托管在 GitHub 上,并使其在线上可用,以展示我们的技能。
在本章中,我们将涵盖以下主要主题:
-
搭建和配置项目
-
介绍 Firebase 服务和配置应用程序
-
实现 Firebase 身份验证及其后端
-
构建用于状态管理的 Redux 组件
-
构建表示层
-
支持国际化
-
使用 Vitest 框架实现测试
-
创建带有 README 文档的 Git 仓库
-
部署应用程序以供公众访问
技术要求
在开始我们项目的旅程之前,你的工作站应该安装了最新的 Node.js 和 npm (nodejs.org/en) 包。在构建此项目的同时,你还需要一个 Netlify (www.netlify.com/) 账户用于部署部分,以及一个 GitHub (github.com/) 账户来托管 Git 仓库。此项目的代码库可以在以下 Git 仓库中找到:github.com/PacktPublishing/React-Interview-Guide/tree/main/Chapter11/one-stop-electronics。
首先,让我们谈谈构建 React 应用程序所需的脚手架和项目配置。
脚手架和配置项目
在第六章中,我们介绍了关于 Redux 及其工作流程的多个主题,这些主题基于 Redux 状态管理解决方案来构建 React 网络应用程序。为了减少与 Redux 逻辑相关的样板代码,Redux 团队建议使用 RTK 来简化许多常见用例,应用最佳实践,并防止常见错误。
如果你从头开始创建项目,使用 Redux 和 TypeScript 模板 for Vite 是 Redux 团队建议的方法之一。因此,在本节中,我们将使用相同的方法来构建项目。在这个项目中,我们将构建一个名为 One Stop Electronics 的电子商务应用程序,用户可以购买电子设备。
以下 degit 项目脚手架命令有助于创建基于 RTK 的项目结构:
npx degit reduxjs/redux-templates/packages/vite-template-redux onestop-electronics
此模板基于 TypeScript(.tsx 或 .ts 文件)创建项目,并为 react、react-dom 和 jest 包提供了所需的类型定义依赖项。
项目将使用 React Router 在应用程序中导航页面,并使用 FormatJS,这是一个支持国际化的模块化 JavaScript 库集合,使用 React Intl 库。以下列出安装这些包的命令:
npm install --save react-router-dom
npm install --save react-intl
存在多种方法来为 React 组件添加样式。我们将在此项目中使用 styled-components 来编写基于组件的样式。此包将与其类型定义依赖项一起安装,如下所示:
npm install --save styled-components
npm install --save-dev @types/styled-components
在第七章中讨论了 styled-components。你还可以在官方网站上找到更多详细信息和新功能:styled-components.com。
此应用程序将使用 Firebase 基于云的开发平台来实现身份验证机制和后端数据存储。因此,让我们使用以下命令安装最新的 Firebase 版本:
npm install --save firebase
一旦创建了应用程序文件夹结构并安装了上述依赖项,让我们再创建几个文件夹来根据技术栈定制项目。以下列出了重要的文件夹:
-
app/store:store文件夹用于创建 RTK 的各种组件,如 actions 或 action creators、reducers 和 selectors -
assets:此文件夹包含所有图像和图标文件 -
backend:此目录包含与 Firebase 相关的 API 和电子商务应用程序数据 -
features:此文件夹专门用于 Web 应用程序的主要页面 -
i18n:此目录包含所有国际化特定文件
Visual Studio(VS)Code 广泛使用,并且对于 React 开发来说相当受欢迎。因此,我们将使用它来完成此项目,并在添加所有上述文件夹后,如图所示:

图 11.1:应用程序文件夹结构
VS Code 在其市场提供大量扩展。在此项目中,将使用 vscode-styled-components 扩展来突出显示 styled-components 的语法,并使用 Prettier 代码格式化扩展来格式化代码。
注意
没有使用特定 IDE 的限制。您可以根据自己的偏好和所需功能使用任何流行的 IDE。
在下一节中,我们将介绍 Firebase 的服务、所需配置以及 Firebase 身份验证和后端,然后再进行其实施。
介绍 Firebase 服务和配置应用程序
Firebase 是由 Google 提供的全面的后端即服务(BaaS)提供商,它提供数据库、身份验证、云存储、分析以及许多其他即时服务。这些后端服务帮助开发者更快、更安全地构建应用程序,而无需编写大量编程代码。
Firebase 的主要功能有哪些?
为了更清楚地了解 Firebase,我们将查看其一些主要功能。它们如下列出:
-
实时数据库:Firebase 实时数据库是一个云托管的 NoSQL 数据库。数据以 JSON 格式存储,并为每个连接的客户端实时同步。数据库支持所有类型的平台,如 Android、iOS 和 Web 平台。
在这里,实时意味着对数据的任何更改都会在几毫秒内立即反映在平台和设备上。此外,实时数据库通过缓存您查询的所有数据,并在没有互联网连接时从缓存中检索它,提供了出色的离线支持。一旦设备重新连接到互联网,数据库就会将本地数据更改同步到原始远程数据,以避免因互联网中断而发生的任何冲突。因此,应用程序保持响应。
-
FirebaseUI库作为一个完整的现成 UI 身份验证解决方案。 -
云存储:云存储是针对需要存储和处理用户生成的大容量文件(如图像、音频、视频和其他任何对象类型)的应用程序开发者的简单且成本效益高的存储服务。您可以使用 Firebase SDK 或 Google Cloud Storage API 访问这些内容文件。Firebase SDK 允许客户端直接进行安全上传和下载。
-
Google Analytics 和 Crashlytics:Google Analytics for Firebase 是一款免费的应用程序测量解决方案,可以帮助您深入了解应用程序的使用情况和用户参与度。此服务提供使用 Firebase SDK 定义的最多 500 个不同事件的无限量报告。这些分析信息可以通过保留更多用户和为营销和性能优化做出明智决策来促进业务增长。
Crashlytics 是一款实时崩溃报告工具,可以收集有关应用程序中发生的错误和崩溃的详细信息。这有助于通过记录错误详情(如错误发生的行号、设备名称、操作系统版本以及问题发生的时间)来解决问题。
如何设置和配置 Firebase 以进行身份验证和数据存储?
如果您构建的项目基于 Firebase 后端,您需要采取以下步骤来创建 Firebase 应用:
-
请访问官方 Firebase 控制台
console.firebase.google.com/并使用您的 Google 账户登录。 -
点击
onestop-electronics。然后,点击 继续 按钮以继续配置向导。 -
您将获得启用项目 Google Analytics 的选项。这是可选的,并且对于此项目不是必需的。
-
然后,点击 创建项目。您需要等待一段时间以配置资源并完成项目的设置。
-
点击您项目主页上的网页图标以创建您的网络应用。这将打开一个输入网络应用名称的表单。然后点击 注册应用 按钮将网络应用与 Firebase 集成。
-
然后,复制网络应用的 Firebase 生成的配置,并将其保存以供项目中的后续使用。
-
然后,添加名为
Authentication的 Firebase 产品以构建安全的身份验证系统,以及用于项目中云托管 NoSQL 数据库的Cloud Firestore。
注意
Authentication 产品用于在注册时存储已验证用户,并在每次登录操作中更新用户信息,而 Firestore 数据库将存储应用程序数据。
-
默认情况下,Firestore 不提供读取或写入权限。此权限标志可以在 规则 选项卡中启用。更新的安全规则看起来如下:
rules_version = '2'; service cloud.firestore { match /databases/{database}/documents { match /{document=**} { allow read, write: if true; } } } -
在
Authentication产品中,您可以选择如 电子邮件/密码 和 Google 提供商等登录方式。您还可以在登录提供商部分找到几个其他第三方提供商。
你可以在单个账户下创建多个项目,但免费计划有限制。
你在哪里安全地放置 Firebase 配置?
不建议将 Firebase 配置存储在将可在公共开发平台上公开的存储库中,例如 GitHub。你可以将配置保存在以下 .env.local 文件中,以键值格式,并将其添加到项目的 .gitignore 文件中:
VITE_FIREBASE_API_KEY = "yourfirebaseapikey"
VITE_FIREBASE_AUTH_DOMAIN = "yourfirebaseauthdomain"
VITE_FIREBASE_PROJECT_ID = "yourfirebaseprojectid"
VITE_FIREBASE_STORAGE_BUCKET = "yourfirebasestoragebucket"
VITE_FIREBASE_MESSAGING_SENDER_ID =
"yourfirebasemessagingsenderid"
VITE_FIREBASE_APP_ID = "yourfirebaseappid"
在基于 Vite 的项目中,密钥应以 VITE 关键字开头,如前述代码所示。相同的密钥可以通过 import.meta.env.VITE 在项目中访问。例如,此配置在 config.ts 文件中实例化 Firebase 应用很有用:
import { initializeApp } from "firebase/app"
const firebaseConfig = {
apiKey: import.meta.env.VITE_FIREBASE_API_KEY,
authDomain: import.meta.env.VITE_FIREBASE_AUTH_DOMAIN,
projectId: import.meta.env.VITE_FIREBASE_PROJECT_ID,
storageBucket: import.meta.env.
VITE_FIREBASE_STORAGE_BUCKET,
messagingSenderId: import.meta.env.
VITE_FIREBASE_SENDER_ID,
appId: import.meta.env.VITE_FIREBASE_APP_ID,
}
export const firebaseApp = initializeApp(firebaseConfig)
前述代码中导出的 firebaseApp 可以在任何需要的地方在项目中使用。
注意
如果你基于 REACT_APP 创建一个 React 项目,它将通过 process.env 变量可访问。
一旦配置了 Firebase 项目,我们需要实现将在注册和登录表单提交时调用的 Firebase 身份验证方法。同时,应将 Cloud Firestore 数据操作以方法的形式实现,用于存储和检索产品信息。以下部分将专注于此 Firebase API 实现。
实现 Firebase 身份验证及其后端
一旦在 Firebase 控制台中配置了 Firebase 应用,你可以使用 Firebase Authentication API 实现 One Stop Electronics 应用的身份验证。这些 API 方法在用户注册和登录以购买此电子商务应用中的产品时,有助于验证合法用户。通常,我们会在一个单独的文件夹内创建所有 API 处理程序(在我们的例子中是 backend->firebase->api),以将其与前端代码分离。
你如何实现 Firebase 身份验证的登录、注册和注销场景?
我们需要从 Firebase/auth 包中导入几个 Firebase 函数和实用工具,以与身份验证机制一起工作。Firebase 包为登录、注册和注销场景提供了单独的方法。让我们从以下 api/auth.ts 文件中导入所有这些 API 方法以及一个 Firebase 实例开始:
import { firebaseApp } from "@/backend/firebase/config"
import {
signInWithEmailAndPassword,
signInWithPopup,
signInWithRedirect,
GoogleAuthProvider,
createUserWithEmailAndPassword,
updateProfile,
signOut,
getAuth,
onAuthStateChanged,
NextOrObserver,
User,
} from "firebase/auth"
const auth = getAuth(firebaseApp)
const googleProvider = new GoogleAuthProvider()
googleProvider.setCustomParameters({
prompt: "select_account",
})
在前述代码中,auth 实例是基于 Firebase 应用实例创建的。此外,Google 提供商已配置为提示 Google 账户详细信息以执行 Google 登录身份验证。
使用上述 API 工具,在上述同一文件中写下两个登录方法——一个使用电子邮件地址和密码,另一个使用 Google 登录身份验证。同样,注册方法可以根据电子邮件地址和密码以及用户名创建。注销方法将用于结束当前用户会话。以下 auth.ts 文件已更新为这些身份验证方法:
export const signInEmailAndPassword = async (
email: string,
password: string,
) => {
if (!email || !password) return
return await signInWithEmailAndPassword(auth,
email, password)
}
export const signInGooglePopup = () => signInWithPopup
(auth, googleProvider)
export const signInGoogleRedirect = () =>
signInWithRedirect(auth, googleProvider)
export const signUpEmailAndPassword = async (
displayName: string,
email: string,
password: string,
): Promise<User> => {
Const userInfo = await createUserWithEmailAndPassword
(auth, email, password)
await updateProfile(userInfo.user, { displayName })
return userInfo.user
}
export const signOutUser = async () => await signOut(auth)
前面的导出方法将在下一节中与 UI 屏幕集成,特别是对于登录或注册表单,我们将构建表示层。
最后,还可以使用以下方法监听身份验证状态的变化。此方法有助于在应用程序中更新当前用户详情:
export const onAuthStateChangedListener =
(callback: NextOrObserver<User>) =>
onAuthStateChanged(auth, callback)
现在,我们已经准备好了所有后端身份验证方法,这些方法有助于在电子商务应用程序中创建一个活跃的用户会话。一旦用户经过身份验证,他们通常可以根据业务需求在应用程序数据上执行创建、读取、更新和删除(CRUD)操作。
您如何实现云存储数据操作?您能否用任何集合解释数据操作?
与 SQL 数据库不同,云存储数据库中没有表和行。在这里,您将数据以文档的形式存储为键值对,这些文档组织成集合。存储的产品信息作为着陆页的一部分显示,这对于这个电子商务应用程序至关重要。因此,让我们通过产品集合来解释数据操作。
首先,我们需要在 Firebase 的Cloud Firestore数据库中创建一个产品集合来存储电子设备数据。如果集合在数据库中不存在,它将自动创建并插入产品数据。在这个应用程序中,产品记录(或文档,使用 Firebase 的术语)通过一个 JSON 文件上传到数据库,该文件位于firebase/data/products-data.json。
让我们从导入firebase/firestore包中的所有必需函数开始,包括产品类型,在以下firebase/data/db-utils.ts文件中:
import {
getFirestore,
doc,
collection,
writeBatch,
query,
getDocs,
QueryDocumentSnapshot,
} from "firebase/firestore"
import { Product } from "@/app/store/product/product.types"
export const db = getFirestore()
在前面的代码片段中,数据库实例是通过getFirestore()函数创建的,以便在应用程序集合上执行数据库操作。
使用之前导入的函数,您可以在同一文件中为产品集合创建读取和写入操作:
export const insertProductsData = async <T extends Product>(
collectionKey: string,
productItems: T[],
) => {
const collectionRef = collection(db, collectionKey)
const batch = writeBatch(db)
productItems.forEach((product) => {
const docRef = doc(collectionRef)
batch.set(docRef, product)
})
await batch.commit()
}
export const fetchProductsData = async () => {
const collectionRef = collection(db, "products")
const queryRef = query(collectionRef)
const querySnapshot = await getDocs(queryRef)
return querySnapshot.docs.map((docSnapshot) =>
docSnapshot.data())
}
在前面的代码中,产品数据通过以下四个步骤插入:
-
首先,创建一个基于数据库实例和唯一键的集合引用。
-
然后,基于数据库实例创建一个批次引用以一次插入多个产品。
-
迭代每个产品,并为更新批次创建一个文档引用。
-
最后,提交批次以同时插入多个记录。
同样,产品数据通过以下四个步骤检索:
-
首先,为产品创建一个集合引用。
-
然后,基于上一步创建的集合引用创建一个查询引用。
-
使用
getDocs方法获取产品集合的所有文档,并将它们存储为快照。 -
最后,遍历查询快照并返回结果。
所有的前述云存储数据函数都是从firebase/firestore包中导入的。
根据每个注册动作创建用户集合也是可能的,这个动作是通过新用户触发的。你可以在代码中找到这个特定的 API 处理程序,并根据需求扩展这个项目的功能。然而,这超出了本节的范围,并且代码片段仅作为参考进行注释。
现在,我们已经完成了后端代码。在下一节中,让我们创建所有 Redux 组件,以实现这个应用的状态管理解决方案。
构建 Redux 组件以进行状态管理
Redux 存储相关的组件涵盖了应用的大部分动作,例如计算业务逻辑、在存储中创建或更新数据,以及获取最新数据以在 UI 上显示。在这个项目中,存储组件被分类到产品、购物车和用户文件夹中。每个文件夹包含实体类型(*.type.ts)、包含动作或还原器的切片(*.slice.ts),以及用于检索数据的选择器(*.selector.ts)。
在我们的电子商务应用中,Product 实体需要显示产品名称、品牌、价格和数量等详细信息,以展示所有可用的产品。因此,让我们首先创建产品实体的存储组件。以下 product.type.ts 文件创建了产品和相关的类型,包含所有可能的属性:
export type Product = {
id: number
productImageUrl: string
name: string
brand: string
price: number
category: string
}
//Holds the list of products,product category and boolean flag to indicate loading state
export type ProductsState = {
products: Product[]
category: string
isLoading: boolean
}
export type ProductMap = {
[key: string]: Product[]
}
一旦创建了产品类型,应该创建产品切片来更新存储中的产品列表和产品类别。RTK 通过将这些动作保持在一个名为产品切片的还原器内部来简化事情。以下是一个名为 product.slice.ts 的产品切片:
import { createSlice } from "@reduxjs/toolkit"
import { ProductsState } from "./product.types"
const INITIAL_STATE: ProductsState = {
products: [],
category: "all",
isLoading: true,
}
export const productsSlice = createSlice({
name: "products",
initialState: INITIAL_STATE,
reducers: {
setProducts(state, action) {
state.products = action.payload
state.isLoading = false
},
setCategory(state, action) {
state.category = action.payload
},
},
})
export const { setProducts, setCategory } =
productsSlice.actions
export const productsReducer = productsSlice.reducer
前面的动作被导出,以便在相应的产品 UI 页面上使用,以响应用户动作,而导出的还原器用于使用 reselect 库根据类别名称获取存储状态,例如产品、类别和产品映射。
以下代码是 product.selector.tsx 文件。这个文件的代码相当长,所以让我们将其分解成更小的代码块。第一个代码块包含用于获取产品、产品加载状态和产品类别的选择器:
import { createSelector } from "reselect"
import { RootState } from "@/app/store/store"
import { Product, ProductMap, ProductsState }
from "./product.types"
const selectProductReducer = (state: RootState):
ProductsState => state.products
export const selectProducts = createSelector(
[selectProductReducer],
(productsSlice) => productsSlice.products,
)
export const selectCategory = createSelector(
[selectProductReducer],
(productsSlice) => productsSlice.category,
)
export const selectProductsIsLoading = createSelector(
[selectProductReducer],
(productsSlice) => productsSlice.isLoading,
)
以下代码块包含每个类别的产品映射:
export const selectProductsMap = createSelector(
[selectProducts],
(products): ProductMap =>
products.reduce(
(acc, product) => {
const { category } = product
acc[category]
? acc[category].push(product)
: (acc[category] = [product])
acc["all"].push(product)
return acc
},
{ all: [] } as ProductMap,
),
)
One Stop Electronics 应用使用前面的 selectProductsMap 根据类别过滤产品。我们的应用有各种类别,如所有类型的笔记本电脑、平板电脑和手机。
同样,我们可以为 User 和 Cart 实体创建存储组件。
你如何在 Redux 应用中处理多个还原器?
一旦完成项目的所有 Redux 实体,所有还原器都需要在一个名为 root-reducer.ts 的单个根还原器文件中合并。在这里,我们将使用 Redux 的 combineReducers 方法,它接受所有还原器作为一个对象的单个参数:
import { combineReducers } from "redux"
import { userReducer } from "./user/user.slice"
import { productsReducer } from "./product/product.slice"
import { cartReducer } from "./cart/cart.slice"
export const rootReducer = combineReducers({
user: userReducer,
products: productsReducer,
cart: cartReducer,
})
此后,可以在 store.ts 文件中使用前面提到的根 reducer 来配置 store:
import { configureStore } from "@reduxjs/toolkit"
export const store = configureStore({
reducer: rootReducer,
})
RTK 自动调用 combineReducers 方法,因此你不需要直接调用它。
现在,store 已准备好用于对应用程序数据进行任何 read 或 update 操作。
使用 RTK,整个应用程序的 Redux 开发现已完成。下一节将专注于使用 UI 组件和基于这些可重用组件的网页构建表示层。
构建表示层
将可重用组件保存在 components 文件夹下是一种常见做法。在本项目中,我们将在 components 文件夹内创建定制的表单组件,如按钮、输入、选择和旋转器,布局组件如页眉和页脚,以及可重用的功能组件如产品和类别。
你如何使用 styled-components 实现自定义按钮?
HTML 按钮元素在样式方面没有不同的变体,但可以使用 React 和 styled-components 创建多个按钮变体。让我们看看按钮组件以及它是如何根据本项目需求进行定制的。
该项目需要更大的基本按钮(主要用于注册和登录页面),具有对比颜色的反转按钮用于 Google 登录,以及屏幕空间有限的情况下的小按钮(通常用于添加产品到购物车)。作为第一步,应在 button.styles.tsx 文件内创建具有这些不同样式的 styled-component。
由于文件内容较多,让我们将代码分解成小的代码块。第一个代码块包含 BasicButton styled-component:
import styled from "styled-components"
export const BasicButton = styled.button`
min-width: 10rem;
width: auto;
height: 2.5rem;
line-height: 2.5rem;
letter-spacing: 0.5px;
padding: 0 2rem;
background-color: rgb(112, 76, 182);
color: white;
font-size: 0.7rem;
font-family: "Barlow Condensed";
font-weight: bolder;
text-transform: uppercase;
border: none;
border-radius: 0.2rem;
cursor: pointer;
display: flex;
justify-content: center;
&:hover {
background-color: white;
color: black;
border: 1px solid black;
}
`
以下代码块包含两个扩展基本按钮的 styled 按钮组件。反转按钮反转了基本按钮的背景和文字颜色,而小按钮看起来与基本按钮相似,但实际上会更小:
export const InvertedButton = styled(BasicButton)`
background-color: white;
color: rgb(112, 76, 182);
border: 1px solid black;
&:hover {
background-color: rgb(112, 76, 182);
border: none;
border: 1px solid white;
color: white;
}
`
export const SmallBasicButton = styled(BasicButton)`
width: 4rem;
height: 1.5rem;
min-width: 0rem;
padding: 0rem;
letter-spacing: 0.1rem;
line-height: 2rem;
font-size: 0.4rem;
align-items: center;
letter-spacing: 0rem;
`
在前面的 styled-components 中,我们使用了 rem 相对单位而不是 px 这样的绝对单位,以便根据屏幕大小动态调整大小或间距。
然后,我们将这些按钮样式导入到下面的 button.tsx 文件中,并根据按钮的 type 属性值动态渲染相应的按钮组件。该组件还接受子元素和其他按钮属性:
import { FC, ButtonHTMLAttributes } from "react"
import { BasicButton, InvertedButton, SmallBasicButton }
from "./button.styles"
export enum BUTTON_TYPE_CLASSES {
basic = "basic",
inverted = "inverted",
small = "small",
}
const getButton = (buttonType = BUTTON_TYPE_CLASSES.basic) =>
({
[BUTTON_TYPE_CLASSES.basic]: BasicButton,
[BUTTON_TYPE_CLASSES.inverted]: InvertedButton,
[BUTTON_TYPE_CLASSES.small]: SmallBasicButton,
}[buttonType])
export type ButtonProps = {
buttonType?: BUTTON_TYPE_CLASSES
} & ButtonHTMLAttributes<HTMLButtonElement>
const MyButton: FC<ButtonProps> = ({ children, buttonType,
...otherProps }) => {
const CustomButton = getButton(buttonType)
return <CustomButton {...otherProps}>
{children}</CustomButton>
}
export default MyButton
前面的 MyButton 组件被类型化为仅接受 button 类型和其他内置 HTML 按钮属性作为属性。如果你传递任何不属于按钮元素的按钮属性,由于使用了 TypeScript,将会出现编译时错误。之前定制的按钮的使用出现在我们应用程序的多个页面上。
实现特定业务 UI 组件
与按钮组件类似,你可以创建一个产品盒组件。产品组件应按照特定的样式布局显示产品图片、名称、品牌和价格详情。让我们在product.styles.tsx文件中应用每个字段所需的样式。
第一个代码块包含 styled-components 的导入和图像背景属性的类型声明:
import styled from "styled-components"
type ImageBackgroundProps = {
$hasWhiteBackgroundImage: boolean
}
以下代码块包含一个用于包装产品图片和其他产品详情的ProductContainer styled 组件:
export const ProductContainer = styled.div<ImageBackgroundProps>`
display: flex;
flex-direction: column;
background-color: #f1f1f1;
padding: 1rem;
border-radius: 0.125rem;
img {
width: 7rem;
height: 5rem;
object-fit: fill;
background-color: #f1f1f1;
transition: 0.5s all ease-in-out;
mix-blend-mode: ${(props) =>
props.$hasWhiteBackgroundImage ? "multiply" :
"normal"};
&:hover {
transform: scale(1.1);
}
}
&:hover {
img {
opacity: 0.8;
}
button {
opacity: 0.85;
display: flex;
}
}
`
以下代码块包含一个用于样式化和包装产品字段的Footer styled-component:
export const Footer = styled.div`
width: 100%;
display: flex;
flex-direction: column;
justify-content: space-between;
font-size: 1rem;
padding-left: 1rem;
`
最后,我们为每个产品字段声明的 styled-component 以赋予它们独特的样式:
export const Name = styled.h2`
font-size: 0.8rem;
line-height: 1rem;
font-weight: 600;
text-transform: capitalize;
margin-bottom: 1rem;
`
export const Brand = styled.div`
font-size: 0.6rem;
line-height: 1rem;
color: rgb(75 85 99);
margin-bottom: 0.5rem;
span {
font-weight: 600;
text-transform: capitalize;
}
`
export const Price = styled.span`
font-size: 0.6rem;
line-height: 1rem;
color: rgb(75 85 99);
margin-bottom: 1rem;
span {
font-weight: 600;
text-transform: capitalize;
color: rgb(85, 118, 209);
}
`
在前面的 styled-component 中,每个产品字段都有自己的样式。例如,产品的名称应该使用更大的字体大小,而价格应该使用不同的字体颜色来突出显示重要数据,同时展示产品。
之前完成的product.styles.tsx文件中的 styled-components 将在以下product.tsx文件中的标记中使用,如下所示。组件代码相当长,所以让我们将其分解成更小的代码片段。
首先,定义我们页面的导入、产品的 prop 类型和验证非白色背景图像的实用函数:
import { FC } from "react"
import { useDispatch } from "react-redux"
import { useAppSelector } from "@/app/store/hooks"
import { selectCurrentUser } from
"@/app/store/user/user.selector"
import MyButton, { BUTTON_TYPE_CLASSES } from
"@/components/button/button"
import { Product } from "@/app/store/product/product.types"
import { addProductToCart } from
"@/app/store/cart/cart.slice"
import { BRAND_NAMES } from "@/constants"
import { ProductContainer, Footer, Name, Brand, Price }
from "./product.styles"
type ProductProps = {
product: Product
}
const hasWhiteBackground = (brand: string) =>
BRAND_NAMES.includes(brand)
现在,让我们添加ProductItem组件,该组件接受product作为 prop,没有任何标记代码。组件声明了currentUser和addCartProduct来将产品派发到购物车:
const ProductItem: FC<ProductProps> = ({ product }) => {
const currentUser = useAppSelector(selectCurrentUser)
const { name, price, productImageUrl, brand } = product
const dispatch = useDispatch()
const addCartProduct = () => dispatch
(addProductToCart(product))
return (
//Markup goes here
)
}
export default ProductItem
最后,添加了渲染的产品标记,如下所示:
<ProductContainer $hasWhiteBackgroundImage=
{hasWhiteBackground(brand)}>
<img src={productImageUrl} alt={`${name}`} />
<Footer>
<Name>{name}</Name>
<Brand>
Brand: <span>{brand}</span>
</Brand>
<Price>
Price:{" "}
<span>
${price}
</span>
</Price>
{currentUser && (
<MyButton
buttonType={BUTTON_TYPE_CLASSES.small}
onClick={addCartProduct} >
Add to cart
</MyButton>
)}
</Footer>
</ProductContainer>
前面的ProductItem组件在应用程序未通过用户认证时隐藏将产品添加到购物车的按钮。
类似地,你可以创建其他 UI 组件以在所需页面上重用。现在,让我们基于所有这些可重用 UI 组件构建应用程序页面。
实现应用程序页面使用 UI 组件
每个 React 应用程序中的网页都是由 UI 组件组成的。我们的应用程序需要产品着陆页、添加产品到购物车、注册和登录页面。根据脚手架结构,这些页面应该在features文件夹下创建。产品着陆页是通过迭代产品列表中的Product组件创建的。以下代码是products.tsx文件中的代码。
这个文件代码相当长,所以让我们将其分解成更小的代码块。第一个代码块包含与 React Hooks、组件、选择器和 styled-components 相关的导入,如下所示:
import { useState, useEffect, Fragment } from "react"
import { useParams } from "react-router-dom"
import { useAppSelector } from "@/app/store/hooks"
import ProductItem from "@/components/product/product"
import MySpinner from "@/components/spinner/spinner"
import { insertProductsData } from
"@/backend/firebase/api/db-utils"
import { Product } from "@/app/store/product/product.types"
import {
selectProductsMap,
selectCategory,
selectProductsIsLoading,
} from "@/app/store/product/product.selector"
import { Categories } from "@/components/
categories/categories"
import {
ProductsContainer,
Title,
LayoutContainer,
LoaderContainer,
} from "./products.styles"
const Products = () => {
// The component code goes here
}
export default Products
以下代码块更新了组件代码,该代码从存储中检索产品信息(即productsMap和category),并包括useEffect Hook,在类别和产品数据发生变化时设置最新的产品:
const Products = () => {
const productsMap = useAppSelector(selectProductsMap)
const category = useAppSelector(selectCategory)
const isLoading = useAppSelector(selectProductsIsLoading)
const [products, setProducts] = useState
(productsMap[category])
useEffect(() => {
setProducts(productsMap[category])
}, [category, productsMap])
return (
// Markup goes here
)
}
export default Products
最后,我们的产品页面的渲染标记如下所示:
<Fragment>
<LayoutContainer>
<Categories></Categories>
<ProductsContainer>
{isLoading ? (
<LoaderContainer>
<MySpinner />
</LoaderContainer>
) : (
products &&
products.map((product: Product) => (
<ProductItem key={product.id} product=
{product} />
))
)}
</ProductsContainer>
</LayoutContainer>
</Fragment>
在前面的代码中,Products 屏幕在数据检索之前显示页面加载器。一旦数据准备就绪,产品列表将以网格格式显示。
相反,可以使用输入和按钮 UI 组件创建登录页面以验证 Firebase 后端。它通过常规电子邮件或 Google 登录机制跟踪登录过程。用于验证用户的登录页面如下所示:

图 11.2:登录页面
通过登录页面进行身份验证有助于将产品添加到购物车。如果存在新用户,它还提供了一个底部的注册链接。以类似的方式,您可以实现注册和购物车页面。
注意
即使用户未登录到应用程序,仍然可以查看可用的产品。
现在,我们已经到达了一个位置,所有必需的 UI 组件和页面都已准备好使用。作为最后一步,可以在以下 App.tsx 文件中使用 Header 和 Footer UI 组件设计应用程序布局,以及为每个路由映射的先前实现的页面:
function App() {
return(
<Fragment>
<Header></Header>
<div className="app-content">
<Routes>
<Route path="/" element={<Products></Products>} />
<Route path="/signin" element={<SignIn />} />
<Route path="/signup" element={<SignUp />} />
<Route path="/cart" element={<CartProducts />} />
</Routes>
</div>
<Footer></Footer>
</Fragment>
)
}
在前面的代码中,为每个页面配置了路由。产品页面被配置为默认路由。这意味着在项目根目录中运行 npm run dev 命令后,将立即显示此页面。

图 11.3:产品展示
每次将产品添加到购物车时,购物车图标上都会显示产品数量。如果用户未登录到应用程序,则隐藏“添加到购物车”按钮。此外,您可以根据所选类别过滤产品列表,作为附加功能。
尽管我们已经完成了所有页面,但文本消息是硬编码在英语中的。这意味着非英语读者阅读消息将很困难。在下一节中,将通过国际化支持增强应用程序,以增加我们全球的用户基础。
支持国际化
如果您想在国际市场上推出当前的服务,您的 Web 应用程序应支持国际化以满足全球用户的需求。在 第四章 中,我们通过 FormatJS 库介绍了国际化。
此项目主要使用相同的 FormatJS 库来翻译文本消息并支持格式化的货币金额。将国际化扩展到应用程序的过程仅涉及几个指令。
首先,在 i18n->translations 文件夹中创建所有翻译,形式为 JSON 文件。这些翻译将根据 i18n->locale.ts 文件中定义的相应区域设置加载:
import ENGLISH from "./translations/en-US.json"
import FRENCH from "./translations/fr-FR.json"
import GERMAN from "./translations/de-DE.json"
export const LOCALES = {
"en-US": ENGLISH,
"fr-FR": FRENCH,
"de-DE": GERMAN
}
然后,你需要在应用程序的根目录下配置IntlProvider。此提供者使用locale值加载特定区域的消息,如上一步映射所示:
import { IntlProvider } from "react-intl"
import { LOCALES } from "@/i18n/locale"
import { DEFAULT_LOCALE } from "@/constants"
import { useAppSelector } from "./app/store/hooks"
import { selectCurrentLocale } from
"@/app/store/user/user.selector"
function App() {
const userLanguage = useAppSelector(selectCurrentLocale)
return (
<IntlProvider
messages={LOCALES[userLanguage]}
locale={userLanguage}
defaultLocale={DEFAULT_LOCALE}
>
// Main layout goes here
</IntlProvider>
)
}
在前面的代码中,当前的locale值是动态的,其值根据 UI 头部区域出现的区域选择下拉菜单进行更新。
现在,你可以开始使用react-intl中的FormattedMessage和FormattedNumber组件分别本地化文本消息和格式化货币数字,如下所示:
import { FormattedMessage, FormattedNumber } from
"react-intl"
const CartProducts = () => {
return(
<CartContainer>
// UI markup goes here
<FormattedMessage id="cart.total" />:
<FormattedNumber
value={cartProductsTotalCost}
style="currency"
currency="USD"
></FormattedNumber>
// UI markup goes here
</CartContainer>
)
}
之前与货币金额相关的翻译不仅包括货币符号,还添加了逗号分隔符。例如,以下添加到购物车屏幕显示了一个添加的产品列表,页面文本使用德语,并且格式化货币金额。

图 11.4:添加到购物车的产品
如您所见,逗号和十进制分隔符以及货币符号的位置根据所选语言进行修改。在页面上点击增加、减少和清除按钮时,产品数量和金额值及其格式将更新。
你如何通过命令式 API 实现国际化?
并非总是可以使用react-intl组件,如FormattedMessage、FormattedNumber或FormattedDate来格式化文本消息、数字或日期——也就是说,你无法使用内置组件在 React 组件的标记区域外或非 React 生态系统(如 Node.js、Redux 存储和测试部分)中的文本属性(如title和aria-label)内格式化文本消息。
在你声明IntlProvider之后,你可以在 React 函数组件内部调用useIntl()钩子来获取intl对象(类型为IntlShape)。例如,此钩子用于支持国际化,在标记区域外准备类别列表:
import { useIntl } from "react-intl"
const categories: Category[] = [
{ type: "all", name: intl.formatMessage({ id: "categories.all" }) },
{ type: "laptop", name: intl.formatMessage({ id: "categories.
laptops" }) },
{ type: "phone", name: intl.formatMessage({ id: "categories.phones"
}) },
{ type: "tab", name: intl.formatMessage({ id: "categories.tabs" }) },
];
如果你想在 React 组件之外支持国际化,你需要通过传递对象形式的locale和messages属性来使用createIntl API 方法。
目前,我们已经完成了完整的后端和前端实现。在下一节中,我们将看到如何编写单元测试以确保功能符合业务需求。
使用 Vitest 框架实现测试
Jest 是一个非常流行的测试框架,拥有完整的测试功能和易于使用的 API,这使得它成为 Web 生态系统中的标准测试框架。本项目基于 Vite 前端工具,也可以在 Vite 设置中集成 Jest。然而,这会导致你需要配置和维护两个不同的工作区,这对开发者来说很困难。因此,我们将使用Vitest来处理这个项目,这是一个基于 Jest 和 Vite 工具的闪电般的单元测试框架。这个框架提供了与 Jest 框架类似的功能和语法,使用现有的 Vite 工具配置或插件。
在这个项目中,我们将为cart.slice.ts下存在的所有操作编写单元测试。首先,让我们创建一个带有初始购物车状态的测试套件:
describe("Cart Reducer", () => {
let initialState: CartState = {
cartProducts: [
{
id: 1,
productImageUrl: "someurl.com",
name: "Inspiron 15",
price: 1200,
quantity: 2,
},
],
}
});
在前面的代码中,初始状态是可用的,有一个购物车产品数量为2。
然后,我们将编写一个测试用例来验证在购物车中增加数量的功能,如下所示:
it("Should handle adding or incrementing products
quantity inside cart", () => {
const productToAdd: Cart = {
id: 1,
productImageUrl: "someurl.com",
name: "Inspiron 15",
price: 1200,
quantity: 1,
}
const { cartProducts } = cartReducer(
initialState,
addProductToCart(productToAdd),
)
expect(cartProducts.length).toEqual(1)
expect(cartProducts[0].quantity).toEqual(3)
})
前面的测试用例验证了产品数量增加到3,而购物车中不同产品的数量保持不变。
类似地,减少购物车中产品数量的测试用例如下:
it("Should handle removing or decreasing products
quantity inside cart", () => {
const productToRemove: Cart = {
id: 1,
productImageUrl: "someurl.com",
name: "Inspiron 15",
price: 1200,
quantity: 1,
}
const state = cartReducer(
initialState,
removeProductFromCart(productToRemove),
)
expect(state.cartProducts.length).toEqual(1)
expect(state.cartProducts[0].quantity).toEqual(1)
})
前面的测试用例检查在减少购物车中的数量后的产品数量。在减少或从购物车中移除一个项目后,产品数量变为1,其初始状态数量值为2。
同样,我们还有测试用例来清除和重置购物车操作。一旦运行npm run test命令,你就可以看到所有通过测试和任何错误代码的失败测试的摘要。
现在,我们的应用程序已经准备好,带有完整的开发代码,并覆盖了单元测试用例,以检测代码中的早期缺陷。让我们将此代码作为 Git 仓库发布到 GitHub 上,以支持版本控制和确保我们的应用程序在线上顺利运行。
创建带有 README 文档的 Git 仓库
目前,项目已经准备好,并且可以在本地系统中成功运行应用程序。为了进一步改进项目,需要一个托管平台,如 GitHub,因为它提供存储和版本控制,部署项目,并使团队成员之间的协作无缝进行。让我们应用以下 Git 命令将本地代码推送到 GitHub 平台:
git init
git add .
git commit -m "Add one stop electronics"
git branch -M main
git remote add origin https://github.com/yourname/one-stop-electronics.git
git push -u origin main
现在,我们的项目已托管在 GitHub 上,这使得我们能够轻松地将应用程序部署到 Netlify。
包含README.md说明的 One Stop Electronics GitHub 项目的最终版本可在 Git 仓库中快速参考:github.com/PacktPublishing/React-Interview-Guide/tree/main/Chapter11/one-stop-electronics。
以下部分将讨论部署过程,以便使项目对公众可访问。
部署应用程序以供公众访问
部署应用程序有几种选择,例如 Netlify、Vercel、GitHub Pages 和 Firebase Hosting。我们将通过在 Netlify 网站上通过 GitHub 账户登录来使用 Netlify 进行部署。在仪表板上,您可以选择仓库并通过设置配置网站。您还可以将随机网站名称更改为新名称。由于环境变量文件(例如,.env.local)在 GitHub 仓库的根目录中不可用,您需要在点击部署 网站按钮之前将环境变量文件导入仪表板。
通过遵循所有这些步骤,应用程序现在已在线上对全球所有人开放。您可以快速浏览所有屏幕,这些屏幕在 Netlify 域名下可供您参考:onestop-electronics.netlify.app/。
通过持续部署,Netlify 会在 GitHub 仓库中的代码更新时自动更新您的网站。
摘要
我们已经到达了本章的结尾,并将之前章节中学到的所有编程概念应用到构建一个健壮的全栈 React 应用程序中。在本章开始时,我们简要介绍了使用 Redux 库的标准模板来搭建 React 项目。后来,我们简要介绍了 Firebase 服务,并提供了使用 Firebase 控制台设置和配置身份验证机制的逐步说明。然后,我们为我们的 One Stop Electronics 应用程序提供了 Firebase 身份验证和 Cloud Firestore 数据库操作的详细实现。
后端准备就绪后,我们开始通过编写几个 React UI 组件来构建 UI 层,包括应用程序布局和登录、注册、产品展示和购物车页面的页面。这一部分之后,我们根据 RTK 指南实现了业务层。然后,通过国际化支持和编写单元测试进一步增强了项目。当应用程序达到完成阶段时,最后一步是创建 Git 仓库并使应用程序可通过网络访问,包括部署流程。
在学习所有这些应用程序开发的主要里程碑之后,您现在应该足够自信,可以在编码挑战中展示您的技能并回答面试问题,尤其是凭借您的实际工作经验。在下一章中,我们将基于不同的技能集(如 Next.js、GraphQL、SWR 和 Vercel)构建另一个 React 应用程序以进行部署。这将帮助您增强对构建各种类型项目的知识,这些技术和概念在本章中没有涉及。
第十二章:基于 Next.js 工具包、身份验证、SWR、GraphQL 和部署构建应用程序
在这本书的旅程中,我们学习了新的技能、方法和更好的思维方式。这一章是我们努力的结晶,所以让我们继续前进,因为现在是时候将我们所学的一切结合起来,以开发一个具有最新现代技术堆栈的令人兴奋的应用程序。这一章将使我们能够从概念到部署一个具有身份验证、stale-while-revalidate(SWR)、GraphQL 和部署实现的 Next.js 应用程序。
我们将从一个关于 REST API 的快速介绍开始,如果你已经熟悉它们,这将是一个极好的复习,如果你是第一次学习这个基本方法,这将是一个很好的介绍。然后,我们将继续探讨如何最好地规划应用程序的架构,我们将深入了解身份验证的工作原理。这将由 SWR 的有效性和为什么它值得使用来跟进,因为它是一个非常有用的方面,可以提高我们应用程序的性能。接下来,我们将了解 GraphQL 以及它与传统的 REST API 相比如何。然后,我们将结束部署部分,这是我们最终将本地开发构建发布到网上,以便全世界都能使用的地方。
在接下来的部分中,我们将讨论业务逻辑背后的理念以及我们编写的代码的重要性,这不仅对我们的应用程序的运行方式有重要意义,而且根据其编写的好坏,它还可以对我们的团队和公司产生积极或消极的影响。继续前进,我们将创建表示层,这对于用户界面和用户体验至关重要。测试将是下一个主要功能,我们将会涉及,因为强烈建议生产就绪的应用程序设置一个强大的测试套件,以确保其稳定并且处于可以发布为最小可行产品(MVP)的状态。
在完成这些功能后,我们将学习如何部署我们的应用程序,以便公众可以访问它。在最后几节中,我们将创建一个带有 README 文件的 Git 仓库。
在接下来的部分中,我们将关注以下主题:
-
REST API 的快速介绍
-
规划应用程序架构,包括身份验证、SWR、GraphQL 和部署
-
构建业务逻辑
-
构建表示层
-
实施测试
-
创建带有 README 文档的 Git 仓库
-
部署应用程序以供公众访问
技术要求
在您的机器上,请确认 nodejs.org/en 已更新,并且 Next.js 的 JavaScript 节点包已设置并运行。您可以使用您想要的任何包管理器,无论是 npm、yarn 还是 pnpm。只需确保在安装时使用适当的命令。为了简化起见,我们将使用 npm。使用您首选的 集成开发环境(IDE)和 命令行界面(CLI)工具来处理您的项目。您还需要在 vercel.com/ 上有一个账户,以便在构建我们的应用程序时进行部署部分。对于认证部分,我们将使用 GitHub 和 Google,所以如果您还没有这两个服务的账户,现在就创建一个。GitHub 也需要用于版本控制部分,因此拥有一个账户是必不可少的。
Next.js 的包可以在 nextjs.org/ 找到。
项目和代码库可以在 github.com/PacktPublishing/React-Interview-Guide/tree/main/Chapter12/coffee-restaurant 上找到。
首先,让我们先谈谈 REST API,以便了解其核心概念。
快速了解 REST API
表示状态转移应用程序编程接口(REST API)是一种特定的技术,它已经确立了自己作为当代网络开发基石的地位。通过在 JavaScript 和 React 应用程序中使用 REST API,开发者可以开发出健壮且动态的网络应用程序,这些应用程序可以与其他平台和信息源进行通信。开发者可以使用 REST API 在服务器和客户端之间传输数据,而无需担心底层应用程序的架构。简单来说,REST API 标准化了通信协议,从而简化了开发。有了正确的工具和技能,REST API 可以完全改变网络开发者工作的方式。
我们可以通过使用餐厅作为例子来轻松理解 REST API 的核心方法和概念。这被认为是一种描述工作流程的流行方式。例如,假设一个用户有一个餐厅应用程序。基本上,用户会请求菜单页面的某些数据,这相当于餐厅里的顾客向服务员点菜。在这种情况下,接受食物请求的服务员与接收显示菜单请求的 API 相同。服务员随后去厨房取食物,这相当于 API 获取应用程序。
因此,让我们想象一下,应用程序有一个菜单上的食物列表。顾客查看菜单,然后向服务员下单。服务员带着顾客的订单去厨房,等待餐点完成。当食物准备好后,厨房工作人员将食物交给服务员,服务员随后通过将食物带回顾客处来响应。在现实世界中,顾客现在可以享用餐点了。如果我们把这种情况想象成一个网站,那么一个页面已经加载,其中包含一个表格,显示菜单中的食物。这些信息是通过 API 从后端服务器应用程序获取的,该应用程序返回了这些信息。
这个例子在图 12.1 中得到了完美的说明。

图 12.1:描述 REST API
现在,让我们学习如何使用各种工具来管理我们的 API。这对开发者来说是一项重要的技能。
我们可以使用哪些工具来测试我们的 API?
在进行 API 开发时,开发者使用 API 工具来管理、测试和与 API 交互是正常的。以下是一些 JavaScript 开发中最受欢迎的 API 开发工具,可以在表 12.1中看到:
| API 工具 | 网站地址 |
|---|---|
| Postman | www.postman.com/ |
| Insomnia | insomnia.rest/ |
| Swagger | swagger.io/ |
| Thunder Client | www.thunderclient.com/ |
表 12.1:流行的 JavaScript REST API 工具
使用这些工具,我们可以彻底测试我们的 REST API,甚至可以将这些工具作为创建文档的基础。然而,在构建 API 时,我们需要注意的一点是跨源资源共享(CORS)。CORS 基本上是一种应用程序集成方法。CORS 指定了一种方法,允许单个域中加载的客户端 Web 应用程序与位于另一个域中的资源进行通信。因此,如果我们没有指定或允许 API 中的 CORS 访问,那么除非它们位于同一服务器上,否则我们将无法访问它们。
例如,我们可以在网站上托管一个 API,假设我们有一个位于不同服务器上的 React 应用程序。如果我们的 API 没有设置 CORS,那么我们的 React 应用程序将无法连接到它以检索任何数据,我们将在浏览器控制台中看到一个 CORS 错误,如下面的图所示:

图 12.2:CORS 错误网页浏览器控制台消息
是时候更深入地了解 REST API 了,因为我们已经更好地理解了它们的设置方式。那么,让我们看看有哪些功能可供选择。
REST API 中有哪些功能可用?
REST API 是一个强大的工具,它为开发者提供了访问广泛功能的能力。REST API 的出色安全系统是其最重要的特性之一。它保证了通过 API 提供的数据不会受到恶意攻击。此外,REST API 的可扩展性是另一个关键方面,这意味着它对于流量大的应用程序是一个很好的选择,因为它可以处理大量的查询。REST API 的另一个显著特点是它们的灵活性,这使得开发者可以使用他们选择的任何架构或技术。由于所有这些特性的结合,REST API 是开发者创建可靠和耐用的应用程序的绝佳工具。
REST API 能够请求和响应请求,但这究竟意味着什么?让我们来探究一下。
在向 REST API 发送请求时,请求和响应之间的区别是什么?
如果你想创建一个连接到 REST API 的网络应用程序,理解 HTTP 请求和响应的工作方式是至关重要的。数据通过 HTTP 请求从客户端发送到服务器,并通过 HTTP 响应从服务器发送回客户端。REST API 通过 GET、POST、PUT 和 DELETE 标准 HTTP 协议在客户端和服务器之间进行交互。通过了解这些请求和响应的工作方式,我们可以设计出更有效率和效率的 Web 应用程序,以便它们能够轻松地与 REST API 接口。虽然还有其他 HTTP 协议可用,但标准协议是最常见的。现在,让我们了解这些 HTTP 协议之间的区别。
什么是 GET 请求?
GET 请求是一种用于从服务器获取数据的方法。它是一个只读过程,因此不会对服务器产生负面影响。GET 请求不会导致数据或服务器改变其状态。
什么是 POST 请求?
使用 POST 请求将信息传输到服务器以建立新的数据。它不是不可变的,这意味着多次提交相同的 POST 请求可能会得到不同的结果。POST 请求通常返回 URL 或新创建的资源实例。
什么是 PUT 请求?
PUT 请求可以用来向现有的服务器数据添加新信息。它是可变的,因此你可以发送相同的 PUT 请求以获得不同的结果。根据其执行方式,如果数据不存在,服务器可能会创建它。
什么是 DELETE 请求?
DELETE 请求用于从服务器删除数据。它也是不可变的,这意味着多次发送相同的 DELETE 请求会产生相同的结果。在删除数据后,服务器通常会提供一个状态,表明操作是成功还是失败。
在这个代码示例中,我们可以看到一个示例 REST API 应用程序:
const express = require('express');
const cors = require('cors');
const path = require('path');
require('dotenv').config();
const app = express();
app.use(cors());
app.use(express.urlencoded({ extended: false }));
app.use(express.json());
app.use('/static', express.static(path.join
(__dirname + '/public')));
app.get('/api', (req, res) => {
res.json({ msg: 'API Route' });
});
app.post('/post/:data', (req, res) => {
const data = req.params.data;
console.log(data);
res.json({ msg: `Data logged ${data}` });
});
const port = process.env.PORT || 8080;
app.listen(port, () =>
console.log(`Server running on port ${port},
http://localhost:${port}`)
);
在这个例子中,我们有一个静态路由,用于提供文件,例如图片、CSS 和 JavaScript,只要它们位于我们应用中的公共文件夹内。我们有一个用于 API 的GET路由,它只返回一个 JSON 对象。最后,我们有一个POST路由,它将日志记录到控制台,并返回用户作为POST请求发送的任何文本的 JSON 对象,例如这个例子:http://localhost:8080/post/helloworld。
HTTP 协议只是 REST API 的一个方面。同时,设置良好的认证也很重要。继续阅读以了解为什么这是这种情况。
使用 REST API 时,认证为什么很重要?
使用 REST API 需要认证以确保私有数据的安全。认证需要在提供 API 功能访问之前确认用户的身份。这有助于避免未经授权的数据访问或使用,这可能会危及用户。在与 API 通信时,你可能会利用各种认证机制,包括基于令牌的认证和 OAuth。这些方法确保只有授权用户才能访问 API 并允许发送请求或获取数据。在与 REST API 交互时理解认证可以帮助我们构建更安全、更高效的解决方案。如果我们通过如图所示的图表来观察认证流程,就有可能理解认证流程。

图 12.3:REST API 认证流程
值得讨论的另一个方面是错误处理,因为检查错误非常重要,尤其是在涉及认证和用户详情时。让我们发现有效的错误处理如何使我们的应用程序更加值得信赖。
我们如何在集成 REST API 时使用错误处理?
与 REST API 连接有时很困难,尤其是在遇到错误时。然而,有一个强大的错误处理计划可以长期节省我们时间和麻烦。一个有用的策略是指定我们的应用程序可以识别并响应的错误代码和消息。此外,安装自动重试和监控系统可以帮助更快地识别和解决问题。另一个重要的话题是处理数据验证错误,因为 API 输入在使用我们应用程序之前必须彻底审查。通过在错误管理方面采取积极主动和全面的方法,我们可以确保轻松集成 REST API 并为我们的客户提供更好的用户体验。
在本节中我们将讨论的最后一个主题是 GraphQL。那么,现在让我们看看 GraphQL 是如何与传统 REST API 进行比较的。
REST API 和 GraphQL 之间的区别是什么?
REST 和 GraphQL 是创建和操作网络服务 API 的两种技术;然而,它们在许多方面有所不同。GraphQL 通常更灵活,因为您可以请求所需的数据,而不是像 REST API 那样一次性请求所有数据。这很好,因为它意味着网络上数据传输请求更少。GraphQL 在版本历史记录方面也表现出色,因为它不依赖于版本,这与 REST API 的情况不同,需要更多手动操作。尽管如此,它们在彼此之间都有优点和缺点,在比较两者时,我们可以看到它们为我们提供了不同的优势和劣势。
获取数据
最终用户通常通过预设的端点使用 REST API 来访问资源,每个端点都与单个资源或一组相关资源相关联。客户端向这些端点发送 HTTP 请求以获取或修改数据。当使用 GraphQL 时,客户端向单个端点发送查询或突变,指示所需的数据或想要进行的修改。与 REST API 相比,这允许客户端仅检索实际需要的数据,消除了请求比所需更多或太少数据的需求。
缓存数据
为了提高效率和减轻服务器负载,REST API 可以使用 ETags 和缓存控制头,这两种是常见的 HTTP 缓存方法。相比之下,由于 GraphQL 的灵活查询形式,缓存更困难。客户端通常需要使用如 Apollo Client 或应用程序级缓存等 GraphQL 客户端库提供的独特缓存技术。
文档
在 REST API 中,文档通常单独提供,程序员有责任保持其更新。如果不及时更新,可能会导致矛盾和不准确的数据。另一方面,由于 GraphQL 包含内置的反射功能,用户可以动态地了解 API 的不同类型和功能。工程师可以更轻松地探索和理解 API,通常使用 GraphiQL 等工具。
为了结束这一部分,我们将查看一些使用 REST 和 GraphQL 获取数据的代码示例,以便清楚地了解这两种方法之间的差异。
我们如何使用 REST API 和 GraphQL 获取数据?
我们将查看使用 REST 和 GraphQL 获取数据的代码示例,从 REST API 开始。在这些示例中,我们将使用作者和帖子。
我们如何使用 REST API 获取数据?
第一步是获取一个 ID 为1的帖子:
GET /posts/1
这可能是一个潜在的示例响应的样子:
{
"id": 1,
"title": "Hello World",
"content": "Welcome to my first blog.",
"authorId": 64
}
当使用 REST API 进行数据检索时,请求返回数据对象中的所有信息。无法请求,比如说,只获取id和title信息。我们不得不在 JavaScript 中编写业务逻辑来过滤对象,以便我们只获取在应用程序中想要使用的数据。
因此,如果我们想获取作者详情,那么我们就必须发送第二个 GET 请求:
GET /authors/64
现在,我们将获得作者的信息:
{
"id": 64,
"name": "Jack Thomas",
"email": "jackthomas@gmail.com"
}
因此,现在我们已经了解了使用 REST API 获取数据的基本概念,让我们看看我们如何使用 GraphQL 来完成同样的操作,但这次是使用 GraphQL。
我们如何使用 GraphQL 获取数据?
当我们使用 GraphQL 时,我们首先需要为我们的 API 创建一个模式,如下面的示例所示:
type Post {
id: ID!
title: String!
content: String!
author: Author!
}
type Author {
id: ID!
name: String!
email: String!
}
type Query {
post(id: ID!): Post
}
然后,我们需要编写一个 GraphQL 查询来获取作者和帖子:
query {
post(id: 1) {
id
title
content
author {
id
name
email
}
}
}
响应将看起来像这样:
{
"data": {
"post": {
"id": "1",
"title": "Hello World",
"content": " Welcome to my first blog.",
"author": {
"id": "64",
"name": " Jack Thomas",
"email": " jackthomas@gmail.com"
}
}
}
}
GraphQL 允许我们通过定义我们想要的字段,用一个请求检索必要的数据。
最终,决定使用 REST API 还是 GraphQL 归结于我们的具体用例和需求。两者都有独特的特性和能力,可以帮助我们在创建 Web 或移动应用时优化开发过程。然而,正如许多技术问题一样,在我们确定最佳方法之前,考虑所有替代方案至关重要。如果我们能够掌握它们的共同特性和特殊优势,我们可以在 React 应用程序中安全地使用 REST API 或 GraphQL 来获得期望的结果。无论我们采用什么技术,目标始终应该是为使用该应用的人提供安全、可扩展和有效的解决方案。
现在我们已经完成了这个入门级复习部分,让我们迈出下一步,在了解架构规划所需内容的同时,为构建我们的应用程序打下基础。
规划应用程序架构,包括认证、SWR、GraphQL 和部署
在今天这个竞争激烈的市场中,掌握设计、开发和部署高效、安全且满足用户需求的应用程序架构的技能至关重要。然而,随着 Next.js 等新兴技术的出现,认证方法从传统的 OAuth 2.0 过渡到 SWR,以及 GraphQL 的流行,规划我们的应用程序架构可能看起来是一项艰巨的任务。
在本节中,我们将获得宝贵的见解,帮助我们组织应用程序以取得成功,从选择适当的技术到融入用户体验设计元素。到本节结束时,我们将有一个路线图来指导我们完成下一个项目的构建,包括明确的步骤以完成整个过程。
我们下一个主题将关注无服务器框架。那么,让我们继续阅读,看看为什么它们是构建现代应用的绝佳选择。
为什么无服务器框架如 Next.js 是构建现代应用的绝佳选择?
随着数字技术的进步,新的框架和架构被开发出来,以更好地构建 Web 应用程序的有效性。Next.js 就是这些架构之一,因为它速度快、灵活。Next.js 应用程序有一个独特的设计,将服务器端代码与客户端逻辑分开,从而实现更快的处理时间和更好的性能。这种架构采用服务器端渲染(SSR)在服务器上生成 HTML 页面,显著减少了客户端的负载。Next.js 应用程序还使用一种混合方法,结合动态和静态渲染,以提高效率和更快的加载时间。由于这些特性,Next.js 是程序员创建现代、强大 Web 应用程序的绝佳选择。
我们接下来要讨论的主题是身份验证。那么,让我们看看某些安全措施如何增强我们的防御。
Next.js 应用程序中提供了哪些身份验证方式?
随着在线应用在我们的日常生活中变得越来越重要,网络安全成为了一个高度优先的事项。保护这些应用程序的关键组成部分是身份验证,这是确认用户身份的方法。在 Next.js 应用程序中提供身份验证可以使用各种技术,包括以下内容:
-
Auth.js
-
JSON Web Tokens (JWTs)
-
Auth0
-
OAuth2
-
登录和密码身份验证
通过利用这些技术,工程师可以保护他们的应用程序免受在线威胁,例如黑客试图窃取用户数据的尝试。这种高程度的验证确保用户在一个安全可靠的环境中,同时也保护了他们的私人数据。鉴于在线隐私日益重要,Next.js 应用程序必须使用适当的身份验证程序。
在构建我们的应用程序基础设施时,速度是一个高度优先的要求,但 SWR 是如何使其变得更好的呢?我们将在下一节中找到答案。
SWR 是如何实现快速数据检索的?
随着世界变得越来越以数据驱动,快速有效的数据获取被提升到开发者优先事项的首位,这并不令人惊讶。这就是stale-while-revalidate(或SWR)发挥作用的地方。SWR 是一个优先考虑速度、缓存和重新验证的远程数据获取 React Hooks 库。使用 SWR 可以简化数据获取,因为它消除了令人烦恼的加载框,并确保数据持续更新。此外,它有一个用户友好的 API,使得任何经验水平的程序员都能轻松使用。
现在,我们将再次审视 GraphQL,看看为什么它已经成为现代应用程序架构的最佳选择。
如何通过 GraphQL 集成优化我们的数据获取?
随着技术的发展,提高生产力和简化操作的需求日益增长。作为回应,GraphQL 集成变得更加普遍。通过允许优化的数据获取,GraphQL 显著提高了数据检索的速度和准确性。使用 GraphQL,查询可以定制以满足特定需求,而无需获取多个数据集并从中筛选出必要的信息。结果是,数据可以更快、更准确地检索,这可以帮助公司保持最新状态。由于这些优势,许多企业已经采用了 GraphQL 集成,因为它能够改善数据检索流程。
部署是我们应用程序上线供公众使用的地方,我们得以向世界展示我们的创作。因此,现在让我们在下一节中学习这一关键步骤。
我们如何将 Next.js 应用程序部署到网上?
在云平台上部署应用程序是我们所有辛勤工作的结论,这个过程可能既具有挑战性又具有回报。最佳特性(这使得 Next.js 成为开发者的最爱)是它允许 SSR。现在,当我们将 Next.js 应用程序部署在云基础设施上时,它将始终可以从任何地点访问。市场上有许多云提供商,每个都有其独特的特点。
最受欢迎的在线云托管提供商之一是Vercel,它是 Next.js 的创造者,这意味着他们的平台是部署我们的 Next.js 应用程序的完美场所。其他流行的无服务器托管提供商包括 Netlify、AWS、Azure、Render、Firebase 和 Supabase。可供选择的选择不计其数。重要的是要找到一个既经济高效又提供许多良好功能和服务的平台。这里提到的大多数都提供免费服务。
最后,我们将通过探讨在应用程序部署后如何进行扩展和维护来结束本节。
随着应用程序的增长,我们可以使用哪些策略来扩展和维护我们的应用程序架构?
随着我们的应用程序的流行度和使用量的增长,制定一个既能够扩展又能够维持我们架构的计划至关重要。如果没有适当的规划,我们的应用程序可能无法应对流量激增,这可能会让潜在客户感到失望。预测潜在的未来增长,并考虑到可能帮助我们实现这一目标的工具和技术,对于可扩展的设计是必要的。应用程序的持续有效性也极大地依赖于我们架构的发展维护。添加用户反馈以增强用户体验,测试我们的软件以查找故障和问题,以及定期审查和修订代码库都是这一过程的一部分。如果我们制定了正确的计划,我们的应用程序可以长期繁荣发展。
对认证、SWR 数据获取策略、GraphQL 集成和部署策略有深入的了解,将极大地有利于我们开始开发 Next.js 应用程序的架构。有了这些计划,我们可以开始考虑随着应用程序的增长进行扩展和管理,这是任何有效应用程序架构的关键组成部分。Next.js 的强大功能,结合这些策略,将带来卓越的用户体验,并有助于提高开发过程的效率。
在我们的咖啡餐厅项目中工作
理论部分完成后,让我们开始构建我们的网站。我们首先需要做的是设置我们的项目,现在就让我们这么做。
我们的技术栈如下:
-
Next.js:React 框架
-
styled-components:CSS 样式
-
apollographql:数据 API
-
authjs:认证
-
React Testing Library 和 Jest:测试
-
GitHub:版本控制
-
Vercel:无服务器在线网站托管
我们将为这个项目创建一个餐厅应用程序。将会有五个主要页面,其中两个页面需要您认证后才能访问。认证将通过 Google 或 GitHub 完成,因此您可以使用任一账户登录。
这就是我们的应用程序最终项目的样子:

图 12.4:Next.js 最终项目主页
项目和代码库可以在网上找到,地址为github.com/PacktPublishing/React-Interview-Guide/tree/main/Chapter12/coffee-restaurant
并且在 GitHub 上有一个指向工作网站的链接,您可以看到所有页面。
构建业务逻辑
现在,随着理论部分的完成,我们准备在构建应用程序的过程中做一些实际工作。这将是一个相当简单的应用程序;这里的主要目的是了解我们如何连接所有这些技术。该应用程序将是一个餐厅网站,其中包含一些需要认证用户才能访问的受限页面。这些认证页面将仅在用户登录时显示我们的 GraphQL API 数据;否则,他们将看到一个登录页面。
在介绍这部分完成后,让我们开始构建业务逻辑。
在您的电脑上创建一个文件夹,例如在桌面上,然后运行以下 Next.js 命令来创建一个 Next.js 项目:
npx create-next-app my-app-restaurant
您可以使用以下设置:
-
您想在这个项目中使用 TypeScript 吗?…否/是 -
您想在这个项目中使用 ESLint 吗?…否/是 -
您想在这个项目中使用 Tailwind CSS 吗?…否/是 -
您想在这个项目中使用`src/`目录吗?…否/是 -
使用 App Router(推荐)?…否/是 -
您想自定义默认导入别名吗?…否/是
现在,我们必须安装我们项目的所有依赖项和包。因此,在 my-app-restaurant 目录中 cd 并运行以下命令以安装包。
运行以下脚本以安装常规依赖项:
npm i @apollo/client @apollo/server @as-integrations/next @testing-library/user-event graphql graphql-tag next-auth styled-components@latest
运行以下脚本以安装开发依赖项:
npm i --save-dev @testing-library/jest-dom @testing-library/react jest jest-environment-jsdom
注意
如果在尝试安装这些包时遇到任何错误,您可以尝试添加 --force 命令或在 Mac 上使用 sudo,如下所示:sudo npm i --save-dev @testing-library/jest-dom @testing-library/react jest jest-environment-jsdom --force。
现在,将以下测试运行脚本添加到 root 文件夹中 package.json 文件的 scripts 部分:
"test": "jest --watch",
现在,在 my-app-restaurant 文件夹的根目录下运行此命令。这将创建我们为此项目所需的全部文件和文件夹:
mkdir data
touch data/menu.js data/profile.js
touch .env.local jest.config.mjs
cd src/app
mkdir account account/menu account/profile
touch account/menu/page.js account/profile/page.js
mkdir api api/auth api/auth/"[...nextauth]"
touch api/auth/"[...nextauth]"/route.js
mkdir components graphql lib nutrition queries rewards utils
touch components/GlobalStyles.js components/MainMenu.js components/Provider.js
touch graphql/route.js
touch lib/registry.js
touch nutrition/page.js nutrition/page.test.js
touch queries/clientQueries.js
touch utils/withApollo.js utils/cors.js
touch not-found.js page.test.js
在我们开始处理主应用文件之前,让我们先整理配置文件。
配置文件设置
将此代码放入 api/auth/[…nextauth]/route.js 文件中。我们将使用此代码来设置我们的身份验证层,其中 GitHub 和 Google 将用于登录受保护的路线:
import NextAuth from 'next-auth';
import GithubProvider from 'next-auth/providers/github';
import GoogleProvider from 'next-auth/providers/google';
export const handler = NextAuth({
providers: [
GithubProvider({
clientId: process.env.GITHUB_ID,
clientSecret: process.env.GITHUB_SECRET,
}),
GoogleProvider({
clientId: process.env.GOOGLE_ID,
clientSecret: process.env.GOOGLE_SECRET,
}),
],
});
export { handler as GET, handler as POST };
接下来,将此代码放入 lib/registry.js 文件中。此代码用于配置 styled-components 以与 Next.js 一起工作:
use client';
import React, { useState } from 'react';
import { useServerInsertedHTML } from 'next/navigation';
import { ServerStyleSheet, StyleSheetManager } from 'styled-components';
export default function StyledComponentsRegistry({ children }) {
// Only create stylesheet once with lazy initial state
// x-ref: https://reactjs.org/docs/hooks-reference.html#lazy-initial-state
const [styledComponentsStyleSheet] = useState(() => new ServerStyleSheet());
useServerInsertedHTML(() => {
const styles = styledComponentsStyleSheet.getStyleElement();
styledComponentsStyleSheet.instance.clearTag();
return <>{styles}</>;
});
if (typeof window !== 'undefined') return <>{children}</>;
return (
<StyleSheetManager sheet={styledComponentsStyleSheet.instance}>
{children}
</StyleSheetManager>
);
}
现在,将以下代码添加到 root 目录下的 jest.config.mjs 文件中。此文件可用于设置 Jest 测试运行器:
import nextJest from 'next/jest.js';
const createJestConfig = nextJest({
// Provide the path to your Next.js app to load next.config.js and .env files in your test environment
dir: './',
});
// Add any custom config to be passed to Jest
/** @type {import('jest').Config} */
const config = {
// Add more setup options before each test is run
// setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
testEnvironment: 'jest-environment-jsdom',
};
// createJestConfig is exported this way to ensure that next/jest can load the Next.js config which is async
export default createJestConfig(config);
现在,更新 root 文件夹中的 next.config.js 文件,使用此代码将 styled-components 启用以及应用目录设置:
/** @type {import('next').NextConfig} */
const nextConfig = {
compiler: {
styledComponents: true,
},
};
module.exports = nextConfig;
我们只需完成最后一步,即 GitHub 和 Google 身份验证设置部分。我们必须为我们的 .env.local 文件创建 ID 和密钥。身份验证设置可在 authjs.dev/getting-started/oauth-tutorial 找到。教程展示了如何使用 GitHub 进行身份验证设置。然而,对于其他身份验证提供者,设置相当相似,您可以在 developers.google.com/identity/protocols/oauth2 找到 Google 的设置。
只需记住,当我们在本地上运行我们的应用时,如果以 GitHub 为例,回调 URL 将看起来像这样:http://localhost:3000/api/auth/callback/github。
当我们将其部署到线上时,它可能看起来像这样:https://your-app-url.vercel.app/api/auth/callback/github。
这很重要,因为除非我们使用正确的 URL,否则我们的认证路由将无法工作。最后,我们只需将 ID 和密钥添加到我们的 .env.local 文件中。此文件还需要为我们的 NEXTAUTH_SECRET 键值对创建一个密钥,这是 NextAuth.js 所必需的:
NEXTAUTH_SECRET="yournextsecret"
GITHUB_ID="yourgithubid"
GITHUB_SECRET="yourgithubsecret"
GOOGLE_ID="yourgoogleid"
GOOGLE_SECRET="yourgooglesecret"
你可以创建自己的密钥,它可以是你想要的任何东西;我建议通过生成一个随机字符串来使其安全,就像你创建强密码一样。有许多免费工具可以为你做这件事,或者你也可以自己随机创建一个,例如这个示例:Y@q7LH@6YoBa$Dkz。它使用大写和小写字母数字字符,还包含特殊字符。我们都知道如何创建安全的密码;只需在创建密钥时使用相同的思维方式。
构建应用
让我们开始吧!由于构建阶段有很多文件,所以文件数量相当多。但在这个部分之后,我们将接近完成!我们有一个数据层将充当我们的数据库。基本上,有一个对象数组,我们将使用 GraphQL 获取并在我们的前端显示。
在我们创建的 data 文件夹中使用以下代码。此代码将用于我们在 data/menu.js 文件中显示的数据:
export const menu = [
{
id: '1',
foodType: 'Drinks',
name: 'Latte',
description: 'Steamed milk',
},
{
id: '2',
foodType: 'Drinks',
name: 'Cappuccino',
description: 'Espresso',
},
];
现在,以下代码是针对 data/profile.js 的:
export const profile = [
{
id: '1',
bio: `Born and raised in London, my name is Jordan Brewer and I am a passionate coffee aficionado with a heart as warm as a freshly brewed cup of java.`,
},
];
接下来,下一批代码是针对菜单和配置文件的认证页面。
以下代码是针对 account/menu/page.js 的。
这段代码相当长,所以让我们将其分解成更小的代码块。这个第一个代码块包含我们的导入和颜色主题设置:
'use client';
import { useSession, signIn, signOut } from 'next-auth/react';
import { useQuery } from '@apollo/client';
import { GET_MENU } from '@/app/queries/clientQueries';
import withApollo from '../../utils/withApollo';
import { styled, ThemeProvider } from 'styled-components';
import GlobalStyle from '../../components/GlobalStyles';
import MainMenu from '../../components/MainMenu';
const theme = {
colors: {
primary: 'rgb(15 23 42)',
},
};
下一个代码块包含我们的 CSS 和 styled-components:
const MainContainer = styled.div`
margin: 2rem auto;
max-width: 120rem;
padding: 2rem;
width: 100%;
`;
const PageTitle = styled.h1`
color: #ffffff;
`;
const LoginStatus = styled.p`
color: #ffffff;
`;
const SignInOutButton = styled.button`
color: #ffffff;
padding: 0.5rem;
cursor: pointer;
margin: 2rem 0 2rem 0;
`;
const ContentContainer = styled.div`
display: flex;
flex-flow: column wrap;
`;
const Content = styled.p`
color: #ffffff;
font-size: 1.4rem;
`;
const ItemContainer = styled.div`
display: flex;
flex-flow: row nowrap;
margin: 2rem 0 2rem 0;
border: 0.1rem solid black;
`;
const ItemDescription = styled.div`
margin-left: 1rem;
`;
最后,我们有我们应用的渲染数据:
const Menu = () => {
const { loading, error, data } = useQuery(GET_MENU);
const { data: session, status } = useSession();
const userEmail = session?.user?.email;
if (loading) return <Content>Loading...</Content>;
if (error) return <Content>Something
went wrong</Content>;
if (status === 'loading') {
return <Content>Hang on there...</Content>;
}
if (status === 'authenticated') {
return (
<>
<ThemeProvider theme={theme}>
<GlobalStyle />
<MainMenu />
<MainContainer>
<PageTitle>Menu</PageTitle>
<LoginStatus>Signed in as {userEmail}</LoginStatus>
<SignInOutButton onClick={() => signOut()}>
Sign out
</SignInOutButton>
{!loading && !error && (
<ContentContainer>
{data.menu.map((items) => (
<ContentContainer key={items.id}>
<ItemContainer>
<ItemDescription>
<Content>{items.name}</Content>
<Content>{items.foodType}</Content>
<Content>{items.description}
</Content>
</ItemDescription>
</ItemContainer>
</ContentContainer>
))}
</ContentContainer>
)}
</MainContainer>
</ThemeProvider>
</>
);
}
return (
<>
<ThemeProvider theme={theme}>
<GlobalStyle />
<MainMenu />
<MainContainer>
<PageTitle>Menu</PageTitle>
<SignInOutButton onClick={() => signIn('')}>
Sign in</SignInOutButton>
<LoginStatus>Not signed in. Sign in to view
the menu.</LoginStatus>
</MainContainer>
</ThemeProvider>
</>
);
};
export default withApollo(Menu);
现在,以下代码是针对 account/profile/page.js 的。
和上次一样,让我们将其分解成更小的代码块。
首先,我们有我们页面的导入:
'use client';
import { useSession, signIn, signOut } from 'next-auth/react';
import { useQuery } from '@apollo/client';
import { GET_PROFILE } from '@/app/queries/clientQueries';
import withApollo from '../../utils/withApollo';
import { styled, ThemeProvider } from 'styled-components';
import GlobalStyle from '../../components/GlobalStyles';
import MainMenu from '../../components/MainMenu';
现在,我们有颜色主题和 styled-components 代码:
const theme = {
colors: {
primary: 'rgb(15 23 42)',
},
};
const MainContainer = styled.div`
margin: 2rem auto;
max-width: 120rem;
padding: 2rem;
width: 100%;
`;
const PageTitle = styled.h1`
color: #ffffff;
`;
const LoginStatus = styled.p`
color: #ffffff;
`;
const SignInOutButton = styled.button`
color: #ffffff;
padding: 0.5rem;
cursor: pointer;
margin: 2rem 0 2rem 0;
`;
const ContentContainer = styled.div`
display: flex;
flex-flow: row wrap;
`;
const Content = styled.p`
color: #ffffff;
font-size: 1.4rem;
margin-top: 2rem;
`;
最后,我们有页面的渲染内容:
const ClientProtectPage = () => {
const { loading, error, data } = useQuery(GET_PROFILE);
const { data: session, status } = useSession();
const userEmail = session?.user?.email;
if (loading) return <Content>Loading...</Content>;
if (error) return <Content>Something went wrong
</Content>;
if (status === 'loading') {
return <Content>Hang on there...</Content>;
}
if (status === 'authenticated') {
return (
<>
<ThemeProvider theme={theme}>
<GlobalStyle />
<MainMenu />
<MainContainer>
<PageTitle>Profile</PageTitle>
<LoginStatus>Signed in as {userEmail}
</LoginStatus>
<SignInOutButton onClick={() => signOut()}>
Sign out
</SignInOutButton>
{!loading && !error && (
<ContentContainer>
{data.profile.map((account) => (
<ContentContainer key={account.id}>
<Content>{account.bio}</Content>
</ContentContainer>
))}
</ContentContainer>
)}
</MainContainer>
</ThemeProvider>
</>
);
}
return (
<>
<ThemeProvider theme={theme}>
<GlobalStyle />
<MainMenu />
<MainContainer>
<PageTitle>Profile</PageTitle>
<SignInOutButton onClick={() => signIn('')}>
Sign in</SignInOutButton>
<LoginStatus>
Not signed in. Sign in to view your profile.
</LoginStatus>
</MainContainer>
</ThemeProvider>
</>
);
};
export default withApollo(ClientProtectPage);
现在,让我们处理一些组件文件。第一个代码是针对 components/MainMenu.js 的:
import Link from 'next/link';
import { styled } from 'styled-components';
const MainNavigation = styled.nav`
position: relative;
z-index: 1;
display: flex;
flex-flow: wrap;
justify-content: space-around;
font-size: 2rem;
padding: 1rem;
background: rgb(250 250 250);
`;
export default function MainMenu() {
return (
<MainNavigation>
<Link href="/">Home</Link>
<Link href="/nutrition">Nutrition</Link>
<Link href="/account/menu">Menu</Link>
<Link href="/account/profile">Profile</Link>
</MainNavigation>
);
}
接下来,以下代码是针对 components/Provider.js 文件的,该文件对于身份验证和会话状态是必需的:
'use client';
import { SessionProvider } from 'next-auth/react';
const Provider = ({ children }) => {
return <SessionProvider>{children}</SessionProvider>;
};
export default Provider;
我们下一个文件包含我们的 GraphQL 架构、解析器和 Apollo 服务器。以下代码放入 graphql/route.js:
mport { ApolloServer } from '@apollo/server';
import { startServerAndCreateNextHandler } from '@as-integrations/next';
import { gql } from 'graphql-tag';
import { menu } from '../../../data/menu';
import { profile } from '../../../data/profile';
import allowCors from '../utils/cors';
// Define the GraphQL schema and resolvers
const typeDefs = gql`
type Menu {
id: String
foodType: String
name: String
description: String
}
type Profile {
id: String
bio: String
}
type Query {
menu: [Menu]
profile: [Profile]
}
`;
const resolvers = {
Query: {
menu: () => menu,
profile: () => profile,
},
};
// Create the Apollo Server
const server = new ApolloServer({
typeDefs,
resolvers,
});
const handler = startServerAndCreateNextHandler(server, {
context: async (req, res) => ({ req, res }),
});
export async function GET(request) {
return handler(request);
}
export async function POST(request) {
return handler(request);
}
export default allowCors(handler);
现在,使用以下代码构建 nutrition/page.js:
'use client';
import { styled, ThemeProvider } from 'styled-components';
import GlobalStyle from '../components/GlobalStyles';
import MainMenu from '../components/MainMenu';
const theme = {
colors: {
primary: 'rgb(15 23 42)',
},
};
const MainContainer = styled.div`
margin: 2rem auto;
max-width: 120rem;
padding: 2rem;
width: 100%;
`;
const PageTitle = styled.h1`
color: #ffffff;
`;
const PageIntro = styled.p`
color: #ffffff;
margin-top: 2rem;
font-size: 1.4rem;
`;
export default function Nutrition() {
return (
<>
<ThemeProvider theme={theme}>
<GlobalStyle />
<MainMenu />
<MainContainer>
<PageTitle>Nutrition</PageTitle>
<PageIntro>Nutrition is good for health
and diet!</PageIntro>
</MainContainer>
</ThemeProvider>
</>
);
}
接下来是 GraphQL 查询,所以以下代码是针对 queries/clientQueries.js 文件的:
import { gql } from '@apollo/client';
const GET_MENU = gql`
query {
menu {
id
name
foodType
description
}
}
`;
const GET_PROFILE = gql`
query {
profile {
id
bio
}
}
`;
export { GET_MENU, GET_PROFILE };
现在,重要的 utils/cors.js 文件是必需的,这样我们在线部署应用时就不会出现 CORS 错误。这将允许我们在认证路由上访问我们的 GraphQL API:
const allowCors = (fn) => async (req, res) => {
res.setHeader('Access-Control-Allow-Credentials', true);
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Origin', req.headers.origin);
res.setHeader(
'Access-Control-Allow-Methods',
'GET,OPTIONS,PATCH,DELETE,POST,PUT'
);
res.setHeader(
'Access-Control-Allow-Headers',
'X-CSRF-Token, X-Requested-With, Accept, Accept-Version,
Content-Length, Content-MD5, Content-Type, Date, X-Api-Version'
);
if (req.method === 'OPTIONS') {
res.status(200).end();
return;
}
await fn(req, res);
};
export default allowCors;
让我们继续前进。这里还有一些代码,它是针对另一个 GraphQL 设置页面的;这次,它有 GraphQL 的端点,这是我们稍后如何访问查询的方式。此文件位于 utils/withApollo.js:
import { ApolloClient, InMemoryCache, ApolloProvider } from '@apollo/client';
import { useMemo } from 'react';
import { SessionProvider } from 'next-auth/react';
export function initializeApollo(initialState = null) {
const _apolloClient = new ApolloClient({
// Local GraphQL endpoint
// uri: 'http://localhost:3000/graphql',
// Your online GraphQL endpoint
uri: 'https://coffee-restaurant.vercel.app/graphql',
cache: new InMemoryCache().restore(initialState || {}),
});
return _apolloClient;
}
export function useApollo(initialState) {
const store = useMemo(() => initializeApollo(initialState),
[initialState]);
return store;
}
export default function withApollo(PageComponent) {
const WithApollo = ({ apolloClient, apolloState, session,
...pageProps }) => {
const client = useApollo(apolloState);
return (
<SessionProvider session={session}>
<ApolloProvider client={client}>
<PageComponent {...pageProps} />
</ApolloProvider>
</SessionProvider>
);
};
// On the server
if (typeof window === 'undefined') {
WithApollo.getInitialProps = async (ctx) => {
const apolloClient = initializeApollo();
let pageProps = {};
if (PageComponent.getInitialProps) {
pageProps = await PageComponent.getInitialProps(ctx);
}
if (ctx.res && ctx.res.finished) {
// When redirecting, the response is finished.
// No point in continuing to render
return pageProps;
}
const apolloState = apolloClient.cache.extract();
return {
...pageProps,
apolloState,
};
};
}
return WithApollo;
}
几乎完成了!我们现在在 layout.js 页面文件:
import './globals.css';
import { Dosis } from 'next/font/google';
import StyledComponentsRegistry from './lib/registry';
const dosis = Dosis({ subsets: ['latin'] });
export const metadata = {
title: 'Resturant App',
description: 'Generated by create next app',
};
export default function RootLayout({ children }) {
return (
<html lang="en">
<StyledComponentsRegistry>
<body className={dosis.className}>{children}</body>
</StyledComponentsRegistry>
</html>
);
}
404 错误页面 not-found.js 包含以下代码:
'use client';
import { styled, ThemeProvider } from 'styled-components';
import GlobalStyle from '../../src/app/components/GlobalStyles';
import MainMenu from './components/MainMenu';
const theme = {
colors: {
primary: 'rgb(15 23 42)',
},
};
const MainContainer = styled.div`
margin: 2rem auto;
max-width: 120rem;
padding: 2rem;
width: 100%;
`;
const PageTitle = styled.h1`
color: #ffffff;
`;
const PageIntro = styled.p`
color: #ffffff;
margin-top: 2rem;
font-size: 1.4rem;
`;
export default function NotFound() {
return (
<>
<ThemeProvider theme={theme}>
<GlobalStyle />
<MainMenu />
<MainContainer>
<PageTitle>Page Not Found</PageTitle>
<PageIntro>Could not find requested
page :(</PageIntro>
</MainContainer>
</ThemeProvider>
</>
);
}
最后,page.js 文件位于 root 文件夹中,这是我们的主页。
为了提高可读性,代码被分为两部分。首先,我们有导入和 CSS 部分:
'use client';
import { styled, ThemeProvider } from 'styled-components';
import GlobalStyle from '../../src/app/components/GlobalStyles';
import MainMenu from './components/MainMenu';
const theme = {
colors: {
primary: 'rgb(15 23 42)',
},
};
const MainContainer = styled.div`
margin: 0 auto;
width: 100%;
`;
const CoverHeadingBG = styled.div`
margin: 2rem auto;
display: flex;
flex-flow: column;
align-items: center;
background-color: rgb(6 95 70);
color: rgb(255 255 255);
border-radius: 2rem;
padding: 2rem;
`;
const CoverHeading = styled.h1`
text-transform: uppercase;
`;
const CoverIntro = styled.p`
font-size: 1.4rem;
margin: 2rem 2rem;
`;
const Hero = styled.div`
margin: 2rem auto;
background-image: url('https://res.cloudinary.com/d74fh3kw/image/
upload/v1692557430/coffee-restaurant/coffee-shop_zlkf7u.jpg');
background-repeat: no-repeat;
background-size: cover;
background-position: center;
background-color: rgb(4 120 87);
height: 67.5rem;
width: 100%;
`;
export default function Home() {
return (
<>
<ThemeProvider theme={theme}>
<GlobalStyle />
<MainContainer>
<MainMenu />
<CoverHeadingBG>
<CoverHeading>Summer time is here!</CoverHeading>
<CoverIntro>
Our summer menu has arrived. Freshen up your day with
our creamy
and delicious coffee range, iced teas and mouth watering
snacks.
</CoverIntro>
</CoverHeadingBG>
<Hero></Hero>
</MainContainer>
</ThemeProvider>
</>
);
}
现在是渲染我们的组件在 JSX 中的 HTML 的函数:
export default function Home() {
return (
<>
<ThemeProvider theme={theme}>
<GlobalStyle />
<MainContainer>
<MainMenu />
<CoverHeadingBG>
<CoverHeading>Summer time is here!</CoverHeading>
<CoverIntro>
Our summer menu has arrived. Freshen up your day with
our creamy
and delicious coffee range, iced teas and mouth watering
snacks.
</CoverIntro>
</CoverHeadingBG>
<Hero></Hero>
</MainContainer>
</ThemeProvider>
</>
);
}
就这样!我们已经完成了大部分代码库,我们的应用几乎完成了!剩下要做的只有创建一个GlobalStyles.js文件和前端所需的图片。我们将在下一节中完成这些。
构建表示层
我们的项目使用styled-components,这意味着组件和页面文件已经有了本地化的 CSS。我们确实有一个全局 CSS 文件,它是一个具有全局样式的样式组件。以下是components/GlobalStyles.js的代码:
import { createGlobalStyle } from 'styled-components';
const GlobalStyle = createGlobalStyle`
html,
body {
color: ${({ theme }) => theme.colors.primary};
padding: 0;
margin: 0;
font-size: 1rem;
background: rgb(6 78 59);
}
* {
box-sizing: border-box;
}
`;
export default GlobalStyle;
我们已经等待得够久了;现在是时候最终看到我们的应用程序运行了!
确保你处于my-app-restaurant的root文件夹中,并使用npm run dev命令运行应用程序。
我们的应用应该运行在http://localhost:3000上,我们的 GraphQL API 应该运行在http://localhost:3000/graphql。
GraphQL API 为测试我们的查询提供了内置文档。以下是一个应该返回menu数据的示例查询:
query {
menu {
id
name
foodType
description
}
}
在我们将应用放在 GitHub 上并在线部署之前,我们将为一些文件完成一些单元测试,这样我们就可以习惯测试驱动开发。
实施测试
我们有两个测试套件。首先,我们有nutrition/page.test.js的代码:
import { render, screen } from '@testing-library/react';
import '@testing-library/jest-dom/extend-expect';
import Nutrition from './page';
describe('Nutrition', () => {
it('renders without crashing', () => {
render(<Nutrition />);
});
it('displays the correct title and intro', () => {
render(<Nutrition />);
expect(
screen.getByText('Nutrition is good for health and diet!')
).toBeInTheDocument();
});
});
最后,以下代码是root文件夹中的page.test.js文件:
import { render, screen } from '@testing-library/react';
import '@testing-library/jest-dom/extend-expect';
import Home from './page';
describe('Home', () => {
it('renders without crashing', () => {
render(<Home />);
});
it('displays the correct heading and intro', () => {
render(<Home />);
expect(screen.getByText('Summer time is here!')).
toBeInTheDocument();
expect(
screen.getByText(
/Our summer menu has arrived. Freshen up your day with our
creamy and delicious coffee range, iced teas and mouth
watering snacks./
)
).toBeInTheDocument();
});
});
运行npm test命令,所有测试应该都是通过的。你可以按A按钮重新运行所有测试。
我们的应用几乎完成了。现在让我们将其放在 GitHub 上,以便我们可以进行版本控制。这也是在我们将应用部署到我们的无服务器主机 Vercel 时使其在线工作的准备。
创建带有 README 文档的 Git 仓库
前往 GitHub,为我们的项目创建一个新的仓库。如果你是 GitHub 的新手或者还不熟悉 GitHub,那么你可以在这里遵循入门指南:docs.github.com/en/get-started。然后,简单地按照命令将你的项目推送到 GitHub。这些是我使用的命令。用你自己的仓库替换它们:
git init
git add .
git commit -m "first commit"
git branch -M main
git remote add origin https://github.com/yourname/yourprojectname.git
git push -u origin main
我们的项目现在已经在 GitHub 上了!我们只剩下一件事要做——将我们的应用部署到 Vercel——然后我们就完成了!
部署应用以供公开访问
登录你的 Vercel 账户,然后在仪表板上应该有一个添加新项目的按钮。添加一个新项目,并导入你为该项目创建的 Git 仓库。
在你点击项目根目录下的.env.local文件之前。这个文件没有上传到你的 GitHub 仓库,因为它包含了你的 GitHub 和 Google ID 以及密钥,你不想让别人看到!完成之后,点击部署并等待构建完成。
我们的应用程序现在应该已经上线。然而,utils/withApollo.js文件中的 URL 设置为http://localhost:3000/graphql;我们的应用程序现在在线,并且不是在我们的机器上运行的,所以地址需要更改。更新为 Vercel 上您应用程序的地址,如下例所示:https://your-app-url.vercel.app/graphql。将最新的更改推送到您的 GitHub 仓库,Vercel 将自动更新和部署新的 URI。
以下提交信息应该是可以的:
git status
git add .
git commit -m "vercel graphql endpoint for uri"
git push
并且记得之前提到的,当我们本地运行我们的应用程序时,如果以 GitHub 为例,回调 URL 将看起来像这样:http://localhost:3000/api/auth/callback/github。
当在线部署时,它将具有以下类型的 URL 结构:https://your-app-url.vercel.app/api/auth/callback/github。
因此,更新您在 GitHub 和 Google 上的应用程序身份验证,以便使菜单和个人资料页面在线工作。
应该都完成了。现在,我们的应用程序应该对全世界可见。做得好!
摘要
我们已经到达了本章的结尾,并且我们的编程技能有了显著的提升。在本章的开头,我们学习了 REST API 以及它们如何帮助我们从互联网上获取数据,然后我们可以在我们的应用程序中使用这些数据。之后,我们介绍了应用程序架构规划,其中我们讨论了身份验证,随后是 SWR,一种 HTTP 缓存失效策略。下一个主题是 GraphQL,它是使用 REST API 的流行替代品,因为它可以只获取我们所需的数据,从而减少了 API 请求。然后我们谈到了部署,因为从一开始就要记住这个设置是很重要的。
在接下来的章节中,我们深入探讨了业务逻辑,并学习了为什么我们的代码需要经过良好的精炼。本节之后是表示层,我们创建了一个使用 styled-components 的设计。测试也被考虑在内,我们学习了如何测试我们为项目编写的代码。随着我们的应用程序完成,最后一步是它的部署,这是我们学习了如何创建一个包含 README 文件和我们的文档的 Git 仓库。在这一步中,我们还把我们的应用程序上线,使其对每个人公开可用。
在吸收了所有这些知识之后,我们可以肯定地说,我们在学习和成为更优秀的程序员方面取得了巨大的进步。我们现在为求职者应对面试做好了更好的准备。


浙公网安备 33010602011771号