React 面试指南(二)
原文:
zh.annas-archive.org/md5/ad14d622e1b86c2f653ec95db37a2432译者:飞龙
第四章:处理路由和国际化
在软件开发的广阔世界中导航并非没有困难。我们的旅程经常需要熟练地导航各种屏幕和语言设置,以确保提供卓越的用户体验。本章深入探讨了路由和国际化以及任何现代 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 开发的三个流行选择,因此让我们进一步扩展我们的知识并掌握这些令人惊叹的库。