React 和 Next.js 简介
原文地址: Just Enough React (and Next.js)
翻译: JulySong
在新手篇教程中,我们构建了一个非常简单的 dApp,它使用了 HTML、CSS 和一些 Javascript。然而,在现实世界中,这些类型的“普通”网站实现已成为过去。
今天,我们使用 Web 框架来简化 Web 开发过程。它更简单吗?取决于你从谁的角度来看。如果您刚刚开始并且以前从未这样做过,那么您可能需要一些时间才能理解所有必要的概念。但是,从长远来看,您会感谢自己并为您花时间学习它而感到高兴。
当今使用的最大和最常见的 Web 框架是:
尽管它们各有优缺点,但 React 迄今为止已经席卷了 Web 开发世界。在 Web3 领域也是如此,其中 React 是构建 dApp 最常用的 Web 框架。在整个入门篇教程以及所有后续教程中,我们将使用大量 React,因此将此级别视为 React 速成课程,它将教你足够的入门知识。这并不是要替代在专注于 Web2 教学的平台上学习 React,而是用作了解入门需要多少知识的指南,这样您就不会陷入教程地狱。😅
实际上,我们将使用 Next.js——它是 React 本身的扩展——但稍后会详细介绍。让我们先学习一些 React。
WTF 是 React?
React 是一个 Web 框架,可以轻松构建和推理 Web 应用程序的“视图”。“视图”是屏幕上显示的内容,它如何变化、如何更新等。React 本质上只是为您提供了一种模板语言,您可以创建返回 HTML 的 Javascript 函数。
普通的 Javascript 函数返回 Javascript 风格的东西——字符串、数字、布尔值、对象等。React 基本上结合了 Javascript 和 HTML 来产生一种他们称为 JSX 的语言。在 JSX 中,类似 Javascript 的函数返回 HTML 而不是常规的 Javascript 内容。基本上就是这样。
返回 HTML 的 Javascript 函数的组合称为组件。组件是用 JSX 编写的。虽然一开始看起来很尴尬,但一旦你习惯了,它们实际上很容易使用。
这是一个简单组件的示例:
1 | function ShoppingList() { |
很好,但这与 HTML 没有太大区别。但是,如果您想基于数组呈现项目列表怎么办?
1 | function ShoppingList() { |
哇,看那个!我们只是在 HTML 中使用了 Javascript。在 JSX 中,您可以通过将 JS 代码包装在花括号{
和 }
中来在 HTML 中编写 Javascript。但实际上,如果您再考虑一下,您就会意识到正在发生的事情。.map()
是一个 Javascript 函数,它遍历一个数组并为每个项目返回一些内容。在这种情况下,它循环遍历 items
数组并返回一个 li
元素,该元素具有其中的 Javascript 变量 item
的值,即 HTML。得到它?🤔
在我们的组件内部,我们基本上嵌入了另一个组件。map 函数是一个返回 HTML 的 JS 函数。它是一个组件。即使它没有明确定义为顶级函数,它仍然是一个组件。
将组件嵌入到其他组件中是 React 的强大功能。这称为组合(Composition
)。我们将多个返回 HTML 的 Javascript 函数组合在一起,并从中构建一个组合的 HTML 文档,该文档将显示在 Web 应用程序上。
组件之间的数据传递
如果组件只是静态的,它们就不是很有用。当然,遍历数组和其他东西都很好,但当今大多数 Web 应用程序都不是静态文档。今天的大多数 Web 应用程序都会从某种服务器、数据库或区块链中动态获取数据。这意味着经常需要相同的组件来显示不同的数据。
拥有组件的主要用例是能够编写可重用的代码,并且可以在其中包含不同的信息,而无需再次重写整个代码。
让我们看一个例子。这两个代码哪个更易读?
1 | <div class="cards"> |
1 | function Cards() { |
纯 HTML 示例重复使用相同的代码 3 次,尽管真正改变的只是人名和他们的职位。
在 JSX 中,我们可以将每个 Card
抽象为一个组件,该组件从其父组件(在本例中为 Cards
)获取某些数据。父组件通过类似 HTML 的属性(name="Alice"
)将数据传递给子组件,子组件访问它们的值就像 JS 函数接收 JS 对象作为参数一样。然后 Card
组件可以根据从父级接收到的内容返回带有可变数据的 HTML。
这段代码更容易重用和扩展。想要稍微改变所有卡片的外观吗?只需修改一个组件!并非所有复制粘贴的 HTML。
交互式组件
好的,所以我们现在可以在组件之间传递数据。这一切都很好,但我们还没有添加交互性。诸如在单击按钮或在输入框中键入文本时能够运行一些代码等。
值得庆幸的是,在 Javascript 中,函数可以在其中包含函数。例如,
1 | function someFunc() { |
otherFunc
只能在 someFunc
自身内部使用。这是常规 Javascript 中很少使用的功能,但在使用 React 时使用非常频繁。让我们通过一个例子来看看为什么。
1 | function Button() { |
我们有一个名为 Button
的 JSX 函数。在这个函数中,我们有另一个函数叫做 handleClick
。在 HTML <button>
标记中,我们指定 onClick={handleClick}
以便在单击按钮时调用 handleClick
函数。此功能仅在 Button
组件内部可用。单击 Web 应用程序上的按钮将在浏览器控制台中打印 Hello
。这就是我们使用 React 构建交互式网站的方式!
这个例子仍然相当简单,因为 handleClick
没有参数。如果我们想在用户输入输入框时在控制台中打印文本怎么办?我们如何将文本传递给函数?
就是这样。
1 | function PrintTypedText() { |
HTML input
元素提供了一个方便的事件侦听器 - onChange
- 每次该输入框中的文本发生更改(键入新字符、删除字符等)时都会触发该事件侦听器。
但是,除了触发一个函数之外,它还传递了更改的 HTML 元素(此处称为 e
)。然后,我们可以获取 HTML 元素 e
并使用 e.target.value
提取文本,并将其作为参数传递给 handleOnChange
,它将文本记录到浏览器控制台。
不同的 HTML 元素有不同的事件处理程序 - onChange
和 onClick
是两个例子,但还有更多!您可以在此处找到所有 HTML 事件的列表。
通过将 HTML 事件与函数处理程序相结合,我们可以做各种很酷的事情!从服务器加载数据、向服务器发送数据、更新我们的视图等。
React Hooks - useState 和 useEffect
好的,我们已经讨论了组合、数据传递和交互性。但是,我们的应用程序仍然很愚蠢。交互性将允许您在单击按钮等时运行一些代码,但是如果您想更新一些变量怎么办?
不幸的是,以下不起作用
1 | function DoesNotWork() { |
无论你点击多少次 Increment
按钮,屏幕上显示的数字都会卡在 0
。这是因为当你从 React 组件中更新 myNumber
等常规变量时,即使值更新了,React 实际上并没有重新渲染 Web 应用程序的视图。它不会自动更新页面的 HTML 视图。
React Hooks 是“挂钩”到 React 组件的不同部分的函数,允许您执行诸如在变量的值更改时更新视图,或者在每次加载页面或更改变量时自动运行一些 JS 代码等操作,以及许多更酷的东西!我们将主要关注 95% 的时间使用的三个 React 钩子 - useState
、useEffect
和 useRef
。
useState
很多时候,您希望 HTML 视图根据某些变量的值变化进行更新。我们可以使用 useState
钩子来维护一个变量,该变量会在每次更改其值时自动重新渲染屏幕上显示的 HTML。这是一个例子:
1 | function ThisWorks() { |
如果您尝试运行上述代码,您将看到 Web 应用程序的视图自动更新以反映变量的新值。
useState
在 React 中使用创建的变量称为状态变量。状态变量可以更新并自动更新应用程序的视图。这是另一个在输入框中使用状态变量的示例。
1 | function StateWithInput() { |
关于 useState,我想说的最后一件事是,您还可以将它用于字符串和数字等基本类型。您还可以使用它们来存储数组和对象。但是,这里有一个警告。让我们看一个例子:
1 | function StateArrayDoesNotWork() { |
如果您尝试运行它,您将看到屏幕上没有显示任何水果。另请注意,我们没有在 setFruits
任何地方使用该函数,而只是尝试.push
使用 fruits
数组。
当我们尝试直接更新数组时,React 不会注册状态更改,这也被认为是无效的状态更新,可能导致应用程序出现意外行为。我们知道我们需要以某种方式使用 setFruits
,但是如何使用呢?答案是我们实际上需要创建水果数组的副本,将水果添加到其中,并将状态变量完全设置为新数组。下面的例子:
1 | function StateArray() { |
如果您尝试运行上述代码,您将看到它按预期工作。每次按下按钮时,输入框中的当前文本都会添加到数组中,这会导致 HTML 上显示的水果列表更新。您可以继续添加任意数量的水果!
对象也是如此。如果您的状态变量包含一个对象,您需要先创建该对象的副本,更新一个值,然后将状态变量完全设置为新对象。
useEffect
所以我们现在可以管理状态,太好了!状态变化也会影响我们渲染的 HTML,也很棒!
但是,通常需要在第一次加载页面时自动运行一些代码——也许是为了从服务器或区块链获取数据——并且还需要在某个状态变量发生变化时自动运行一些代码。
这些类型的功能称为副作用。React 为我们提供了 useEffect
钩子,它允许我们编写这些效果。useEffect
有两个参数——一个函数和一个依赖数组。函数是运行效果时运行的代码,依赖数组指定何时触发副作用。
考虑一个示例,当网站首次加载时,它想从服务器加载一些数据。这样做时,它希望向用户显示一个加载屏幕,然后在加载数据后,移除加载屏幕并显示实际内容。我们如何做到这一点?
1 | function LoadDataFromServer() { |
如果您从链接运行上述代码,您将看到它在屏幕上显示 Loading...
5 秒钟,然后显示 ABCDEF
。这是因为 apiCall
是一个只等待 5 秒然后返回字符串 ABCDEF
的函数。
首次加载页面时 useEffect
调用 loadData
- 由于依赖项数组为空 - 并且状态变量使 HTML 呈现适当的内容。
这对于在页面第一次加载时运行代码很有用,但是每次状态变量的值发生变化时运行一些代码呢?例如,当您在 Facebook 上搜索某人的姓名时,Facebook 如何在您每次添加/删除角色时获取并显示推荐?
您也可以使用 useEffect
来做到这一点,方法是在依赖数组中提供状态变量。每次该变量的值发生变化时,都会运行效果。
1 | function DependentEffect() { |
如果您运行上述代码并尝试输入一些字母,您将看到推荐列表会随着您在搜索框中添加/删除新字符而自动更新。这是因为当您更新输入框时,searchText
的值会通过 onChange
处理程序进行更新,该处理程序会触发 useEffect
,它会更新 recommendations
列表,从而更新 HTML 视图。
您也可以类似地创建依赖于多个状态变量的副作用,而不仅仅是一个。如果任何因变量发生变化,就会产生副作用。您只需将更多状态变量添加到依赖项数组即可。
useRef
useRef
是另一个比较常用的 React 钩子。它表面上与 useState
非常相似,但有一些实际上非常重要的细微差别使得这个 React Hook 学习起来很重要。
useRef
变量的创建方式如下:
1 | function Component() { |
1. 没有重新渲染
与 useState
类似,useRef
挂钩也允许我们将变量存储在组件中,该变量可以随时间更新。但是,与状态变量不同,更新 ref 变量的值不会导致 HTML 视图重新呈现。
因此,如果您有一个 useRef
变量并且您在 HTML 视图中显示它的值,则更新该变量不会更新 HTML 视图。
1 | function CounterWithRef() { |
如果您运行上面的代码,您会注意到每次单击按钮时,值都会递增并打印在浏览器控制台中,但 HTML 视图实际上并没有更新。事实上,HTML 视图不显示任何内容,因为 myNumber.current
的初始值是undefined
的,并且由于 HTML 没有更新,因此就 HTML 而言,即使值实际上正在更新,它仍保持undefined
。
2. 同步更新
关于 useState
,我们之前没有提到的是,当我们使用 setXYZ
函数更新状态变量时,它实际上不会立即更新。
在 React 中为状态变量设置新值是异步发生的,这意味着如果您在将状态变量设置为新值后立即尝试使用它的值,您可能实际上看不到新值被反映,因为它是异步发生的。
我们再来看看使用 useState
时的 Counter 例子。
1 | function AsyncStateVariables() { |
当您运行它时,请注意视图上发生了什么以及控制台中发生了什么。当您第一次单击按钮时,状态变量应该更新为 1
- 这就是视图上发生的情况,网页显示 1
。但是如果您查看浏览器控制台,则会打印值 0
而不是 1
。这种模式继续单击按钮。
这是因为 setNumber
调用是异步运行的,当我们到达 console.log(number)
行时,该值还没有更新,所以它打印了 number
的旧值。当它确实被更新时,HTML 被重新渲染以显示新值。
另一方面,useRef
允许同步更新。当您使用 myVar.current = newValue
更新参考变量的值时,它会立即更新,并且没有延迟。这有时会派上用场。
3. 引用 DOM 元素
useRef
让我们做的另一件很酷的事情是它允许我们直接引用 DOM 元素。这是 useState
无法实现的。
例如,您可以使用 useRef
直接引用input
元素
1 | function InputFocus() { |
当您运行上面的示例时,您会注意到页面加载后,该 input
元素已经在焦点上,即您可以开始输入而无需先单击它。这是因为我们持有对 input
元素的引用,并且由于有一个空的依赖数组,因此在页面加载时运行的 useEffect
专注于 input
元素。
React 文件结构
太好了,如果你刚刚开始使用 React,我们已经介绍了你应该知道的主要概念。但到目前为止,我们只处理了孤立的组件示例。一个实际的 React 项目是什么样子的?
React 应用程序通常使用 create-react-app
(CRA)之类的工具创建。CRA 是一个命令行工具,可帮助您设置新的 React 项目并安装所有必需的依赖项,而无需手动创建所有样板。
当您使用 CRA 时,您最终会得到如下所示的文件结构。
package.json
文件应该是可识别的。CRA 通过 Node.js 环境工作,而 package.json
是创建所有依赖项和项目元数据的地方 - 与任何其他 Node.js 项目一样。
src/
文件夹包含组件和 CSS 样式,基本上是任何特定于 React 的代码。这里的主要组件是App.js
,它是您第一次设置 React 应用程序时创建的自动生成的组件。index.js
是 React 应用程序的主要入口点,但通常您不需要太多(或根本不需要)修改它。它只包含一些样板 React 代码,这些代码获取您的组件并将其转换为可以在浏览器中运行的实际 HTML 和 JS。
然后,public/
文件夹默认只包含一个文件 - index.html
。你通常不会自己碰这个。这是一个超级简单的准系统 HTML 文件。当一个 React 应用程序运行时,React 会在后台执行一些魔法,它会获取所有组件和 Javascript 代码,将其转换为可以在浏览器中运行的实际 HTML 和 JS,并用所有这些替换 index.html
的内容。然后,更新后的 index.html
就是向用户显示的内容。
如果您想将图像、字体、音乐等添加到您的网站,它们也会进入 public/
文件夹。public/
文件夹基本上包含您希望在您的网站上直接访问的所有内容。
例如,如果您在 public/
文件夹中添加了一个名为 avatar.png
的图像,那么您可以在 React 组件中显示该图像,如下所示:
1 | <img src="/avatar.png" /> |
虽然这可能看起来很奇怪,因为您的组件位于 src/
文件夹中而不是 public/
文件夹中——它起作用的原因是因为图像与 index.html
位于同一文件夹中——而 index.html
是你的 React 代码实际结束的地方向上。因此,当使用 /avatar.png
的相对路径引用图像时,它知道 avatar.png
必须在public
文件夹内。
后端时间
到目前为止,我们一直在讨论 React,以及它的所有前端功能。但是后端呢?
React 不是后端框架,因此如果您想创建自己的 API 后端,则必须使用 Node.js 和 Express 之类的东西建立一个单独的项目。然而这很麻烦,就好像后端和前端是同一个项目一样,你可能有很多代码可以在两者之间重用和共享。此外,维护两个项目总是比只维护一个项目更难。
输入,Next.js
接下来是 React 的元框架。这是什么意思?嗯,React 本身就是一个构建 Web 应用程序的框架。接下来是一个 React 框架,它还引入了一些 React 没有的附加功能。
如果你了解 React,那么 Next 90% 是完全一样的东西,你可以很快开始使用它,但是我想谈谈 Next 带来的这些额外功能。
首先,正如标题和介绍所暗示的那样,Next 允许您在单个项目中编写前端和后端代码。您使用 React 构建前端,并使用与使用 Express 类似的语法编写后端 API 端点 - 但都在同一个项目中。
其次,Next 使创建多页 Web 应用程序变得非常容易。React 最初旨在帮助创建单页应用程序(SPA),组件非常适合!但是,如果您的网站有多个页面怎么办?例如https://learnweb3.io/
和https://learnweb3.io/about
和https://learnweb3.io/tracks
等等。
为此,React Router
引入了诸如此类的库,这使之成为可能,但也有点麻烦。“下一步”通过允许基于文件名的自动页面路由大大简化了这一点。
最后,Next 还具有服务器端渲染 (SSR) 和静态站点生成 (SSG)。这些不是我们将在我们的曲目中使用的功能,所以我不会在这里花太多时间,但如果您想了解更多关于它们的信息,请随时阅读推荐阅读。
Next 路由
在讨论创建后端服务器之前,我们将讨论路由,因为这将帮助您了解它是如何工作的。
与 create-react-app
类似,Next 有一个名为 create-next-app
的工具,可以自动帮助您轻松设置新的 Next.js 项目。
当您创建一个新的 Next.js 项目时,您最终会得到一个如下所示的文件结构:
这是很多文件!但别担心,其中很多与我们已经讨论过的 React 类似。
public/
文件夹的工作方式完全相同,但不包含 index.html
文件。但是,如果您想添加图像、图标、字体、音乐等,您可以将它们全部放在 public/
文件夹中。
styles/
文件夹是一个很好的补充,为您的所有 CSS 文件提供了一个专用位置。
pages/
很棒。_app.js
是一个自动生成的文件,通常你不会自己去碰它,它设置了一些样板代码,允许 Next 渲染正确的组件。
pages/index.js
是您网站的主页。基本上,文件 pages
夹中的每个文件都是您网站的路线。遵循 Javascript/HTML 样式的命名约定,这意味着 index
文件是“主”文件。因此,pages/index.js
是您首次打开网站时将加载的视图。
如果您在 pages
文件夹下添加更多文件,例如名为 about.js
的文件 - 它将在 YOUR_DOMAIN/about
中可用(有趣的事实:LearnWeb3 的网站是使用 Next 创建的,这正是我们的https://learnweb3.io/about
页面工作)。
这很棒,因为您不必处理诸如 React Router 之类的事情,并且构建多页面网站就像在 pages/
文件夹下创建一个新文件一样简单,Next 会自动为您生成基于文件名的路由。
您还可以通过在文件夹下创建子文件夹来进行多级路由 pages/
。例如,类似 pages/tracks/freshman.js
的东西会有 route YOUR_DOMAIN/tracks/freshman
。
在 Next 中编写 API
pages/
下有一个特殊文件夹,但是它也是自动生成的。pages/api
文件夹。与呈现 HTML 视图的常规页面不同,pages/api
文件夹下的任何内容都充当 API 端点。
让我们看一下自动生成的 pages/api/hello.js
文件:
1 | export default function handler(req, res) { |
这是一个非常类似于 Express 的功能。如果您要转到 YOUR_DOMAIN/api/hello
- 而不是呈现 HTML 视图,这将返回一个 JSON 对象 {name: 'John Doe'}
- 这是一个超级简单的 API 端点。
与常规 HTML 视图类似,您可以通过创建新文件在 pages/api
下创建 API 端点,并且端点路由基于文件名。
结论
我希望这篇文章对您有所帮助,并且可以作为速成课程。我故意在这里更多地关注 React 而不是 Next,因为习惯前端部分对我们来说将比后端部分更相关。此外,后端代码基本上是常规的 Javascript,而前端是 JSX,我想让你更熟悉它。
我列出了一些额外的阅读材料和视频,我建议这些阅读材料和视频可以更好地理解这些概念。与往常一样,如果您有任何问题,请在#web2-support 的频道上留言,我们很乐意为您提供帮助。
读物/视频
练习题
🤔 JSX 的强大之处是什么?
A: 它是 Javascript 的扩展,增加了静态类型
B: 它是 Javascript 的扩展,允许您从 JS 函数返回 HTML 元素
C: 它是一个 Web 框架,可以帮助您推断应用程序的视图
🤔 什么是组合(
Composition
)?A: 它是多个组件的组合,以创建单个分层 HTML 文档
B: 它是 React 库的依赖构成
C: 这是 React 官方歌曲
🤔 你可以通过 props 向组件传递任意数据吗?
A: 是
B: 否
🤔 React 组件如何让你的代码更容易理解?
A: 通过使用 Github Copilot 自动为您生成代码
B: 通过数据传递实现组件的可重用性
C: 使用更简单的编程语言
🤔 以下代码在运行时会做什么?
A: 返回数字 3
B: 因错误而崩溃
🤔 点击增量按钮会发生什么?
A: 应用程序的视图将更新以显示新的递增值 num
B: 什么都不会发生
C: 应用程序会崩溃
🤔 为什么在 React 中使用状态变量(useState)?
A: 它们用于维护在值更改时重新呈现 HTML 的值
B: 它们只是语法糖,没有真正的用途
C: 它们是在 React 组件中定义变量的唯一方法
🤔 useEffect hook 只能在页面首次加载时运行?
A: 是
B: 否
🤔 点击开始按钮会发生什么?
A:
num
的值加一并且 HTML 视图更新以显示新值B: 什么都没发生
C: React 应用程序陷入无限循环
🤔 useEffect 只能依赖一个值?
A: 是
B: 否
🤔 useRef 和 useState 有什么区别?
A: 对引用变量的更新是同步完成的,而对状态变量的更新是异步完成的
B: 没有区别
C: useRef 仅用于引用 DOM 元素,而 useState 用于维护变量值
🤔 在网页间路由方面,React 和 Next.js 的主要区别是什么?
A: React 自动进行基于文件的路由,而 Next 需要一个路由库
B: 它们完全一样
C: React 需要一个路由库,而 Next 自动执行基于文件的路由
🤔 如何在 Next.js 应用程序中编写 REST API 端点?
A: 通过在 pages/ 子目录中创建文件
B: 通过在 pages/api/ 子目录中创建文件
C: 通过在 api/ 子目录中创建文件
🤔 React 组件在 JSX 中使用时必须以大写字母开头?
A: 是
B: 否
参考答案:
- B
- A
- A
- B
- B
- B
- A
- B
- C
- B
- A
- C
- B
- A