React 项目第二版(一)
原文:
zh.annas-archive.org/md5/54872467feccdd4d547352263ad84982译者:飞龙
前言
本书将通过展示如何将基本和高级 React 模式应用于创建跨平台应用,帮助你将 React 知识提升到新的水平。React 的概念将以新开发者和老开发者都能理解的方式描述;虽然不需要有 React 的先验经验,但这会有所帮助。
在本书的 10 个章节中,你将使用 React 或 React Native 创建一个项目。这些章节中创建的项目实现了流行的 React 功能,如用于复用逻辑的 Hooks、用于状态管理的 context API 和 Suspense。对于路由,使用了流行的库,如 React Router 和 React Navigation;而 JavaScript 测试框架 React Testing Library 和 Cypress 用于编写应用的单元和集成测试。一些更高级的章节还涉及 GraphQL 服务器,并使用 Expo 帮助你创建 React Native 应用。
本书面向对象
本书面向希望探索 React 工具和框架以构建跨平台应用的 JavaScript 开发者。对 Web 开发、ECMAScript 和 React 的基本了解将有助于理解本书中涵盖的关键概念。
本书支持的 React 版本包括:
-
React - v18.0
-
React Native - v0.64
本书涵盖内容
第一章*,在 React 中创建单页应用*,将探讨构建可扩展的 React 项目的基石。将讨论如何组织文件、使用的包和工具的最佳实践,并在实践中学习。在构建单页应用的过程中,你将了解 React 架构。此外,webpack 和 Babel 用于编译代码。
第二章*,使用可复用组件和路由在 React 中创建投资组合*,将解释如何在整个应用程序中设置和复用 React 组件的样式。我们将构建一个 GitHub 卡应用来展示如何在 JavaScript 中使用 CSS 并复用组件和样式。此外,你还将学习如何使用 React Router v6 实现导航。
第三章*,构建动态项目管理板*,将介绍如何通过使用 Hooks 复用组件中的应用状态逻辑。你将学习如何构建自定义 Hooks 并与 Web API 交互以制作可拖拽组件。引入了 Styled Components,以便以可扩展的方式为 React 组件设置样式。
第四章*,使用 Next.js 构建服务器端渲染的社区动态*,将讨论路由,从设置基本路由、动态路由处理到如何为服务器端渲染设置路由。因此,在基于 Stack Overflow 构建应用程序的过程中,将使用 React 网络框架 Next.js。
第五章*,使用 Context 和 Hooks 构建个人购物清单应用程序*,将向您展示如何使用 React 上下文 API 与 Hooks 处理应用程序中的数据流。我们将创建一个个人购物清单,以了解如何使用 Hooks 和上下文 API 从父组件到子组件以及反之亦然访问和更改数据。
第六章*,使用 React Testing Library 和 Cypress 构建探索 TDD 的应用程序*,将重点关注使用断言和快照进行单元测试。你将学习如何管理测试覆盖率,并使用 Cypress 框架实现视觉集成测试。我们将构建一个酒店评论应用程序,以了解如何测试组件和数据流。
第七章*,使用 Next.js 和 GraphQL 构建全栈电子商务应用程序*,将使用 GraphQL 为应用程序提供后端。本章将向您展示如何使用 Next.js 设置全栈 React 应用程序,包括一个基本的 GraphQL 服务器。我们将构建一个电子商务应用程序,以了解如何创建服务器、向其发送请求以及处理身份验证。
第八章*,使用 React Native 和 Expo 构建动画游戏*,将讨论动画和手势,这是真正区分移动应用程序和 Web 应用程序的因素。本章将解释如何实现它们。此外,将通过构建一个具有动画和手势响应功能的卡片游戏应用程序来展示 iOS 和 Android 之间手势的差异。
第九章*,使用 React Native 和 Expo 构建全栈社交媒体应用程序*,将涵盖扩展和构建 React Native 应用程序,这与使用 React 创建的 Web 应用程序略有不同。本章将概述如何在构建全栈社交媒体应用程序的同时使用移动设备的原生 API,例如使用相机。我们将通过构建全栈社交媒体应用程序来检查 React Native 的最佳实践。
第十章*,使用 React 和 Three.js 创建虚拟现实应用程序*,将讨论如何通过创建一个允许用户在虚拟世界中四处张望并在其中创建组件的全景查看器来开始使用 React 和 Three.js。你将构建的应用程序将看起来像可以在虚拟现实(VR)中玩的游戏。
为了充分利用这本书
本书中的所有项目都是使用 React 或 React Native 创建的。本书的大部分章节需要具备 JavaScript 的先验知识。尽管本书描述了 React 和相关技术的所有概念,但我们建议您在需要了解更多关于某个功能时参考 React 文档。在下一节中,您可以找到有关为本书设置机器以及如何下载每章代码的一些信息。
对于本书中创建的应用程序,您需要在您的机器上至少安装 Node.js v14.19.1,以便运行 npm 命令。如果您尚未在您的机器上安装 Node.js,请访问nodejs.org/en/download/,在那里您可以找到 macOS、Windows 和 Linux 的下载说明。
安装 Node.js 后,请在您的命令行中运行以下命令以检查安装的版本:
-
对于 Node.js(应该是 v14.19.1 或更高版本):
node -v -
对于 npm(应该是 v6.14.14 或更高版本):
npm -v
如果您正在使用本书的数字版,我们建议您亲自输入代码或从本书的 GitHub 仓库(下一节中提供链接)获取代码。这样做将帮助您避免与代码复制和粘贴相关的任何潜在错误。
本书的内容使用的是截至 2022 年 4 月本书完成时的最新可用版本。在此日期之后的任何更新可能无法与本书中描述的功能兼容。建议您遵循官方的 React 和 React Native 文档以获取有关本书发布后发布的功能的更多信息。
下载示例代码文件
您可以从 GitHub 下载本书的示例代码文件github.com/PacktPublishing/React-Projects-Second-Edition。如果代码有更新,它将在 GitHub 仓库中更新。
我们还有其他来自我们丰富的图书和视频目录的代码包,可在github.com/PacktPublishing/找到。查看它们吧!
下载彩色图像
我们还提供了一份包含本书中使用的截图和图表彩色图像的 PDF 文件。您可以从这里下载:static.packt-cdn.com/downloads/9781801070638_ColorImages.pdf。
使用的约定
本书使用了多种文本约定。
文本中的代码:表示文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 昵称。以下是一个示例:“如果我们查看App.js中此组件的源代码,我们会看到return函数中已经有一个 CSS header元素。”
代码块设置如下:
.App-logo {
height: 40vmin;
pointer-events: none;
}
当我们希望您注意代码块中的特定部分时,相关的行或项目将以粗体显示:
import './Header.css';
- function Header() {
+ function Header({ logo }) {
return (
<header className='App-header'>
任何命令行输入或输出都应如下编写:
npx create-react-app chapter-2
粗体:表示新术语、重要单词或您在屏幕上看到的单词。例如,菜单或对话框中的单词以粗体显示。以下是一个示例:“从管理面板中选择系统信息。”
小贴士或重要注意事项
看起来像这样。
联系我们
我们始终欢迎读者的反馈。
一般反馈:如果您对本书的任何方面有疑问,请通过电子邮件发送至 customercare@packtpub.com,并在邮件主题中提及书名。
勘误:尽管我们已经尽最大努力确保内容的准确性,但错误仍然可能发生。如果您在这本书中发现了错误,我们将不胜感激,如果您能向我们报告,我们将不胜感激。请访问www.packtpub.com/support/err…并填写表格。
盗版:如果您在互联网上发现我们作品的任何非法副本,我们将不胜感激,如果您能提供位置地址或网站名称,我们将不胜感激。请通过 copyright@packt.com 与我们联系,并提供材料的链接。
如果您有兴趣成为作者:如果您在某个领域有专业知识,并且您有兴趣撰写或为书籍做出贡献,请访问authors.packtpub.com。
分享您的想法
一旦您阅读了《React Projects》,我们很乐意听到您的想法!请点击此处直接访问此书的亚马逊评论页面并分享您的反馈。
您的评论对我们和科技社区都至关重要,并将帮助我们确保我们提供高质量的内容。
第一章:使用 React 创建单页应用程序
当您购买这本书时,您可能已经听说过 React,甚至可能尝试过一些在线可找到的代码示例。这本书的构建方式使得每个章节中的代码示例的复杂度逐渐增加,因此即使您的 React 经验有限,如果您已经阅读了前一章节,每个章节都应该容易理解。到本书结束时,您将了解如何使用 React 及其稳定功能,直到版本 18,并且您还将拥有使用 GraphQL 和 React Native 的经验。
本章首先介绍如何基于流行的电视剧 Rick and Morty 构建单页应用程序;该应用程序将向我们提供从外部来源获取的角色信息。我们将应用 React 入门的核心概念,如果您有使用 React 构建应用程序的先前经验,这将容易理解。如果您之前没有使用过 React,也没有问题;本书描述了代码示例中使用的 React 功能。
在本章中,我们将涵盖以下主题:
-
设置新的 React 项目
-
项目结构
让我们开始吧!
项目概述
在本章中,我们将使用 React 创建一个单页应用程序,从 API 获取数据,并在浏览器中使用 Webpack 和 Babel 运行。样式将使用 Bootstrap 完成。您将构建的应用程序将展示关于流行电视剧 Rick and Morty 的信息,以及图片。
构建时间为 1 小时。
开始
本章的完整代码可以在 GitHub 上找到:github.com/PacktPublishing/React-Projects-Second-Edition/tree/main/Chapter01。
对于本书中创建的应用程序,您需要在您的机器上至少安装 Node.js v14.17.0,以便运行 npm 命令。如果您尚未在您的机器上安装 Node.js,请访问 nodejs.org/en/download/,在那里您可以找到 macOS、Windows 和 Linux 的下载说明。
安装 Node.js 后,请在您的命令行中运行以下命令以检查安装的版本:
-
对于 Node.js(应至少为 v14.17.0 或更高版本),请使用以下命令:
node -v -
对于 npm(应至少为 v6.14.3 或更高版本),请使用以下命令:
npm -v
此外,您还应该已经安装了 React 开发者工具插件(适用于 Chrome 和 Firefox)并将其添加到您的浏览器中。此插件可以从 Chrome 网上应用店(chrome.google.com/webstore)或 Firefox 插件(addons.mozilla.org)安装。
创建单页应用程序
在本节中,我们将从头开始创建一个新的单页 React 应用程序,从使用 Webpack 和 Babel 设置新项目开始。从头开始设置 React 项目将帮助您了解项目的基本需求,这对于您创建的任何项目都至关重要。
设置项目
每次创建一个新的 React 项目时,第一步是在您的本地机器上创建一个新的目录。由于这是您将要构建的单页应用程序的第一章,因此将此目录命名为chapter-1。
在这个新目录中,从命令行执行以下操作:
npm init -y
运行此命令将创建一个全新的package.json文件,其中包含运行 JavaScript/React 项目所需的最基本信息。通过在命令中添加-y标志,我们可以自动跳过设置名称、版本和描述等信息的过程。
运行此命令后,将为项目创建以下package.json文件:
{
"name": "chapter-1",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC"
}
注意
要了解更多关于package.json的工作原理,请确保阅读 npm 的文档:docs.npmjs.com/cli/v6/configuring-npm/package-json。
在本节创建package.json后,我们就可以添加 Webpack 了,我们将在下一节中这样做。
设置 Webpack
要运行 React 应用程序,我们需要安装 Webpack 5(在撰写本文时,Webpack 的当前稳定版本是版本 5)和 Webpack CLI 作为devDependencies。Webpack 是一个库,它允许我们创建一个由 JavaScript/React 代码组成的包,可以在浏览器中使用。以下步骤将帮助您设置 Webpack:
-
使用以下命令从 npm 安装所需的包:
npm install --save-dev webpack webpack-cli -
安装后,这些包将包含在
package.json文件中,我们可以在start和build脚本中运行它们。但首先,我们需要将一些文件添加到项目中:chapter-1 |- node_modules |- package.json + |- src + |- index.js
这将把index.js文件添加到一个名为src的新目录中。稍后,我们将配置 Webpack,使此文件成为我们应用程序的起点。
-
首先,必须将以下代码块添加到该文件中:
console.log('Rick and Morty'); -
要运行前面的代码,我们将使用 Webpack 将
start和build脚本添加到我们的应用程序中。在本章中不需要测试脚本,因此可以将其删除。此外,main字段可以更改为private,使用true值,因为我们正在构建的是本地项目:{ "name": "chapter-1", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" + "start": "webpack --mode development", + "build": "webpack --mode production" }, "keywords": [], "author": "", "license": "ISC" }
npm start命令将以开发模式运行 Webpack,而npm run build将使用 Webpack 创建一个生产包。最大的区别是,以生产模式运行 Webpack 将最小化我们的代码并减小项目包的大小。
-
现在,我们从命令行运行
start或build命令;Webpack 将启动并创建一个名为dist的新目录:chapter-1 |- node_modules |- package.json + |- dist + |- main.js |- src |- index.js -
在此目录中,将有一个名为
main.js的文件,它包含我们的项目代码,也被称为我们的包。如果成功,以下输出将可见:asset main.js 794 bytes [compared for emit] (name: main) ./src/index.js 31 bytes [built] [code generated] webpack compiled successfully in 67 ms
根据我们是否以开发或生产模式运行 Webpack,此文件中的代码将被最小化。
-
你可以通过从命令行运行你的包中的
main.js文件来检查你的代码是否工作:node dist/main.js
此命令运行我们应用程序的打包版本,并应返回以下输出:
> node dist/main.js
Rick and Morty
现在,我们能够从命令行运行 JavaScript 代码。在本节下一部分,我们将学习如何配置 Webpack 以使其与 React 一起工作。
配置 Webpack 以与 React 一起工作
现在我们已经为 JavaScript 应用程序设置了一个基本的开发环境,我们可以开始安装运行任何 React 应用程序所需的包。
这些包是 react 和 react-dom,前者是 React 的通用核心包,后者提供了浏览器 DOM 的入口点并渲染 React。通过在命令行中执行以下命令来安装这些包:
npm install react react-dom
仅安装 React 的依赖项不足以运行它,因为默认情况下,并非每个浏览器都能读取你的 JavaScript 代码所使用的格式(如 ES2015+ 或 React)。因此,我们需要将 JavaScript 代码编译成每个浏览器都能读取的格式。
为此,我们将使用 Babel 及其相关包创建一个工具链,以便在浏览器中使用 Webpack 运行 React。这些包可以通过运行以下命令作为 devDependencies 安装:
npm install --save-dev @babel/core babel-loader @babel/preset-env @babel/preset-react
除了 Babel 核心包之外,我们还将安装 babel-loader,这是一个辅助工具,使得 Babel 可以与 Webpack 一起运行,并安装两个预设包。这些预设包有助于确定哪些插件将被用来将我们的 JavaScript 代码编译成浏览器可读的格式(@babel/preset-env)以及编译 React 特定的代码(@babel/preset-react)。安装了 React 的包和正确的编译器后,下一步是让它们与 Webpack 一起工作,以便在运行我们的应用程序时使用它们。
要做到这一点,需要在项目的 src 目录中创建 Webpack 和 Babel 的配置文件:
chapter-1
|- node_modules
|- package.json
+ |- babel.config.json
+ |- webpack.config.js
|- dist
|- main.js
|- src
|- index.js
将 Webpack 的配置添加到 webpack.config.js 文件中,以使用 babel-loader:
module.exports = {
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader'
},
},
],
},
};
此文件中的配置告诉 Webpack 使用 babel-loader 处理所有具有 .js 扩展名的文件,并排除 node_modules 目录中的文件供 Babel 编译器使用。
要使用 Babel 预设,必须在 babel.config.json 文件中添加以下配置:
{
"presets": [
[
"@babel/preset-env",
{
"targets": {
"esmodules": true
}
}
],
[
"@babel/preset-react",
{
"runtime": "automatic"
}
]
]
}
@babel/preset-env 必须设置为 esmodules 以使用最新的 Node 模块。此外,定义 JSX 运行时为 automatic 也是必要的,因为 React 18 已经采用了新的 JSX 转换功能:reactjs.org/blog/2020/09/22/introducing-the-new-jsx-transform.html。
注意
babel-loader 的配置也可以放在 webpack.config.json 内部的配置中。但通过为这个创建一个单独的 Babel 配置文件,这些设置也可以被 JavaScript/React 生态系统中的其他工具使用。
现在我们已经设置了 Webpack 和 Babel,我们可以从命令行运行 JavaScript 和 React。在本节的下一部分,我们将创建我们的第一个 React 代码,并在浏览器中运行它。
渲染 React 项目
使用我们在前面的部分中安装和配置的包来设置 Babel 和 Webpack,我们需要创建一个实际的 React 组件,该组件可以被编译和运行。创建一个新的 React 项目涉及到向项目中添加一些新文件,并修改 Webpack 的设置:
-
让我们编辑
src目录中已经存在的index.js文件,以便我们可以使用react和react-dom。此文件的 内容可以替换为以下内容:import ReactDOM from 'react-dom/client'; function App() { return <h1>Rick and Morty</h1>; } const container = document.getElementById('app'); const root = ReactDOM.createRoot(container); root.render(<App />);
如您所见,此文件导入了 react 和 react-dom 包,定义了一个简单的组件,该组件返回一个包含您应用程序名称的 h1 元素,并且使用 react-dom 在浏览器中渲染此组件。代码的最后一行将 App 组件挂载到文档中具有 root ID 选择器的元素上,这是应用程序的入口点。
-
我们可以在一个名为
public的新目录中创建一个包含此元素的文件,并将其命名为index.html:chapter-1 |- node_modules |- package.json |- babel.config.json |- webpack.config.js |- dist |- main.js + |- public + |- index.html |- src |- index.js -
在此目录中添加一个名为
index.html的新文件后,我们在其中添加以下代码:<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta http-equiv="X-UA-Compatible" content="ie=edge" /> <title>Rick and Morty</title> </head> <body> <section id="root"></section> </body> </html>
这添加了一个 HTML 标题和正文。在 head 标签中是应用程序的标题,在 body 标签中是一个具有 "root" ID 选择器的部分。这与我们在 src/index.js 文件中挂载 App 组件的元素相匹配。
-
渲染我们的 React 组件的最终步骤是扩展 Webpack,以便在运行时将压缩后的包代码作为脚本添加到 body 标签中。因此,我们应该将
html-webpack-plugin包安装到我们的devDependencies中:npm install --save-dev html-webpack-plugin
要使用这个新包用 React 渲染我们的文件,webpack.config.js 文件中的 Webpack 配置必须被扩展:
+ const HtmlWebpackPlugin =
require('html-webpack-plugin');
module.exports = {
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
},
},
],
},
+ plugins: [
+ new HtmlWebpackPlugin({
+ template: './public/index.html',
+ filename: './index.html',
+ }),
+ ],
};
现在,如果我们再次运行 npm start,Webpack 将以开发模式启动,并将 index.html 文件添加到 dist 目录中。在这个文件中,我们会看到,在我们的 body 标签内插入了一个新的 scripts 标签,它将我们引导到我们的应用程序捆绑包——即 dist/main.js 文件。如果我们在这个浏览器中打开这个文件,或者从命令行运行 open dist/index.html,它将直接在浏览器中返回结果。当运行 npm run build 命令以启动生产模式下的 Webpack 时,我们可以做同样的事情;唯一的区别是我们的代码将被压缩:
图 1.1 – 在浏览器中渲染 React
通过设置带有 Webpack 的开发服务器,可以加快此过程。我们将在本节的最后部分完成此操作。
创建开发服务器
在开发模式下工作期间,每次我们更改应用程序中的文件时,都需要重新运行 npm start 命令。由于这有点繁琐,我们将安装另一个名为 webpack-dev-server 的包。这个包添加了在每次我们更改项目文件时强制 Webpack 重新启动的选项,并且它通过在内存中管理应用程序文件而不是构建 dist 目录来管理我们的应用程序文件。
可以使用 npm 安装 webpack-dev-server 包:
npm install --save-dev webpack-dev-server
此外,我们还需要编辑 package.json 文件中的 dev 脚本,使其使用 webpack-dev-server 而不是 Webpack。这样,每次代码更改后,你就不必重新编译并在浏览器中重新打开捆绑包:
{
"name": "chapter-1",
"version": "1.0.0",
"description": "",
"private": true,
"scripts": {
- "start": "webpack –mode development",
+ "start": "webpack serve –mode development",
"build": "webpack –mode production"
},
"keywords": [],
"author": "",
"license": "ISC"
…
}
上述配置用 webpack-dev-server 替换了 start 脚本中的 Webpack,它以开发模式运行 Webpack。这将创建一个本地开发服务器,运行应用程序,并确保每次更新任何项目文件时 Webpack 都会重新启动。
从命令行运行以下命令:
npm start
这将使本地开发服务器在 http://localhost:8080/ 上变得活跃,并且每次我们更新项目中的任何文件时,它都会刷新。
现在,我们已经为我们的 React 应用程序创建了基本开发环境,我们将在本章的下一节中进一步开发和结构化它。
结构化项目
在设置好开发环境后,是时候开始创建单页应用程序了。在前面的章节中,我们已经在项目中添加了新的目录。但让我们回顾一下项目的当前结构,其中项目根目录下的两个目录很重要:
-
第一个目录称为
dist,其中包含 Webpack 打包的应用程序输出。 -
第二个目录称为
src,包含我们应用程序的源代码。注意
在我们项目的根目录中还可以找到一个名为
node_modules的目录。这是放置我们使用npm安装的每个包的源文件的目录。建议您不要对此目录中的文件进行任何手动更改。
在以下子节中,我们将学习如何构建我们的 React 项目。这种结构将在本书的其余章节中也被使用。
创建新组件
尽管 React 的官方文档没有说明关于如何结构化我们的 React 项目的首选方法,尽管在社区中有两种常见的方法:要么按功能/页面结构化文件,要么按文件类型结构化。
本章中的单页应用程序将采用混合方法,首先按文件类型结构化文件,其次按功能结构化。在实践中,这意味着将有两种类型的组件:顶层组件,有时也称为容器,以及与这些顶层组件相关的低级组件。创建这些组件需要我们添加以下文件和代码更改:
-
实现这种结构的第一步是创建一个名为
components的新子目录,位于src目录下。在这个目录内,创建一个名为List.js的文件:chapter-1 |- node_modules |- package.json |- babel.config.json |- webpack.config.js |- dist |- main.js |- index.html |- public |- index.html |- src + |- components + |- List.js |- index.js
此文件将返回列出有关 Rick and Morty 所有信息的组件:
function List() {
return <h2>Characters</h2>;
}
export default List;
-
此组件应包含在我们的应用程序入口点中,以便它可见。因此,我们需要在
src目录下的index.js文件中包含它,并引用它:import ReactDOM fr'm 'react-dom/client'; + import List from './components/List'; function App() { - return <h1>Rick and Morty</h1>; + return ( + <div> + <h1>Rick and Morty</h1> + <List /> + </div> + ); }; // ...
如果我们仍然在运行开发服务器(如果不是,请再次执行 npm start 命令),我们会看到我们的应用程序现在在标题下方返回了 Characters 标题。
-
下一步是将组件添加到
List组件中,使其成为一个所谓的组合组件,即由多个组件组成的组件。这个组件将被命名为Character,并且也应该位于名为components的src子目录中。在这个目录内,创建一个名为Character.js的文件,并将以下代码块添加到其中:function Character() { return <h3>Character</h3>; }; export default Character;
如您从该组件的名称中可能猜到的,它将被用于稍后返回有关 Rick and Morty 中角色的信息。
-
现在,将此
Character组件导入到List组件中,并在h2元素之后返回此组件,用以下代码替换return函数:+ import Character from './Character'; function List() { - return <h2>Characters</h2>; + return ( + <div> + <h2>Characters</h2> + <Character /> + <Character /> + </div> + ); } export default List;
如果我们再次在浏览器中访问我们的应用程序 http://localhost:8080/,标题和标题下方将显示 Character 字样。
图 1.2 – 向 React 添加组件
从这个角度来看,我们无法看到浏览器中正在渲染哪些组件。但幸运的是,我们可以在浏览器中打开 React 开发者工具插件;我们会注意到,应用程序目前由多个堆叠的组件组成:
<App>
<List>
<Character>
在本节的下一部分,我们将利用我们对构建 React 项目的知识,创建新的组件来检索我们想要在这个单页应用程序中显示的 Rick and Morty 的数据。
检索数据
随着我们的开发服务器和项目结构都已设置,现在是时候向其中添加一些数据了。为此,我们将使用 Rick and Morty REST API (rickandmortyapi.com/documentation/#rest),该 API 提供了关于这部热门电视剧的信息。
可以使用 JavaScript 中的 fetch 方法检索来自 API 的信息,例如,我们的浏览器已经支持此方法。这些数据只会在顶层组件中检索,这意味着我们应该在 List 容器中添加一个 fetch 函数来检索和存储这些信息。
为了存储信息,我们将使用 React 中的内置状态管理 (reactjs.org/docs/state-and-lifecycle.html)。存储在状态中的任何内容都可以传递给低级组件,之后它们被称为 props。在 React 中使用状态的简单示例是通过使用 useState Hook,它可以用来存储和更新变量。每次使用 useState Hook 返回的 update 方法更改这些变量时,我们的组件都会重新渲染。
注意
自从 16.8.0 版本发布以来,React 采用了 Hooks 的概念,这些是 React 提供的方法,允许您在不使用类组件的情况下使用其核心功能。有关 Hooks 的更多信息,请参阅文档:reactjs.org/docs/hooks-intro.html。
在添加从 Rick and Morty REST API 检索数据的逻辑之前,让我们检查该 API 以查看将返回哪些字段。API 的基本 URL 是 rickandmortyapi.com/api。
此 URL 返回一个包含此 API 所有可能端点的 JSON 输出,这些端点都是 GET 请求,意味着只读,并且通过 https 工作。从此基本 URL 开始,我们将使用 /character 端点来获取 Rick and Morty 中角色的信息。此端点返回的所有信息都不会被使用;以下是我们实际上将使用的字段:
-
id(int): 角色的唯一标识符 -
name(string): 角色的名字 -
origin(object): 包含角色起源名称和链接的对象 -
image(string): 角色图像的链接,尺寸为 300 x 300 像素
在检索 Rick and Morty 的数据之前,需要准备 Character 组件以接收这些信息。为了显示关于 Rick and Morty 的信息,我们需要在 Character 组件中添加以下行:
- function Character() {
- return <h3>Character</h3>;
+ function Character(character) {
+ return (
+ <div>
+ <h3>{character.name}</h3>
+ <img src={character.image} alt={character.name}
width='300' />
+ <p>{'Origin: ${character.origin &&
character.origin.name}'}</p>
+ </div>
+ );
};
export default Character;
现在,可以通过从 React 中导入 useState 并将此 Hook 添加到 List 组件中来实现检索数据的逻辑,该组件将包含一个空数组作为字符的占位符:
+ import { useState } from 'react';
import Character from './Character';
function List() {
+ const [characters, setCharacters] = useState([]);
return (
// ...
要进行实际的数据检索,还需要导入另一个 Hook,即 useEffect Hook。这个 Hook 可以用来处理副作用,无论是当应用程序挂载时还是当状态或 prop 被更新时。这个 Hook 有两个参数,第一个是一个回调函数,第二个是一个包含此 Hook 所依赖的所有变量的数组——所谓的依赖数组。当这些依赖中的任何一个发生变化时,此 Hook 的回调函数将被调用。当这个数组中没有值时,Hook 将不断被调用。在从源获取数据后,状态将使用结果进行更新。
在我们的应用程序中,我们需要添加此 Hook 并从 API 检索数据,我们应该使用 async/await 函数,因为 fetch API 返回一个承诺。在检索数据后,state 应通过用字符信息替换数据中的空数组来更新:
- import { useState } from 'react';
+ import { useEffect, useState } from 'react';
import Character from './Character';
function List() {
const [characters, setCharacters] = useState([]);
+ useEffect(() => {
+ async function fetchData() {
+ const data = await fetch(
'https://rickandmortyapi.com/api/character');
+ const { results } = await data.json();
+ setCharacters(results);
+ }
+ fetchData();
+ }, [characters.length]);
return (
// ...
在 useEffect Hook 内部,将调用新的 fetchData 函数,因为建议不要直接使用 async/await 函数。当 characters 状态的长度发生变化时,Hook 仅调用从 API 检索数据的逻辑。你可以通过向应用程序添加一个 loading 状态来扩展此逻辑,这样用户就会知道数据仍在被检索:
function List() {
+ const [loading, setLoading] = useState(true);
const [characters, setCharacters] = useState([]);
useEffect(() => {
async function fetchData() {
const data = await fetch(
'https://rickandmortyapi.com/api/character');
const { results } = await data.json();
setCharacters(results);
+ setLoading(false);
}
fetchData();
}, [characters.length]);
return (
// ...
注意
我们之前使用 fetch 从 JSON 文件中检索信息的方法没有考虑到对这个文件的请求可能会失败。如果请求失败,loading 状态将保持为 true,这意味着用户将一直看到加载指示器。如果你想在请求不成功时显示错误消息,你需要将 fetch 方法包裹在一个 try...catch 块中,这将在本书的后面部分展示。
为了在应用程序中显示字符信息,我们需要将其传递给 Character 组件,它最终可以在我们第一步中更改的 Character 组件中显示。
当从 API 检索数据时,loading 状态为 true,所以我们还不能显示 Character 组件。当数据检索完成后,加载将变为 false,我们可以遍历 character 状态,返回 Character 组件,并将字符信息作为 props 传递。此组件还将获得一个 key prop,这是在迭代中渲染的每个组件所必需的。由于此值需要是唯一的,因此使用字符的 id,如下所示:
// ...
return (
<div>
<h2>Characters</h2>
- <Character />
- <Character />
+ {loading ? (
+ <div>Loading...</div>
+ ) : (
+ characters.map((character) => (
+ <Character
+ key={character.id}
+ name={character.name}
+ origin={character.origin}
+ image={character.image}
+ />
+ ))
+ )}
</div>
);
}
export default List;
如果我们再次在浏览器中访问我们的应用程序,我们会看到它现在显示了一个字符列表,包括一些基本信息和一张图片。在这个时候,我们的应用程序将类似于以下截图:
图 1.3 – 从本地状态渲染组件列表
如您所见,已经对应用程序应用了有限的样式,并且它只渲染从 API 获取的信息。在下一部分中,我们将使用名为 Bootstrap 的包添加样式。
添加样式
只显示角色信息是不够的。我们还需要对项目应用一些基本的样式。通过 Bootstrap 包添加样式到项目,该包根据类名对我们的组件添加样式。
可以使用以下命令从 npm 安装 Bootstrap 并将其添加到devDependencies中:
npm install --save-dev bootstrap
还需要将此文件导入我们的 React 应用程序的入口点src/index.js,这样我们就可以在整个应用程序中使用这些样式:
import ReactDOM from 'react-dom/client';
import List from './containers/List';
+ import 'bootstrap/dist/css/bootstrap.min.css';
function App() {
// ...
Webpack 无法自行编译 CSS 文件;我们需要添加适当的加载器来实现这一点。我们可以通过运行以下命令来安装这些加载器:
npm install --save-dev css-loader style-loader
我们需要将这些包作为规则添加到 Webpack 配置中:
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
},
},
+ {
+ test: /\.css$/,
+ use: ['style-loader', 'css-loader'],
+ },
],
},
plugins: [
new HtmlWebpackPlugin({
template: './public/index.html',
filename: './index.html',
}),
],
};
注意
添加加载器的顺序很重要,因为css-loader负责编译 CSS 文件,而style-loader将编译后的 CSS 文件添加到 React DOM 中。Webpack 从右到左读取这些设置,CSS 需要在附加到 DOM 之前进行编译。
现在应用程序应该在浏览器中正确运行,并且应该已经从默认的 Bootstrap 样式表中获取了一些小的样式更改。首先让我们对index.js文件进行一些更改,并将其样式设置为整个应用程序的容器。我们需要更改渲染到 DOM 中的App组件,并将List组件包裹在一个div容器中:
// ...
function App() {
return (
- <div>
+ <div className='container'>
<h1>Rick and Morty</h1>
<List />
</div>
);
};
const root = ReactDOM.createRoot(
document.getElementById('root'));
root.render(<App />);
在List组件内部,我们需要设置网格以显示Characters组件,这些组件用于显示角色信息。将map函数包裹在一个div元素中,将其视为 Bootstrap 的行容器:
// ...
return (
<div>
<h2>Characters</h2>
+ <div className='row'>
{loading ? (
<div>Loading...</div>
) : (
// ...
))
)}
+ </div>
</div>
);
}
export default List;
Character组件的代码也必须进行更改,以使用 Bootstrap 添加样式;您可以将该文件的当前内容替换为以下内容:
function Character(character) {
return (
<div className='col-3'>
<div className='card'>
<img
src={character.image}
alt={character.name}
className='card-img-top'
/>
<div className='card-body'>
<h3 className='card-title'>{character.name}</h3>
<p>{'Origin: ${character.origin &&
character.origin.name}'}</p>
</div>
</div>
</div>
);
};
export default Character;
这使我们能够使用 Bootstrap 容器布局,列大小为 3(getbootstrap.com/docs/5.0/layout/columns/),并将Character组件样式化为 Bootstrap 卡片组件(getbootstrap.com/docs/5.0/components/card/))。
为了添加最后的修饰,打开index.js文件并插入以下代码以添加一个标题,该标题将放置在我们的应用程序中瑞克和莫蒂角色列表的上方:
// ...
function App() {
return (
<div className='container'>
- <h1>Rick and Morty</h1>
+ <nav className='navbar sticky-top navbar-light
bg-dark'>
+ <h1 className='navbar-brand text-light'>
Rick and Morty</h1>
+ </nav>
<List />
</div>
);
// ...
确保开发服务器正在运行后,我们会看到应用程序已经通过 Bootstrap 应用了样式,这在浏览器中看起来如下:
图 1.4 – 使用 Bootstrap 样式化的我们的应用程序
Bootstrap 的样式规则已被应用到我们的应用中,使其看起来比之前更加完整。在本节的最后部分,我们将向项目中添加 ESLint 包,这将通过在整个项目中同步模式来简化我们代码的维护。
添加 ESLint
最后,我们将把 ESLint 添加到项目中,以确保我们的代码符合某些标准——例如,我们的代码遵循正确的 JavaScript 模式。
通过运行以下命令从 npm 安装 ESLint:
npm install --save-dev eslint eslint-webpack-plugin eslint-plugin-react
第一个包,名为 eslint,是核心包,帮助我们识别 JavaScript 代码中任何可能存在问题的模式。eslint-webpack-plugin 是一个由 Webpack 使用,每次我们更新代码时都会运行 ESLint 的包。最后,eslint-plugin-react 为 React 应用程序添加了特定的规则。
要配置 ESLint,我们需要在项目的根目录中创建一个名为 .eslintrc 的文件,并将以下代码添加到其中:
{
"env": {
"browser": true,
"node": true,
"es6": true
},
"parserOptions": {
"ecmaVersion": 2020,
"sourceType": "module"
},
"plugins": ["react"],
"extends": ["eslint:recommended",
"plugin:react/recommended"],
"rules": {
"react/react-in-jsx-scope": "off"
}
}
env 字段设置了代码实际运行的环境,并将使用其中的 es6 函数,而 parserOptions 字段为使用 jsx 和现代 JavaScript 添加了额外的配置。然而,有趣的地方在于 plugins 字段,这是我们指定代码使用 react 作为框架的地方。extends 字段用于使用 eslint 的推荐设置,以及 React 的特定框架设置。此外,rules 字段包含一个规则来禁用关于 React 未导入的通知,因为在 React 18 中这不再需要。
注意
我们可以通过运行 eslint --init 命令来创建自定义设置,但推荐使用前面的设置,以确保我们 React 代码的稳定性。
如果我们查看命令行或浏览器,我们将看到没有错误。然而,我们必须将 eslint-webpack-plugin 包添加到 Webpack 配置中。在 webpack.config.js 文件中,您需要导入此包并将其添加到配置中:
const HtmlWebpackPlugin = require('html-webpack-plugin');
+ const ESLintPlugin = require('eslint-webpack-plugin');
module.exports = {
// ...
plugins: [
new HtmlWebpackPlugin({
template: './public/index.html',
filename: './index.html',
}),
+ new ESLintPlugin(),
],
};
通过重启开发服务器,Webpack 现在将使用 ESLint 检查我们的 JavaScript 代码是否遵守 ESLint 的配置。在我们的命令行(或浏览器中的 控制台 选项卡),任何 React(或 JavaScript)功能的误用都将显示出来。
恭喜!您已经使用 React、ReactDOM、Webpack、Babel 和 ESLint 从零开始创建了一个基本的 React 应用程序。
摘要
在本章中,您从零开始创建了一个 React 单页应用程序,并学习了核心的 React 概念。本章从您使用 Webpack 和 Babel 创建新项目开始。这些库帮助您在浏览器中以最小的设置编译和运行您的 JavaScript 和 React 代码。然后,我们描述了如何构建一个 React 应用程序,这个结构将在整本书中使用。此外,您还学习了使用 React Hooks 进行状态管理和数据获取,以及使用 Bootstrap 进行基本样式设计。应用的原则为您提供了从零开始创建 React 应用程序并按可扩展方式构建它们的基础。
如果您之前使用过 React,那么这些概念可能并不难理解。如果您没有,那么请不要担心,如果某些概念让您感到陌生。接下来的章节将基于本章使用的功能,给您足够的时间完全理解它们。
在下一章中,您将构建的项目将专注于创建具有更高级样式的可重用 React 组件。这将离线可用,因为它将被设置为渐进式 Web 应用程序(PWA)。
进一步阅读
第二章: 使用可重用组件和路由创建 React 个人作品集
完成第一章后,你是否已经熟悉了 React 的核心概念?太好了!本章对你来说将不成问题!如果不熟悉,别担心——你在上一章中遇到的大多数概念都会被重复。然而,如果你想更多地了解 Webpack 和 Babel,建议你再次尝试在 第一章 中创建项目,即 创建 React 单页应用程序,因为本章不会涉及这些主题。
在本章中,你将使用 Create React App,这是 React 核心团队创建的一个启动工具,可以帮助你快速开始使用 React。它将使模块打包器和编译器(如 Webpack 和 Babel)的配置变得不必要,因为 Create React App 包会处理这些。这意味着你可以专注于构建你的个人作品集应用程序,该应用程序重用 React 组件并具有路由。除此之外,我们还将使用 react-router v6 添加路由,这是 React 中路由的领先库。
在设置 Create React App 的同时,本章将涵盖以下主题:
-
使用 Create React App 创建新项目
-
构建可重用 React 组件
-
使用 react-router 进行路由
等不及了?让我们开始吧!
项目概述
在本章中,我们将使用 Create React App 和 styled-components 创建一个应用程序,该应用程序利用可重用的 React 组件和样式。该应用程序将使用从公共 GitHub API 获取的数据。
构建时间约为 1.5-2 小时。
入门
本章中你将创建的项目将使用来自 GitHub 的公共 API,你可以在 docs.github.com/en/rest 找到这些 API。要使用此 API,你需要有一个 GitHub 账户,因为你将需要从 GitHub 用户账户中检索信息。如果你还没有 GitHub 账户,你可以在 GitHub 网站上创建一个。此应用程序的完整源代码也可以在 GitHub 上找到:github.com/PacktPublishing/React-Projects-Second-Edition/tree/main/Chapter02。
使用 React 创建个人作品集
在本节中,我们将学习如何使用 Create React App 创建一个新的 React 项目,并使用 react-router 添加可重用的 React 组件和路由。
使用 Create React App 创建个人作品集
每次创建新的 React 项目时都需要配置 Webpack 和 Babel,这可能会非常耗时。此外,每个项目的设置都可能发生变化,当我们想要向项目中添加新功能时,管理所有这些配置会变得很困难。
因此,React 核心团队引入了一个名为 Create React App 的启动工具包,目前版本为 5。通过使用 Create React App,我们不再需要担心管理编译和构建配置,即使 React 发布了新版本也是如此,这意味着我们可以专注于编码而不是配置。
本节将向我们展示如何使用 Create React App 创建一个 React 应用程序。
在做任何事情之前,让我们看看如何安装 Create React App。
安装 Create React App
Create React App 不必全局安装。相反,我们可以使用 npx,这是一个与 npm(v5.2.0 或更高版本)预安装的工具,它简化了我们执行 npm 包的方式:
npx create-react-app chapter-2
这将启动 Create React App 的安装过程,这可能需要几分钟,具体取决于您的硬件。尽管我们只执行了一个命令,但 Create React App 的安装程序将安装我们运行 React 应用程序所需的包。因此,它将安装 react、react-dom 和 react-scripts,最后一个包包含了编译、运行和构建 React 应用程序的配置。
如果我们进入项目的根目录,该目录以我们的项目名称命名,我们会看到它具有以下结构:
chapter-2
|- node_modules
|- package.json
|- public
|- index.html
|- src
|- App.css
|- App.test.js
|- App.js
|- index.css
|- index.js
注意
并非所有由 Create React App 创建的文件都被列出;相反,只有本章中使用的文件被列出。
这种结构看起来与我们第一章节中设置的结构非常相似,尽管有一些细微的差别。public 目录包含了所有不应该包含在编译和构建过程中的文件,而这个目录内的文件是唯一可以直接在 index.html 文件中使用的文件。
在另一个名为 src 的目录中,我们将找到在执行 package.json 文件中任何脚本时将被编译和构建的所有文件。有一个名为 App 的组件,它由 App.js、App.test.js 和 App.css 文件定义,还有一个名为 index.js 的文件,它是 Create React App 的入口点。
如果我们打开 package.json 文件,我们会看到已经定义了四个脚本:start、build、test 和 eject。由于最后两个在此阶段尚未处理,我们可以暂时忽略这两个脚本。为了能够在浏览器中打开项目,我们只需在命令行中输入以下命令,这将以开发模式运行 package react-scripts:
npm start
注意
除了 npm start,我们还可以运行 yarn start,因为 Create React App 推荐使用 Yarn。
如果我们访问 localhost:3000/,默认的 Create React App 页面将如下所示:
图 2.1 – 默认的 Create React App 模板
由于react-scripts默认支持热重载,我们对代码所做的任何更改都将导致页面重新加载。如果我们运行构建脚本,项目根目录中将会创建一个名为build的新目录,其中可以找到我们应用程序的压缩包。
在 Create React App 的基本安装完成后,我们将开始创建我们项目的组件并对它们进行样式设计。
构建可重用的 React 组件
在上一章中简要讨论了使用 JSX 创建 React 组件,但在这章中,我们将通过创建可以在整个应用程序中重用的组件来进一步探讨这个主题。首先,让我们看看如何构建我们的应用程序,这是基于上一章的内容。
构建应用程序结构
我们的项目仍然只包含一个组件,这并不使其非常可重用。为了开始,我们需要以与第一章相同的方式构建我们的应用程序。这意味着我们需要将App组件拆分成多个更小的组件。如果我们查看App.js中此组件的源代码,我们会看到在return函数中已经有一个 CSS header元素。让我们将这个header元素改为 React 组件:
-
首先,在
src目录下的components新目录中创建一个名为Header.css的新文件,并将classNames、App-header、App-logo和App-link的样式复制到其中:.App-logo { height: 40vmin; pointer-events: none; } @media (prefers-reduced-motion: no-preference) { .App-logo { animation: App-logo-spin infinite 20s linear; } } .App-header { background-color: #282c34; min-height: 100vh; display: flex; flex-direction: column; align-items: center; justify-content: center; font-size: calc(10px + 2vmin); color: white; } .App-link { color: #61dafb; } @keyframes App-logo-spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } -
现在,在这个目录中创建一个名为
Header.js的文件。此文件应返回与<header>元素相同的内容:import './Header.css'; function Header() { return ( <header className='App-header'> <img src={logo} className='App-logo' alt='logo' /> <p>Edit <code>src/App.js</code> and save to reload. </p> <a className='App-link' href='https://reactjs.org' target='_blank' rel='noopener noreferrer' > Learn React </a> </header> ); } export default Header; -
在你的
App组件中导入此Header组件并将其添加到return函数中:+ import Header from './components/Header'; import './App.css'; import logo from './logo.svg'; function App() { return ( <div className="App"> - <header className='App-header'> - <img src={logo} className='App-logo' alt='logo' /> - <p>Edit <code>src/App.js</code> and save to reload. </p> - <a - className='App-link' - href='https://reactjs.org' - target='_blank' - rel='noopener noreferrer' - > - Learn React - <a> - </header> + <Header /> </div> ); } export default App;
需要从App.css中删除标题的样式。此文件应只包含以下样式定义:
.App {
text-align: center;
}
.App-link {
color: #61dafb;
}
当我们再次在浏览器中访问我们的项目时,我们会看到一个错误,表明 logo 的值未定义。这是因为新的Header组件无法访问在App组件内部定义的logo常量。根据我们在第一章中学到的知识,我们知道这个logo常量应该作为属性添加到Header组件中,以便它可以显示。现在让我们这样做:
-
将
logo常量作为属性发送到src/App.js中的Header组件:// ... function App() { return ( <div className='App'> - <Header /> + <Header logo={logo} /> </div> ); } } export default App; -
获取
logo属性,以便可以在src/components/Header.js中的img元素作为src属性使用:import './Header.css'; - function Header() { + function Header({ logo }) { return ( <header className='App-header'> // ...
在这里,当我们用浏览器打开项目时,我们不会看到任何可见的变化。但如果我们打开 React 开发者工具,我们会看到项目现在被分为一个App组件和一个Header组件。此组件以.svg文件的形式接收logo属性,如下面的截图所示:
![Figure 2.2 – The React Developer Tools]
![img/Figure_2.2_B17390.jpg]
![Figure 2.2 – The React Developer Tools]
Header组件仍然被分割成多个元素,这些元素可以被分割成单独的组件。观察img和p元素,它们看起来已经很简单了。然而,a元素看起来更复杂,并具有诸如url、title和className等属性。为了将这个a元素转换成一个可重用的组件,我们需要将其移动到我们项目的不同位置。
要做到这一点,在components目录中创建一个名为Link.js的新文件。此文件应该返回我们在Header组件中已经得到的相同的a元素。此外,我们还可以将url和title作为属性发送到这个组件。现在让我们这样做:
-
从
src/components/Header.css中删除App-link类的样式,并将其放置在一个名为Link.css的文件中:.App-link { color: #61dafb; } -
创建一个名为
Link的新组件,该组件接受url和title属性。此组件将这些属性作为属性添加到src/components/Link.js中的<a>元素:import './Link.css'; function Link({ url, title }) { return ( <a className='App-link' href={url} target='_blank' rel='noopener noreferrer' > {title} </a> ); }; export default Link; -
在
src/components/Header.js中导入此Link组件并将其放置在Header组件内部:+ import Link from './Link.js'; import './Header.css'; function Header({ logo }) { return ( <header className='App-header'> <img src={logo} className='App-logo' alt='logo' /> <p>Edit <code>src/App.js</code> and save to reload. </p> - <a - className='App-link' - href='https://reactjs.org' - target='_blank' - rel='noopener noreferrer' - > - Learn React - <a> + <Link + url='https://reactjs.org' + title='Learn React' + /> </header> ); } export default Header; -
我们现在的代码应该看起来像以下这样,这意味着我们已经成功将
App组件分割成components目录中的不同文件。此外,logo.svg文件可以被移动到一个名为assets的新目录:chapter-2 |- node_modules |- package.json |- public |- index.html |- src |- assets |- logo.svg |- components |- Header.css |- Header.js |- Link.css |- Link.js |- App.css |- App.js |- index.css |- index.js -
不要忘记也更改
src/App.js文件中的import语句,其中logo.svg文件被导入为一个组件:import Header from './components/Header'; import './App.css'; - import logo from './logo.svg'; + import logo from './assets/logo.svg'; function App() { return ( // ...
然而,如果我们查看浏览器中的项目,则没有明显的更改。然而,在 React 开发者工具中,我们应用程序的结构已经形成。App组件在组件树中显示为父组件,而Header组件是一个子组件,它将Link作为子组件。
在本节的下一部分,我们将向此应用程序的组件树中添加更多组件,并使它们在整个应用程序中可重用。
在 React 中重用组件
本章中我们正在构建的项目是一个个人作品集页面;它将展示我们的公开信息和公开仓库列表。因此,我们需要获取官方 GitHub REST API(v3)并从两个端点获取信息。在第一章中,我们已经执行了数据获取操作,但这次信息不会来自本地 JSON 文件。获取信息的方法几乎相同。我们将使用fetch API 来完成此操作。
我们可以通过执行以下命令从 GitHub 获取我们的公开 GitHub 信息(将代码加粗部分的末尾的username替换为你的用户名):
curl 'https://api.github.com/users/username'
注意
如果你没有 GitHub 个人资料或者还没有填写所有必要的信息,你也可以使用octocat用户名。这是 GitHub 的吉祥物用户名,并且已经填充了示例数据。
此请求将返回以下输出:
{
"login": "octocat",
"id": 583231,
"node_id": "MDQ6VXNlcjU4MzIzMQ==",
"avatar_url":
"https://avatars.githubusercontent.com/u/583231?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/octocat",
"html_url": "https://github.com/octocat",
"followers_url":
"https://api.github.com/users/octocat/followers",
"following_url":
"https://api.github.com/users/octocat/following{
/other_user}",
"gists_url":
"https://api.github.com/users/octocat/gists{/gist_id}",
"starred_url":
"https://api.github.com/users/octocat/starred{/owner}{
/repo}",
"subscriptions_url":
"https://api.github.com/users/octocat/subscriptions",
"organizations_url":
"https://api.github.com/users/octocat/orgs",
"repos_url":
"https://api.github.com/users/octocat/repos",
"type": "User",
"site_admin": false,
"name": "The Octocat",
"company": "@github",
"blog": "https://github.blog",
"location": "San Francisco",
"email": null,
"hireable": null,
"bio": null,
"twitter_username": null,
"public_repos": 8,
"public_gists": 8,
"followers": 3555,
"following": 9
}
JSON 输出中的多个字段被突出显示,因为这些是我们将在应用程序中使用的字段。这些是avatar_url、html_url、repos_url、name、company、location、email和bio,其中repos_url字段的值实际上是我们需要调用的另一个 API 端点,以检索此用户的全部仓库。这是我们将在本章后面完成的事情。
由于我们想在应用程序中显示这个结果,我们需要做以下事情:
-
要从 GitHub 检索这些公开信息,请在新的目录
pages中创建一个名为Profile的新组件。这个目录将持有代表我们应用程序中页面的所有组件。在这个文件中,将以下代码添加到src/pages/Profile.js:import { useState, useEffect } from 'react'; function Profile({ userName }) { const [loading, setLoading] = useState(false); const [profile, setProfile] = useState({}); useEffect(() => { async function fetchData() { const profile = await fetch( 'https://api.github.com/users/${userName}'); const result = await profile.json(); if (result) { setProfile(result); setLoading(false); } } fetchData(); }, [userName]); return ( <div> <h2>About me</h2> {loading ? ( <span>Loading...</span> ) : ( <ul></ul> )} </div> ); } export default Profile;
这个新组件从 React 中导入了两个 Hooks,用于处理状态管理和生命周期。我们在上一章中已经使用了useState Hook,用于创建loading和profile的状态。在第二个 Hook 中,即useEffect Hook 中,我们从 GitHub API 进行异步数据获取。由于我们还需要创建新的组件来显示数据,因此还没有渲染任何结果。
-
现在,将这个新组件导入到
App组件中,并将userName属性传递给它。如果您没有 GitHub 账户,可以使用用户名octocat:import Header from './Header'; + import Profile from './pages/Profile'; import './App.css'; function App() { return ( <div className='App'> <Header logo={logo} /> + <Profile userName="octocat" /> </div> ); } } export default App; -
快速查看运行我们项目的浏览器,我们可以看到这个新的
Profile组件目前还不可见。这是因为Header.css文件中有一个height属性,其view-height值为100,这意味着该组件将占用页面的整个高度。要更改这一点,请打开src/components/Header.css文件,并更改以下突出显示的行:.App-logo { - height: 40vmin; + height: 60px; pointer-events: none; } // ... .App-header { background-color: #282c34; - min-height: 100vh; + min-height: 100%; display: flex; flex-direction: column; align-items: center; justify-content: center; font-size: calc(10px + 2vmin); color: white; } -
页面上应该有足够的空间来显示
Profile组件,因此我们可以再次打开scr/pages/Profile.js文件,并显示 GitHub API 返回的avatar_url、html_url、repos_url、name、company、location、email和bio字段:// ... return ( <div> <h2>About me</h2> {loading ? ( <span>Loading...</span> ) : ( <ul> + <li><span>avatar_url: </span> {profile.avatar_url}</li> + <li><span>html_url: </span> {profile.html_url}</li> + <li><span>repos_url: </span> {profile.repos_url}</li> + <li><span>name: </span> {profile.name}</li> + <li><span>company: </span> {profile.company}</li> + <li><span>location: </span> {profile.location}</li> + <li><span>email: </span> {profile.email}</li> + <li><span>bio: </span> {profile.bio}</li> </ul> )} </div> ); } export default Profile;
保存此文件并访问浏览器中的项目后,我们将看到 GitHub 信息的子弹列表正在显示。
由于这看起来不太美观,并且标题与页面内容不匹配,让我们对这两个组件的styling文件做一些更改:
-
更改
Header组件的代码,使其显示页面不同的标题。此外,可以删除此处的Link组件,因为我们将在后面的Profile组件中使用它:import './Header.css'; - import Link from './Link'; function Header({ logo }) { return ( <header className='App-header'> <img src={logo} className='App-logo' alt='logo' /> - <p> - Edit <code>src/App.js</code> and save to reload. - </p> - <Link url='https://reactjs.org' title='Learn React' /> + <h1>My Portfolio</h1> </header> ); } export default Header; -
在更改
Profile组件的样式之前,我们首先需要创建一个 CSS 文件,该文件将包含组件的样式规则。为此,在pages目录中创建Profile.css文件,并添加以下内容:.Profile-container { width: 50%; margin: 10px auto; } .Profile-avatar { width: 150px; } .Profile-container > ul { list-style: none; padding: 0; text-align: left; } .Profile-container > ul > li { display: flex; justify-content: space-between; } .Profile-container > ul > li > span { font-weight: 600; } -
在
src/pages/Profile.js中,我们需要导入此文件以应用样式。还记得我们之前创建的Link组件吗?我们也导入此文件,因为它将被用于创建链接到我们的个人资料和 GitHub 网站上的仓库列表:import { useState, useEffect } from 'react'; + import Link from '../components/Link'; + import './Profile.css'; function Profile({ userName }) { // .. -
在
return语句中,我们将添加我们在样式定义中定义的classNames函数,并将头像图像与项目符号列表分开。通过这样做,我们还需要将项目符号列表包裹在一个额外的div中:// ... return ( - <div> + <div className='Profile-container'> <h2>About me</h2> {loading ? ( <span>Loading...</span> ) : ( + <div> + <img + className='Profile-avatar' + src={profile.avatar_url} + alt={profile.name} + /> <ul> - <li><span>avatar_url: </span> {profile.avatar_url}</li> - <li><span>html_url: </span> {profile.html_url}</li> - <li><span>repos_url: </span> {profile.repos_url}</li> + <li> + <span>html_url: </span> + <Link url={profile.html_url} title={profile.html_url} /> + </li> + <li> + <span>repos_url: </span> + <Link url={profile.repos_url} title={profile.repos_url} /> + </li> <li><span>name: </span> {profile.name}</li> <li><span>company: </span> {profile.company}</li> <li><span>location: </span> {profile.location}</li> <li><span>email: </span> {profile.email}</li> <li><span>bio: </span> {profile.bio}</li> </ul> + </div> ); } // ..
最后,我们可以看到应用程序开始看起来像一个加载你的 GitHub 信息的投资组合页面,包括你的头像和公共信息列表。这导致了一个看起来类似于以下截图的应用程序:
图 2.3 – 我们定制的投资组合应用程序
如果我们查看 Profile 组件中的代码,我们会看到有很多重复的代码,因此我们需要将显示我们公共信息的列表转换为一个单独的组件。让我们开始吧:
-
在
components目录内创建一个名为List.js的新文件,它将接受一个名为items的属性:function List({ items }) { return ( <ul></ul> ); } export default List; -
在
Profile组件中,我们可以导入这个新的List组件。应该创建一个名为items的新变量,它是一个包含我们想要在此列表中显示的所有项目的数组:import { useState, useEffect } from 'react'; + import List from '../components/List'; import Link from '../components/Link'; import './Profile.css'; function Profile({ userName }) { // … + const items = [ + { + field: 'html_url', + value: <Link url={profile.html_url} title={profile.html_url} />, + }, + { + field: 'repos_url', + value: <Link url={profile.repos_url} title={profile.repos_url} />, + }, + { field: 'name', value: profile.name }, + { field: 'company', value: profile.company }, + { field: 'location', value: profile.location }, + { field: 'email', value: profile.email }, + { field: 'bio', value: profile.bio }, + ]; // ... -
这将作为属性发送到
List组件,因此这些项目可以从该组件中渲染。这意味着你可以删除ul元素以及其中所有的li元素:// ... return ( <div className='Profile-container'> <h2>About me</h2> {loading ? ( <span>Loading...</span> ) : ( <div> <img className='Profile-avatar' src={profile.avatar_url} alt={profile.name} /> - <ul> - // ... - </ul> + <List items={items} /> </div> )} </div> ); } export default Profile;
你可以看到,对于具有 html_url 和 repos_url 字段的列表项,我们将发送 Link 组件作为值,而不是从 GitHub API 返回的值。在 React 中,你还可以将完整的组件作为属性发送给不同的组件,因为属性可以是任何东西。
-
在
List组件中,我们现在可以遍历items属性并返回列表项:// ... function List({ items }) { return ( <ul> + {items.map((item) => ( + <li key={item.field}> + <span>{item.field}: </span> + {item.value} + </li> + ))} </ul> ); } export default List;
样式是从 Profile 组件继承的,因为 List 组件是一个子组件。为了更好地结构化你的应用程序,你可以将信息列表的样式移动到一个单独的 List.css 文件中,并在 List 组件内导入它。
假设我们正确执行了前面的步骤,你的应用程序在美学上不应该发生变化。然而,如果我们查看 React 开发者工具,我们会看到组件树已经发生了一些变化。
在下一节中,我们将添加使用 react-router 的路由,并显示与我们的 GitHub 账户链接的仓库。
使用 react-router 进行路由
react-router v6 是 React 中最受欢迎的路由库,它支持许多功能,可以帮助你充分利用它。使用这个库,你只需添加组件即可向 React 应用程序添加声明式路由。这些组件可以分为三种类型:路由组件、路由匹配组件和导航组件。
使用 react-router 设置路由包括多个步骤:
-
要使用这些组件,你需要通过执行以下命令安装
react-router网络包,称为react-router-dom:npm install react-router-dom -
在安装
react-router-dom之后,下一步是将此包中的路由和路由匹配组件导入到应用程序的容器组件中。在这种情况下,是App组件,它位于 src 目录中:import React from 'react'; + import { BrowserRouter, Routes, Route } from 'react-router-dom'; import logo from './assets/logo.svg'; import './App.css'; import Header from './components/Header'; import Profile from './pages/Profile'; function App() { // … -
实际的路由必须添加到此组件的
return语句中,其中所有路由匹配组件(Route)都必须包裹在一个名为Router的路由组件中。当你的 URL 与Route的任何迭代定义的路由匹配时,此组件将渲染作为子组件传递的 React 组件:// ... function App() { return ( <div className='App'> + <BrowserRouter> <Header logo={logo} /> - <Profile userName='octocat' /> + <Routes> + <Route + path='/' + element={<Profile userName='octocat' />} + /> + </Routes> + </BrowserRouter> </div> ); } export default App;
如果你现在再次在浏览器中访问 http://localhost:3000 上的项目,将渲染 Profile 组件。
除了我们的 GitHub 个人资料外,我们还想展示我们一直在工作的项目。让我们向应用程序添加一个新的路由,该路由将渲染我们 GitHub 账户的所有仓库:
-
这个新组件将使用端点来获取所有你的仓库,你可以通过执行以下命令来尝试(将粗体部分代码末尾的
username替换为你自己的用户名):curl 'https://api.github.com/users/username/repos'
调用此端点的输出将类似于以下内容:
[
{
"id": 132935648,
"node_id": "MDEwOlJlcG9zaXRvcnkxMzI5MzU2NDg=",
"name": "boysenberry-repo-1",
"full_name": "octocat/boysenberry-repo-1",
"private": false,
"html_url":
"https://github.com/octocat/boysenberry-repo-1",
"description": "Testing",
"fork": true,
"created_at": "2018-05-10T17:51:29Z",
"updated_at": "2021-01-13T19:56:01Z",
"pushed_at": "2018-05-10T17:52:17Z",
"stargazers_count": 9,
"watchers_count": 9,
"forks": 6,
"open_issues": 0,
"watchers": 9,
"default_branch": "master"
},
// ...
]
如前一个示例响应所示,仓库数据是一个包含对象的数组。我们将使用前面突出显示的字段来在 /projects 路由上显示我们的仓库。
-
首先,我们需要在
pages目录中创建一个新的组件,命名为Projects。这个组件将具有与Profile组件几乎相同的逻辑用于状态管理和数据获取,但它将调用不同的端点来获取仓库:import { useState, useEffect } from 'react'; import Link from '../components/Link'; import List from '../components/List; function Projects({ userName }) { const [loading, setLoading] = useState(true); const [projects, setProjects] = useState({}); useEffect(() => { async function fetchData() { const data = await fetch( 'https://api.github.com/users/${ userName}/repos', ); const result = await data.json(); if (result) { setProjects(result); setLoading(false); } } fetchData(); }, [userName]); // ... -
在将端点的信息放入本地状态变量 projects 之后,我们将使用相同的
List组件来渲染关于仓库的信息:// ... return ( <div className='Projects-container'> <h2>Projects</h2> {loading ? ( <span>Loading...</span> ) : ( <div> <List items={projects.map((project) => ({ field: project.name, value: <Link url={project.html_url} title={project.html_url} />, }))} /> </div> )} </div> ); } export default Projects; -
要在访问
/profile路由时渲染此组件,我们需要使用Route组件将其添加到App组件中:import React from 'react'; import { BrowserRouter, Routes, Route } from 'react-router-dom'; import logo from './assets/logo.svg'; import './App.css'; import Header from './components/Header'; import Profile from './pages/Profile'; + import Projects from './pages/Projects'; function App() { return ( <div className='App'> <Header logo={logo} /> <BrowserRouter> <Routes> <Route path='/' element={ <Profile userName='octocat' />} /> + <Route path='/projects' element= {<Projects userName='octocat' />} /> </Routes> </BrowserRouter> // ...
现在,Profile 组件只有在访问 / 路由时才会渲染,而 Projects 组件在访问 /projects 路由时渲染。如果你访问任何其他路由,除了 Header 组件外,不会渲染任何组件。
注意
你可以通过将 * 作为路径传递给 Route 组件来设置一个组件,当没有路由可以匹配时将显示该组件。
虽然我们已经设置了两个路由,但访问这些路由的唯一方法是通过在浏览器中更改 URL。使用 react-router,我们还可以创建动态链接,从任何组件访问这些路由。在我们的 Header 组件中,我们可以添加一个导航栏,渲染指向这些路由的链接:
import './Header.css';
+ import { Link as RouterLink } from 'react-router-dom';
function Header({ logo }) {
return (
<header className='App-header'>
<img src={logo} className='App-logo' alt='logo' />
<h1>My Portfolio</h1>
+ <nav>
+ <RouterLink to='/' className='App-link'>
+ About me
+ </RouterLink>
+ <RouterLink to='/projects' className='App-link'>
+ Projects
+ </RouterLink>
+ </nav>
</header>
);
}
export default Header;
由于我们已自行定义了 Link 组件,因此我们将从 react-router-dom 中导入 Link 组件作为 RouterLink。这将防止您在以后进行更改时或在使用 IDE 中的自动完成功能时产生混淆。
最后,我们可以在 Header.css 中添加一些样式,以便我们的路由链接显示得更好:
.App-header {
background-color: #282c34;
min-height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: calc(10px + 2vmin);
color: white;
}
+ .App-header > nav {
+ margin-bottom: 10px;
+ }
+ .App-header > nav > .App-link {
+ margin-right: 10px;
+ }
如果您现在在浏览器中访问应用程序的 http://localhost:3000/projects,它应该看起来像以下截图。点击页眉中的链接将在两个不同的路由之间导航:
图 2.4 – 应用程序中的项目路由
在这些路由设置到位后,还可以向 router 组件添加更多路由。一个合理的做法是为单个项目添加一个路由,该路由有一个额外的参数,用于指定应显示哪些项目。因此,我们有一个名为 ProjectDetailpages 的新组件目录,其中包含从 GitHub API 获取单个仓库的逻辑。当路径匹配 /projects/:name 时,该组件会被渲染,其中 name 代表在项目页面点击的项目名称:
-
此路由使用一个名为
ProjectDetail.js的文件中的新组件,该组件与Projects组件类似。您也可以在pages目录中创建此文件,但将是从api.github.com/repos/userName/repo端点获取数据,其中userName和repo应替换为您自己的用户名和您想要显示的仓库名称:import { useState, useEffect } from 'react'; Import { useParams } from 'react-router-dom'; function Project({ userName }) { const [loading, setLoading] = useState(false); const [project, setProject] = useState([]); const { name } = useParams(); useEffect(() => { async function fetchData() { const data = await fetch( 'https://api.github.com/repos/${ userName}/${name}', ); const result = await data.json(); if (result) { setProject(result); setLoading(false); } } if (userName && name) { fetchData(); } }, [userName, name]); // ...
在前面的章节中,您可以看到如何使用您的用户名和仓库名称从 GitHub API 获取数据。仓库名称来自 react-router-dom 的 useParams 钩子,该钩子为您从 URL 获取 name 变量。
-
通过从 GitHub 获取的仓库数据,您可以使用我们在之前的路由中也使用过的
List组件创建items变量,以渲染有关此项目的信息。添加到items中的字段来自 GitHub,也可以在我们之前检查的https://api.github.com/users/username/repos端点的响应中看到。此外,仓库名称之前也已列出:// ... return ( <div className='Project-container'> <h2>Project: {project.name}</h2> {loading ? ( <span>Loading...</span> ) : ( <div></div> )} </div> ); } export default Project; -
要在
/projects/:name路由上渲染此组件,我们需要在App.js中的Router组件内添加此组件:// ... + import ProjectDetail from './pages/ProjectDetail'; function App() { return ( <div className='App'> <BrowserRouter> <Header logo={logo} /> <Routes> <Route exact path='/' element= {<Profile userName='octocat' />} /> <Route path='/projects' elements= {<Projects userName='octocat' />} /> + <Route path='/projects/:name' element= {<ProjectDetail userName='octocat' />} /> </Routes> </BrowserRouter> ); } -
您可以通过更改浏览器中的 URL 来导航到此路由,但您还希望在
Projects组件中添加到该页面的链接。因此,您需要做出更改,以导入RouterLink从react-router-dom并使用它来代替您自己的Link组件:import { useState, useEffect } from 'react'; + import { Link as RouterLink } from 'react-router-dom' import List from '../components/List'; - import Link from '../components/Link'; // ... return ( <div className='Projects-container'> <h2>Projects</h2> {loading ? ( <span>Loading...</span> ) : ( <div> <List items={projects.map((project) => ({ field: project.name, - value: <Link url={project.html_url} title={project.html_url} />, }))items} /> + value: <RouterLink url={project.html_url} title={project.html_url} />, }))items} /> </div> )} </div> ); } export default Projects;
如果你现在在浏览器中访问http://localhost:3000/projects页面,你可以点击项目,进入一个新页面,该页面显示了特定项目的所有信息。
通过这些最后的更改,你已经创建了一个使用react-router进行动态路由的资料库应用。
摘要
在本章中,你使用了 Create React App 来创建你的 React 应用的起始项目,该应用附带了对 Babel 和 Webpack 等库的初始配置。通过这样做,你不必自己配置这些库,也不必担心你的 React 代码如何在浏览器中运行。我们已经探讨了如何构建可重用组件,并学习了如何使用react-router添加动态路由。使用这个库,你可以创建具有大量路由的应用程序,并且能够使用 URL 的变化来更改应用程序内部显示的内容。
接下来的章节都将展示使用 Create React App 或其他零配置库创建的项目,这意味着这些项目不需要你对 Webpack 或 Babel 进行任何更改。
在下一章中,我们将通过使用 React 创建一个动态的项目管理板来构建本章内容,该板使用 styled-components 进行样式设计,并通过自定义 Hooks 重用逻辑。
进一步阅读
-
使用 npx:
medium.com/@maybekatz/introducing-npx-an-npm-package-runner-55f7d4bd282b -
Create React App:
create-react-app.dev/ -
React Router:
reactrouter.com/web/guides/quick-start
第三章: 构建动态项目管理板
在本书的前两章中,你独立创建了两个 React 项目,到现在你应该已经对 React 的核心概念有了扎实的理解。你到目前为止所使用的概念也将在本章中用于创建你的第三个 React 项目,包括一些新的更高级的概念,这些概念将展示使用 React 的优势。再次提醒,如果你觉得你缺少完成本章内容所需的一些知识,你总是可以重复你迄今为止所构建的内容。
本章将再次使用 Create React App,这是你在上一章中使用过的。在本章的项目管理板应用开发过程中,我们将创建并使用一个自定义钩子进行数据获取。将使用 HTML5 网络 API 动态拖放组件,这些组件被设置为可重用组件,并使用styled-components进行样式设置。随后,你将使用更高级的 React 技术来控制组件间的数据流,例如通过创建自定义钩子。
本章将涵盖以下主题:
-
处理数据流
-
使用自定义钩子
-
使用 HTML5 网络 API
-
使用
styled-components为 React 添加样式
项目概述
在本章中,我们将创建一个动态项目管理板,它使用 Create React App 和styled-components具有可重用 React 组件和样式。该应用将具有一个使用 HTML5 拖放 API 的动态拖放界面。
构建时间约为 1.5-2 小时。
开始
本章我们将创建的项目基于 GitHub 上可找到的初始版本:github.com/PacktPublishing/React-Projects-Second-Edition/tree/main/Chapter03-initial。该应用的完整源代码也可以在 GitHub 上找到:github.com/PacktPublishing/React-Projects-Second-Edition/tree/main/Chapter03。
下载初始应用后,我们可以先进入其根目录,并运行npm install命令。这将安装 Create React App 的核心包(react、react-dom和react-scripts)。安装完成后,我们可以通过执行npm start命令来启动应用,并在浏览器中通过访问http://localhost:3000来访问项目。
如以下截图所示,应用有一个带有标题的基本页眉,并分为四个列。这些列是项目管理板的通道,一旦我们将项目连接到数据文件,它们将包含个别任务。
![图 3.1 – 初始应用
![图 3.1]
图 3.1 – 初始应用
如果我们查看项目的结构,我们会看到它与上一章的项目结构相同。应用程序的入口点是src/index.js文件,它渲染一个名为App的组件,该组件包含两个其他组件,即Header和Board。第一个是应用程序的实际头部,而Board组件包含我们在应用程序中可以看到的四个列。这些列由Lane组件表示。
在这个应用程序中,你可以看到我们已经将组件进一步拆分到单独的目录中。现在,components或pages目录中的每个组件都有自己的子目录:
chapter-3-initial
|- /node_modules
|- /public
|- /src
|- /components
|- /Header
|- Header.css
|- Header.js
|- /Lane
|- Lane.css
|- Lane.js
|- /pages
|- /Board
|- Board.js
|- Board.css
|- App.js
|- App.css
|- index.js
|- index.css
package.json
创建项目管理板应用程序
在本节中,我们将创建一个使用自定义钩子进行数据获取和 HTML5 拖放 API 使其动态化的项目管理板 PWA。我们将使用一个由 Create React App 设置的样板应用程序,我们可以在本章的 GitHub 仓库中找到它。
处理数据流
在应用程序的初始版本就绪后,下一步是获取项目管理板初始数据并处理其通过组件的流动。为此,我们将创建一个用于数据获取的自定义钩子,该钩子可以在其他组件中重用。
本节的第一部分将展示我们如何使用 React 生命周期方法从数据源加载数据,并在 React 组件中显示这些数据。
加载和显示数据
加载和显示从数据源检索到的数据是我们上一章所做的事情。本章使用的数据来自一个模拟的 REST API,由 Typicode 的 My JSON Server 创建。使用名为db.json的文件,该文件位于本书的仓库中,我们可以自动创建 REST 端点。
图 3.2 – 使用 My JSON Server
使用 My JSON Server,my-json-server.typicode.com/PacktPublishing/React-Projects-Second-Edition/tasks端点返回一个任务列表,在本节中我们将将其加载到我们的项目管理板中。响应是一个包含 id、标题、正文和通道字段中定义的任务信息的对象数组。
本节将进一步探讨这个问题。按照以下步骤开始:
-
我们将首先从数据文件中获取项目数据。为此,我们需要向
Board组件添加必要的函数。我们需要这些函数来使用 Hooks 访问 React 生命周期,这我们在前面的章节中已经做了:+ import { useState, useEffect } from 'react'; import Lane from '../../components/Lane/Lane'; import './Board.css'; // ... function Board() { + const [loading, setLoading] = useState(false); + const [tasks, setTasks] = useState([]); + const [error, setError] = useState(''); + useEffect(() => { + async function fetchData() { + try { + const tasks = await fetch( `https://my-json-server.typicode.com/ PacktPublishing/React-Projects-Second- Edition/tasks`, ); + const result = await tasks.json(); + if (result) { + setTasks(result); + setLoading(false); + } + } catch (e) { + setLoading(false); + setError(e.message); + } + } + fetchData(); + }, []); return ( // ...
在useEffect钩子中,数据是在try..catch语句中获取的。这个语句捕获数据获取过程中返回的任何错误,并用此消息替换错误状态。
-
现在,我们可以将任务分配到相应的通道上:
// ... return ( <div className='Board-wrapper'> {lanes.map((lane) => ( <Lane key={lane.id} title={lane.title} + loading={loading} + error={error} + tasks={tasks.filter((task) => task.lane === lane.id)} /> ))} </div> ); } export default Board;
在return语句中,你可以看到一个迭代lanes常量的函数,并将这些值作为 props 传递给Lane组件。另外,当我们向组件传递任务时,有一些特别的事情正在发生,因为filter函数正在被用来只返回与 lane ID 匹配的任务状态。
-
接下来,我们需要对
Lane组件做一些修改,以便它能够使用我们从 REST API 获取的数据来显示任务:+ import Task from '../Task/Task'; import './Lane.css'; - function Lane({ title }) { + function Lane({ title, loading, error, tasks }) { return ( <div className='Lane-wrapper'> <h2>{title}</h2> + {loading || error ? ( + <span>{error || 'Loading...'}</span> + ) : ( + tasks.map((task) => ( + <Task + key={task.id} + id={task.id} + title={task.title} + body={task.body} + /> + )) + )} </div> ); } export default Lane;
Lane组件现在接受另外三个 props,分别是tasks、loading和error,其中tasks包含从 REST API 获取的任务数组,loading指示是否应显示加载消息,而error包含当存在错误时的错误消息。我们可以看到,在map函数中迭代任务时,将渲染显示信息的Task组件。
-
要创建此任务,我们需要在
components目录中创建一个名为Task的目录,并在其中放置一个名为Task.js的新文件,用于Task组件:import './Task.css'; function Task({ title, body }) { return ( <div className='Task-wrapper'> <h3>{title}</h3> <p>{body}</p> </div> ); } export default Task; -
此组件从我们需要在
Task目录内创建的另一个文件中获取样式,该文件名为Task.css,内容如下:.Task-wrapper { background: darkGray; padding: 20px; border-radius: 20px; margin: 0% 5% 5% 5%; } .Task-wrapper h3 { width: 100%; margin: 0; }
如果我们在网页浏览器中访问我们的应用程序http://localhost:3000,我们将看到以下内容:
![图 3.3 – 我们的应用程序,使用模拟 REST API 的数据
图 3.3 – 我们的应用程序,使用模拟 REST API 的数据
从数据源获取数据是可以在我们的应用程序中复用的逻辑。在下一节中,我们将探讨如何通过创建自定义 Hook 来跨多个组件复用这种逻辑。
使用自定义 Hooks
Hooks 是一种使用 React 功能来创建本地状态或使用生命周期来监视该状态更新的方法。但 Hooks 也是复用你为 React 应用程序创建的逻辑的一种方式。这是许多为 React 创建功能库中流行的一种模式,例如react-router。
注意
在 React 引入 Hooks 之前,创建高阶组件(HOCs)以复用逻辑是一种流行的模式。HOCs 是 React 的高级特性,专注于组件的可复用性。React 文档这样描述它们:“一个高阶组件是一个函数,它接受一个组件并返回一个新的组件。”
在本节的第一个部分,我们将创建我们的第一个自定义 Hook,它使用逻辑从我们在上一节创建的数据源中检索数据。
创建自定义 Hooks
我们已经看到我们可以在 React 中重用组件,但下一步是重用这些组件内部的逻辑。为了解释这在实践中意味着什么,让我们创建一个示例。我们的项目有一个 Board 组件,它获取 REST API 并渲染所有通道和任务。这个组件中有逻辑,以 useState Hook 创建的本地状态、useEffect Hook 内部的数据获取和关于每个 Lane 组件如何渲染的信息的形式存在。如果我们只想显示没有通道且只有任务的板怎么办?我们只是从 Board 组件发送不同的属性吗?当然,这是可能的,但在 React 中,这就是自定义 Hook 的用途。
没有通道的 Board 组件不会遍历所有通道并渲染带有任务的相应通道作为属性。相反,它会遍历所有任务并直接渲染它们。尽管渲染的组件不同,但设置初始状态、获取数据和渲染组件的逻辑可以重用。自定义 Hook 应该能够使用本地状态并在任何使用它的组件中执行数据获取。
要创建自定义 Hook,在 src 目录下创建一个名为 hooks 的新目录,并在其中创建一个名为 useDataFetching.js 的新文件。现在,按照以下步骤操作:
-
从 React 中导入
useState和useEffectHook 并为 Hook 创建一个新的函数,该函数成为默认导出。这个函数将接受一个名为dataSource的参数。由于这个 Hook 将使用数据获取的生命周期,让我们将这个自定义 Hook 命名为useDataFetching并使其返回一个空数组:import { useState, useEffect } from 'react'; function useDataFetching(dataSource) { return []; } export default useDataFetching; -
在这个函数内部,添加
useStateHook 来创建loading、error和data的本地状态,其结构与Board组件内部的本地状态几乎相同:import { useState, useEffect } from 'react'; function useDataFetching(dataSource) { + const [loading, setLoading] = useState(false); + const [data, setData] = useState([]); + const [error, setError] = useState(''); return []; } export default useDataFetching; -
接下来,我们需要使用
useEffectHook,这是数据获取将发生的地方。dataSource参数用作获取的位置。注意,常量名称现在更加通用,不再指定单一用途:import { useState, useEffect } from 'react'; function useDataFetching(dataSource) { // ... - return []; + useEffect(() => { + async function fetchData() { + try { + const data = await fetch(dataSource); + const result = await data.json(); + if (result) { + setData(result); + setLoading(false); + } + } catch (e) { + setLoading(false); + setError(e.message); + } + } + fetchData(); + }, [dataSource]); + return [loading, error, data]; } export default useDataFetching;
这添加了进行数据获取的方法,并且在 return 语句中,我们返回 data、loading 和 error 状态。
恭喜!你已经创建了你的第一个 Hook!然而,它仍然需要添加到一个支持数据获取的组件中。因此,我们需要重构我们的 Board 组件以使用这个自定义 Hook 进行数据获取:
-
从
src/hooks/useDataFetching.js文件中导入自定义 Hook 并删除 React Hook 的导入:- import { useState, useEffect } from 'react'; + import useDataFetching from '../../hooks/useDataFetching'; import Lane from '../../components/Lane/Lane'; import './Board.css'; // ... -
随后,我们可以删除
Board组件中useState和useEffectHook 的使用:// ... function Board() { - const [loading, setLoading] = useState(false); - const [tasks, setTasks] = useState([]); - const [error, setError] = useState(''); - useEffect(() => { - async function fetchData() { - try { - const tasks = await fetch( `https://my-json-server.typicode.com/ PacktPublishing/React-Projects-Second- Edition/tasks`, ); - const result = await tasks.json(); - if (result) { - setTasks(result); - setLoading(false); - } - } catch (e) { - setLoading(false); - setError(e.message); - } - } - fetchData(); - }, []); return ( // ... -
相反,使用导入的自定义 Hook 来处理我们的数据获取。该 Hook 返回与之前相同的
loading、error和tasks状态,但数据状态被重命名为tasks以适应我们组件的需求:import useDataFetching from '../../hooks/useDataFetching'; import Lane from '../../components/Lane/Lane'; import './Board.css'; function Board() { + const [loading, error, tasks] = useDataFetching(`https://my-json-server. typicode.com/PacktPublishing/React-Projects- Second-Edition/tasks`); return ( // ...
在下一节中,我们将学习如何通过从不同的组件导入来重复使用自定义钩子。
重复使用自定义钩子
在放置了第一个自定义钩子之后,是时候考虑其他可以进行数据获取的组件了,例如仅显示任务的组件。创建此组件的过程包括两个步骤:创建实际组件和使用自定义钩子进行数据获取。让我们开始吧:
-
在
pages目录中,我们需要在名为Backlog的新目录中创建一个名为Backlog.js的新文件。在此文件中,我们可以放置以下代码以创建组件,导入自定义钩子,并导入 CSS 进行样式设置:import Task from '../../components/Task/Task'; import useDataFetching from '../../hooks/useDataFetching'; import './Backlog.css'; function Backlog() { const [loading, error, tasks] = useDataFetching( 'https://my-json-server.typicode.com/ PacktPublishing/React-Projects-Second-Edition/ tasks', ); return ( <div></div> ); } export default Backlog; -
现在的
return语句正在返回一个空的div元素,因此我们需要在此处添加代码以渲染任务:// ... return ( - <div> + <div className='Backlog-wrapper'> + <h2>Backlog</h2> + <div className='Tasks-wrapper'> + {loading || error ? ( + <span>{error || 'Loading...'}</span> + ) : ( + tasks.map((task) => ( + <Task + key={task.id} + title={task.title} + body={task.body} + /> + )) + )} + </div> </div> ); } export default Backlog; -
此组件导入
Backlog.css文件进行样式设置,我们还在此文件中的元素上添加了类。但我们也需要在Backlog.css中创建并添加一些基本的样式规则:.Backlog-wrapper { display: flex; flex-direction: column; margin: 5%; } .Backlog-wrapper h2 { width: 100%; padding-bottom: 10px; text-align: center; border-bottom: 1px solid darkGray; } .Tasks-wrapper { display: flex; justify-content: space-between; flex-direction: row; flex-wrap: wrap; margin: 5%; } -
在
App组件中,我们可以导入这个组件,在Board组件下方渲染它:import './App.css'; import Board from './pages/Board/Board'; import Header from './components/Header/Header'; + import Backlog from './pages/Backlog/Backlog'; function App() { return ( <div className='App'> <Header /> <Board /> + <Backlog /> </div> ); } export default App;
这将在我们的板子下方渲染新的Backlog组件,其中包含所有不同的任务。这些任务与Board组件中的任务相同,因为我们使用了相同的 REST API 端点。此外,您还可以为这个项目设置react-router,以便在另一个页面上渲染Backlog组件。
在车道上显示的所有任务仅在我们应用程序的一个部分,因为我们希望能够将这些任务拖放到不同的车道中。我们将在下一节中学习如何做到这一点,我们将为板子添加动态功能。
使板子动态化
项目管理板通常具有很好的用户交互能力之一是能够将任务从一个车道拖放到另一个车道。这是可以使用 HTML5 拖放 API 轻松实现的事情,该 API 在所有现代浏览器中都是可用的,包括 IE11。
HTML5 拖放 API 使我们能够在项目管理板之间拖放元素。为了实现这一点,它使用拖放事件。onDragStart、onDragOver和onDrop将用于此应用程序。这些事件应该放置在Lane和Task组件上。
在Board组件的文件中,让我们添加响应拖放事件的函数,这些事件需要发送到Lane和Task组件。让我们开始吧:
-
首先为
onDragStart事件添加事件处理函数,该事件在拖动操作开始时触发,并将其添加到Board组件中。这个函数需要传递给Lane组件,然后可以传递给Task组件。此函数为正在拖动的任务设置一个 ID,并将其放置在元素的dataTransfer对象中,浏览器使用该对象来识别拖放元素:// ... + function onDragStart(e, id) { + e.dataTransfer.setData('id', id); + } function Board() { const [loading, error, tasks] = useDataFetching( 'https://my-json-server.typicode.com/ PacktPublishing/React-Projects-Second-Edition/ tasks', ); return ( <div className='Board-wrapper'> {lanes.map((lane) => ( <Lane key={lane.id} title={lane.title} loading={loading} error={error} tasks={tasks.filter((task) => task.lane === lane.id)} + onDragStart={onDragStart} /> ))} </div> ); } export default Board; -
在
Lane组件中,我们需要将这个事件处理函数传递给Task组件:// ... - function Lane({ title, loading, error, tasks }) { + function Lane({ title, loading, error, tasks, onDragStart }) { return ( <div className='Lane-wrapper'> <h2>{title}</h2> {loading || error ? ( <span>{error || 'Loading...'}</span> ) : ( tasks.map((task) => ( <Task key={task.id} title={task.title} body={task.body} + onDragStart={onDragStart} /> )) )} </div> ); } export default Lane; -
现在,我们可以在
Task组件中调用此函数,在那里我们还需要将可拖动属性添加到具有Task-wrapper类名的div元素上。在这里,我们将元素和任务 ID 作为参数传递给事件处理程序:import './Task.css'; - function Task({ title, body }) { + function Task({ id, title, body, onDragStart }) { return ( <div className='Task-wrapper' + draggable + onDragStart={(e) => onDragStart(e, id)} > <h3>{title}</h3> <p>{body}</p> </div> ); } export default Task;
在进行这些更改后,我们应该能够看到每个任务都可以被拖动。但不要将它们放在任何地方——还需要添加其他更新状态的拖放事件和事件处理程序。将任务从一个车道拖动到另一个车道可以通过单击任务而不释放鼠标并将其拖动到另一个车道来完成,如下面的截图所示:
图 3.4 – 交互式项目管理板
实现了 onDragStart 事件后,可以实施 onDragOver 和 onDrop 事件。让我们开始吧:
-
默认情况下,无法将元素拖放到另一个元素中——例如,将
Task组件拖放到Lane组件中。这可以通过调用onDragOver事件的preventDefault方法来防止:// ... function onDragStart(e, id) { e.dataTransfer.setData('id', id); } + function onDragOver(e) { + e.preventDefault(); + }; function Board() { const [loading, error, tasks] = useDataFetching( 'https://my-json-server.typicode.com/ PacktPublishing/React-Projects-Second-Edition/ tasks', ); return ( <div className='Board-wrapper'> {lanes.map((lane) => ( <Lane key={lane.id} title={lane.title} loading={loading} error={error} tasks={tasks.filter((task) => task.lane === lane.id)} onDragStart={onDragStart} + onDragOver={onDragOver} /> ))} </div> ); } export default Board; -
此函数需要被导入,并放置在
Lane组件中具有Lane-wrapper类名的div元素上作为事件处理程序:// ... - function Lane({ title, loading, error, tasks, onDragStart }) { + function Lane({ title, loading, error, tasks, onDragStart, onDragOver }) { return ( - <div className='Lane-wrapper'> + <div className='Lane-wrapper' onDragOver={onDragOver}> <h2>{title}</h2> // ... -
onDrop事件是事情变得有趣的地方,因为这个事件使得我们在完成拖动操作后能够修改状态成为可能。在我们能够添加此事件处理程序之前,我们需要在Board组件中创建一个新的局部状态变量tasks。当从useDataFetching钩子获取数据时,此状态变量会被覆盖,并用于显示来自Lane组件的任务:+ import { useEffect, useState } from 'react'; import Lane from '../../components/Lane/Lane'; import useDataFetching from '../../hooks/useDataFetching'; import './Board.css'; // ... function Board() { const [ loading, error, - tasks + data] = useDataFetching( 'https://my-json-server.typicode.com/ PacktPublishing/React-Projects-Second-Edition/ tasks', ); + const [tasks, setTasks] = useState([]); + useEffect(() => { + setTasks(data); + }, [data]); // ... return ( // ... -
现在可以创建新的事件处理函数,当它被调用时,我们可以从
useState钩子中的任务状态调用setTasks函数:// ... function Board() { // ... + function onDrop(e, laneId) { + const id = e.dataTransfer.getData('id'); + const updatedTasks = tasks.filter((task) => { + if (task.id.toString() === id) { + task.lane = laneId; + } + return task; + }); + setTasks(updatedTasks); + } return ( // ... -
此外,此事件处理函数应作为属性传递给
Task组件:// ... Return ( <div className='Board-wrapper'> {lanes.map((lane) => ( <Lane key={lane.id} + laneId={lane.id} title={lane.title} loading={loading} error={error} tasks={tasks.filter((task) => task.lane === lane.id)} onDragStart={onDragStart} onDragOver={onDragOver} + onDrop={onDrop} /> ))} </div> ); } export default Board;
此 onDrop 事件处理函数接受一个元素和车道 ID 作为参数,因为它需要拖动元素的 ID 和它应该放置的新车道。有了这些信息,函数使用 filter 函数找到需要移动的任务,并更改车道的 ID。这个新信息将用 setState 函数替换状态中当前的任务对象。
-
由于
onDrop事件是从Lane组件触发的,因此它作为属性传递给此组件。此外,还添加了车道 ID 作为属性,因为需要将其传递给来自Lane组件的onDrop事件处理函数:import Task from '../Task/Task'; import './Lane.css'; function Lane({ + laneId, title, loading, error, tasks, onDragStart, onDragOver, + onDrop, }) { return ( <div className='Lane-wrapper' onDragOver={onDragOver} + onDrop={(e) => onDrop(e, laneId)} > // ...
通过这种方式,我们能够将任务拖放到我们板上的其他车道上——你也可以为 Backlog 组件做同样的事情——或者甚至通过另一个自定义钩子使此逻辑可重用。但相反,我们将探讨如何通过使用 styled-components 库来使我们的组件样式更加灵活和可重用,在下一节中。
使用 styled-components 在 React 中进行样式设计
到目前为止,我们一直在使用 CSS 文件来为我们的 React 组件添加样式。然而,这迫使我们必须在不同的组件之间导入这些文件,这使得我们的代码的可重用性降低。因此,我们将向项目中添加 styled-components 包,这允许我们在 JavaScript 中编写 CSS(所谓 CSS-in-JS)并创建组件。
通过这样做,我们将获得更多样式的灵活性,能够防止由于 classNames 而导致的样式重复或重叠,并且可以轻松地为组件添加动态样式。所有这些都可以使用我们用于 CSS 的相同语法,直接在我们的 React 组件内部完成。
第一步是使用 npm 安装 styled-components:
npm install styled-components
注意
如果您查看 styled-components 的官方文档,您会注意到他们强烈建议您也使用此包的 Babel 插件。但是,由于您正在使用 Create React App 来初始化项目,因此您不需要添加此插件,因为 react-scripts 已经处理了您应用程序所需的全部编译。
在安装 styled-components 之后,让我们尝试从我们的组件中删除 CSS 文件。一个好的开始是 Task 组件,因为这个组件非常小,功能有限:
-
首先,导入
styled-components包并创建一个新的样式组件,命名为TaskWrapper。该组件扩展了一个div元素,并采用了我们已经在Task.css中的Task-wrapper类名所拥有的 CSS 规则。此外,我们不再需要导入此文件,因为所有的样式现在都在这个 JavaScript 文件内部完成:+ import styled from 'styled-components'; - import './Task.css'; + const TaskWrapper = styled.div` + background: darkGray; + padding: 20px; + border-radius: 20px; + margin: 0% 5% 5% 5%; + h3 { + width: 100%; + margin: 0; + } + `; function Task({ id, title, body, onDragStart }) { return ( - <div className="Task-wrapper" + <TaskWrapper draggable onDragStart={(e) => onDragStart(e, id)} > <h3>{title}</h3> <p>{body}</p> - </div> + </TaskWrapper> ); } export default Task; -
在前面的代码块中,我们在
TaskWrapper样式组件中添加了h3元素的样式,但我们也可以在特定的样式组件内部做同样的事情:import styled from 'styled-components'; // ... - h3 { - width: 100%; - margin: 0; - } - `; + const Title = styled.h3` + width: 100%; + margin: 0; + `; function Task({ id, title, body, onDragStart }) { return ( <TaskWrapper draggable onDragStart={(e) => onDragStart(e, id)} > - <h3>{title}</h3> + <Title>{title}</Title> <p>{body}</p> </TaskWrapper> ); } export default Task; -
我们也可以为项目中的其他组件做同样的事情,从
Lane组件开始,我们需要首先创建使用与Lane.css文件中相同的样式的样式组件:+ import styled from 'styled-components'; import Task from '../Task/Task'; - import './Lane.css'; + const LaneWrapper = styled.div` + text-align: left; + padding: 0; + background: lightGray; + border-radius: 20px; + min-height: 50vh; + width: 20vw; + @media (max-width: 768px) { + margin-bottom: 5%; + } + `; + const Title = styled.h2` + width: 100%; + padding-bottom: 10px; + text-align: center; + border-bottom: 1px solid darkGray; + `; function Lane({ // ... -
将现有的
div和h3元素替换为这些新组件:// ... function Lane({ laneId, title, loading, error, tasks, onDragStart, onDragOver, onDrop, }) { return ( - <div className="Lane-wrapper" + <LaneWrapper onDragOver={onDragOver} onDrop={(e) => onDrop(e, laneId)} > - <h3>{title}</h3> + <Title>{title}</Title> {loading || error ? ( <span>{error || 'Loading...'}</span> ) : ( // ... )} - </div> + </LaneWrapper> ); } export default Lane;
如果我们在浏览器中再次运行 npm start 后访问我们的项目,我们会看到在删除了 Ticket 和 Lane 组件的 CSS 文件后,我们的应用程序仍然看起来一样。当然,您也可以为项目中的其他组件做同样的事情。
让我们继续将另一个组件转换为使用 styled-components 而不是 CSS,例如 src/App.js 文件中的组件。这个组件正在使用 src/App.css 文件来为包含我们应用程序中所有组件的 div 元素添加样式:
- import './App.css';
+ import styled from 'styled-components';
import Board from './pages/Board/Board';
import Header from './components/Header/Header';
import Backlog from './pages/Backlog/Backlog';
+ const AppWrapper = styled.div`
+ text-align: center;
+ `;
function App() {
return (
- <div className='App'>
+ <AppWrapper>
<Header />
<Board />
<Backlog />
- </div>
+ </AppWrapper>
);
}
export default App;
在进行这些更改后,您可以删除 src/App.css 文件,因为我们不再使用它来为 App 组件添加样式。
使用styled-components的另一种可能性是为我们的应用程序创建一个全局样式,这目前在src/index.css中完成。此文件在src/index.js中导入,因此被加载到应用程序的每一页,因为它是我们 React 应用程序的入口。但是src/App.js中的App组件也包装了所有我们的组件,我们可以在其中复制src/index.css中的样式规则,并使用它们来创建一个GlobalStyle组件:
- import styled from 'styled-components';
+ import styled, { createGlobalStyle } from
'styled-components';
import Board from './pages/Board/Board';
import Header from './components/Header/Header';
import Backlog from './pages/Backlog/Backlog';
+ const GlobalStyle = createGlobalStyle`
+ body {
+ margin: 0;
+ font-family: -apple-system, BlinkMacSystemFont,
'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu',
'Cantarell', 'Fira Sans', 'Droid Sans',
'Helvetica Neue', sans-serif;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ }
+ `;
const AppWrapper = styled.div`
// ...
我们刚刚创建的全局样式必须添加到App组件的返回语句中,在AppWrapper组件之上。因为我们只能从return语句返回一个元素或组件,所以我们需要将内容包装到另一个元素中。如果我们想给这个元素应用样式,我们可以使用一个div元素。但由于在这个场景中我们不希望这样做,我们将使用 React 片段。使用片段,我们可以包装元素和组件,而不会在浏览器中渲染任何内容:
// ...
function App() {
return (
+ <>
+ <GlobalStyle />
<AppWrapper>
<Header />
<Board />
<Backlog />
</AppWrapper>
+ </>
);
}
export default App;
注意
<>符号是<React.Fragment>的简写;你可以在 React 中使用这两种符号。对于<React.Fragment>符号,你也可以从 React 中导入Fragment来编写<Fragment>。
最后,你可以删除src/index.css文件和src/index.js中导入此文件的行:
import React from 'react';
import ReactDOM from 'react-dom/client';
- import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
const root = ReactDOM.createRoot(
document.getElementById('root'));
// ...
通过这些最终添加,我们已使用styled-components而不是 CSS 来设计了我们应用程序的大部分样式。通过直接在组件中编写样式规则,我们可以减少项目中的文件数量,并使查找应用于我们元素的风格变得更加容易。
摘要
在本章中,你创建了一个项目管理板,允许你使用 HTML5 拖放 API 将任务从一个通道拖放到另一个通道。此应用程序的数据流由本地状态和生命周期处理,并确定哪些任务显示在不同的通道中。本章还介绍了 React 的高级模式自定义 Hooks。使用自定义 Hooks,你可以在你的应用程序中跨功能组件重用状态逻辑。
这种高级模式也将在下一章中使用,下一章将处理 React 应用程序中使用 Next.js 的服务器端渲染(SSR)和路由。你有没有尝试过使用 Stack Overflow 来寻找你曾经遇到过的编程问题的解决方案?我有!在下一章中,我们将构建一个使用 Stack Overflow 作为数据源并使用 React 来渲染应用程序的社区动态。
进一步阅读
-
HTML 拖放 API:
developer.mozilla.org/en-US/docs/Web/API/HTML_Drag_and_Drop_API -
DataTransfer:
developer.mozilla.org/en-US/docs/Web/API/DataTransfer -
React DnD:
github.com/react-dnd/react-dnd