NextJS 笔记
这是一篇课堂笔记,课程为 Udemy React - The Complete Guide 2024。该笔记包含了该课程中有关 NextJS 的部分内容。
NextJS 允许你使用 React 开发全栈应用。
服务端组件
在 NextJS 中,你使用常规的 JSX 语法创建组件。但在默认情况下,这些组件都是服务端组件(React Server Components, RSC),这意味着,如果你在其中添加一句 console.log()
,然后打开浏览器的控制台,你并不能在此处看到任何输出。相反,你可以在你运行这个 NextJS App 的终端中看到它们。
1 | export default function Page() { |
原生 React 中就存在着客户端与服务端组件的区分,但是当我们使用 Vite 等构建系统时,默认都使用客户端组件。
使用服务端组件的好处是:服务端会将最后渲染完成的代码发给客户端显示,它们更小,并且对 SEO 更友好(因为如果使用客户端组件,页面实际上都是空的,其中的内容都是其中的 JS 代码在填充;而使用服务端组件,右键查看源代码,可以看到显示的就是已经渲染的组件的代码)。
但是,不是所有的 React 功能都在服务端组件可用,例如 Hooks、事件处理就仅在客户端组件中可用(这是合理的)。
为了在 NextJS 中使用客户端组件,需要在组件文件首行添加:
1 | "use client"; |
App 路由
NextJS 使用一个基于目录的路由,默认推荐使用的是 App Router。在项目目录中,你可以找到一个名为 app
的目录。通过在此目录下创建相应的文件,就可以创建出路由。例如,如果希望创建 /about
页面,就需要在 app
中创建 about
目录,并在其中添加 page.js
。你必须遵循特定的名称,这些特定名称的文件对应不同的功能:
page.js
:定义页面组件layout.js
:定义页面的包装器not-found.js
:定义 404 页面error.js
:定义错误页面loading.js
:定义加载页面route.js
:创建 API 路由,它不返回 JSX 代码,而直接返回 JSON 数据- ...(更多见 文档)
顺便一说,既可以使用 .js
,也可以使用 .jsx
。
现在你可以在 app/about/page.js
中定义关于页面的组件了。该组件的名称是无关紧要的:
1 | export default function AboutPage() { |
为了在 NextJS 中切换路由,可以这样做:
1 | import Link from "next/link"; |
动态路由
如果我们有很多博客文章,它们位于 /posts/post-1
、/posts/post-2
等等路由下。我们可以在 posts
目录下,创建一个特殊的以 []
包裹的目录,例如 [slug]
,然后在其中编辑 page.js
:
1 | export default function BlogPostPage({ params }) { |
"Slug" 指的是一种用于生成 URL 的字符串。它是一个较短的、可读的、通常是唯一的标识符,用于在 URL 中表示某个特定的资源。它通常是从资源的标题或名称中派生出来的,并且通常是小写字母、数字和连字符的组合,以便在浏览器中更容易阅读和理解。
例如,如果你有一篇博客文章标题为 “如何学习编程”,那么对应的 slug 可能是 “ru-he-xue-xi-bian-cheng”。
获取路由的路径
为了获取当前路由,可以使用 usePathname
:
1 | "use client"; |
Layout
NextJS 允许你使用 layout.js
来决定页面应该如何显示在什么位置。项目中必须包含一个 RootLayout
,它定义在 app/layout.js
中:
1 | import "./global.css"; |
注意到两件事情:
- 我们在该函数组件中使用了
<html>
和<body>
,这在一般的 React 项目中是没有的; - 我们没有在
<html>
中包含<head>
并定义一些 metadata,相反,我们导出了一个名为metadata
的变量,并在其中配置了一些 metadata。事实上,变量metadata
在 NextJS 中是保留的,它会自动地插入到最后生成的 HTML 中。
你也可以创建一个嵌套的 Layout,只需要在子目录中创建一个 layout.js
。这个 Layout 始终会嵌套在上层 Layout 中。
网站图标
通过在 app
目录下直接添加一个名为 icon
的文件(例如 icon.png
),就可以将该图片作为 NextJS App 的 Favicon。
自定义组件
可以创建一个与 app
同级的目录 components
专门用于存放自定义组件,例如,如果存在 components/header.js
,可以在需要使用它的地方按如下方法使用:
1 | import Header from "@/components/header"; |
导入语句中的 @
用于标识根目录。它实际上是个别名,定义于根目录下的 jsconfig.json
文件中。
图像
就像在原生 React 中那样,你可以使用类似于下面的语句导入一张图片:
1 | import logoImg from "@/assets/logo.png"; |
但是,与原生 React 不同的是,你不能这样做:
1 | <!-- wrong way in next.js --> |
而必须使用其 src
属性:
1 | <img src={logoImg.src} alt="Logo" /> |
但是更建议的做法是,使用 NextJS 提供的 <Image>
。它提供了自动的懒加载、自动检测图片大小、自动为不同设备提供不同大小的图片、自动以 WebP 格式传输以提高效率以及大量其他高级功能(这里添加了 priority
关闭了懒加载功能,因为这是个 Logo,始终应该显示):
1 | import Image from 'next/image'; |
可以使用 fill
prop,来指定图片填充于父组件定义的空间中。这种方法适用于需要显示长宽未知的图片的情况。
CSS
在前面我们已经看到了全局 CSS 的导入方法,在我们的 Root Layout 中:
1 | import "./global.css"; |
NextJS 还支持 CSS 模块,只需要使用类似 *.module.css
的命名方式,就可以使用下面的方法导入并使用该 CSS 定义的内容,并且将其限定在特定的组件中:
1 | .header { |
1 | import classes from './header.module.css'; // you can choose any name |
配置 SQLite 数据库
下面使用 better-sqlite3
配置了一个 SQLite 数据库供后面使用:
1 | npm install better-sqlite3 |
1 | const sql = require("better-sqlite3"); |
获取数据
在 NextJS 中,你仍然可以像原生 React 中做的那样,使用 useEffect
配合 fetch
用请求的方式从某个后端获取数据。
但是,由于我们在使用 NextJS 这样一个全栈框架,我们已经拥有了一个后端。所以,在这里直接操作数据库也是安全的。
创建 meals.js
:
1 | import sql from "better-sqlite3"; |
然后,在需要使用它的组件代码中:
- 与原生 React 不同,你可以将组件函数使用
async
修饰
1 | import { getMeals } from "@/lib/meals"; |
加载
为了添加加载页面,只需要在对应路由的目录下,添加 loading.js
,并编辑一些自定义的加载代码即可:
1 | export default function MealsLoadingPage() { |
这样的加载是面向整个页面组件的。如果我们希望只是加载部分元素,可以使用 Suspense
:
1 | import { Suspense } from "react"; |
错误
通过在对应路由目录下添加 error.js
,可以创建错误页面。该组件必须是客户端组件:
1 | "use client"; |
Not Found
通过在对应路由目录下添加 not-found.js
,可以创建 Not Found 页面。
1 | export default function NotFound() { |
如果希望以编程方式手动切换到 Not Found 组件,可以使用:
1 | import { notFound } from "next/navigation"; |
Server Action
通过在函数中添加 'use server';
可以确保某个函数在服务器上执行,此外,还必须将该函数使用 async
修饰。
顺便一说,这个功能在原生 React 中是存在的。但为了让其生效,你必须使用一个像是 NextJS 这样的框架。
1 | export default function ShareMealPage() { |
'use server';
无法在客户端组件中使用,所以你可能需要将 Server Actions 存储到单独的文件中:
1 | "use server"; |
添加 XSS 保护
如果我们需要将用户填写的内容创建为 HTML,就必须添加 XSS 保护。下面的例子还使用 Slugify 来根据标题创建 Slug:
1 | // npm install slugify xss |
保存文件和数据
使用 NodeJS 提供的 fs
,可以存储将文件存储到服务器本地:
1 | import fs from 'node:fs'; |
但这不是推荐的做法。为了存储文件,最好还是使用像是 AWS S3 这样的服务。
使用 useFormStatus
管理表单提交状态
1 | "use client"; |
使用 useFormState
来接受 Server Action Response
注意这里是 useFormState
,与 useFormStatus
区分。它有点像 useState
。
- 它接受两个参数:第一个是实际应当在表单提交时触发的 Server Action;第二个是表单的初始值。
- 它返回两个参数:第一个是表单初始值,或者 Server Action 的响应;第二个用于和
form
绑定。
需要注意useFormState
仅在客户端组件中可用。
1 | 'use client'; |
我们还需要更改我们的 Server Action shareMeal
(添加 prevState
作为第一个参数):
1 | export async function shareMeal(prevState, formData) { |
生产部署和缓存
运行下面的命令来创建生产环境的部署,并启动生产环境的服务器:
1 | npm run build |
我们会遇到一个问题,即我们新添加的内容并不显示。这是因为 NextJS 默认情况下采取一个非常激进的缓存策略,即在创建生产环境的项目文件时,默认静态生成所有页面以供缓存。
为了解决这个问题,我们可以使用 revalidatePath
:
1 | import revalidatePath from "next/cache"; |
默认情况下,revalidatePath
仅会对 exact 那个目录生效,这是因为它其实可以接受第二个参数,默认值为 page
。如果希望对当前路由及其子路由均生效,可以使用 layout
:
1 | revalidatePath("/meals", "layout"); |
元数据
通过在 Root Layout 中导出 metadata
这一变量,我们已经为整个站点设置了元数据:
1 | export const metadata = { |
我们也可以为页面单独设置元数据,只需要在对应页面中导出名为 metadata
的变量即可。
对于动态页面,你可能希望添加动态的元数据,可以这样做:
1 | export async function generateMetadata({ params }) { |