服务端渲染的 React 应用构建指南(二)
三、Next.js
在前一章中,我们学习了一个叫做 React.js 的 JavaScript 框架,以及如何使用 React.js 框架创建一个客户端渲染应用。在这一章中,我们将学习一个叫做“Next.js”的框架,它用于构建在服务器端呈现的应用。
我们将了解 Next.js 框架的特性,路由使用 Next.js 构建的应用,动态加载内容,配置 webpack 和 Babel,等等。作为本章的一部分,我们还将从头开始创建一个交互式 Next.js 应用。
随后,我们将学习如何将用于状态管理的 Redux 和用于 API 查询的 GraphQL 等框架集成到 Next.js 应用中。让我们开始吧。
Next.js 简介
Next.js 是一个帮助我们在服务器端呈现应用的框架。正如上一章所讨论的,当您创建一个 React 应用时,所有的内容都使用客户端 JavaScript 呈现给浏览器。与此相关的有几个问题。下面是一个简单的列表:
-
浏览器中未启用 JavaScript 的客户端可能无法查看内容。
-
出于安全原因,我们可能只想在服务器端呈现某些内容,这在普通的 React.js 应用中是不可能的。
-
在客户端呈现所有内容会显著增加应用的加载时间。
-
搜索引擎很难索引使用普通 React.js 构建的单页面应用。
所有这些问题都可以在服务器端渲染的帮助下解决。Next.js 就是这样一个框架。每次收到请求时,它都会在服务器的帮助下,在运行时动态生成一个页面。它被网飞、Docker、GitHub、优步、星巴克等领先公司使用。让我们看看 Next.js 框架的特性。
Next.js 的特性
以下是 Next.js framework 的一些主要功能:
-
热重新加载–每次在页面上检测到更改时,Next.js 都会重新加载页面,以便立即反映更改。
-
基于页面的路由–URL 被映射到文件系统上的“pages”文件夹,无需任何配置即可使用。但是,也支持动态路由。
-
自动代码分割–页面只加载所需的代码,从而加快加载速度。
-
页面预取–您可以在链接页面时使用<链接>标签上的“预取”属性,以便在后台预取页面。
-
热模块替换(HMR)–您可以使用 HMR 在运行时替换、添加或删除应用中的模块。
-
服务器端呈现(SSR)–您可以从服务器端呈现页面,而不是在客户端生成整个 HTML。这使得内容丰富的页面的加载时间更短。SSR 还可以确保你的页面很容易被搜索引擎索引。
让我们从自己的 Next.js 应用开始,看看这些功能的实际应用。
入门指南
为了开始使用自己的 Next.js 应用,必须在系统上安装 Node.js。您必须在前一章练习 React.js 示例时安装它。如果没有,可以从 https://nodejs.org/ 下载安装。安装完成后,您可以在编辑器中打开一个终端并运行“node -v”命令来检查 node.js 是否安装正确。如果是,终端将显示 Node.js 的安装版本号。我们将使用 npm(节点包管理器)来初始化我们的应用并安装我们项目的依赖项。npm 与 Node.js 捆绑在一起,如果您已经安装了 Node.js,它应该已经安装在您的系统中了。您可以在终端中执行“npm -v”命令。如果安装正确,此命令将显示系统中安装的 npm 版本。
我将使用可以从 https://code.visualstudio.com/download 下载的 Visual Studio 代码编辑器。但是,您可以使用自己选择的任何编辑器。
一旦完成安装,就可以为 Next.js 应用创建一个目录。我已经创建了一个名为“我的下一个应用”的目录。我们现在将从终端导航到这个新创建的目录,并运行“npm init”命令来创建 package.json 文件。运行这个命令时,您可能需要为 JSON 文件输入一些值,比如包名、版本、描述、git 存储库、关键字等等。您可以选择继续使用默认值,或者输入一些您自己的值。成功执行“npm init”命令后,您可能会注意到在目录中创建了一个 package.json 文件。它应该具有以下代码:
{
"name": "my-next-app",
"version": "1.0.0",
"description": "My Next.js Application",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" &&
exit 1"
},
"author": "Mohit Thakkar",
"license": "ISC"
}
如果您在初始化期间指定了一组不同的值,这些值可能会有所不同。现在,我们将使用以下命令安装应用的 next、react 和 react-dom:
npm install react react-dom next --save
“- save”命令将指示 npm 将已安装的软件包作为依赖项添加到 package.json 文件中。如果您检查该文件,将会向其中添加以下部分:
"dependencies": {
"next": "⁹.1.5",
"react": "¹⁶.12.0",
"react-dom": "¹⁶.12.0"
}
为了启动服务器,我们必须在 package.json 文件中指定启动脚本。让我们通过用以下代码替换文件中的脚本部分来添加它:
"scripts": {
"start": "next"
}
现在您已经指定了启动脚本,您可能想要启动服务器并启动您的应用。您可以使用“npm start”命令来完成此操作。但是,在这个时间点上,您将不能这样做,因为 Next.js 应用在应用启动时会在“pages”文件夹中查找启动页面。因为我们还没有一个“pages”文件夹,所以当我们试图启动我们的应用时,会得到一个编译时错误。让我们在应用的根目录下创建一个空的“pages”文件夹。创建之后,您可以使用“npm start”命令启动您的应用。当您导航到服务器上的应用 URL–http://localhost:3000/时,您会注意到一个 404 错误。如图 3-1 所示。
图 3-1
Next.js 应用启动
请注意这个错误页面圆滑且用户友好的设计。这就是 Next.js 处理错误的方式。它还可以处理其他错误,如 500-内部服务器错误。404 错误的原因是我们的应用中没有页面。在应用启动时,Next.js 试图在页面中找到“index.js”文件,并在默认情况下呈现它。在我们的例子中,由于它无法找到它,我们会遇到一个 404 错误。现在让我们创建我们的第一页。使用以下代码将“index.js”文件添加到“pages”文件夹中:
Pages/index.js
import React from "react";
function MyComponent(){
return(
<div>Hello from Next.js!</div>
);
}
export default MyComponent;
现在,如果您使用“npm start”命令启动服务器,并浏览到http://localhost:3000/或http://localhost:3000/Index,您将能够看到您刚刚创建的页面,如图 3-2 所示。
图 3-2
Next.js 的第一页
现在让我们测试内容是否真的在服务器端呈现。如果右键单击页面并查看页面源代码,您会注意到代码中生成的 HTML 内容直接填充到了根 HTML 标记中。这是因为一切都是在服务器端呈现的。如果对使用 plain React.js 构建的应用进行同样的操作,您将会注意到页面源代码中的根 HTML 标记,而不是代码生成的内容。这是因为,在 React.js 应用中,内容是在页面加载后在客户端呈现的。因此,我们可以确信 Next.js 会在服务器端呈现我们的页面。
在下一节中,我们将为我们的应用创建另一个页面,并了解如何使用 Next.js 路由在页面之间导航。
Next.js 中的路由
到目前为止,我们只有一个页面,但一个应用可能有多个页面,在这些页面之间轻松导航是任何应用的一个重要方面。让我们用下面的代码创建一个“关于”页面:
Pages/about.js
import React from "react";
function About(){
return(
<div>
This is an application built using next.js to demonstrate the effectiveness of server-side rendering!
</div>
);
}
export default About;
如果您运行应用并导航到http://localhost:3000/About,您将看到您刚刚创建的页面,类似于图 3-3 。并不是说您不需要重新启动 npm 进程就可以看到变化。只要您保存了任何更改,Next.js 就会执行热重装,您无需重新启动服务器就可以看到这些更改。但是,您可能需要刷新浏览器页面。
图 3-3
Next.js 中的第二页
现在,为了在两个页面之间建立链接,您可能会想到创建一个锚标记,将页面 URL 传递给“href”属性。让我们这样做,看看会发生什么:
Pages/index.js
import React from "react";
function MyComponent(){
return(
<div>
<p>Hello from Next.js!</p>
<a href='/About'>About</a>
</div>
);
}
export default MyComponent;
如果你运行这个应用,你会看到一个“关于”页面的链接。但是,如果您单击该链接,您会注意到整个页面被重新加载。这是因为锚标签向服务器发送新的请求,并且路由将发生在服务器端。这可能会导致性能问题,因此您可能希望保持到客户端的路由。
Next.js 提供了用于为客户端路由创建链接的<Link>组件。它是一个可以与任何接受“onClick”属性的组件一起工作的包装器。因此,我们将它与一个空的锚标记一起使用。考虑代码中的以下变化:
Pages/index.js
import React from "react";
import Link from 'next/link'
function MyComponent(){
return(
<div>
<p>Hello from Next.js!</p>
<Link href='/About'>
<a>About</a>
</Link>
</div>
);
}
export default MyComponent;
为了使用<Link>组件,您必须首先从“下一个/链接”模块导入它。在执行前面的代码时,您将得到与图 3-4 相同的输出,但是当您单击链接时,您会注意到网络上没有生成额外的服务器请求。你可以在 Chrome 浏览器的开发者工具窗口的“网络”标签中验证这一点。这是因为客户端路由在这里起作用。将预取在<链接>组件中指定的页面,并且导航将在没有服务器请求的情况下发生。
图 3-4
Next.js 中的路由
大多数客户端路由场景都会破坏浏览器导航按钮。然而,Next.js 完全支持历史 API,所以它不会破坏你的浏览器导航按钮。
Note
历史 API 允许您与浏览器历史交互,触发浏览器导航方法,以及更改地址栏内容。这在单页应用中特别有用,在这种应用中,您永远不会真正改变页面,只是内容发生了变化。维护一个栈。每当用户在同一个网站中导航时,新页面的 URL 被放在栈的顶部。每当用户触发浏览器导航按钮时,就会调整栈的指针,并呈现适当的内容。
这就是 Next.js 中的路由。现在让我们看看 Next.js 中的动态页面。
动态页面
大多数实时应用都有动态生成的内容。因此,在实际场景中,我们不能依赖静态页面。让我们看看如何为我们的应用生成动态内容。首先,我们将创建一个文件 DynamicRouter.js,它将基于属性创建链接。考虑以下代码:
shared components/dynamic router . js
import React from "react";
import Link from 'next/link'
function GetLink(props) {
return (
<div>
<Link href=">
<a>{props.title}</a>
</Link>
</div>
);
}
export default GetLink;
Pages/index.js
import React from "react";
import GetLink from "../SharedComponents/DynamicRouter";
function MyComponent(){
return(
<div>
<GetLink title='Page 1'></GetLink>
<GetLink title='Page 2'></GetLink>
<GetLink title='Page 3'></GetLink>
</div>
);
}
export default MyComponent;
如果您看到浏览器窗口,您会注意到在索引页面上生成了三个链接,如图 3-5 所示。但是,这些是空链接,不会导航到任何页面。
图 3-5
Next.js 中的动态链接
现在让我们创建一个页面,它将根据接收到的参数动态显示内容。然后,我们将设置这三个链接,用不同的参数导航到这个动态页面。考虑新页面的以下代码:
页/秒。js
export default (props) => (
<h1>
Welcome to {props.url.query.content}
</h1>
);
shared components/dynamic router . js
...
<Link href={`/SecondPage?content=${props.title}`}>
<a>{props.title}</a>
</Link>
...
现在,如果你点击链接,你将被重定向到一个动态加载内容的页面,如图 3-6 所示。
图 3-6
Next.js 中的动态页面
如果您注意到生成的 URL,您会看到查询参数显示在地址栏上。您可能希望用户看到不显示查询参数的干净 URL。这在 Next.js 中可以通过使用“Link”组件的“as”属性来实现。传递给“as”属性的任何内容都将显示在地址栏中。让我们试试这个:
Pages/index.js
...
<GetLink title='Page 1' Disp='page-1'>
</GetLink>
<GetLink title='Page 2' Disp='page-2'>
</GetLink>
<GetLink title='Page 3' Disp='page-3'>
</GetLink>
...
shared components/dynamic router . js
...
<Link href={`/SecondPage?content=${props.title}`}
as={props.Disp}>
<a>{props.title}</a>
</Link>
...
现在,如果你点击链接并导航到其中一个页面,你会看到一个没有任何参数的干净的 URL,如图 3-7 所示。
图 3-7
Next.js 页面的自定义 URL
这就是 Next.js 中的动态页面。现在让我们学习如何在 Next.js 应用中处理多媒体内容。
使用 CSS 添加多媒体内容
有时,您可能希望在应用中添加多媒体内容,如图像和视频。一般来说,最好在 CSS 本身中添加这些内容的 URL,以便于维护。让我们在索引页面的链接旁边添加图片。我已经下载了三张图片,并将它们添加到应用根目录下的“static/Images”文件夹中。js 提供了一个叫做 JSS(JS 中的 CSS)的东西,它允许我们在 JSX 代码中直接定义样式。让我们将以下代码添加到“index.js”文件中,以便使用 CSS 添加图像:
index.js
...
return (
<div>
...
<style jsx global>
{`
a{
color:blue;
}
.img{
height: 50px;
width: 50px;
background-size: cover!important;
background-repeat: no-repeat!important;
background-position: center!important;
border: 1px solid black;
border-radius: 10px;
display: inline-block;
margin-top: 10px;
}
.p1{
background: url(../statimg/1.jpg);
}
.p2{
background: url(../statimg/2.jpg);
}
.p3{
background: url(../statimg/3.jpg);
}
`}
</style>
</div>
);
...
既然我们已经定义了样式,我们可能想在我们的页面上使用这些样式。为此,我们需要将“index.js”文件中的类名作为属性传递给<Link>组件,并在“DynamicRouter.js”文件中使用它来为图像创建一个<div>,并为其设置类名。请考虑以下代码更改:
Index.js
...
<GetLink title='Page 1'
Disp='page-1'
Class='img p1'>
</GetLink>
<GetLink title='Page 2'
Disp='page-2'
Class='img p2'>
</GetLink>
<GetLink title='Page 3'
Disp='page-3'
Class='img p3'>
</GetLink>
...
DynamicRouter.js
...
return (
<div>
<div className={props.Class}></div>
<Link
href={`/SecondPage?content=${props.title}`}
as={props.Disp}>
<a>{props.title}</a>
</Link>
</div>
);
...
如果保存更改,您将看到类似于图 3-8 的浏览器输出。
图 3-8
Next.js 中的多媒体内容
您可能希望为所有样式创建一个单独的 CSS 文件。但是,您不能在 Next.js 应用中直接这样做。你必须首先安装一个 CSS 加载器。您可以使用以下命令将@zeit/next-css 模块安装到您的应用中:
npm install @zeit/next-css --save
安装后,您必须使用以下代码将配置文件“next.config.js”添加到应用的根目录中:
Next.config.js
const withCSS = require('@zeit/next-css')
module.exports = withCSS({})
现在,您必须在应用的根目录下创建一个文件“style.css”。然后可以从“index.js”文件中删除<style jsx global>组件,并将样式表代码移动到“style.css”文件中,如下所示:
a{
color:blue;
}
.img{
height: 50px;
width: 50px;
background-size: cover!important;
background-repeat: no-repeat!important;
background-position: center!important;
border: 1px solid black;
border-radius: 10px;
display: inline-block;
margin-top: 10px;
}
.p1{
background: url(/statimg/1.jpg);
}
.p2{
background: url(/statimg/2.jpg);
}
.p3{
background: url(/statimg/3.jpg);
}
请注意,重要的是,您的图像放置在“静态”文件夹中,该文件夹在您的应用中与“页面”文件夹处于同一级别。在您的“index.js”文件中,您将能够使用以下代码行像导入任何其他文件一样导入 CSS 文件:
import "../style.css";
如果保存更改并转到浏览器窗口,您将看到类似于图 3-8 的输出。包括视频在内的所有其他多媒体内容都可以以类似的方式呈现到您的应用中。现在让我们看看如何在 Next.js 应用中从远程服务器获取数据。
从远程服务器获取数据
您可能还记得,在前一章中,我们使用 Axios 库来执行 AJAX 请求,以便从远程端点获取数据。我们将在 Next.js 应用中做同样的事情。这里的区别是 AJAX 调用将在服务器端执行,而不是在客户端。首先,我们将使用以下命令将 Axios 库安装到我们的项目中:
$ npm install axios
一旦安装完毕,我们可以在页面中使用它的get()方法从远程端点获取数据。然而,在 Next.js 应用中,事情会有一些变化。之前,我们在组件的componentDidMount()方法中执行了 AJAX 调用。但是在这种情况下,我们将使用由 Next.js 提供的特殊方法getInitialProps(),它帮助我们设置组件的属性。我们将在getInitialProps()方法中启动我们的 Axios 请求。考虑下面的页面,它使用 GitHub 的公共 API 来获取 GitHub 用户列表,并在我们的应用中显示这些用户:
Pages/GithubUsers.js
import React from 'react'
import axios from 'axios';
import '../style.css';
export default class extends React.Component {
static async getInitialProps() {
try {
const res = await axios.get('https://api.github.com/users');
return { data: res.data }
}catch(e){
return {error:e}
}
}
render() {
if (this.props.error) {
return (
<div>
Error: {this.props.error.message}
</div>
);
}
else {
return (
<div>
<h1>Github Users</h1>
<br />
{this.props.data.map((item, index) => (
<div key={index}
className='UserBlock'>
<img src={item.avatar_url}
alt='User Icon'>
</img>
<div className="UserDetails">
<p>Username: {item.login}</p>
<p>ID: {item.id}</p>
</div>
</div>
))}
</div>
);
}
}
}
因为 Axios 请求是异步的,所以我们需要一种方法来捕捉可用的响应。之前,我们使用 Axios 库的get()方法的then()扩展方法来完成这项工作。这一次,我们在 Axios 方法调用中使用了 await 关键字,并将getInitialProps()方法标记为异步。async…await 关键字帮助我们处理异步请求,而不必使用回调或承诺,并且对于我们的应用来说很方便。我们将这个请求包装在一个 try…catch 块中,这样我们就可以知道网络上是否发生了错误。一旦请求被处理,我们从getInitialProps()方法返回一个包含数据或错误的对象。此方法返回的对象将被设置为 props 对象。在 render 方法中,我们通过使用“this.props.error”检查 error 属性来检查错误是否存在。如果是这样,用户将在浏览器上看到一条错误消息,类似于图 3-9 。
图 3-9
Axios 请求中的错误
如果没有错误,那么我们将使用 JavaScript 的array.map()方法迭代“this.props.data”对象,并在浏览器上显示 GitHub 用户的详细信息。输出应该类似于图 3-10 。
图 3-10
成功的 Axios 请求
如果您的输出与图 3-10 中的略有不同,不要担心。您的代码中还缺少一点。我已经在我们之前创建的“style.css”文件中添加了一些样式,并将其导入到我们的页面中。照着做,你就能很好地搭配这个造型了。可以参考下面的样式表代码:
style.css
body {
font-family: sans-serif;
}
body div h1 {
text-align: center;
border-bottom: 1px solid grey;
}
img {
height: 50px;
width: 50px;
border: 1px solid black;
}
.UserBlock {
display: inline-block;
border: 1px solid black;
border-radius: 5px;
padding: 10px;
margin: 15px;
width: 255px;
}
.UserDetails {
display: inline-block;
margin-left: 15px;
}
.error {
color: red;
font-weight: bold;
font-size: 26px;
text-align: center;
}
就这样。您现在可以从任何远程端点获取数据,并在您的应用中使用它。
使用 Next.js 创建交互式应用
让我们尝试在应用中添加一些用户交互。我们将从浏览器获得作为文本输入的 GitHub 用户 id,并显示该特定 GitHub 用户的详细信息。为此,我们需要从 props 获取初始数据,并使用构造函数将其设置在我们的状态对象中,就像我们在前一章中对传统的 React 应用所做的那样。这里唯一的区别就是属性会来自getInitialProps()法。
Note
我们需要将属性转移到状态对象,因为属性对象是不可编辑的,因此,我们不能直接使用它进行数据操作。
当从浏览器输入一个 id 时,我们将进行一个 API 调用来获取用户详细信息并修改状态对象中的数据。状态对象一改变,React 就会重新渲染 UI。考虑以下代码:
Pages/GithubUsers.js
import React from 'react';
import axios from 'axios';
import '../style.css';
export default class extends React.Component {
static async getInitialProps() {
try {
const res = await axios.get('https://api.github.com/users');
return { data: res.data }
} catch (e) {
return { error: e }
}
}
constructor(props) {
super(props);
this.state = {data: props.data,
error: props.error };
}
GetUser = async () =>
{
try {
const res = await axios.get('https://api.github.com/users/' + document.getElementById('inputTextbox').value);
this.setState({
data: [res.data],
error: null
});
} catch (e) {
this.setState({
data: null,
error: e
});
}
}
render() {
if (this.state.error) {
return(
<div>
<h1>Github Users</h1>
<br />
<div className="center">
<input id="inputTextbox" type="text">
</input>
<button type="button"
onClick={this.GetUser}>
Get User
</button>
</div>
<br />
<p className="error">
Error: {this.state.error.message}</p>
</div>
);
}
else {
return (
<div>
<h1>Github Users</h1>
<br />
<div className="center">
<input id="inputTextbox" type="text">
</input>
<button type="button"
onClick={this.GetUser}>
Get User
</button>
</div>
<br />
{this.state.data.map((item, index) => (
<div key={index} className="UserBlock">
<img src={item.avatar_url}
alt='User Icon'></img>
<div className="UserDetails">
<p>Username: {item.login}</p>
<p>ID: {item.id}</p>
</div>
</div>
))}
</div>
);
}
}
}
下面是我们在前面的代码中所做的事情的列表:
-
getInitialProps()方法获取 Github 用户的初始列表,并返回设置为页面属性的数据。这些属性可以使用“this.props”访问,并且不可编辑。 -
方法用作为 props 传递的值初始化状态对象。每当我们获取特定用户请求的 GitHub 用户详细信息时,这个状态对象就会更新。
-
GetUser()方法处理按钮的 click 事件,并在每次用户请求特定 GitHub 用户的详细信息时进行 API 调用。GitHub 的用户 id 从输入框中获取,并作为参数发送给 API 调用。用 API 调用返回的数据更新状态对象。状态对象一更新,React 就会重新渲染视图。 -
方法检查状态对象,如果请求成功,则显示用户详细信息,如果请求中有错误,则显示错误消息。
如果您导航到浏览器上的“GithubUsers”页面并搜索有效用户,您将看到该用户的详细信息,如图 3-11 所示。
图 3-11
交互式 Next.js 应用
如果您搜索一个不存在的用户,您将看到如图 3-12 所示的错误信息。您可以根据自己的喜好定制错误消息。
图 3-12
找不到 GitHub 用户
就是这样。我们刚刚创建了一个交互式应用,它使用 Next.js 和 React.js 在服务器端呈现内容。
对 Next.js 使用 Redux
市场上的大规模应用大多使用 MVC 架构进行状态管理。然而,在使用客户端库构建的应用中实现 MVC 是一项痛苦的任务,因为与传统 MVC 中的中心模型不同,客户端应用中的状态分散在页面上,而不是在应用级别。为了在客户端库中实现 MVC 风格的状态管理,我们使用 Redux。Redux 的架构如图 3-13 所示。
图 3-13
Redux 架构
为了理解 Redux 架构,您需要知道以下内容:
-
视图触发一个动作,该动作在 Reducers 的帮助下更新存储。然后,存储隐式地将更新后的数据发送回视图。
-
动作将信息传递给缩减器,然后缩减器根据收到的信息决定在存储中更新什么数据。
-
商店可以被视为应用级别的状态对象。此对象中的更改将触发视图更新。
-
动作是特殊的方法,每当视图中的变化触发这些方法时,它们就更新应用状态。
-
与传统的 MVC 模式不同,这里的数据流是单向的。这意味着商店不能触发任何操作。只有视图可以触发操作。这大大降低了无限循环的可能性。
当我们开始用一个例子工作时,你会更好地理解它。让我们看看 Redux 的三个基本原则:
-
单一事实来源–整个应用的状态驻留在单一存储对象中。
-
状态是只读的–改变状态的唯一方法是触发一个动作。视图没有直接更新状态的权限。它们触发一个动作,告诉 Reducer 更新状态。这确保了对状态的所有更改都集中地一个接一个地发生,以便出于调试目的可以跟踪它们。
-
用纯函数进行修改–为了指定状态如何被动作修改,编写了纯 Reducers。这些函数将当前状态和动作作为输入,并将下一个状态作为输出返回。还记得我们在上一章学习的作为 React 基本概念的纯度概念吗?这正是减肥药所坚持的。因为它们是纯函数,所以要确保它们返回一个新的状态对象,而不是修改现有的状态对象。
Note
纯函数从不修改输入参数的值。而是在每次被调用时返回一个新的对象。此外,无论您调用一个 pure 函数多少次,对于相同的输入参数集,它总是返回相同的输出。最后,一个纯函数只依赖于它的输入参数,从不修改它范围之外的任何东西。
让我们更详细地了解一下存储、归约器和动作。
商店
Store 是存储整个应用状态的对象。如前所述,它是“真理的唯一来源”。使用以下代码片段可以轻松创建商店:
import {createStore} from 'redux';
import reducer from 'reducer';
const store = createStore(reducer);
以下是 Store 对象提供的一些方法:
-
**store . getstate()–**该方法返回当前状态。
-
**store . dispatch(action)–**通过调度动作来更新状态。将使用当前状态和动作调用与商店相关联的 Reducer 函数。它的返回值将被认为是下一个状态。一旦状态改变,改变监听器也将被立即通知。
-
**store . subscribe(listener)–**用于向状态添加一个更改监听器。您可以将函数作为参数传递。每次调度动作时都会调用这个函数。您可以在侦听器中使用 getState()方法来获取更新后的状态值。
-
**unsubscribe()–**当状态改变时,如果不想再调用监听器方法,就使用这个方法。当您订阅侦听器时会返回此方法,因此您可能希望在订阅期间将它保存在一个变量中,以便能够取消订阅。考虑下面的代码片段:
// Subscribing
const unsubscribe = store.subscribe(someListener);
// Unsubscribing
unsubscribe();
行动
动作是将数据从应用发送到商店的信息负载。它们是普通的 JavaScript 对象,包含一个类型和一个可选的有效载荷。他们是商店唯一的信息来源。下面的代码片段演示了如何创建和调度一个操作:
...
const action = {
type: 'Multiply',
payload: { value: 10 },
};
store.dispatch(action)
Redux 没有严格的规则集来定义您的操作。这意味着,除了“type”属性之外,你如何构造你的动作完全取决于你自己。您可以直接在 Action 对象中定义“value属性,而不是在“payload属性中定义。事实上,您可以定义自己的属性和值。但是在“payload”属性中定义您的所有属性是推荐的方法之一。
还原剂
Reducers 是指定状态如何根据由动作分派的信息的类型和有效负载而改变的函数。这些函数将当前状态和动作作为输入参数,在处理信息后生成一个新状态,并将这个新状态作为输出返回。现在,即使我们整个应用的状态是一个单一的 Store 对象,我们也可能想要编写多个 Reducers 来修改这个对象。Redux 为您提供了这样做的灵活性。您可以为每个场景编写一个小的缩减器,而不是编写一个处理所有场景的缩减器函数。这将帮助我们最小化代码的复杂性。
Note
因为所有的动作都是顺序执行的,所以我们永远不会面临多个 reducers 试图同时修改状态的情况。
以下是返回初始状态的示例 Reducer 函数的代码片段:
function sampleReducer(state, action) {
return state
}
让我们创建一个基本的例子来理解 Redux 的概念。首先,我们将使用以下命令将 redux 安装到我们的应用中:
npm install redux --save
完成后,我们将创建两个单独的文件夹——“Actions”和“Reducers”。让我们修改“pages”文件夹中的“index.js”文件。我们将有一个输入框,一个按钮,和一个标签。单击按钮时,应该用输入框中的值更新状态,标签应该用状态值更新自身。将为状态设置一些初始值。考虑以下代码:
Pages/index.js
import React from "react";
import '../style.css';
export default class extends React.Component {
static async getInitialProps() {
return { text: 'Initial label value.' }
}
constructor(props) {
super(props);
this.state = { text: props.text };
}
render() {
return(
<div>
<h1>Redux Demo</h1>
<br />
<div className="center">
<input id="inputTextbox" type="text">
</input>
<button type="button"
onClick={this.GetUser}>
Update Label
</button>
</div>
<br />
<p>{this.state.text}</p>
</div>
);
}
}
如果您在浏览器中访问该页面,您将看到类似于图 3-14 的输出。
图 3-14
Redux 演示
请注意,我们刚刚创建了一个带有标签使用的初始状态值的页面。我们使用getInitialProps()方法传递一个静态字符串作为属性。在构造函数中,我们获取这个属性的值,并将其设置为状态。请注意,我们没有编写任何更改更新状态的逻辑。我们将使用 Redux 来实现。但是在我们这样做之前,我们必须安装一些依赖项来帮助我们。使用以下命令执行相同的操作:
npm install redux react-redux next-redux-wrapper redux-thunk redux-devtools-extension --save
我们总共安装了五个新的依赖项。我们已经拥有的其他依赖项有react、react-dom、next、axios和@zeit/next-css。安装新的依赖项后,我的“package.json”如下所示:
package.json
{
"name": "my-next-app",
"version": "1.0.0",
"description": "My Next.js Application",
"main": "index.js",
"scripts": {
"start": "next"
},
"author": "Mohit Thakkar",
"license": "ISC",
"dependencies": {
"@zeit/next-css": "¹.0.1",
"axios": "⁰.19.0",
"next": "⁹.1.6",
"next-redux-wrapper": "⁴.0.1",
"react": "¹⁶.12.0",
"react-dom": "¹⁶.12.0",
"react-redux": "⁷.1.3",
"redux": "⁴.0.5",
"redux-devtools-extension": "².13.8",
"redux-thunk": "².3.0"
}
}
如果您正在从头开始创建一个新的应用,请确保您更新了前面代码片段中提到的“package.json ”,并从终端运行“npm install”命令来更新依赖项。是时候创建我们的第一个动作了。我在应用的根目录下创建了一个“Actions”文件夹,其中包含我们的操作。考虑以下代码:
*Actions/*Actions . js
export const InitialState = {
text: 'Initial label value.'
}
export const changeState = () => dispatch => {
return dispatch({
type:'ChangeLabel',
text: document.getElementById('inputTextbox').value
})
}
这里,我们定义了一个InitialState,它是一个普通的 JavaScript 对象,以及一个在点击按钮时用输入文本更新状态值的动作。我们将在 Reducer 函数中直接使用InitialState对象,在第一次创建商店时也是如此。我们稍后会看到。然而,为了更新状态,我们必须向 Reducer 发送一个动作。为此,我们创建了一个方法,changeState(),它将在按钮点击时被调用。这个方法调度我们的动作。
对于我们正在调度的操作,我们已经定义了一个强制的"type"属性,它决定了正在执行的操作的类型,还定义了一个"text"属性,它将新数据发送到 reducer。是时候创建我们的 Reducer 函数了,它将基于从动作接收到的数据来更新存储。我在应用的根目录下创建了一个“Reducers”文件夹,其中包含了我们的 reducer。考虑以下代码:
减速器/ 减速器. js
import { InitialState } from '../Actions/actions'
export const reducer = (state = InitialState, action) => {
if (action.type == 'ChangeLabel') {
return Object.assign({}, state, {
text: action.text
})
}
else {
return state;
}
}
如前所述,减速器接受两个输入参数——当前状态和动作。在定义我们的缩减器时,我们将“InitialState”指定为第一个输入参数 state 的默认值。如果 reducer 在空状态下被触发,我们的“InitialState”对象中定义的初始状态值将被设置为该状态。
Note
“InitialState”是从我们的操作文件中导入的,它与我们之前创建的对象相同。
如果动作的类型是“ChangeLabel”,reducer 将知道状态值需要用动作分派的数据来更新。在这种情况下,Reducer 函数创建一个新的对象,将当前状态值赋给该对象,并用动作分派的新值替换“text”属性的值。这个新对象将被 Reducer 返回,并被视为应用的新状态。React 将在检测到状态变化时自动更新视图。因此,一旦执行了 Reducer,视图就会反映状态的变化。我们没有定义任何其他动作,所以如果动作的类型不是“ChangeLabel”,我们将只返回收到的状态对象。现在是时候编写第一次创建我们的商店的代码了。我在应用的根目录下创建了一个“Store”文件夹,其中包含我们的商店初始化代码。考虑以下代码:
Store/??【Store . js
import { createStore, applyMiddleware } from 'redux'
import thunkMiddleware from 'redux-thunk'
import { reducer } from '../Reducers/reducer'
import { InitialState } from '../Actions/actions.js'
export const initStore = (initialState = InitialState) => {
return createStore(
reducer,
initialState,
applyMiddleware(thunkMiddleware)
)
}
这里,我们再次使用了在我们的 Actions 文件中创建的“InitialState”对象,这一次,当第一次创建存储时,用初始应用状态初始化存储。createStore()是由“redux”库提供的方法,以便第一次创建和初始化 Redux 存储。
Note
你的应用中应该只有一个商店。
我们将以下三个参数传递给createStore()方法:
-
reducer (Function)–这是我们为商店创建的减压器功能。给定当前状态和一个操作,它返回下一个应用状态。 -
这是我们应用的初始状态。它是我们在动作文件中创建的普通 JavaScript 对象。
-
enhancer (Function)–您可以选择指定一些使用第三方代码的功能来增强您的应用。在我们的例子中,我们使用了“redux-thunk”库提供的“thunkMiddleware”。这个中间件帮助我们编写与存储交互的异步逻辑。对于没有这个中间件的基本 Redux 存储,我们将只能通过分派一个动作来执行对存储的同步更新。我们将使用“redux”库提供的 applyMiddleware()将“thunkMiddleware”转换为增强器。
请注意,商店尚未创建。我们刚刚定义并导出了“initStore”函数,可以调用该函数来创建商店。
您一定已经注意到,我们已经创建了使用 Redux 执行状态管理所需的一切。是时候在我们的应用生命周期中注入 Redux 功能了。为此,我们将不得不使用“react-redux”库,它是我们之前作为依赖项之一安装的。这是 Redux 的官方 React 绑定。它帮助 React 组件从 Redux 存储中读取数据,并将操作分派给存储以更新数据。因为我们需要我们的状态对象在整个应用中都可用,所以我们必须将它注入到一个组件中,其余的组件都是从这个组件继承的。该父组件可以被称为高阶组件(HOC)。在 Next.js 中,我们可以创建一个特殊的组件“_App.js”,它包装了所有的页面,并可用于共享整个应用中常见的内容。我们将使用这个“_App”组件向应用生命周期注入 Redux。使用以下代码将"_App.js"文件添加到“Pages”文件夹中:
页/_App.js
import React from 'react'
import { Provider } from 'react-redux'
import App from 'next/app'
import withRedux from 'next-redux-wrapper'
import { initStore } from '../Store/store'
export default withRedux(initStore)(
class MyApp extends App {
static async getInitialProps({ Component, ctx }){
return {
pageProps: Component.getInitialProps
? await Component.getInitialProps(ctx)
: {},
}
}
render() {
const { Component, pageProps, store } = this.props
return (
<Provider store={store}>
<Component {...pageProps} />
</Provider>
)
}
}
)
这里,我们已经创建了一个组件,它被包装在一个特殊的withRedux()()包装器中,这个包装器是由“next-redux-wrapper”库提供的,我们之前把它作为一个依赖项安装的。作为这个包装器的第一个输入,我们传递创建初始存储的方法,在我们的例子中是来自“??”的“??”。这个包装器的第二个输入是我们的高阶组件(HOC)。
该组件从“下一个”库提供的“应用”组件扩展而来。这是 Next.js 用来初始化页面的组件。因为我们覆盖了页面的默认初始化,所以我们必须在组件中编写getInitialProps()方法,并让它调用页面的getInitialProps()方法。它接受两个参数——“组件”和“ctx”。“组件”是页面组件,“ctx”是上下文。如果页面的getInitialProps()方法返回任何数据,我们从我们的 HOC 的getInitialProps()方法返回该数据,否则我们返回一个空对象。
然后我们写我们的 HOC 的render()方法。在 Props 对象中我们已经有了 Component 和 PageProps。Component 是正在呈现的页面的页面组件,PageProps 是我们在该页面中拥有的属性。因为我们将我们的 HOC 封装在一个 Redux 包装器中,所以当执行getInitialProps()方法时,Store 对象也被创建并传递给render()方法。我们已经指定了创建初始存储的方法。同样的将被用来创建商店,并通过它到特设属性。我们将使用析构语法从 Props 对象中获取 Component、pageProps 和 store 的值。
我们将使用“react-redux”库提供的<Provider>组件来包装我们的页面组件。我们将把 Store 对象传递给这个组件,它将对我们所有的容器组件可用。在这个组件中,我们将放置页面组件,并将页面属性传递给它。页面组件将根据所呈现的页面动态地保持变化。
这就是如何使用高阶组件(HOC)将 Redux 注入下一个. js 生命周期的方法。现在让我们在索引页面中使用我们的商店,并在点击按钮时发送一个动作。您必须对“index.js”文件进行以下修改:
Pages/index.js
import React from "react";
import "../style.css";
import { connect } from 'react-redux'
import { bindActionCreators } from 'redux'
import { changeState } from '../Actions/actions'
class ReduxDemo extends React.Component {
render() {
return (
<div>
<h1>Redux Demo</h1>
<br />
<div className="center">
<input id="inputTextbox" type="text">
</input>
<button type="button"
onClick={this.props.changeState}>
Update Label
</button>
</div>
<br />
<p>{this.props.text}</p>
</div>
);
}
}
const mapDispatchToProps = dispatch => {
return {
changeState: bindActionCreators(changeState, dispatch)
}
}
export default connect((state) => ({ text: state.text }), mapDispatchToProps)(ReduxDemo)
你一定已经注意到我们已经从代码中移除了getInitialProps()方法。这是因为属性现在将使用由“react-redux”库提供的特殊“connect()()”包装器来注入。第一个参数包含要注入到页面中的与状态相关的实体,第二个参数是页面本身。我们正在注入驻留在状态对象中的文本属性。我们还注入了用于调度修改 Store 对象的操作的方法。我们通过使用“this.props.changeState”在页面中访问按钮,将该方法传递给按钮的 click 事件。我们还使用“this.props.text”将标签(
)绑定到“
”、“状态对象的属性”。如果您执行应用并访问浏览器,您将看到类似于图 3-14 的输出。以下是运行应用时发生的事情的顺序列表:
-
当浏览器第一次请求页面时,
"_App.js"中编写的代码被执行。 -
用初始值创建商店对象,并将其注入页面。
initStore()首次使用方法创建商店。我们已在“Store/store.js”中定义了该方法,并在特设中提供了参考。 -
"_App.js"中的 HOC 呈现页面组件。现在控制转移到“Pages/index.js”文件中编写的代码。 -
"
connect()()" wrapper 通过 props 将状态数据注入页面。更新状态值的方法也作为 props 传递。然后,页面就像普通的 Next.js 页面一样呈现出来。您将看到标签上显示的初始状态值。 -
只要你输入一些文本并点击按钮,就会调用“Actions/action.js”中定义的
changeState()方法。 -
该方法将使用您在输入框中输入的文本作为数据来调度类型为"
ChangeLabel"的操作。 -
现在,控制将被转移到写在“
Reducers/reducer.js”的 Reducer 方法。在检查了动作的类型之后,Reducer 将使用动作分派的数据更新 State 中的“text”属性。 -
一旦状态对象被更改,React 将重新呈现视图,并更新其数据已被修改的所有字段。因此,UI 上绑定到状态的“text”属性的标签将被重新呈现,您将在 UI 上看到更新后的值。
注意,我们没有在页面中的任何地方直接使用 React 的内置状态对象。这里所有的状态管理都是由 Redux 完成的。这就是关于 Redux 的工作。现在让我们了解一下 GraphQL,以及如何在 Next.js 应用中使用它。
将 GraphQL 与 Next.js 一起使用
GraphQL 是 API 的查询语言。它给了客户确切地提出要求的权力。我们可以向 API 发送一个 GraphQL 查询,并向服务器传达我们在响应中需要的确切字段。请看图 3-15 以便更好地理解。
图 3-15
GraphQL 查询变量
图 3-15 完美的描绘了 GraphQL 的概念。一个 API 可能返回多个参数,但是我们的应用可能不需要所有这些参数。在这种情况下,我们可以发送我们需要的参数名作为查询变量,API 将返回同样多的参数。
为了理解工作中的 GraphQL,我们必须首先在我们的应用中创建一个常规 API 并使用它。然后我们将看到如何将 GraphQL 与该 API 一起使用。Next.js 为在应用中构建 API 提供了一个简单的解决方案。“Pages/api”文件夹中的所有文件都被视为 api 端点,而不是页面,可以在“/API/∫”处使用。为了让 API 工作,您必须从您的文件中导出一个请求处理程序,它只是一个接受以下两个参数的函数:
-
req–传入请求的一个实例。您可以使用这个对象来标识请求类型、输入参数、请求头和生成请求的 URL 等。
-
RES–传出响应的实例。您可以使用此参数设置响应的状态代码、标头和数据。
让我们从一个只有索引页面的基本 Next.js 应用开始。我们将创建“Pages/api”文件夹,并添加我们的第一个 API“testapi . js”文件,代码如下:
页/API/test pi . js
const data = {
name: 'Jhon Doe',
address: '7th Avenue, Brooklyn',
contact: '099251456',
bloodgroup: 'A +ve',
favouriteSnack: 'Hotdog',
vehicle: 'Hyundai Tucson'
}
export default (req, res) => {
res.statusCode = 200
res.setHeader('Content-Type', 'application/json')
res.end(JSON.stringify(data))
}
我们在这里做的是定义一个静态数据对象并发送它作为响应。我们在响应的end()方法中传递这些数据。这个方法向服务器发出信号,表明已经设置了响应头和响应体,服务器应该认为这个响应是完整的。我们可以通过在浏览器中访问 URL“http://localhost:*/api/testapi”来使用这个 API。或者,我们可以在索引页面中使用这个 API。让我们使用下面的代码来实现它:
页/ 索引. js
import React from "react";
import axios from 'axios';
import "../style.css";
export default class extends React.Component {
static async getInitialProps() {
try {
const res = await axios.get('http://localhost:3000/api/testapi');
return { data: res.data, error: null }
} catch (e) {
return { data: ", error: e }
}
}
render() {
return (
<div>
<h1>Hello, world!</h1>
<table>
{Object.keys(this.props.data).map((key, index) => (
<tr key={index}>
<td>{key}:</td>
<td>{this.props.data[key]}</td>
</tr>
))}
</table>
</div>
);
}
}
我们使用页面的 getInitialProps()方法中的 Axios 库向 API 发出请求。然后,我们将 API 响应作为属性发送到页面的其余部分。在 render 方法中,我们迭代数据并将其呈现给浏览器。很简单。我们已经创建了一个 API 并使用它。您将看到类似于图 3-16 的输出。
图 3-16
Next.js API 响应
现在让我们考虑一个场景,我们只需要 API 中的姓名和地址字段。目前,您必须从 API 获取所有数据,然后在消费端缩小所需的字段。然而,使用 GraphQL 有一种更好的方法来处理这种情况。让我们使用以下命令将 GraphQL 库安装到我们的应用中:
npm install graphql --save
我们现在必须稍微修改一下我们的 API。我们将为我们的数据定义一个模式。该模式将指定我们将从 API 返回的数据类型。然后,我们将模式、GraphQL 查询和数据对象传递给 GraphQL,graph QL 将根据收到的查询为我们过滤数据。考虑以下 API 代码:
*页/API/*test pi . js
import { graphql, buildSchema } from 'graphql'
const schema = buildSchema(`
type Query {
name: String,
address: String,
contact: String,
bloodgroup: String,
favouriteSnack: String,
vehicle: String
}
`);
const data = {
name: 'Jhon Doe',
address: '7th Avenue, Brooklyn',
contact: '099251456',
bloodgroup: 'A +ve',
favouriteSnack: 'Hotdog',
vehicle: 'Hyundai Tucson'
}
export default async (req, res) => {
const response = await graphql(schema, req.body.query, data);
res.end(JSON.stringify(response.data))
}
是 GraphQL 提供的帮助我们构建模式的方法。它是我们的数据对象中的每个属性与其对应的数据类型的映射。我们将从请求中获取 GraphQL 查询。然后,我们将模式、查询和数据传递给 GraphQL,并等待过滤后的响应。最后,我们将把响应发送给用户。为了从消费端使用 GraphQL,您需要做的就是在您的 Axios 请求中添加一个“query”参数。将索引页中的 Axios 调用替换为以下内容:
...
const res = await axios.get('http://localhost:3000/api/testapi', { data: { query: `{ name, address }` }});
...
如果在所有的更改之后,您访问我们的应用的索引页面,您将看到类似于图 3-17 的输出。您会注意到只显示了两个字段,而不是之前显示的所有字段。这就是 GraphQL 在我们的应用中的作用。
图 3-17
GraphQL API 响应
这就是 GraphQL。随着这个话题的结束,我们来到本章的结尾。让我们总结一下我们所学到的东西。
摘要
-
Next.js 是一个帮助我们在服务器端呈现应用的框架。
-
它解决了诸如加载时间长、索引能力差以及与客户端渲染相关的安全漏洞等问题。
-
它提供了热重载、基于页面的路由、自动代码分割、页面预取、热模块替换和服务器端呈现等功能。
-
Next.js 提供了组件,帮助我们向应用添加链接。当我们导航到这样的链接时,不会对资源发出额外的服务器请求。这要归功于 Next.js 的客户端路由功能。
-
由于使用了历史 API,Next.js 中的客户端路由不会破坏浏览器的后退按钮。
-
我们应该在应用根目录的“static”文件夹中添加媒体文件。向我们的应用添加多媒体内容的最佳方法是在 CSS 文件中添加这些内容的 URL。
-
js 提供了 JSS(JS 中的 CSS ),允许我们直接在 JSX 代码中定义样式。我们将不得不在我们的应用中使用@zeit/next-css 库(或任何其他 css 加载器)来在单独的 CSS 文件中编写样式代码。
-
我们可以在页面中使用 getInitialProps()方法将属性传递给组件。我们可以在这个方法中调用 Axios API 来从远程服务器获取页面数据。作为 props 传递的数据可用于初始化构造函数中的状态对象。
-
Redux 可以在 Next.js 应用中使用,以便在应用级别管理状态。它使用动作、缩减器和存储来模仿 MVC 架构。
-
我们使用高阶组件(HOC)将 Redux 注入下一个. js 生命周期。
-
可以在 Next.js APIs 中使用 GraphQL,为客户端提供查询响应中所需字段的能力。
四、向 React 应用添加服务器端呈现
在前一章中,我们学习了如何使用 Next.js 框架创建服务器端应用。但是,我们可能希望创建一个部分在客户端呈现,部分在服务器端呈现的应用,这样我们就可以利用客户端呈现和服务器端呈现的优势。在本章中,我们将创建一个客户端渲染的 React 应用,并学习如何使用 Next.js 框架将服务器端渲染集成到应用中。在这个过程中,我们还将学习服务器端渲染的重要性,设计我们的应用,为我们的应用添加引导程序,以及其他一些主题。大多数情况下,我们将使用我们之前已经学过的东西来创建一个功能应用。
服务器端渲染的重要性
在前一章中,我们讨论了与客户端渲染相关的问题。在单页应用(SPA)的开发过程中面临的主要问题是。尽管 spa 提供了惊人的用户体验,但所有这些都是基于浏览器的。用户停留在同一个页面上,而不是从一个页面导航到另一个页面,并且页面上的内容基于用户交互而动态变化。这些更改是由浏览器使用客户端编写的 JavaScript 代码完成的。
虽然这是在应用启动并运行后更改页面的一个很好的方法,但对于第一次加载应用来说,这不是一个推荐的方法。在 SPA 可供用户交互之前,浏览器需要进行大量处理。此外,web 服务器和用户浏览器之间需要多次交互。正如您在图 4-1 中看到的,当用户第一次访问应用时,一个请求被发送到 web 服务器以获取页面。服务器返回一个 HTML 页面,其中包含一个空的根元素(< div >)和一些 JavaScript 代码。然后,浏览器发送更多对数据、样式表、脚本文件和其他可能需要的资源的请求。一旦所有的资源对浏览器可用,它就处理 JavaScript 代码,以确保 JSX 代码被正确编译,JSON 数据使用 REST API 调用被加载,所有的事件被绑定,承诺被履行。只有这样,用户才可以使用该页面。
图 4-1
单页应用的客户端呈现
Note
不要混淆用于客户端渲染的服务器和用于服务器端渲染的服务器。这里使用的 web 服务器可以称为“瘦服务器”。这是因为,在客户端呈现的情况下,所有的逻辑都是以 JavaScript 代码的形式编写的,这些代码将由浏览器处理。服务器充当纯数据 API,只是将 JavaScript 代码交付给浏览器。另一方面,在服务器端呈现的情况下,服务器处理所有的逻辑,并向浏览器提供一个随时可以呈现的 HTML 页面。
采用这种方法可能会花费大量的时间,并由于页面加载时的等待而导致糟糕的用户体验。这就是服务器端渲染介入的地方。我们可以在服务器端准备初始页面,并将其提供给用户的浏览器,然后浏览器可以轻松地下载页面并呈现它。这样,初始应用加载可以通过一个 web 请求来执行。在前一章中,我们已经学习了如何使用 Next.js 进行服务器端渲染。在本章的后面,我们将会看到如何使用客户端渲染和服务器端渲染来创建一个全功能的应用。让我们从创建一个简单的 React 应用开始。
构建一个简单的 React 应用
我们将创建一个简单的应用,在浏览器上显示时间。使用以下命令创建一个 starter React 应用:
npx create-react-app my-app
成功执行该命令后,导航到“my-app”文件夹,从“src”目录中删除除“index.js”文件之外的所有文件。用以下代码替换“index.js”文件中的代码:
import React from 'react';
import ReactDOM from 'react-dom';
ReactDOM.render(<h1>Hello from React.</h1>, document.getElementById('root'));
在使用“npm-start”命令执行应用时,您应该会看到“Hello from React”打印在您的浏览器窗口上。我们的 starter React 应用已经启动并运行。现在让我们在 React 组件的帮助下完成这项工作。
创建功能 React 组件
我们的组件文件将位于应用根目录下的“src/Components”文件夹中。让我们使用以下代码将“App.js”文件添加到“Components”文件夹中:
src/Components/App.js
import React from 'react';
function App(){
return(
<div>
<h1>Hello from React.</h1>
</div>
);
}
export default App;
我们还需要对“index.js”文件进行一些修改,以便呈现组件,而不是直接呈现 JSX 代码。按照以下代码更新“index.js”文件:
src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import App from './Components/App';
ReactDOM.render(<App/>, document.getElementById('root'));
如果您运行应用并访问浏览器,您应该会看到“React 的 Hello”印在窗户上。到目前为止,我们还没有在浏览器上显示时间。让我们使用 React 属性来实现这一点。
将属性传递给功能 React 组件
我们将简单地把当前时间作为属性传递给 App 组件。该组件将从 props 中获取值,并将其呈现给浏览器。这很简单。让我们通过如下方式修改代码来实现这一点:
src/Components/App.js
import React from 'react';
function App(props){
return(
<div>
<h1>Time: {props.time}</h1>
</div>
);
}
export default App;
src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import App from './Components/App';
ReactDOM.render(
<App time={new Date().toLocaleTimeString()}/>,
document.getElementById('root')
);
如您所见,我们将时间字符串作为属性传递给了组件,然后呈现给浏览器。然而,React 还不会更新 DOM,因为我们已经传递了一个静态时间字符串,React 还不知道时间何时改变。为了实现这个功能,我们将不得不编写一些 JavaScript 代码来随着实际时间的流逝更新存储在“props”中的时间。但是我们不能这样做,因为“属性”是只读的。因此,我们将不得不使用 React 生命周期提供的“状态”功能。
将功能组件转换为类组件
让我们将函数组件转换成类组件。一旦我们这样做了,我们将能够使用 React 的“state”属性来跟踪时间的变化。我们的应用的当前目录结构如图 4-2 所示。
图 4-2
当前目录结构
您必须对代码进行以下更改:
src/Components/App.js
import React from 'react';
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
time: new Date().toLocaleTimeString()
}
}
tick() {
this.setState(() => {
return ({
time: new Date().toLocaleTimeString()
});
});
}
componentDidMount() {
this.timer = setInterval(() => this.tick(), 1000);
}
componentWillUnmount(){
clearInterval(this.timer);
}
render() {
return (
<div>
<h1>Time: {this.state.time}</h1>
</div>
);
}
}
export default App;
src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import App from './Components/App';
ReactDOM.render(<App/>, document.getElementById('root'));
如您所见,我们不再将时间字符串作为属性传递给组件。我们在类组件的构造函数中将 state 属性设置为当前时间。然后我们使用 render 方法将其呈现给浏览器。这里唯一的区别是,我们现在从“状态”中获取值,而不是从“props”中获取值。使用“state”属性是必要的,因为正如我们前面讨论的,“props”是只读的,我们不能直接修改它们。
接下来,我们必须找到一种方法来随着时间的变化更新状态属性。为此,我们使用 JavaScript 的setInterval()方法创建了一个间隔。我们在 React 生命周期的componentDidMount()方法中这样做,是为了确保只有在组件被挂载到 DOM 时才设置时间间隔。
Note
如果你已经创建了一个函数组件,你可以使用 React 钩子来钩住 React 生命周期方法componentDidMount()。你可以回到这本书的第二章来看看 React hooks 的工作原理。
如果你已经创建了一个函数组件,你可以使用 React 钩子来钩住 React 生命周期方法componentDidMount()。
interval 每秒调用一次tick()方法,最终用新时间更新“state”对象。一旦状态被修改,React 就会重新渲染视图。因此,用户在浏览器上看到一个每秒滴答作响的数字时钟。我们可能希望清除在componentWillUnmount()方法中安装组件时创建的计时器,以避免在组件从 DOM 中移除时出现内存泄漏。
就是这样。我们的应用功能齐全。我们已经完全通过使用 React 的客户端渲染方法实现了这一点。让我们看看如何使用 Next.js 框架向该应用添加服务器端呈现。
使用 Next.js 进行服务器端呈现
为了使用 Next.js 框架进行服务器端渲染,我们必须使用以下命令将其安装到我们的应用中:
npm install next -–save
安装完成后,我们需要按如下方式更改“package.json”文件中的“scripts”部分:
...
"scripts": {
"start": "next",
"build": "next build",
"test": "echo \"Error: no test specified\" && exit 1"
}
...
如果您尝试启动该应用,您将遇到一个错误,指出没有找到“Pages”目录。我们必须创建它,并将我们的“app.js”文件添加到该目录,因为 Next.js 从那里加载页面。我们可以删除我们的“index.js”文件(它包含呈现组件的代码),因为 Next.js 会为我们处理呈现工作。我们也可以删除“src”和“public”文件夹,因为它们对我们没有用。为了简单起见,我们可以将“Pages”目录中的“app.js”文件重命名为“index.js ”,因为正如我们在前一章中了解到的,Next.js 遵循基于页面的路由,它在应用启动时查找“index.js”页面。现在,如果您启动应用,您将看到一个与使用客户端渲染创建的计时器相同的计时器。如果您想验证内容是否在服务器端呈现,您可以右键单击页面并查看页面源代码。您会注意到计时器的 HTML 代码的出现,而不是一个空的<div>标签。这告诉我们计时器不是使用客户端 JavaScript 生成的,而是在服务器端生成的。但是,页面并不是每秒都被重新加载。这意味着客户端的 React 正在处理状态的更新。这正是我们想要的,服务器端的初始应用呈现和客户端的其他 DOM 更改。现在我们的应用已经启动并运行了,让我们给它添加一些样式。
将 CSS 添加到 Next.js
正如上一章所讨论的,为了给 Next.js 应用添加样式,我们必须使用一个外部 CSS 加载器。让我们将"@zeit/next-css"模块安装到我们的应用中,它将作为 CSS 加载器。使用以下命令:
npm install @zeit/next-css --save
安装后,我们必须使用以下代码将配置文件“next.config.js”添加到应用的根目录中:
next.config.js
const withCSS = require('@zeit/next-css')
module.exports = withCSS({})
这个配置文件充当我们的应用的 webpack 配置的入口点,默认情况下,它被 Next.js 框架隐藏。配置完成后,我们可以为应用创建一个样式表,并将其与其他导入内容一起导入到页面中。考虑我在“Resources”文件夹中创建的以下样式表:
Resources/style.css
body{
margin-top: 45vh;
text-align: center;
}
h1{
display:inline-block;
border: 5px solid black;
border-radius: 10px;
padding: 10px;
}
我们的应用的当前目录结构如图 4-3 所示。
图 4-3
当前目录结构
我使用下面的语句将它导入到我们的索引页面:
import '../Resources/style.css'
如果您启动应用并访问浏览器,您将看到类似于图 4-4 的输出。
图 4-4
使用 Next.js 和 React 的数字时钟
我们还可以在应用中添加 Bootstrap,以提高应用的响应能力。让我们看看如何做到这一点。
将 Bootstrap 集成到您的应用中
为了使用 bootstrap,我们必须首先使用以下命令将其安装到我们的应用中:
npm install --save bootstrap
成功执行该命令后,您将看到引导模块被添加到我们的“node_modules”文件夹中。因为我们已经安装了 Zeit CSS loader 并在我们的应用中配置了它,所以我们将能够直接在我们的页面中导入 bootstrap CSS 文件并使用 bootstrap 框架提供的类。请参考下面的代码,以了解它是如何完成的:
Note
如果您需要在应用中全局应用 CSS,您应该创建一个包装所有组件的高阶组件(HOC ),然后在 HOC 中导入 CSS 文件。这将省去您在创建的每个组件中导入 CSS 文件的麻烦。我们在前一章中学习了如何创建一个 HOC,同时学习了 Redux 中的 Reducers。如果你不记得了,你可以回去看看。
Pages/index.js
import React from 'react';
import 'bootstrap/dist/css/bootstrap.min.css';
class App extends React.Component {
constructor(props) {
super(props);
...
}
tick() {
...
}
componentDidMount() {
...
}
componentWillUnmount() {
...
}
render() {
return (
<div>
<div className="jumbotron text-center">
<h1>Digital Clock with React, Next.js, and Bootstrap</h1>
</div>
<div className="text-center">
<p>Time: {this.state.time}</p>
</div>
</div>
);
}
}
export default App;
我们已经从页面中完全删除了自定义样式表“style.css ”,并替换为引导样式表。我们使用了 bootstrap 中的两个类——“jumbotron ”,它允许我们为应用定义一个标题部分,以及“text-center ”,它确保内容居中对齐。前面代码的输出应该类似于图 4-5 。
图 4-5
带自举的数字钟
就这样,我们到了这一章的结尾。您可以向应用添加更多的页面,在它们之间建立链接,并使用引导类。你探索得越多,你学到的就越多。让我们总结一下本章所学的内容。
摘要
-
第一次加载单页面应用时,服务器端呈现非常有用。由于等待时间更短,它带来了更好的用户体验。后续的 DOM 更改可以在客户端进行。
-
可以使用函数组件创建一个简单的客户端 React 应用。我们可以将属性传递给组件,然后组件可以将属性数据呈现给浏览器窗口。
-
由于属性是只读的,我们不能直接修改它们。如果我们在处理页面生命周期中需要修改的属性,更好的方法是使用 React 的状态对象。
-
componentDidMount()是一个 React 生命周期方法,当类组件安装到 DOM 时,它可以用来触发事件。在我们的例子中,我们使用这个方法以每秒更新的时间来修改状态对象。
-
为了将渲染转移到服务器端,我们使用 Next.js 框架。
-
因为 Next.js 负责组件的渲染,所以我们不需要担心这个问题。我们可以简单地将我们的客户端代码移动到 Next.js 页面,它将呈现在服务器端。
-
我们可以使用外部加载器在应用中添加自定义 CSS 和引导程序。
五、使用 Jest 的单元测试
在前几章中,我们学习了如何使用 React 和 Next.js 等库创建 web 应用。现在我们知道了如何使用这些库开发应用。接下来呢?
一旦开发了一个应用,知道它按预期工作是很重要的。为了做到这一点,我们可以编写自动化单元测试来验证我们的应用的每个组件都恰当地完成了它的工作。这正是我们在本章要学习的内容。我们将使用 Jest 框架在 React 应用上执行单元测试。
Note
作为一名开发人员,编写单元测试可能看起来交付了非常小的价值,同时给已经很紧张的时间表增加了很多工作。然而,从长远来看,当您的应用规模扩大时,它将帮助您减少工作量,提供一种非常有效的方法来检测代码中的错误和漏洞。
市场上还有很多其他的 JavaScript 测试框架,比如 Mocha 和 Jasmine。然而,由于 Jest 框架越来越受欢迎和实用,我们将使用它。我们将学习如何在我们的应用中安装和设置 Jest,然后我们将创建一个基本的单元测试来熟悉 Jest 的概念,最后,我们将学习有助于我们测试 React 组件的匹配器和酶。让我们从建立 Jest 框架开始。
设置笑话
Jest ( https://jestjs.io/ )是一个 JavaScript 测试框架,我们将使用它来测试 React 应用。让我们从创建应用目录“jest-testing-app”开始。导航到目录并执行“npm init”命令,在目录中创建一个 package.json 文件。确保系统中安装了该节点,以便该命令能够运行。一旦成功执行,您将看到一个包含以下代码的“package.json”文件:
{
"name": "jest-testing-app",
"version": "1.0.0",
"description": "My Jest Application",
"main": "index.js",
"scripts": {
"test": "jest"
},
"author": "Mohit Thakkar",
"license": "ISC"
}
如果您在初始化期间指定了一组不同的值,这些值可能会有所不同。因为我们将使用 jest 进行测试,所以请确保您将“Jest”指定为“scripts”部分中“test”属性的值。我们现在将使用以下命令将 Jest 安装到我们的应用中:
npm install jest --save
安装后,您将看到以下依赖项部分被添加到您的“package.json”文件中:
"dependencies": {
"jest": "²⁴.9.0"
}
就是这样。Jest 现已成功安装。让我们使用 Jest 编写我们的第一个测试。
使用 Jest 编写您的第一个测试
让我们首先创建一个包含一些基本函数的简单 JavaScript 文件。我用下面的代码在应用的根目录下添加了一个名为“functions.js”的文件:
functions.js
const functions = {
add: (n1, n2) => n1 + n2
}
module.exports = functions
这个文件包含一个简单的“add”函数,它接受两个数字作为输入,并返回它们的和作为输出。注意,我们使用简单的 JavaScript 语法将函数列表导出为一个模块。避免使用 ES6 语法,因为 Jest 希望文件在导入时是普通的 JavaScript。现在我们已经创建了一个 JavaScript 函数,让我们使用 Jest 来测试它。我们将在“tests”目录中添加我们的测试文件。将测试文件命名为您正在测试的相同的 JavaScript 文件是一个很好的实践,带有".test.js"后缀。考虑下面的测试文件,它包含测试“add”功能的代码:
tests/function.test.js
functions = require('../functions.js')
test('Test Add Function',()=>{
expect(functions.add(2,3)).toBe(5)
})
就是这样。我们已经使用 Jest 框架创建了我们的第一个测试。让我们理解这里发生了什么:
-
在测试代码中,我们简单地调用“
test()”函数,它接受两个输入参数——第一个是测试的描述,第二个是实际的测试函数。 -
在测试函数中,我们使用了"
expect()"函数,它接收我们正在测试的函数并对其求值,在我们的例子中,是" add()"函数。 -
我们使用 JavaScript 的"
require()"方法从"functions.js"导入函数列表,因为为了调用"add()"函数,我们必须从定义它的文件中导入它。 -
我们在“
expect()”函数上使用一个匹配器,在本例中是“toBe()”函数,以便将评估值与期望值进行比较。我们将期望值作为输入参数传递给匹配器。我们将在下一个主题中学习更多关于匹配器的知识。
使用以下命令运行测试:
npm test
成功执行命令后,您将在终端中看到测试执行的摘要,如图 5-1 所示。
图 5-1
使用 Jest 的首次测试(成功)
Note
在一个文件中编写多个测试是可能的。
“测试套件”表示测试文件的数量,而“测试”表示这些文件中测试的组合数量。在前面的例子中,如果我们将期望值更改为其他值,比如说“4”,测试执行将会失败。让我们试试。考虑对“function.test.js”文件的以下更改:
tests/function.test.js
functions = require('../functions.js')
test('Test Add Function',()=>{
expect(functions.add(2,3)).toBe(4)
})
现在,如果您运行“npm test”命令,您将在终端中看到测试执行失败。如图 5-2 所示,您还会看到测试执行失败的原因,在这种情况下,接收值不等于预期值。您还将在测试执行总结中看到接收值和期望值。
图 5-2
使用 Jest 的首次测试(失败)
既然我们已经学习了如何使用 Jest 为 JavaScript 函数编写测试,那么让我们更深入地研究一下,了解一下可以用来测试代码的不同匹配器。
匹配项
匹配器是 Jest 使用的函数,用于将评估值与期望值进行比较。在前面的例子中,我们使用了 Jest 提供的“toBe()”匹配器。注意,我们使用“expect()”函数来计算实际值。这个函数返回一个期望对象,我们在这个对象上调用我们的匹配器函数,将它与期望值进行比较。让我们看看 Jest 提供的所有匹配器。
常见匹配器
以下是一些非常常用的通用匹配器:
-
toBe(expected value)–这是完全相等的匹配器。它检查“expect()”函数返回的值是否与“expectedValue”完全匹配。
-
to equal(expected value)–这类似于“toBe()”匹配器,只是它用于比较对象的值。它递归地检查对象的每个属性。
让我们看一个例子来理解工作中常见的匹配器。考虑对“functions.test.js”文件进行以下更改:
*tests/*functions . test . js
functions = require('../functions.js')
test('toBe Demo',()=>{
expect(functions.add(2,3)).toBe(5)
})
test('toEqual Demo',()=>{
var data = {name:'Mohit'}
data['country'] = 'India'
expect(data).toEqual({
name:'Mohit',
country:'India'
})
})
为了演示"toBe()"匹配器的效用,我们使用了与上一个例子中测试的"add()"函数相同的函数。该函数返回“5”,并且“toBe()”匹配器断言它为真,因为它是我们所期望的值。
对于“toEqual()”匹配器,我们定义了一个新的测试“toEqual Demo”。我们用一个属性定义一个“数据”对象,然后向该对象添加一个新属性。我们现在将“数据”对象传递给“expect()”函数,并使用“toEqual()”匹配器将它与预期的输出进行比较。由于两个值匹配,Jest 将断言测试为真。上例的输出应该类似于图 5-3 。
图 5-3
笑话中常见的媒人
如果您想尝试更多的场景,您可以更改前面示例中的期望值,并注意到测试失败了。
Note
如果您使用的是 Visual Studio 代码编辑器,则可以通过“Orts”来使用“Jest”扩展。它为 Jest 提供了 IntelliSense,对于调试您编写的测试也非常有帮助。
真理匹配者
这些匹配器允许您检查评估值是 null、未定义、已定义、true 还是 false。您不需要向这些匹配器传递任何输入参数:
-
tobe null()–匹配空值
-
tobe undefined()–匹配未定义的值
-
tobe defined()–匹配未定义的值
-
tobe truthy()–匹配评估为 true 的值
-
toBeFalsy()–匹配评估为 false 的值
让我们看一个例子来理解真理匹配器的工作。以下新测试需要添加到“functions.test.js”文件中:
tests/functions.test.js
...
test('truth of null', () => {
const n = null;
expect(n).toBeNull();
expect(n).toBeDefined();
expect(n).not.toBeUndefined();
expect(n).not.toBeTruthy();
expect(n).toBeFalsy();
});
test('truth of zero', () => {
const n = 0;
expect(n).not.toBeNull();
expect(n).toBeDefined();
expect(n).not.toBeUndefined();
expect(n).not.toBeTruthy();
expect(n).toBeFalsy();
});
...
在前面的例子中,我们编写了两个新的测试,一个测试“null”的真实性,另一个检查数字零的真实性。null 值应评估为 null、defined 和 not true。另一方面,数字 0 应该计算为 not null、defined 和 false。如果您使用除零以外的任何数字,它的计算结果应该为 true。
请注意,我们使用了“not”关键字来否定某些匹配器。因此,如果表达式的计算结果为“false”,我们可以使用“not”关键字和“toBeTruthy()”匹配器来断言它。上例的输出应该类似于图 5-4 。
图 5-4
玩笑中的真理匹配者
比较匹配器
这些匹配器允许您将实际值与另一个值进行比较。要与实际值进行比较的值将作为输入参数传递给比较匹配器:
-
toBeGreaterThan(value)–如果实际值大于提供的值,则置位。
-
toBeGreaterThanOrEqual(value)–如果实际值大于或等于提供的值,则置位。
-
tobe less than(value)–如果实际值小于规定值,则置位。
-
tobelesthanorequal(value)–如果实际值小于或等于提供的值,则置位。
-
tobe closeto(value)–如果实际值接近提供的值,则断言。这是在处理浮点值时专门使用的。在这种情况下,期望值和实际值的精度可能不同,因此“
toBe()”匹配器(精确相等)将不起作用。
为了理解工作中的比较匹配器,让我们看一个例子。以下是添加到“functions.test.js”文件中的新测试:
tests/functions.test.js
...
test('comparison', () => {
const value = 4 + 0.2;
expect(value).toBeGreaterThan(3);
expect(value).toBeGreaterThanOrEqual(3.5);
expect(value).toBeLessThan(5);
expect(value).toBeLessThanOrEqual(4.5);
expect(value).toBeCloseTo(4.2);
});
...
前面示例中的实际值将计算为“4.2”。使用比较匹配器将它与多个值进行比较。请注意,为了断言确切的值,我们使用了“toBeCloseTo()”匹配器,而不是“toBe()”匹配器,这可能是因为精度上的差异。上例的输出应该类似于图 5-5 。
图 5-5
玩笑中的比较匹配器
字符串匹配
此匹配器用于将实际值与正则表达式进行比较:
- to match(regex)–断言计算出的字符串是否与提供的正则表达式匹配
考虑以下示例:
tests/functions.test.js
...
test('String Matcher', () => {
expect('Mohit is a Developer').toMatch(/Mohit/);
});
...
前面的测试断言子字符串“Mohit”存在于计算出的字符串中。输出应类似于图 5-6 。
图 5-6
笑话中的字符串匹配器
Iterables 匹配器
这个匹配器用于检查一个项目是否存在于一个 iterable 中,比如一个列表或一个数组:
- to contain(item)–如果计算的 iterable 包含提供的项目,则断言
考虑以下示例:
tests/functions.test.js
...
const countries = [
'India',
'United Kingdom',
'United States',
'Japan',
'Canada',
];
test('Matcher for Iterables', () => {
expect(countries).toContain('India');
expect(new Set(countries)).toContain('Canada');
});
...
在前面的测试中,我们定义了一个状态列表,并使用“toContain()”匹配器来检查“印度”是否出现在列表中。我们还将列表转换为不同的 iterable,即集合,并检查“Canada”是否出现在新的集合中。两个匹配器都应该断言 true。输出应类似于图 5-7 。
图 5-7
笑话中可重复的匹配者
Matcher 异常
此匹配器用于断言在评估特定代码段时是否引发了特定异常:
- to throw(expected exception)–如果被评估的代码段抛出给定的异常,则断言
为了测试这个匹配器,我们将回到我们的“function.js”文件并定义一个抛出错误的函数。然后,我们将在“functions.test.js”文件中添加一个测试,它将调用函数并断言异常。考虑以下示例:
function.js
const functions = {
add: (n1, n2) => n1 + n2,
invalidOperation: () => {
throw new Error('Operation not allowed!')
}
}
module.exports = functions
tests/functions.test.js
functions = require('../functions.js')
...
test('Exception Matcher', () => {
expect(functions.invalidOperation)
.toThrow(Error);
expect(functions.invalidOperation)
.toThrow('Operation not allowed!');
expect(functions.invalidOperation)
.toThrow(/not allowed/);
});
...
在前面的示例中,我们调用了抛出错误的函数,并使用“toThrow()”匹配器将计算值与预期值进行匹配。请注意,我们可以将其与一般的错误对象、错误返回的特定字符串或正则表达式进行比较。上例的输出应该类似于图 5-8 。
图 5-8
玩笑中的异常匹配器
就是这样。我们已经介绍了大多数常用的笑话匹配器。现在让我们学习如何使用我们到目前为止所学的知识来测试我们的 React 组件。
使用 Jest 和酶测试 React 组分
为了测试 React 组件,我们必须首先创建一个 React 组件。让我们使用以下命令创建一个 starter React 应用:
npx create-react-app react-jest-app
一旦创建了 starter 应用,您就可以删除所有不必要的文件。我已经删除了“src”文件夹中除“index.js”以外的所有文件,以及“public”文件夹中除“index.html”和“favicon.ico”以外的所有文件。我还清理了“index.html”文件。以下是代码,供您参考:
public/index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon"
href="%PUBLIC_URL%/favicon.ico" />
<title>React App</title>
</head>
<body>
<noscript>
You need to enable JavaScript to run this app.
</noscript>
<div id="root"></div>
</body>
</html>
现在我们已经清理了我们的 starter 应用,让我们将列表组件添加到“src”文件夹中。考虑以下代码:
*src/*list . js
import React from 'react';
function List(props) {
const { items } = props;
if (!items.length) {
return(
<span className="empty-message">
No items in list
</span>;
);
}
return (
<ul className="list-items">
{items.map(item =>
<li key={item} className="item">{item}</li>
)}
</ul>
);
}
export default List;
前面的代码是一个简单的函数组件,它从 props 中获取项目,并将它们显示为一个列表。既然我们的组件已经创建,我们可能希望指示“index.js”文件在浏览器上呈现它。考虑“index.js”文件的以下代码:
*src/*index . js
import React from 'react';
import ReactDOM from 'react-dom';
import List from './List';
const data = ['one', 'two', 'three']
ReactDOM.render(<List items={data} />, document.getElementById('root'));
如果您启动应用并访问浏览器窗口,您应该会看到类似于图 5-9 的输出。
图 5-9
使用 React 列出组件
现在我们已经创建了一个 React 组件,让我们学习如何测试它。让我们首先使用以下命令安装 Jest 框架:
npm install jest@24.9.0 --save
Note
我们已经安装了一个特定版本的 Jest 框架。这是因为使用“create-react-app”命令初始化的应用依赖于这个版本的 Jest。如果您的 Jest 框架版本与所需的版本不匹配,您将在应用启动期间得到一个错误,指出您需要的版本。您可以通过安装应用所需的版本来解决该错误。
在安装 Jest 框架之后,您还必须在“package.json”文件中添加测试脚本,如下所示:
package.json
{
...
"scripts": {
...
"test": "jest",
...
},
...
}
在测试简单的 JavaScript 函数时,我们习惯于简单地调用测试中的函数,并使用 Jest 匹配器将评估值与期望值进行比较。但是您可能想知道在 react 组件的情况下应该做什么,因为我们不能仅仅调用一个组件。
我们将完全按照 React 所做的去做。我们将组件呈现在 DOM 上,但它不是实际的浏览器 DOM;它将是由一个叫做 Enzyme 的框架创建的一个代表性的 DOM。这个框架帮助我们模拟运行时环境,以便我们可以测试我们的组件。让我们使用以下命令安装 Enzyme framework 的依赖项:
npm install enzyme enzyme-adapter-react-16 –save
请注意,我们还安装了一个适配器以及与我们正在使用的 React 版本相对应的酶框架。在使用这个框架之前,我们必须在 Enzyme 中配置这个适配器。为此,我们将在应用的根目录下创建一个“enzyme.js”文件,并向其中添加以下配置代码:
enzyme.js
import Enzyme, { configure, shallow, mount } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
configure({ adapter: new Adapter() });
export { shallow, mount };
export default Enzyme;
在前面的代码中,我们从酶框架中导入“酶”和“配置”,从酶适配器中导入“适配器”。然后,我们使用酶框架提供的“configure()”方法为我们将要使用的酶实例设置适配器。配置完 Enzyme 后,我们只需导出它。我们还从酶框架中导入 shallow 和 mount,并按原样导出它们。这些是我们将用来呈现 React 组件进行测试的方法。最后,我们测试所需的酶的所有实体都将从文件“enzyme.js”中导入,而不是直接从框架中导入。如果您尝试直接从 Enzyme framework 安装文件夹导入模块,您可能会遇到错误,因为它们没有配置适配器。
现在一切都配置好了,让我们为列表组件编写测试。让我们将测试写在“src”文件夹中的“List.test.js”文件中。请参考以下代码:
src/??【list . test . js
import React from "react";
import { shallow, mount } from '../enzyme';
import List from './List';
test('List Component Test', () => {
const items = ['one', 'two', 'three'];
const wrapperShallow = shallow(<List items={items} />);
const wrapperFull = mount(<List items={items} />);
console.log(wrapperFull.debug());
expect(wrapperShallow.find('.list-items'))
.toBeDefined();
expect(wrapperShallow.find('.item'))
.toHaveLength(items.length);
expect(wrapperFull.find('.list-items'))
.toBeDefined();
expect(wrapperFull.find('.item'))
.toHaveLength(items.length);
})
请注意,我们使用了两种不同的方法来呈现我们的组件-浅层和挂载。让我们了解一下两者的区别。顾名思义,“shallow()”方法将呈现范围限制在指定的组件上,不呈现其子组件。另一方面,“mount()”方法呈现整个组件树。在这种情况下,我们没有任何子组件,因此两种情况下的渲染是相同的。
我们将三个项目传递给组件进行呈现。其余的语法类似于我们测试 JavaScript 函数的语法。“shallow()”和“mount()”方法返回一个 React 包装器。如果您想查看包装器包含的内容,您可以调用包装器上的“debug()”方法,并将输出记录到控制台,就像我们在前面的代码中所做的一样。您可以在前面测试的输出中看到,如图 5-10 所示,我们组件的整个 HTML 呈现都记录在控制台上。我们可以在这个包装器上使用“find()”方法来查找呈现的代码中的元素。我们可以将多种选择器传递给“find()”方法。这应该在"expect()"方法中完成,这样我们就可以为断言使用匹配器。在前面的例子中,我们使用了类选择器来评估和断言列表项的存在和长度。以下是您可以使用的一些其他选择器:
图 5-10
使用 Jest 测试列表组件
ID 选择器
wrapper.find('#item1')
标签和类别的组合
wrapper.find('div.item')
标签和 ID 的组合
wrapper.find('div#item1')
属性选择器
wrapper.find('[htmlFor="checkbox"]')
如果您使用“npm test”命令执行测试,您可能仍然会在渲染时收到有关意外标记的错误。这是因为 Enzyme 不理解我们提供给“shallow()”和“mount()”方法的 JSX 代码。我们将不得不安装和配置一个巴别塔变压器插件,将为我们转换 JSX 代码。使用以下命令安装它:
npm install babel-plugin-transform-export-extensions --save
还有,我们需要创造一个”。babelrc "文件,并提供以下配置:
.babelrc
{
"env": {
"test": {
"presets": ["@babel/preset-env",
"@babel/preset-react"],
"plugins": ["transform-export-extensions"],
"only": [
"./∗∗/∗.js",
"node_modules/jest-runtime"
]
}
}
}
如果您在安装和配置 babel transform 插件后运行测试,测试应该会成功运行,并且输出应该类似于图 5-10 。
就是这样。我们已经使用 Jest 和酶成功测试了我们的 React 组件。随着这个话题的结束,我们来到本章的结尾。
让我们总结一下我们所学到的东西。
摘要
-
Jest 是一个测试框架,可以用来测试使用 JavaScript 构建的应用。
-
用“. test.js”作为测试文件的后缀是一个很好的习惯。
-
测试时,“expect()”方法用于评估 JavaScript 函数或指定需要测试的值。
-
Jest 提供了各种匹配器,可以在“expect()”方法上使用这些匹配器来断言计算值是否与期望值匹配。
-
由于 React 组件不能像函数一样被直接调用,我们将不得不使用 Enzyme 框架,该框架为我们提供了在为测试而创建的代表性 DOM 上呈现组件的功能。
-
Enzyme 框架提供了两种主要的方法来呈现组件——shallow()和 mount()。
-
我们可以使用带有“find()”方法的选择器来查找呈现组件中的特定内容。