Nextjs App Router 开发指南
Next.js是一个用于构建全栈web应用的React框架。App Router 是 nextjs 的基于文件系统的路由器,它使用了React的最新特性,比如 Server Components, Suspense, 和 Server Functions。
术语
- 树(Tree): 一种用于可视化的层次结构。例如,包含父组件和子组件的组件树,文件夹结构等。
- 子树(Subtree): 树的一部分,从新的根节点(第一个)开始,到叶子节点(最后一个)结束。
- 根节点(Root): 树或子树中的第一个节点
- 叶子节点(Leaf): 子树中没有子节点的节点,例如 URL 路径的最后一段。
- URL 段(URL Segment): 用斜杠分隔的 URL 路径的一部分
- URL 路径(URL Path): 域名之后的URL的一部分(由段组成)。
1. 路由段 Route Segments
在app目录中,嵌套的文件夹定义了路由结构,路由中的每个文件夹代表一个路由段。每个路由段都映射到 URL 路径中的对应段。文件(如page.jsx和布局layout.jsx)用于创建所在段的UI。
- 嵌套路由 (Nested Routes), 如
/dashboard/settings
路由可以通过在app
目录下添加两级目录实现。 - 组件在嵌套路由中递归呈现,这意味着路由段的组件将嵌套在其父段的组件中。
- 约定只有 page.js 或 route.js 的内容会被发送到客户端。这意味着其他文件可以安全地放在app目录的路由段中,但不会被路由。
2. 组件层次 Component Hierarchy
- 在同一路由段的文件中定义的组件会在特定的层次结构中呈现
- 在一个嵌套路由中,一个段的组件将被嵌套在它的父段的组件中
- 除了特殊文件之外,您还可以选择将自己的文件放在文件夹中。例如,样式、测试、组件等等。
可以按特性或路由拆分项目文件:
3. 文件夹和文件约定
顶级文件夹用于组织应用程序的代码和静态资产,顶级文件用于配置应用程序、管理依赖关系、运行中间件、集成监视工具和定义环境变量。
layout.jsx, 定义布局组件
- 默认情况下,文件夹层次结构中的布局组件也是嵌套的。
- 应用顶级布局必须包含
<html>
和<body>
标签。 - 不要直接添加
<head>
标签到 layout.jsx, 而是使用Metadata APIs
。定义并导出Metadata
对象 、通过generateMetadata
方法动态生成、或使用约定的文件如sitemap.xml
、robots.txt
等。 - 可以使用 React的缓存函数
cache function
只获取一次数据。 - 通过 ImageResponse 可以使用 JSX 和 CSS 动态生成 Open Graph 图像。
page.jsx, 定义一个具体路由的UI
- 必须有一个 page.jsx 才能使路由段可公开访问。
- 两个 Promise 类型的可选参数:params(动态路由)、searchParams(查询参数)
app/shop/[category]/[item]/page.js
, 访问/shop/1/2
, 获得参数:Promise<{ category: '1', item: '2' }>
/shop?a=1&b=2
, 获得参数:Promise<{ a: '1', b: '2' }>
- 客户端组件中通过 React 的 use 方法获取 Promise 参数值
loading.jsx, 为路由段及其子段创建加载界面
- loading.jsx 将被嵌套在 layout.jsx 中。它会自动将 page.jsx 文件及其下的所有子文件包装在
<Suspense>
中 <Suspense>
用于显示一个临时UI,直到它的子节点完成加载。在页面中使用Suspense
可以让服务端渲染流式传输Streaming
流允许您将页面的HTML分解为更小的块,并逐步将这些块从服务器发送到客户端
not-found.jsx, 抛出 notFound
异常时显示的界面
- 顶级
app/not-found.jsx
文件可处理整个应用程序未捕获的notFound
异常 - 如果需要使用客户端钩子(比如usePathname)来显示基于路径的内容,须使用客户端组件
error.jsx, 当发生运行时错误时显示的备用UI
error.jsx
把路由段和它的嵌套子段包装在一个React Error Boundary
中- 可以使用位于应用程序根目录中的
global-error.js
来处理根布局或模板中的错误 - 全局错误UI必须定义自己的
<html>
和<body>
标签。该文件在活动时替换根布局或模板
route.jsx, 创建自定义 api 请求处理程序
- 支持 GET, POST, PUT, PATCH, DELETE, HEAD, and OPTIONS.
- 参数 request:
NextRequest
对象, Web请求的扩展。提供了对传入请求的进一步控制,可访问cookie
和扩展的URL对象nextUrl
。 - 参数 context (optional)
context.params
包含当前路由的动态路由参数
template.jsx, 类似 layout.jsx,当需要创建一个新的组件实例时使用
- 与跨路由持久化和维护状态的
layout
布局不同,template
模板被赋予了一个唯一的键,这意味着在导航时会刷新子客户端组件的状态。 - layout 内部的
<Suspense>
仅在布局第一次加载时显示,而在切换页面时则不显示。对于模板,每次导航都会重新渲染<Suspense>
的回退UI。
default.jsx, 在并行路由中呈现回退UI
- 对于硬导航(整个页面重新加载),Next.js无法恢复部分UI插槽的活动状态。可以为与当前URL不匹配的子页面呈现 default UI。
4. 路由:以服务器路由为中心的客户端导航
与使用客户端路由的 pages 目录不同,app 目录中的新路由器使用以服务器为中心的路由,以便与服务器上的服务端组件和数据获取保持一致。客户端不必下载路由图,并且可以使用对服务端组件的相同请求来查找路由。这种优化对所有应用都有用,对路由多的应用影响更大。
虽然路由是以服务器为中心的,但使用 Link 组件的客户端导航具有类似于单页应用程序的行为。这意味着当用户导航到新路由时,浏览器不会重新加载页面。但 URL 将被更新,Next.js 将只呈现更改的部分。
此外,当用户浏览应用程序时,路由器会将服务端组件数据存储在客户端缓存(内存)中。缓存按路由段分割,允许在任何级别失效,并确保并发渲染的一致性。这意味着在某些情况下,可以重用先前获取的段的缓存,从而进一步提高性能。
局部渲染 Partial Prerendering
部分预呈现(PPR)是一种呈现策略,它允许您在同一路由中组合静态和动态内容。这提高了初始页面的性能,同时仍然支持个性化的动态数据。
如果一个组件使用了以下api,它就会变成动态的:
cookies // 读写 cookies 的方法headers //一个async函数,它允许你从服务端组件读取HTTP传入请求的 headersconnection // 指示强制变为动态,希望它在运行时动态呈现,而不是在构建时静态呈现。draftModesearchParams // 路由查询参数fetch with { cache: 'no-store' } // 不使用缓存每次请求时都从远程服务器获取资源
- 包裹在
<Suspense>
中的动态组件开始从服务器并行流式传输到客户端。 export const experimental_ppr = true
可以将路由段的所有子节点开启PPR,不需要把它添加到每个文件中,只需要把它添加到路由的顶部段- 组件只在访问动态属性时才会变成动态渲染。例如,从
<Page />
组件中读取searchParams
,可以将这个值作为prop转发给另一个组件,进行隔离 - 当前只能在使用最新的
nextjs canary
版本时启用,未来会成为 Next.js 的默认构建方式
静态渲染及动态渲染组件示例:
流式传输
通过流式传输,您可以防止缓慢的数据请求阻塞整个页面。这允许用户查看页面的部分内容并与之交互,而无需等待所有数据加载完毕,然后才能向用户显示任何UI。
在Next.js中有两种实现流的方法:
- 在页面级别,通过
loading.jsx
文件(自动创建<Suspense>
)。 - 在组件级别,使用
<Suspense>
进行更细粒度的控制,将阻塞页面的组件单独封装进行流式传输。
5. 路由模式
- 动态路由:如果事先不知道确切的段名,并希望根据动态数据创建路由,则可以使用在请求时填充或在构建时预呈现的动态路由。
- 并行路径: 允许您在同一视图中同时显示两个或多个可以独立导航的页面。用于具有自己的子导航的分屏视图,例如仪表板。
- 拦截路由: 允许你拦截一条路由,并在另一条路由的上下文中显示它。在保持当前页面的上下文很重要时使用。例如,在编辑一个任务时查看所有任务,或在 Feed 中查看图片。
- 条件路由: 允许您根据条件有条件地呈现路由。例如:只在用户登录后才显示。
路由组和私有文件夹
(folder)
:在不参与路由的情况下对路由进行分组,可实现为子路由分别定义布局UI layout.jsx_folder
:让子目录不参与路由, 可用于将UI逻辑与路由逻辑分离。
动态路由
[folder]
, 动态路由段,可作为params
参数传递给layout、page、route 和 generateMetadata函数。例如,一个博客的路由app/blog/[slug]/page.js
,其中[slug]
是博客文章的动态段。[...folder]
, 捕获全部路由,例如,app/shop/[…slug]/page.js
会匹配/shop/clothes
,还会匹配/shop/clothes/tops
,/shop/clothes/tops/t-shirts
等。[[...folder]]
, 全捕获,除了本级及子路由,也捕获父路由段,例如,app/shop/[[…slug]]/page.js
也会匹配/shop
并行路由
@folder,并行路由是使用命名槽创建的。槽作为参数传递给共享的父布局,并可以与 children 并行呈现
- 在客户端导航期间,Next.js 将执行部分渲染,更改槽内的UI,同时保持另一个槽的当前UI,即使它们与当前URL不匹配。
- 整个页面刷新重载,无法确定与当前URL不匹配的插槽的活动状态。不匹配的槽呈现 default.js 界面,如果default.js不存在,则呈现404。
- 实现条件路由,根据登录用户的角色展示不同的槽。
- 可以在插槽中添加布局 layout.jsx,以允许用户独立地导航插槽。这对于创建选项卡很有用。
- 并行路由可以和拦截路由一起使用来创建支持深度链接的对话框。
- 并行路由可以独立流化,允许你为每条路由定义独立的错误UI error.jsx 和加载UI loading.jsx
拦截路由
(.)folder
, 匹配在同一级别上路由段(..)folder
, 匹配上一级路由段(..)(..)folder
, 上两级中的路由段(...)folder
, 匹配顶级路由段
用户可以使用客户端导航从图库打开照片对话框,也可以通过URL导航到照片页面:
6. 组件
默认情况下,布局和页面是服务端组件,它允许您在服务器上获取数据并呈现UI的部分,可选地缓存结果,并将其流式传输到客户端。当您需要交互性或调用浏览器api时,可以使用客户端组件来实现。
- Next.js默认使用服务端组件
- 在服务器端服务端组件被渲染为一种特殊的数据格式(RSC Payload), 客户端组件和
RSC Payload
用于呈现HTML Hydration
是React将事件处理程序附加到DOM的过程,以使静态HTML具有交互性- 为了减少客户端JavaScript包的大小,在特定的交互组件中添加“use client”,而不是将UI的大部分标记为客户端组件
- 当使用依赖于客户端特性的第三方组件时,可以将其包装在客户端组件中,以确保其按预期工作
- 通过
import 'server-only'
防止在客户端组件中意外使用服务端方法。
使用客户端组件的场景:
- 包含状态管理及事件处理程序
- 处理了生命周期钩子,如useEffect
- 需要调用浏览器API. 如 localStorage, window, Navigator.geolocation 等
- 自定义 hooks
使用服务端组件的场景:
- 从服务端靠近数据源的数据库或api中获取数据
- 使用API密钥、令牌和其他秘密,而客户端不可见
- 减少发送到浏览器的JavaScript数量
- 加速首页渲染,流式传输到客户端
Link 导航组件
<Link>
是一个React组件,它扩展了HTML <a>
元素,在路由之间提供预加载和客户端导航。这是 Next.js 中导航路由的主要方式。
Image 图片组件
<Image>
组件扩展了HTML <img>
元素,提供以下功能:
- 图像大小优化:自动为每个设备提供正确大小的图像,使用现代图像格式,如WebP
- 视觉稳定性:防止加载图像时的布局抖动
- 提升页面加载速度:仅在图片进入视窗时使用本地浏览器延迟加载加载,并带有可选的模糊占位符
- 资产灵活性:按需调整图像大小,甚至是存储在远程服务器上的图像。
fonts 字体模块
next/font
模块通过内置自托管自动优化字体并减少网络请求。
- 字体的作用域是使用它们的组件。要将字体应用于整个应用程序,将其添加到根布局中
- 字体作为静态资产存储,并从部署的位置获取,这意味浏览器不会向谷歌发送请求
- 通过
next/font/local
加载一个或多个本地字体
7. CSS 样式
提供了多种加载样式的方法:
-
CSS Modules:模块化导入
import styles from './blog.module.css'
使用:<main className={styles.blog}></main>
-
Global CSS:全局样式,创建一个
app/global.css
文件,并将其导入到根布局中,这些样式应用到应用中的每个路由视图。外部包发布的样式表也可以被导入 -
CSS的顺序取决于你在代码中导入样式的顺序。
-
使用 Tailwind CSS, Tailwind v4 之后版本不再通过
tailwind.config.js
配置,可以在全局导入的地方通过CSS自定义配置,并采用OKLCH
色彩空间作为调色板。 -
使用 CSS-in-JS UI库
-
使用 Tailwind CSS
建议:
- 通过一个如可js文件导入css 模块
- 在应用程序的根目录中导入全局样式和 Tailwind 样式
- 对嵌套组件使用CSS模块而不是全局样式
- 将共享样式提取到共享组件中,以避免重复导入
- 关闭编辑器的自动排序格式功能
- CSS顺序在开发及生产环境中可能表现不同,始终检查构建结果以验证
获取数据
- 在服务端组件中直接通过
fetch
或 db 客户端获取数据 - 在客户端组件需要配合 React 的
use()
函数 或 其他库(SWR、 React Query)
更新数据
- 通过 React 的
Server Functions
服务器函数来更新数据 - 在客户端组件中可以通过表单动作或事件响应函数来调用服务器函数
- 通过
useActionState
hook 可以监控调用状态
8. Middleware 中间件
中间件允许您在请求完成之前运行代码。然后,根据传入的请求,您可以通过重写、重定向、修改请求或响应头或直接修改响应。
中间件有效的一些常见场景包括:
- 在读取部分传入请求后快速重定向
- 基于A/B测试重定向到不同的页面
- 修改所有页面或页面子集的标头 headers
- 可以通过配置或条件判断来指定哪些请求运行。
matcher: ['/about/:path*', '/dashboard/:path*']
- 设置CORS头以允许跨域请求
- 访问或设置 cookies
- 中间件不应用于获取数据或会话管理
9. 懒加载
通过懒加载 Lazy loading
可以减少渲染路由所需的JavaScript量,有助于提高应用程序的初始加载性能。
- 懒加载适用于客户端组件,
const ComponentC = dynamic(() => import('../components/C'), { ssr: false })
- 动态导入一个服务端组件,只有作为服务器组件的子组件的客户端组件才会被懒加载
- 通过
import()
可以按需加载外部库const Fuse = (await import('fuse.js')).default
确保最佳性能和用户体验的建议
- 使用布局来跨页面共享UI,并在导航上启用部分呈现
- 使用
<Link>
组件进行客户端导航和预取 - 通过创建自定义错误页面,优雅地处理生产中的所有错误和404错误
- 遵循服务器和客户端组件的推荐组合模式,并检查
"use client"
边界的位置,以避免不必要地增加客户端Js包 - 像cookie和searchParams这样的动态api会导致整个路由动态渲染。确保动态API的使用是有意的,并将它们通过
<Suspense>
包装起来 - 利用服务端组件在服务器上获取数据
- 使用
Route Handlers
路由处理程序从客户端组件访问后端资源。但不要从服务器组件调用路由处理程序,以避免额外的服务器请求 - 使用 Loading UI 和 React Suspense 来逐步将UI从服务器发送到客户端,并防止在获取数据时阻塞路由
- 通过并行获取数据来减少网络瀑布。此外,考虑在适当的地方预加载数据
- 验证您的数据请求是否被缓存,并在适当的情况下多使用缓存。确保不使用fetch的请求被缓存
- 使用 Server Actions 处理表单提交、服务器端验证和错误处理
- 通过使用字体模块将字体文件与其他静态资产一起托管,以减少外部网络请求,及布局抖动
- 通过使用
<Script>
组件来优化第三方脚本,该组件可以自动延迟脚本并防止它们阻塞主线程 - 使用内置的
eslint-plugin-jsx-a11y
插件来尽早捕获可访问性问题 - 确保你的
.env.*
文件被添加到.gitignore
中,并且只公共变量添加NEXT_PUBLIC_
前缀 - 使用元数据API通过添加页面标题、描述等来改进应用程序的搜索引擎优化(SEO)
官方开发示例
dashboard-app: https://nextjs.org/learn/dashboard-app
一个基于 App Router 的全栈web应用,访问 PostgresSQL 数据库,仪表板展示,流式加载,账单的分页增删改查,用户登录等功能全面展示 nextjs 基础功能特性。
END
如果这篇文章对您有所帮助,欢迎点赞、分享和留言,让更多的人受益。感谢您的细心阅读,如果发现了任何错误或需要补充的地方,请随时告诉我,我会尽快处理
^_^