App Router
文件目录
Next.js 使用基于文件系统的路由器,使用文件夹来定义路由。
每个目录下都要有一个page.js。
在此示例中,/dashboard/analytics URL 路径不可公开访问,因为它没有相应的 page.js 文件。该文件夹可用于存储组件、样式表、图像或其他并置文件。
这个文件约定规则是用来为每个路由创建ui的,
例如,要创建第一个页面,请在应用程序目录中添加 page.js 文件并导出 React 组件:
javascript
export default function Page() {
return <h1>Hello, Next.js!</h1>
}
Router
Pages
Pages 用来表示路由指定的ui,你可以默认从page.js文件导出组件来定义页面。
例如,要创建初始页面 / ,需要在app下创建page.js
javascript
// `app/page.tsx` is the UI for the `/` URL
export default function Page() {
return <h1>Hello, Home page!</h1>
}
再创建别的页面,就要新增一个文件夹,并在这个文件夹下添加page.js
javascript
// `app/dashboard/page.tsx` is the UI for the `/dashboard` URL
export default function Page() {
return <h1>Hello, Dashboard Page!</h1>
}
good to know
-
.js,.jsx,.tsx都可以用于pages
-
page永远是文件夹的叶子节点
-
每个路由都必须要有page文件,才可以被访问到
-
默认情况下,page是服务器组件,但可以设置为客户端组件。
-
pages里面可以请求数据。
Layouts and templates
layout是在多个路由间共享的ui。
在导航的时候,layout 可以保留状态和交互性,不会重新渲染,layout可以嵌套使用。
你可以通过导出一个react component来定义一个layout,这个component应该接受一个children prop,这个prop将在渲染期间填充子布局。
javascript
export default function DashboardLayout({
children, // will be a page or nested layout
}: {
children: React.ReactNode
}) {
return (
<section>
{/* Include shared UI here e.g. a header or sidebar */}
<nav></nav>
{children}
</section>
)
}
下图的例子中,layout将与 /dashboard 和 /dashboard/settings 页面共享:
Root layout
根布局是必要的,而且必须包含html和body
javascript
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<body>
{/* Layout UI */}
<main>{children}</main>
</body>
</html>
)
}
Nesting Layouts
嵌套布局。
layout默认是通过文件结构层层嵌套的,他们通过 Children prop 包裹子布局。
如果你要组合下面的两个布局,根布局 (app/layout.js) 将包装仪表板布局 (app/dashboard/layout.js),后者会将route 包装在 app/dashboard/* 内。
Good to know
- .js,.jsx,.tsx都可以用于layout
- 同一个文件夹下有page也有layout,那layout会包裹page
- 默认情况下,page是服务器组件,但可以设置为客户端组件。
- layout里面可以请求数据。
- 在父布局与其子布局之间传递数据是不可能的。但是,你可以在一个路由中多次获取相同的数据,React 会自动删除请求的重复数据,而不会影响性能。
- layout没有权限获取深层的路由段,你可以在客户端组件中使用
useSelectedLayoutSegment
或useSelectedLayoutSegments
。 - 你可以使用 route groups来制定共享的路由
- 你可以使用route groups创建多个root layouts
Templates
template和布局类似,可以嵌套child layout或page,但是她不可以保存状态,template会给每个子级创建新的实例,that means 用户在共享模版的路由间跳转的时候,会mount组件的新实例,重新创建dom,不保留状态。(而layout相反)
在下面的场景下,template比layout更合适。
- 依赖于 useEffect(例如记录页面视图)和 useState(例如每页反馈表)的功能。
- 更改默认框架行为。例如,layout内的 Suspense Boundaries 仅在第一次加载布局时显示回退按钮,而不是在切换页面时显示。对于模板,回退按钮显示在每次导航上。
可以通过从 template.js 文件导出默认的 React 组件来定义模板。该组件应该接受一个children prop。
javascript
export default function Template({ children }: { children: React.ReactNode }) {
return <div>{children}</div>
}
在嵌套方面,template.js 在布局及其子布局之间呈现。这是一个伪代码的输出:
xml
<Layout>
{/* Note that the template is given a unique key. */}
<Template key={routeParam}>{children}</Template>
</Layout>
metadata
你可以在app目录下,通过Metadata apis修改 元素,例如标题和元数据。
可以通过在layout.js或page.js文件中导出元数据对象或generateMetadata
函数来定义元数据。
javascript
import { Metadata } from 'next'
export const metadata: Metadata = {
title: 'Next.js',
}
export default function Page() {
return '...'
}
Good to know
你不应该手动将 标签(例如 和 )添加到根布局。相反,你应该使用Metadata Apis,它会自动处理高级要求,例如流式传输和删除重复 元素。
Linking 和 navigating
next有四种方式处理导航
Link 组件
link扩展自标签,提供prefetching和客户端间导航的功能,是推荐的导航方式。
[使用实例
javascript
import Link from 'next/link'
export default function Page() {
return <Link href="/dashboard">Dashboard</Link>
}
获取当前路由
那你可以使用usePathname()
获取当前路由
javascript
'use client'
import { usePathname } from 'next/navigation'
import Link from 'next/link'
export function Links() {
const pathname = usePathname()
return (
<nav>
<ul>
<li>
<Link className={`link ${pathname === '/' ? 'active' : ''}`} href="/">
Home
</Link>
</li>
<li>
<Link
className={`link ${pathname === '/about' ? 'active' : ''}`}
href="/about"
>
About
</Link>
</li>
</ul>
</nav>
)
}
滚动到特定位置
next路由的默认行为是在打开路由的时候滚动到头部或者是维持之前的距离。](https://link.juejin.cn?target= "")
如果你想滚动到导航上的特定 ID,你可以在 URL 后附加 #
哈希链接,或者仅将哈希链接传递给 href 属性。这是可能的,因为 呈现为元素。
[```ini
// Output
Settings
###### 不存储滚动位置
next默认会存储上次滚动的位置,如果你不想存储,就这么传
```javascript
// next/link
<Link href="/dashboard" scroll={false}>
Dashboard
</Link>
// useRouter
import { useRouter } from 'next/navigation'
const router = useRouter()
router.push('/dashboard', { scroll: false })
useRouter
javascript
'use client'
import { useRouter } from 'next/navigation'
export default function Page() {
const router = useRouter()
return (
<button type="button" onClick={() => router.push('/dashboard')}>
Dashboard
</button>
)
}
Redirect
对于服务器组件,要使用用redirect函数
javascript
import { redirect } from 'next/navigation'
async function fetchTeam(id: string) {
const res = await fetch('https://...')
if (!res.ok) return undefined
return res.json()
}
export default async function Profile({ params }: { params: { id: string } }) {
const team = await fetchTeam(params.id)
if (!team) {
redirect('/login')
}
// ...
}
使用原生的history api
window.history.replaceState
window.history.pushState
原理
Next 的App router是一种混合的导航方式。
在server端,你的应用程序代码会自动按路由段进行代码分割
在客户端,next会预获取和缓存路由段。这意味着,当用户导航到新路由时,浏览器不会重新加载页面,而只会重新渲染发生更改的路线段,从而改善导航体验和性能。
Prefetching
预取是一种在用户访问路由之前在后台预加载路由的方法。
预获取的方式
- Link component:当路线在用户视口中可见时,会自动预取路线。当页面首次加载或通过滚动进入视图时,会发生预取。如果需要取消预获取,传入 prefetch:false
- router.prefetch():useRouter 钩子可用于以编程方式预取路由。
部分渲染
部分渲染意味着仅在客户端上重新渲染导航时发生变化的路线段,并且保留所有共享段。
例如,当在两个同级路由 /dashboard/settings 和 /dashboard/analytics 之间导航时,将呈现settings和analytics页面,并且将保留共享的dashboard布局。
错误处理
error.js 文件允许你优雅地处理嵌套路由中的意外运行时错误。
- 根据文件系统的层次结构对特定模块的错误ui定义展示
- 使错误隔离,不影响别的路由段
- 可以添加功能reload指定路由段,不重新加载整个页面
javascript
'use client' // Error components must be Client Components
import { useEffect } from 'react'
export default function Error({
error,
reset,
}: {
error: Error & { digest?: string }
reset: () => void
}) {
useEffect(() => {
// Log the error to an error reporting service
console.error(error)
}, [error])
return (
<div>
<h2>Something went wrong!</h2>
<button
onClick={
// Attempt to recover by trying to re-render the segment
() => reset()
}
>
Try again
</button>
</div>
)
}
error.js是如何工作的?
](https://link.juejin.cn?target= "")
-
error.js会自动创建一个React Error Boundary,包含嵌套的子组件或page.js组件
-
从 error.js 文件导出的 React 组件用作fallback组件。
-
如果在错误边界内抛出错误,则错误将被包含,并呈现后备组件。
重试
错误组件可以使用reset()函数来提示用户尝试从错误中恢复。执行时,该函数将尝试重新渲染错误边界的内容。如果成功,后备错误组件将替换为重新渲染的结果。
javascript
'use client'
export default function Error({
error,
reset,
}: {
error: Error & { digest?: string }
reset: () => void
}) {
return (
<div>
<h2>Something went wrong!</h2>
<button onClick={() => reset()}>Try again</button>
</div>
)
}
嵌套路由
通过特殊文件创建的 React 组件在特定的嵌套层次结构中呈现。
嵌套组件层次结构对嵌套路由中 error.js 文件的行为有影响:
- 错误会向上冒泡到最近的父错误边界。这意味着 error.js 文件将处理其所有嵌套子段的错误。通过将 error.js 文件放置在路由的嵌套文件夹中的不同级别,可以实现或多或少的粒度错误 UI。
- error.js 边界不会处理同一段中的 layout.js 组件中抛出的错误,因为错误边界嵌套在该布局的组件内。
处理layout中的error
要处理特定布局或模板中的错误,请将 error.js 文件放置在布局的父段中。
要处理根布局或模板中的错误,请使用名为 global-error.js 的 error.js 变体。
处理根布局中的错误
与根 error.js 不同,global-error.js 错误边界包裹了整个应用程序,因此,需要注意的是,global-error.js 必须定义自己的 和 标记。
global-error.js 是最细粒度的错误 UI,可以被视为整个应用程序的"包罗万象"错误处理。它不太可能经常被触发,因为根组件通常不太变动,并且其他 error.js 边界将捕获大多数错误。
javascript
'use client'
export default function GlobalError({
error,
reset,
}: {
error: Error & { digest?: string }
reset: () => void
}) {
return (
<html>
<body>
<h2>Something went wrong!</h2>
<button onClick={() => reset()}>Try again</button>
</body>
</html>
)
}
global-error.js 仅在生产中启用。在开发过程中,我们的error overlay将会显示。
Loading UI and Streaming
特殊文件loading.js可帮助你使用React Suspense创建有意义的加载UI。
即时加载状态
即时加载状态是后备 UI,在导航时立即显示。你可以预渲染加载指示器,例如骨架和旋转器,或未来屏幕的一小部分但有意义的部分,例如封面照片、标题等。这有助于用户了解应用程序正在响应,并提供更好的用户体验。
javascript
export default function Loading() {
// You can add any UI inside Loading, including a Skeleton.
return <LoadingSkeleton />
}
在同一文件夹中,loading.js 将嵌套在layout.js 中。它会自动将 page.js 文件和下面的所有子文件包装在 边界中。
Streaming
先来说传统ssr的步骤
- 首先,在服务器上获取给定页面的所有数据。
- 然后服务器呈现该页面的 HTML。
- 页面的 HTML、CSS 和 JavaScript 将发送到客户端。
- 使用生成的 HTML 和 CSS 显示非交互式用户界面。
- React渲染用户界面以使其具有交互性。
这个步骤是串行且阻塞的。这意味着服务器只能在获取所有数据后才能呈现页面的 HTML。
Streaming传输允许你将页面的 HTML 分解为更小的块,并逐步将这些块从服务器发送到客户端。
当你想要防止长数据请求阻止页面渲染时,流式处理特别有用,因为它可以减少第一个字节的时间 (TTFB) 和首次内容绘制 (FCP)。它还有助于缩短交互时间 (TTI),尤其是在速度较慢的设备上。
示例
的工作原理是包装一个执行异步操作(例如获取数据)的组件,在操作发生时显示回退 UI(例如骨架、旋转器),然后在操作完成后交换组件。
javascript
import { Suspense } from 'react'
import { PostFeed, Weather } from './Components'
export default function Posts() {
return (
<section>
<Suspense fallback={<p>Loading feed...</p>}>
<PostFeed />
</Suspense>
<Suspense fallback={<p>Loading weather...</p>}>
<Weather />
</Suspense>
</section>
)
}
通过使用suspense组件,有以下好处
- 流式服务器渲染 - 逐步将 HTML 从服务器渲染到客户端。
- 选择性水合 - React 根据用户交互优先考虑哪些组件首先进行交互。
SEO
Next.js的流式渲染特性是在服务器端进行的,而不是在客户端。这意味着,当你的网页在浏览器中加载时,服务器已经生成了完整的HTML,而这个HTML是可以被搜索引擎(如Google)爬取和索引的。
搜索引擎优化(SEO)通常取决于服务器端渲染的HTML,因为搜索引擎爬虫通常只会抓取和解析服务器返回的HTML,而不会执行客户端的JavaScript代码。这就是为什么传统的单页面应用(SPA)可能会有SEO问题,因为它们的内容是通过在客户端运行JavaScript代码来动态生成的,而这些内容可能无法被搜索引擎爬虫正确抓取和索引。
当我们谈论流式渲染(streaming)时,我们指的是服务器将HTML分块发送到浏览器的过程。然而,这并不意味着搜索引擎爬虫在接收到HTML的同时开始解析。实际上,大多数搜索引擎爬虫会等待接收到完整的HTML文档后再开始解析。
这是因为HTML文档通常需要整体解析,以确定其中的链接、元数据以及其他重要信息。如果在接收到完整的HTML之前就开始解析,可能会导致解析错误或遗漏重要信息。
因此,虽然在流式渲染中,HTML是分块发送的,但是搜索引擎爬虫在接收并解析HTML时,仍然是按照完整的文档来处理的。
这也是为什么流式渲染不会影响SEO的原因。虽然HTML是分块发送的,但是最终搜索引擎爬虫接收并解析的仍然是完整的HTML文档,因此能够正确地索引网页内容。
重定向
next中处理重定向的方式有
redirect方法
javascript
'use server'
import { redirect } from 'next/navigation'
import { revalidatePath } from 'next/cache'
export async function createPost(id: string) {
try {
// Call database
} catch (error) {
// Handle errors
}
revalidatePath('/posts') // Update cached posts
redirect(`/post/${id}`) // Navigate to the new post page
}
你可以在服务器组件、路由处理程序和服务器操作中调用重定向。
permanentRedirect方法
permanentRedirect
函数允许你将用户永久重定向到另一个 URL。你可以在服务器组件、路由处理程序和服务器操作中调用 permanentRedirect
。
javascript
'use server'
import { permanentRedirect } from 'next/navigation'
import { revalidateTag } from 'next/cache'
export async function updateUsername(username: string, formData: FormData) {
try {
// Call database
} catch (error) {
// Handle errors
}
revalidateTag('username') // Update all references to the username
permanentRedirect(`/profile/${username}`) // Navigate to the new user profile
}
重定向和永久重定向的区别
- 永久重定向通常使用HTTP状态码
301 Moved Permanently
来表示。 - 这种类型的重定向表明页面或资源已经永久移动到新的位置,当搜索引擎(如Google)爬取网站并遇到301永久重定向时,它会更新自己的索引库,将旧的URL替换为新的URL。这是因为301重定向表示旧的URL已经永久性地改变,搜索引擎需要更新索引以保持最新。同时,浏览器在遇到301永久重定向时,为了优化性能,可能会缓存这个重定向。也就是说,浏览器会记住这个重定向的信息。当用户再次访问旧的URL时,浏览器会直接从缓存中读取重定向的信息,而不会再向服务器发送请求。这样就可以省去了服务器处理请求和发送响应的时间,从而提高了页面加载速度。
UseRouter Hook
如果你需要在客户端组件中的事件处理程序内部进行重定向,则可以使用 useRouter 挂钩中的 Push 方法。例如:
javascript
'use client'
import { useRouter } from 'next/navigation'
export default function Page() {
const router = useRouter()
return (
<button type="button" onClick={() => router.push('/dashboard')}>
Dashboard
</button>
)
}
redirects in next.config.js
这在Next.js的配置文件next.config.js
中定义重定向规则。redirects
函数返回一个数组,数组中的每个对象都定义了一个重定向规则。
javascript
module.exports = {
async redirects() {
return [
// Basic redirect
{
source: '/about',
destination: '/',
permanent: true,
},
// Wildcard path matching
{
source: '/blog/:slug',
destination: '/news/:slug',
permanent: true,
},
]
},
}
- 第一个规则是基本的重定向。它表示将所有访问
/about
的请求重定向到/
(首页)。permanent: true
表示这是一个永久重定向,也就是说,这个重定向不会在短时间内发生改变。 - 第二个规则是使用通配符路径匹配的重定向。它表示将所有访问
/blog/:slug
的请求重定向到/news/:slug
。这里的:slug
是一个路径参数,可以匹配任何值。例如,访问/blog/hello-world
的请求将被重定向到/news/hello-world
。这同样是一个永久重定向。
通过在next.config.js
中定义重定向规则,你可以在不修改应用程序代码的情况下,方便地管理和更新你的URL结构。
NextResponse.redirect in Middleware
中间件允许你在请求完成之前运行代码。然后,根据传入请求,使用 NextResponse.redirect 重定向到不同的 URL。如果你想根据条件(例如身份验证、会话管理等)重定向用户或有大量重定向,这非常有用。
例如,如果用户未经过身份验证,则将用户重定向到 /login 页面:
javascript
import { NextResponse, NextRequest } from 'next/server'
import { authenticate } from 'auth-provider'
export function middleware(request: NextRequest) {
const isAuthenticated = authenticate(request)
// If the user is authenticated, continue as normal
if (isAuthenticated) {
return NextResponse.next()
}
// Redirect to login page if not authenticated
return NextResponse.redirect(new URL('/login', request.url))
}
export const config = {
matcher: '/dashboard/:path*',
}
大量重定向管理(高级)
要管理大量重定向(1000+),你可以考虑使用中间件创建自定义解决方案。这允许你以编程方式处理重定向,而无需重新部署应用程序。
你需要考虑
- 创建并存储重定向映射。
- 优化数据查找性能。
创建并存储重定向映射。
重定向映射是可以存储在数据库(通常是键值存储)或 JSON 文件中的重定向列表。
json
{
"/old": {
"destination": "/new",
"permanent": true
},
"/blog/post-old": {
"destination": "/blog/post-new",
"permanent": true
}
}
在中间件中,你可以从 Vercel 的 Edge Config 或 Redis 等数据库中读取数据,并根据传入请求重定向用户:
typescript
import { NextResponse, NextRequest } from 'next/server'
import { get } from '@vercel/edge-config'
type RedirectEntry = {
destination: string
permanent: boolean
}
export async function middleware(request: NextRequest) {
const pathname = request.nextUrl.pathname
const redirectData = await get(pathname)
if (redirectData && typeof redirectData === 'string') {
const redirectEntry: RedirectEntry = JSON.parse(redirectData)
const statusCode = redirectEntry.permanent ? 308 : 307
return NextResponse.redirect(redirectEntry.destination, statusCode)
}
// No redirect found, continue without redirecting
return NextResponse.next()
}
-
优化数据查找性能。
为每个传入请求读取大型数据集可能既缓慢又昂贵。有两种方法可以优化数据查找性能:
- 使用针对快速读取进行优化的数据库,例如 Vercel Edge Config 或 Redis。
- 在读取较大的重定向文件或数据库之前,使用布隆过滤器等数据查找策略来有效地检查重定向是否存在。
路由组
在应用程序目录中,嵌套文件夹通常映射到 URL 路径。但是,你可以将文件夹标记为路由组,这样文件夹就不会映射在url上了
可以通过将文件夹名称括在括号中来创建路由组:(folderName)
即使(marketing)
和(shop)
内部的路由共享相同的 URL 层次结构,你也可以通过在其文件夹内添加 layout.js 文件来为每个组创建不同的布局。
要指定特定的url作用到当前布局中,请创建一个路线组(例如(shop))并将共享相同布局的路线移到该组中(例如account和购物cart)。组外的路线不会共享布局(例如checkout)。
还可以用路由组来创建多个根布局。
要创建多个根布局,请删除顶级layout.js 文件,并在每个路由组内添加一个layout.js 文件。这对于将应用程序划分为具有完全不同的 UI 或体验的部分非常有用。需要将 和 标记添加到每个根布局中。
项目架构
即使路由结构是通过文件夹定义的,在将 page.js 或 Route.js 文件添加到路由段之前,路由也无法公开访问。
而且,即使路由可供公开访问,也只有 page.js 或 Route.js 返回的内容会发送到客户端。
这意味着项目文件可以安全地共置在应用程序目录的路由段内,而不会意外地被路由访问。
可以通过在文件夹前添加下划线前缀来创建私人文件夹:_folderName
动态路由
可以通过将文件夹名称括在方括号中来创建动态分段:[folderName]。例如,[id] 或 [slug]。
动态分段作为 params 属性传递给布局、页面、路由和生成元数据函数。
javascript
export default function Page({ params }: { params: { slug: string } }) {
return <div>My Post: {params.slug}</div>
}
并行路由
并行路由允许你同时或有条件地渲染同一布局中的一个或多个页面。它们对于应用程序的高度动态部分非常有用,例如dashboard和社交网站上的信息流。
例如仪表板,你可以使用并行路由来同时呈现团队team
和分析analytics
页面:
并行路由是使用命名槽创建的。插槽是使用 @folder 约定定义的。例如,以下文件结构定义了两个槽:@analytics
和 @team
:
插槽作为props传递给共享父级layout。对于上面的示例,app/layout.js 中的组件现在接受 @analytics 和 @team slot 属性,并且可以与 Children 属性一起并行渲染它们:
javascript
export default function Layout({
children,
team,
analytics,
}: {
children: React.ReactNode
analytics: React.ReactNode
team: React.ReactNode
}) {
return (
<>
{children}
{team}
{analytics}
</>
)
}
但是,slot不是路由段,不会影响 URL 结构。例如,对于 /@analytics/views,URL 将为 /views,因为 @analytics 是一个slot。
active状态和导航
slot内的内容会收到导航类型的影响
- 软导航:在客户端导航期间,Next.js 将执行部分渲染,更改插槽内的子页面,同时保留其他插槽的活动子页面,即使它们与当前 URL 不匹配。
- 硬导航:全页加载(浏览器刷新)后,Next.js 无法确定与当前 URL 不匹配的插槽的活动状态。相反,它会为不匹配的槽渲染一个 default.js 文件,如果 default.js 不存在则渲染 404。
示例:考虑以下文件夹结构。 @team 插槽有一个 /settings 页面,但 @analytics 没有。
导航到 /settings 时,@team 插槽将呈现 /settings 页面,同时维护 @analytics 插槽的当前活动页面。
刷新时,Next.js 将为 @analytics 呈现 default.js。如果 default.js 不存在,则会呈现 404。
此外,由于children是隐式槽,因此你还需要创建一个default.js文件,以便在Next.js无法恢复父页面的活动状态时为子级呈现后备。
useSelectedLayoutSegment
useSelectedLayoutSegment 和useSelectedLayoutSegments 都接受parallelRoutesKey 参数,该参数允许你读取槽内的 active router。
javascript
'use client'
import { useSelectedLayoutSegment } from 'next/navigation'
export default function Layout({ auth }: { auth: React.ReactNode }) {
const loginSegment = useSelectedLayoutSegment('auth')
// ...
}
当用户导航到 app/@auth/login (或 URL 栏中的 /login)时,loginSegment 将等于字符串"login"。
示例
条件路由
你可以使用并行路由根据某些条件(例如用户角色)有条件地渲染路由。例如,为 /admin 或 /user 角色呈现不同的仪表板页面
javascript
import { checkUserRole } from '@/lib/auth'
export default function Layout({
user,
admin,
}: {
user: React.ReactNode
admin: React.ReactNode
}) {
const role = checkUserRole()
return <>{role === 'admin' ? admin : user}</>
}
loading和error ui
并行路由可以独立流式传输,允许你为每个路由定义独立的错误和加载状态:
示例
拦截路由可以与并行路由一起使用来创建模态弹窗。这使你可以解决构建模式时的常见问题,例如:
- 通过URL分享模态窗口的内容:
- 当页面刷新时保留上下文,而不是关闭模态窗口:通常,当你刷新页面时,所有的模态窗口都会被关闭。但是使用路由拦截,你可以在刷新页面时保留模态窗口和它的状态。
- 在用户点击浏览器的后退按钮时,不是跳转到上一个页面(路由),而是关闭当前的模态窗口。
- 在前进导航时重新打开模态窗口
用户可以使用客户端导航从布局打开登录模式,或访问单独的/登录页面:
要实现此模式,首先创建一个 /login 路由来呈现你的主登录页面。
login/page.tsx
javascript
export default function Page() {
return <div>登录页面</div>
}
然后,在 @auth 槽内添加返回 null 的 default.js 文件。这可确保在单独访问login时,auth没有匹配到对应的路由段,会提示404。
@auth/default.tsx
javascript
export default function Default() {
return ''
}
在 @auth 槽内,通过更新 /(.)login 文件夹来拦截 /login 路由。将 组件及其子组件导入到 /(.)login/page.tsx 文件中:
@auth/(.)login/page.tsx
less
"use client"; // 使用了userRouter 所以要使用use client
import { useRouter } from "next/navigation";
export default function Page() {
const router = useRouter();
return (
<div
className="modal"
style={{
position: "fixed",
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: "rgba(0, 0, 0, 0.5)",
}}
>
<div
style={{
position: "relative",
backgroundColor: "white",
top: "50%",
marginTop: "-100px",
}}
>
<button
onClick={() => {
router.back();
}}
>
Close modal
</button>
<div>登录</div>
</div>
</div>
);
}
打开弹窗
现在,你可以利用 Next.js 路由器来打开和关闭模式。这可确保在模式打开以及前后导航时正确更新 URL。
app/layout.tsx
javascript
import type { Metadata } from "next";
import { Inter } from "next/font/google";
import Link from "next/link";
import "./globals.css";
const inter = Inter({ subsets: ["latin"] });
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
};
export default function RootLayout({
auth,
children,
}: Readonly<{
auth: React.ReactNode;
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body className={inter.className}>
<nav>
<Link href="/login">Open modal</Link>
</nav>
<div>{auth}</div>
<div>{children}</div>
</body>
</html>
);
}
当用户单击 时,弹窗将打开,而不是导航到 /login 页面。但是,在刷新或初始加载时,导航到 /login 会将用户带到主登录页面。
关闭弹窗
你可以通过调用 router.back() 或使用 Link 组件来关闭模式。
javascript
'use client'
import { useRouter } from 'next/navigation'
export function Modal({ children }: { children: React.ReactNode }) {
const router = useRouter()
return (
<>
<button
onClick={() => {
router.back()
}}
>
Close modal
</button>
<div>{children}</div>
</>
)
}
如何使用Next.js的Link
组件从一个页面导航到另一个页面时,如何确保不再渲染@auth
槽位。
在这个例子中,@auth
槽位用于显示认证相关的组件(如登录或注册模态窗口)。然而,在某些页面中,你可能不希望渲染这个槽位。为了实现这一点,你需要确保并行路由匹配到一个返回null的组件。
例如,当你导航回根页面时,你可能不希望显示任何认证相关的组件。为了实现这一点,你可以创建一个@auth/page.tsx
组件,这个组件仅仅返回null。这样,当你导航回根页面时,@auth
槽位就不会被渲染。
这种方法可以让你更灵活地控制哪些组件在哪些页面中被渲染,特别是对于那些需要在多个页面中共享的组件,如模态窗口或侧边栏。通过使用并行路由和返回null的组件,你可以确保这些组件只在需要时被渲染,从而提高你的应用程序的性能和用户体验。
使用catchAll Slot
或者,如果导航到任何其他页面(例如 /foo、/foo/bar 等),你可以使用 catch-all slot:
app/@auth/[...catchAll]/page.tsx
javascript
export default function CatchAll() {
return '...'
}
拦截路由
使用场景:
当单击feed流中的照片时,你可以在modal中显示照片,覆盖feed流。在本例中,Next.js 拦截 /photo/123 路由,屏蔽 URL,并将其覆盖在 /feed 上。
但是,当通过单击可共享 URL 或刷新页面导航到照片时,应该呈现整个照片页面而不是模态页面。不应发生路由拦截。
拦截路由可以使用 (..) 约定来定义,这与相对路径约定 ../ 类似,但这里指的是路由段。
- (.) 匹配同一级别的段
- (..) 匹配上一级的段
- (..)(..) 匹配上面两级的段
- (...) 匹配根应用程序目录中的段
例如,你可以通过创建 (..)photo 目录从 feed 段中拦截照片路由段。
Route handler
路由处理程序允许你使用 Web 请求和响应 API 为给定路由创建自定义请求处理程序。
路由处理器仅在app根文件夹下可用。跟pages模式下的api routers等同。
使用方式
路由处理程序可以嵌套在应用程序目录内的任何位置的route.js | ts文件内,类似于 page.js 和 layout.js。但不能有与 page.js 处于同一路由段级别的 Route.js 文件。
例如,如果你的文件结构如下:
bash
/app
/user
page.js
route.js
这样的文件结构就是不对的,一般我们会用api文件嵌套她
markdown
/app
- /user
- page.js
- /api
- route.js
app/api/route.ts
javascript
export async function GET(request: Request) {}
支持的HTTP Methods
GET
, POST
, PUT
, PATCH
, DELETE
, HEAD
, and OPTIONS
。如果调用不受支持的方法,Next.js 将返回 405 Method Not allowed 响应。
扩展 NextRequest 和 NextResponse API
除了支持本机请求和响应 API 之外,Next.js 还使用 NextRequest 和 NextResponse 扩展它们,为高级用例提供方便的帮助。nextjs.org/docs/app/ap...
行为
缓存
默认情况下,Route Handlers(路由处理器)不会被缓存。这意味着每次用户请求一个由路由处理器处理的路由时,都会执行完整的服务器端逻辑。例如,如果你的路由处理器是一个API端点,每次请求都会重新从数据库中获取数据,然后将数据返回给客户端。
然而,对于GET请求,你可以选择启用缓存。这意味着服务器会存储先前请求的结果,然后在接下来的请求中重用这些结果,而不是重新执行服务器端逻辑。这可以显著提高你的应用程序的性能,特别是当你的服务器端逻辑复杂或者数据获取耗时时。
要启用缓存,你可以在你的路由处理器文件中使用export const dynamic = 'force-static'
这样的路由配置选项。这将告诉Next.js你希望对这个路由处理器启用静态生成和缓存。在静态生成时,Next.js会在构建时生成HTML,并且在每次请求时重用这个HTML,而不是每次都重新生成。这意味着你的服务器端逻辑只会在构建时运行一次,而不是每次请求都运行,从而提高你的应用程序的性能。
app/items/route.ts
javascript
export const dynamic = 'force-static'
export async function GET() {
const res = await fetch('https://data.mongodb-api.com/...', {
headers: {
'Content-Type': 'application/json',
'API-Key': process.env.DATA_API_KEY,
},
})
const data = await res.json()
return Response.json({ data })
}
动态函数
Cookie
你可以使用 next/headers 中的 cookie 读取或设置 cookie。该服务器函数可以直接在路由处理程序中调用,也可以嵌套在另一个函数中。
或者,你可以使用 Set-Cookie 返回新的响应
javascript
import { cookies } from 'next/headers'
export async function GET(request: Request) {
const cookieStore = cookies()
const token = cookieStore.get('token')
return new Response('Hello, Next.js!', {
status: 200,
headers: { 'Set-Cookie': `token=${token.value}` },
})
}
你还可以使用底层 Web API 从请求 (NextRequest) 中读取 cookie:
javascript
import { type NextRequest } from 'next/server'
export async function GET(request: NextRequest) {
const token = request.cookies.get('token')
}
Headers
你可以从 next/headers 中读取带有 headers 的 headers。该服务器函数可以直接在路由处理程序中调用,也可以嵌套在另一个函数中。 此标头实例是只读的。要设置标头,你需要返回带有新标头的新响应。
javascript
import { headers } from 'next/headers'
export async function GET(request: Request) {
const headersList = headers()
const referer = headersList.get('referer')
return new Response('Hello, Next.js!', {
status: 200,
headers: { referer: referer },
})
}
你还可以使用底层 Web API 从请求 (NextRequest) 中读取标头:
javascript
import { type NextRequest } from 'next/server'
export async function GET(request: NextRequest) {
const requestHeaders = new Headers(request.headers)
}
重定向
javascript
import { redirect } from 'next/navigation'
export async function GET(request: Request) {
redirect('https://nextjs.org/')
}
动态路由段
app/items/[slug]/route.ts
javascript
export async function GET(
request: Request,
{ params }: { params: { slug: string } }
) {
const slug = params.slug // 'a', 'b', or 'c'
}
查询参数
传递给 Route Handler 的请求对象是一个 NextRequest 实例,它有一些额外的便利方法,包括更轻松地处理查询参数。
javascript
import { type NextRequest } from 'next/server'
export function GET(request: NextRequest) {
const searchParams = request.nextUrl.searchParams
const query = searchParams.get('query')
// query is "hello" for /api/search?query=hello
}
流
流通常与大型语言模型 (LLM)(例如 OpenAI)结合使用,用于 AI 生成的内容。
javascript
import { openai } from '@ai-sdk/openai'
import { StreamingTextResponse, streamText } from 'ai'
export async function POST(req) {
const { messages } = await req.json()
const result = await streamText({
model: openai('gpt-4-turbo'),
messages,
})
return new StreamingTextResponse(result.toAIStream())
}
你还可以直接使用底层Web API。
javascript
// https://developer.mozilla.org/docs/Web/API/ReadableStream#convert_async_iterator_to_stream
function iteratorToStream(iterator: any) {
return new ReadableStream({
async pull(controller) {
const { value, done } = await iterator.next()
if (done) {
controller.close()
} else {
controller.enqueue(value)
}
},
})
}
function sleep(time: number) {
return new Promise((resolve) => {
setTimeout(resolve, time)
})
}
const encoder = new TextEncoder()
async function* makeIterator() {
yield encoder.encode('<p>One</p>')
await sleep(200)
yield encoder.encode('<p>Two</p>')
await sleep(200)
yield encoder.encode('<p>Three</p>')
}
export async function GET() {
const iterator = makeIterator()
const stream = iteratorToStream(iterator)
return new Response(stream)
}
Request Body
你可以使用标准 Web API 方法读取请求正文:
javascript
export async function POST(request: Request) {
const res = await request.json()
return Response.json({ res })
}
你可以使用 request.formData() 函数读取 FormData:
csharp
export async function POST(request: Request) {
const formData = await request.formData()
const name = formData.get('name')
const email = formData.get('email')
return Response.json({ name, email })
}
CORS
你可以使用标准 Web API 方法为特定路由处理程序设置 CORS 标头:
javascript
export async function GET(request: Request) {
return new Response('Hello, Next.js!', {
status: 200,
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
},
})
}
要将 CORS 标头添加到多个路由处理程序,你可以使用中间件或 next.config.js 文件。 或者,请参阅我们的 CORS 示例包。
Non-UI
你可以使用路由处理程序返回非 UI 内容。请注意,sitemap.xml、robots.txt、应用程序图标和开放图形图像都具有内置支持。
xml
export async function GET() {
return new Response(
`<?xml version="1.0" encoding="UTF-8" ?>
<rss version="2.0">
<channel>
<title>Next.js Documentation</title>
<link>https://nextjs.org/docs</link>
<description>The React Framework for the Web</description>
</channel>
</rss>`,
{
headers: {
'Content-Type': 'text/xml',
},
}
)
}
路由配置
路由处理程序使用与页面和布局相同的路由段配置。
dart
export const dynamic = 'auto'
export const dynamicParams = true
export const revalidate = false
export const fetchCache = 'auto'
export const runtime = 'nodejs'
export const preferredRegion = 'auto'
Middlewares
中间件允许你在请求完成之前运行代码。然后,根据传入的请求,你可以通过重写、重定向、修改请求或响应标头或直接响应来修改响应。
使用案例
- 身份验证和授权:在授予对特定页面或 API 路由的访问权限之前,确保用户身份并检查会话 cookie。
- 服务器端重定向:根据某些条件(例如区域设置、用户角色)在服务器级别重定向用户。
- 路径重写:可以根据请求的属性动态地将路径重写为API路由或页面。比如A/B测试,你可能希望将一部分用户引导到新的特性或页面,而另一部分用户仍然访问旧的特性或页面。
- 机器人检测:通过检测和阻止机器人流量来保护你的资源。
- 日志记录和分析:在页面或 API 处理之前捕获并分析请求数据以获取见解。
- 功能标记:动态启用或禁用功能,以实现无缝功能部署或测试。
⚠️注意场景:以下场景不应该由中间件处理
- 复杂的数据获取和操作:中间件并不是为直接数据获取或操作而设计的,这应该在路由处理程序或服务器端实用程序中完成。
- 繁重的计算任务:中间件应该是轻量级的并且响应快速,否则可能会导致页面加载延迟。繁重的计算任务或长时间运行的进程应在专用的路由处理程序中完成。
- 广泛的会话session管理:虽然中间件可以管理基本会话任务,但广泛的会话管理应由专用身份验证服务或在路由处理程序内进行管理。
- 直接数据库操作:不建议在中间件中执行直接数据库操作。数据库交互应在路由处理程序或服务器端实用程序中完成。
文件协议
使用项目根目录中的文件 middleware.ts(或 .js)来定义中间件,每个项目仅支持一个middleware.ts。例如,与pages
或app
处于同一级别,或者在 src 内部(如果适用)。
注意:虽然每个项目仅支持一个 middleware.ts 文件,但你仍然可以模块化地组织中间件逻辑。将中间件功能分解为单独的 .ts 或 .js 文件,并将它们导入到主 middleware.ts 文件中。这允许对特定于路由的中间件进行更清晰的管理,这些中间件聚合在 middleware.ts 中以进行集中控制。通过强制执行单个中间件文件,它可以简化配置,防止潜在冲突,并通过避免多个中间件层来优化性能。
javascript
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
// This function can be marked `async` if using `await` inside
export function middleware(request: NextRequest) {
return NextResponse.redirect(new URL('/home', request.url))
}
// See "Matching Paths" below to learn more
export const config = {
matcher: '/about/:path*',
}
Next执行顺序
next.js的执行顺序:
- 首先,Next.js会处理在next.config.js文件中定义的HTTP头信息。
- 接着,Next.js会处理在next.config.js文件中定义的重定向规则。
- 然后,Next.js会执行中间件,包括路径重写(rewrites)、重定向(redirects)等操作。
- 在处理文件系统路由之前,Next.js会处理在next.config.js文件中定义的beforeFiles阶段的路径重写规则。
- 接着,Next.js会处理文件系统路由,包括public、_next/static、pages、app等目录中的路由。
- 在处理文件系统路由之后,Next.js会处理在next.config.js文件中定义的afterFiles阶段的路径重写规则。
- 然后,Next.js会处理动态路由,例如/blog/[slug]这样的路由。
- 最后,如果没有匹配到任何路由,Next.js会处理在next.config.js文件中定义的fallback阶段的路径重写规则。
这个执行顺序决定了当一个HTTP请求到达时,Next.js如何查找和匹配路由,以及在哪个阶段应用中间件和路径重写规则。
匹配器
这句话意味着在Next.js项目中,每个路由请求都会触发中间件的执行。因此,使用匹配器(matchers)来精确指定或排除特定的路由是非常重要的。
有两种方法可以定义特定路由
Matcher
matcher 可以使中间件只在特定路径上运行。
arduino
export const config = {
matcher: '/about/:path*',
}
你可以使用数组语法匹配单个路径或多个路径:
arduino
export const config = {
matcher: ['/about/:path*', '/dashboard/:path*'],
}
匹配器配置允许完整的正则表达式
arduino
export const config = {
matcher: [
/*
* Match all request paths except for the ones starting with:
* - api (API routes)
* - _next/static (static files)
* - _next/image (image optimization files)
* - favicon.ico (favicon file)
*/
'/((?!api|_next/static|_next/image|favicon.ico).*)',
],
}
你还可以使用 Missing 或 Has 数组或两者的组合来绕过某些请求的中间件:
css
export const config = {
matcher: [
/*
* Match all request paths except for the ones starting with:
* - api (API routes)
* - _next/static (static files)
* - _next/image (image optimization files)
* - favicon.ico (favicon file)
*/
{
source: '/((?!api|_next/static|_next/image|favicon.ico).*)',
missing: [
{ type: 'header', key: 'next-router-prefetch' },
{ type: 'header', key: 'purpose', value: 'prefetch' },
],
},
{
source: '/((?!api|_next/static|_next/image|favicon.ico).*)',
has: [
{ type: 'header', key: 'next-router-prefetch' },
{ type: 'header', key: 'purpose', value: 'prefetch' },
],
},
{
source: '/((?!api|_next/static|_next/image|favicon.ico).*)',
has: [{ type: 'header', key: 'x-present' }],
missing: [{ type: 'header', key: 'x-missing', value: 'prefetch' }],
},
],
}
匹配器值需要是常量,以便可以在构建时对其进行静态分析。诸如变量之类的动态值将被忽略。
配置规则
-
必须以 / 开头
-
可以包含命名参数: /about/:path 匹配 /about/a 和 /about/b 但不匹配 /about/a/c
-
可以对命名参数进行修饰符(以 :) 开头: /about/:path* 匹配 /about/a/b/c 因为
*
0或更多。?
为0或1,+
表示1或多个。 -
可以使用括号括起来的正则表达式:/about/(.) 与/about/:path 相同
条件语句
javascript
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
if (request.nextUrl.pathname.startsWith('/about')) {
return NextResponse.rewrite(new URL('/about-2', request.url))
}
if (request.nextUrl.pathname.startsWith('/dashboard')) {
return NextResponse.rewrite(new URL('/dashboard/user', request.url))
}
}
使用 NextReaponse
后面的内容与路由处理器相似
以下是NextResponse API允许你做的事情:
- 重定向到另一个URL:你可以将当前的HTTP请求重定向到另一个URL。
- 重写响应:你可以改变HTTP响应的内容,显示给定URL的内容。
- 设置请求头:你可以为API路由、getServerSideProps函数、以及重写目标设置HTTP请求头。
- 设置响应的cookies:你可以在HTTP响应中设置cookies。
- 设置响应头:你可以设置HTTP响应的头信息。
要从中间件产生一个响应,你有以下两种选择:
- 重写到一个路由:你可以重写到一个能产生响应的路由,这个路由可以是一个页面路由,也可以是一个Edge API路由。
- 直接返回一个NextResponse:你也可以直接从中间件返回一个NextResponse对象。具体的方法可以参见文档中的"Producing a Response"部分。
总的来说,NextResponse API提供了一些方法来处理HTTP请求和响应,使得你可以在中间件中进行一些如重定向、重写响应、设置请求和响应头等操作。
使用cookie
Cookie 是常规标头。在请求中,它们存储在 Cookie 标头中。在响应中,它们位于 Set-Cookie 标头中。 Next.js 通过 NextRequest 和 NextResponse 上的 cookie 扩展提供了一种访问和操作这些 cookie 的便捷方法。
- 对于传入的请求,cookie 具有以下方法:get、getAll、set 和 delete cookies。你可以使用 has 检查 cookie 是否存在,或使用clear 删除所有 cookie。
- 对于传出的响应,cookie 有以下方法 get、getAll、set 和 delete。
javascript
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
// Assume a "Cookie:nextjs=fast" header to be present on the incoming request
// Getting cookies from the request using the `RequestCookies` API
let cookie = request.cookies.get('nextjs')
console.log(cookie) // => { name: 'nextjs', value: 'fast', Path: '/' }
const allCookies = request.cookies.getAll()
console.log(allCookies) // => [{ name: 'nextjs', value: 'fast' }]
request.cookies.has('nextjs') // => true
request.cookies.delete('nextjs')
request.cookies.has('nextjs') // => false
// Setting cookies on the response using the `ResponseCookies` API
const response = NextResponse.next()
response.cookies.set('vercel', 'fast')
response.cookies.set({
name: 'vercel',
value: 'fast',
path: '/',
})
cookie = response.cookies.get('vercel')
console.log(cookie) // => { name: 'vercel', value: 'fast', Path: '/' }
// The outgoing response will have a `Set-Cookie:vercel=fast;path=/` header.
return response
}
设置Header
你可以使用 NextResponse API 设置请求和响应标头(从 Next.js v13.0.0 开始可以设置请求标头)。
javascript
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
// Clone the request headers and set a new header `x-hello-from-middleware1`
const requestHeaders = new Headers(request.headers)
requestHeaders.set('x-hello-from-middleware1', 'hello')
// You can also set request headers in NextResponse.next
const response = NextResponse.next({
request: {
// New request headers
headers: requestHeaders,
},
})
// Set a new response header `x-hello-from-middleware2`
response.headers.set('x-hello-from-middleware2', 'hello')
return response
}
避免设置大标头,因为它可能会导致 431 请求标头字段太大错误,具体取决于你的后端 Web 服务器配置。
CORS
你可以在中间件中设置 CORS 标头以允许跨源请求,包括简单请求和预检请求。
javascript
import { NextRequest, NextResponse } from 'next/server'
const allowedOrigins = ['https://acme.com', 'https://my-app.org']
const corsOptions = {
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
}
export function middleware(request: NextRequest) {
// Check the origin from the request
const origin = request.headers.get('origin') ?? ''
const isAllowedOrigin = allowedOrigins.includes(origin)
// Handle preflighted requests
const isPreflight = request.method === 'OPTIONS'
if (isPreflight) {
const preflightHeaders = {
...(isAllowedOrigin && { 'Access-Control-Allow-Origin': origin }),
...corsOptions,
}
return NextResponse.json({}, { headers: preflightHeaders })
}
// Handle simple requests
const response = NextResponse.next()
if (isAllowedOrigin) {
response.headers.set('Access-Control-Allow-Origin', origin)
}
Object.entries(corsOptions).forEach(([key, value]) => {
response.headers.set(key, value)
})
return response
}
export const config = {
matcher: '/api/:path*',
}
设置response值
你可以通过返回 Response 或 NextResponse 实例直接从中间件进行响应。 (自 Next.js v13.1.0 起可用)
javascript
import type { NextRequest } from 'next/server'
import { isAuthenticated } from '@lib/auth'
// Limit the middleware to paths starting with `/api/`
export const config = {
matcher: '/api/:function*',
}
export function middleware(request: NextRequest) {
// Call our authentication function to check the request
if (!isAuthenticated(request)) {
// Respond with JSON indicating an error message
return Response.json(
{ success: false, message: 'authentication failed' },
{ status: 401 }
)
}
}
waitUntil 和 NextFetchEvent
NextFetchEvent
对象是原生 FetchEvent
对象的扩展。FetchEvent
是 Service Workers API 中的一个事件,它在 fetch 请求发生时被触发。Next.js 对这个对象进行了扩展,增加了 waitUntil()
方法。
waitUntil()
方法接受一个 promise 作为参数,并将 Middleware 的生命周期延长到这个 promise 完成(即 promise 状态决议为 fulfilled 或 rejected)为止。这个方法对于在后台执行工作非常有用。
例如,如果你需要在 Middleware 中执行一些异步操作,如读取数据库或调用远程 API,你可以将这个异步操作包装为一个 promise,然后使用 waitUntil()
方法来确保 Middleware 不会在这个操作完成之前结束。
这样,即使这个操作需要一些时间来完成,Next.js 也会保持 Middleware 的运行,直到这个操作完成。这使得你可以在 Middleware 中执行一些需要等待的异步操作,而不用担心这个操作因为 Middleware 的结束而被中断。
javascript
import { NextResponse } from 'next/server'
import type { NextFetchEvent, NextRequest } from 'next/server'
export function middleware(req: NextRequest, event: NextFetchEvent) {
event.waitUntil(
fetch('https://my-analytics-platform.com', {
method: 'POST',
body: JSON.stringify({ pathname: req.nextUrl.pathname }),
})
)
return NextResponse.next()
}
高级用法
在 Next.js v13.1 中,为中间件引入了两个附加标志:skipMiddlewareUrlNormalize
和skipTrailingSlashRedirect
,以处理高级用例。
skipTrailingSlashRedirect
的作用是禁用 Next.js 对于添加或删除尾随斜线(trailing slashes)的重定向。尾随斜线就是 URL 结尾的 /
。例如,https://example.com/path/
这个 URL 就有一个尾随斜线,而 https://example.com/path
这个 URL 则没有。
默认情况下,Next.js 会将带有尾部斜杠的 URL 重定向到不带尾部斜杠的对应 URL。例如/about/将重定向到/about。
但是在一些情况下,你可能希望对尾随斜线的处理有更多的控制。例如,你可能希望保留一部分 URL 的尾随斜线,而删除其他 URL 的尾随斜线。这种情况下,你可以使用 skipTrailingSlashRedirect
来禁用 Next.js 的自动重定向,然后在中间件中自定义对尾随斜线的处理。
javascript
const legacyPrefixes = ['/docs', '/blog']
export default async function middleware(req) {
const { pathname } = req.nextUrl
if (legacyPrefixes.some((prefix) => pathname.startsWith(prefix))) {
return NextResponse.next()
}
// apply trailing slash handling
if (
!pathname.endsWith('/') &&
!pathname.match(/((?!.well-known(?:/.*)?)(?:[^/]+/)*[^/]+.\w+)/)
) {
return NextResponse.redirect(
new URL(`${req.nextUrl.pathname}/`, req.nextUrl)
)
}
}
skipMiddlewareUrlNormalize
在 Next.js 中,/_next/data/build-id/page-path.json
这样的 URL 通常用于获取静态生成(Static Generation)或服务器端渲染(Server Side Rendering)页面的数据。这些 URL 是 Next.js 自动生成的,其中的 build-id
是每次构建时生成的唯一标识,page-path
是实际的页面路径。
当 Next.js 在服务器端处理这样的请求时,它会把 URL 标准化为实际的页面路径,即去掉前面的 /_next/data/build-id/
部分。这样可以使服务器端的路由处理更简单,因为服务器端只需要关心实际的页面路径,而不需要处理构建 ID 和其他 Next.js 特定的路径部分。
然而,在一些高级用例中,你可能需要在中间件中访问原始的 URL。这时,你可以设置 skipMiddlewareUrlNormalize
选项来禁用 URL 标准化。
例如,你可能需要在中间件中区分直接访问页面的请求和获取页面数据的请求,或者你需要在中间件中获取构建 ID 等信息。通过设置 skipMiddlewareUrlNormalize
选项,你可以在中间件中获取到原始的 URL,从而进行更精细的请求处理
javascript
module.exports = {
skipMiddlewareUrlNormalize: true,
}
export default async function middleware(req) {
const { pathname } = req.nextUrl
// GET /_next/data/build-id/hello.json
console.log(pathname)
// with the flag this now /_next/data/build-id/hello.json
// without the flag this would be normalized to /hello
}
Data Fetching
Fetching
应该在客户端获取数据还是服务端获取?
决定是在服务器上还是在客户端上获取数据取决于你正在构建的 UI 类型。
-
服务端执行:代码在服务器的环境中运行,这意味着它可以直接访问服务器资源,如数据库、文件系统等。服务器通常拥有比客户端更强大的处理能力和更高的带宽。
-
客户端执行:代码在用户的浏览器中运行,依赖于用户设备的性能。客户端环境受限于浏览器的安全沙箱模型,无法直接访问操作系统底层资源。
在大多数情况下,如果不需要实时数据(例如轮询),可以使用服务器组件在服务器上获取数据。这样做有几个好处
- 你可以在一个服务器往返(server-round trip)中获取数据,减少了网络请求和客户端-服务器瀑布流(waterfalls)的数量。瀑布流是指一系列的网络请求,其中每个请求都依赖于前一个请求的结果。
- 防止敏感信息(如访问令牌和API密钥)暴露给客户端。如果在客户端获取数据,你可能需要使用一个中间的API路由,这可能会暴露敏感信息。
- 通过在靠近数据源的地方获取数据,可以减少延迟。例如,如果你的应用代码和数据库在同一地区,那么在服务器端获取数据可以减少网络延迟。
- 数据请求可以被缓存和重新验证。
然而,服务器端数据获取也有一些限制。每次获取数据时,整个页面都需要在服务器上重新渲染。这可能会导致较大的服务器负载,并可能增加页面的响应时间。
在一些情况下,客户端数据获取可能更适合。例如,如果你需要改变/重新验证UI的一小部分,或者需要持续获取实时数据(如直播视图),那么你可以在客户端获取数据,并在客户端重新渲染UI的特定部分。这样可以减少服务器负载,并可以实时更新UI。
在next.js 有四种获取数据的途径
Fetch API(服务端)
Next.js 扩展了原生的 fetch Web API ,允许你为服务器上的每个 fetch 请求配置缓存和重新验证行为。你可以在服务器组件、路由处理程序router.js和服务器操作中使用 fetch。例如:
javascript
export default async function Page() {
const data = await fetch('https://api.example.com/...').then((res) =>
res.json()
)
return '...'
}
默认情况下,fetch请求没有缓存,每次都会请求新数据,所以请求的时候会导致整个route会动态渲染,而且数据也不会缓存,你可以通过将缓存选项设置为强缓存来缓存请求的数据,这意味着组件会被静态渲染···
php
fetch('https://...', { cache: 'force-cache' })
或者,如果使用部分预渲染(PPR),我们推荐将需要请求的组件包装在suspense边界中,这可以保证页面的动态行和流式渲染。
javascript
import { Suspense } from 'react'
export default async function Cart() {
const res = await fetch('https://api.example.com/...')
return '...'
}
export default function Navigation() {
return (
<>
<Suspense fallback={<LoadingIcon />}>
<Cart />
</Suspense>
<>
)
}
Request Memoization
如果你需要在树中的多个组件中获取相同的数据,则不必全局获取数据并向下传递 props。相反,你可以在需要数据的组件中获取数据,而不必担心对同一数据发出多个请求对性能的影响。
ORM 和数据库客户端(服务端)
你可以在服务器组件、路由处理程序和服务器操作中调用 ORM 或数据库客户端。
你可以使用 React cache
在 React 渲染过程中记住数据请求。例如,虽然在布局和页面中都调用了 getItem 函数,但只会对数据库进行一次查询:
typescript
// app/utils.ts
import { cache } from 'react'
export const getItem = cache(async (id: string) => {
const item = await db.item.findUnique({ id })
return item
})
//app/item/[id]/layout.tsx
import { getItem } from '@/utils/get-item'
export default async function Layout({
params: { id },
}: {
params: { id: string }
}) {
const item = await getItem(id)
// ...
}
//app/item/[id]/page.tsx
import { getItem } from '@/utils/get-item'
export default async function Page({
params: { id },
}: {
params: { id: string }
}) {
const item = await getItem(id)
// ...
}
你还可以使用实验性的unstable_cache 和unstable_noStore API 配置这些请求的缓存和重新验证行为。
数据获取库(客户端)
你可以使用 SWR 或 React Query 等数据获取库来获取客户端组件中的数据。这些库提供了自己的 API,用于缓存、重新验证和更改数据。 例如,使用SWR在客户端定期获取数据:
javascript
"use client"
import useSWR from 'swr'
import fetcher from '@/utils/fetcher'
export default function PollingComponent {
// Polling interval set to 2000 milliseconds
const { data } = useSWR('/api/data', fetcher, { refreshInterval: 2000 });
return '...'
}
Route Handlers(服务端或客户端)
Patterns
并行和串行的数据获取
在组件内部获取数据时,需要注意两种数据获取模式:并行和顺序。
串行:组件树中的请求相互依赖。这可能会导致加载时间更长。
并行:路由中的请求是同时发起的,并且会同时加载数据。这减少了加载数据所需的总时间。
串行获取
如果你有嵌套组件,并且每个组件都获取自己的数据。例如,只有在 Artist 组件完成获取数据后,Playlists 组件才会开始获取数据,因为 Playlists 依赖于 ArtistID 属性:
javascript
export default async function Page({
params: { username },
}: {
params: { username: string }
}) {
// Get artist information
const artist = await getArtist(username)
return (
<>
<h1>{artist.name}</h1>
{/* Show fallback UI while the Playlists component is loading */}
<Suspense fallback={<div>Loading...</div>}>
{/* Pass the artist ID to the Playlists component */}
<Playlists artistID={artist.id} />
</Suspense>
</>
)
}
async function Playlists({ artistID }: { artistID: string }) {
// Use the artist ID to fetch playlists
const playlists = await getArtistPlaylists(artistID)
return (
<ul>
{playlists.map((playlist) => (
<li key={playlist.id}>{playlist.name}</li>
))}
</ul>
)
}
你可以使用loading.js(对于路由段)或React (对于嵌套组件)来显示即时加载状态,同时React流式传输结果。 这将防止整个路由被数据请求阻塞,并且用户将能够与页面中准备好的部分进行交互。
并行获取
默认情况下,组件和路由段是并行呈现的。这意味着请求默认情况下是并行发起的。
同一个组件和路由段需要用promise.all去并行获取
javascript
import Albums from './albums'
async function getArtist(username: string) {
const res = await fetch(`https://api.example.com/artist/${username}`)
return res.json()
}
async function getAlbums(username: string) {
const res = await fetch(`https://api.example.com/artist/${username}/albums`)
return res.json()
}
export default async function Page({
params: { username },
}: {
params: { username: string }
}) {
const artistData = getArtist(username)
const albumsData = getAlbums(username)
// Initiate both requests in parallel
const [artist, albums] = await Promise.all([artistData, albumsData])
return (
<>
<h1>{artist.name}</h1>
<Albums list={albums} />
</>
)
}
数据预加载
在下面的例子中,page.tsx
页面依靠isAvailable
来决定要不要渲染Item
,但是可以先在checkIsAvailable
这个异步任务之前调用preload
,先执行getItem
,但不返回任何值,等到isAvailable
获取成功,再去渲染Item
的时候,getItem
已经被提前获取,在item
中就可以直接获取result
了,相当于将串行的获取变成了并行。
在JavaScript中,
void
操作符用于执行一个表达式,但不返回任何值(返回undefined
)。在这个例子中,void
用于触发getItem
函数
typescript
// components/Item.tsx
import { getItem } from '@/utils/get-item'
export const preload = (id: string) => {
// void evaluates the given expression and returns undefined
// https://developer.mozilla.org/docs/Web/JavaScript/Reference/Operators/void
void getItem(id)
}
export default async function Item({ id }: { id: string }) {
const result = await getItem(id)
// ...
}
javascript
// app/item/[id]/page.tsx
import Item, { preload, checkIsAvailable } from '@/components/Item'
export default async function Page({
params: { id },
}: {
params: { id: string }
}) {
// starting loading item data
preload(id)
// perform another asynchronous task
const isAvailable = await checkIsAvailable()
return isAvailable ? <Item id={id} /> : null
}
将 React 缓存和仅服务器与预加载模式结合使用
在下面的示例中preload
函数的作用仍然是预加载数据,它调用getItem
函数但不等待其返回结果。
getItem
函数则有所不同,它是一个被cache
函数包装的异步函数。cache
函数来自React,它的作用是缓存异步函数的结果,这样当多次调用这个函数时,如果输入参数相同,那么可以直接返回缓存的结果,而不需要重新执行异步操作
server-only 保证数据获取只在服务器端进行
typescript
import { cache } from 'react'
import 'server-only'
export const preload = (id: string) => {
void getItem(id)
}
export const getItem = cache(async (id: string) => {
// ...
})
经过这种优化,你可以立即获取数据,缓存response,并保证数据获取只在服务器端进行。
保护隐私数据不暴露在客户端
我们建议使用 React 的 taint API、taintObjectReference 和 taintUniqueValue,以防止将整个对象实例或敏感值传递给客户端。
zh-hans.react.dev/reference/r...
所谓"taint",在计算机科学中,通常指的是对数据进行标记,以表示该数据可能不安全或者不应该在某些场景下使用。在这里,React的taint API允许你将特定的对象或值进行标记(taint),以防止它们被发送到客户端。
要在应用程序中启用taint,请将 Next.js Config Experimental.taint 选项设置为 true:
java
// next.config.js
module.exports = {
experimental: {
taint: true,
},
}
然后将要标记的对象或值传递给experimental_taintObjectReference
或experimental_taintUniqueValue
函数:
javascript
// app/utils.ts
import { queryDataFromDB } from './api'
import {
experimental_taintObjectReference,
experimental_taintUniqueValue,
} from 'react'
export async function getUserData() {
const data = await queryDataFromDB()
experimental_taintObjectReference(
'Do not pass the whole user object to the client',
data
)
experimental_taintUniqueValue(
"Do not pass the user's address to the client",
data,
data.address
)
return data
}
javascript
// app/page.tsx
import { getUserData } from './data'
export async function Page() {
const userData = getUserData()
return (
<ClientComponent
user={userData} // this will cause an error because of taintObjectReference
address={userData.address} // this will cause an error because of taintUniqueValue
/>
)
}
缓存和重新验证
缓存
缓存是存储数据以减少向服务器发出的请求数量的过程。 Next.js 为单个数据请求提供内置数据缓存,让你可以精细控制缓存行为。
Fetch 请求
Next.js 15 中默认不缓存 fetch 请求。 要缓存单个获取请求,你可以使用cache: 'force-cache' 选项:
php
fetch('https://...', { cache: 'force-cache' })
数据库和orm
要缓存对数据库或 ORM 的特定请求,你可以使用unstable_cache API:
javascript
import { getUser } from './data'
import { unstable_cache } from 'next/cache'
const getCachedUser = unstable_cache(async (id) => getUser(id), ['my-app-user'])
export default async function Component({ userID }) {
const user = await getCachedUser(userID)
return user
}
跨多个功能重用数据
Next.js 使用generateMetadata
和generateStaticParams
等API,您需要使用在页面中获取的相同数据。
如果您使用 fetch,请求会自动记录。这意味着您可以使用相同的选项安全地调用相同的 URL,并且只会发出一个请求。
typescript
import { notFound } from 'next/navigation'
interface Post {
id: string
title: string
content: string
}
async function getPost(id: string) {
let res = await fetch(`https://api.example.com/posts/${id}`)
let post: Post = await res.json()
if (!post) notFound()
return post
}
export async function generateStaticParams() {
let posts = await fetch('https://api.example.com/posts').then((res) =>
res.json()
)
return posts.map((post: Post) => ({
id: post.id,
}))
}
export async function generateMetadata({ params }: { params: { id: string } }) {
let post = await getPost(params.id)
return {
title: post.title,
}
}
export default async function Page({ params }: { params: { id: string } }) {
let post = await getPost(params.id)
return (
<article>
<h1>{post.title}</h1>
<p>{post.content}</p>
</article>
)
}
重新验证数据
再验证就是清除数据缓存并重新获取最新的数据。当你的数据发生变化,并且你希望在依然享受静态渲染的速度优势的同时,确保展示最新的信息时,这将非常有用。
数据缓存可以通过两种方式进行再验证:
- 基于时间的再验证:在一定的时间过后自动重新验证数据。这对于数据变化不频繁且新鲜度不那么重要的情况很有用。
- 按需再验证:根据某个事件(例如表单提交)手动重新验证数据。按需再验证可以使用基于标签或者基于路径的方法来一次性重新验证一组数据。这在你希望尽快确保显示最新数据时非常有用(例如,当你的无头CMS的内容被更新时)。
总的来说,再验证是一种有效的数据管理策略,它允许你在享受缓存带来的性能优势的同时,也能保证数据的实时性。这对于构建响应式和实时更新的应用来说非常重要。
基于时间的再验证
要定期重新验证数据,可以使用 fetch 的 next.revalidate 选项来设置资源的缓存生命周期(以秒为单位)。
php
fetch('https://...', { next: { revalidate: 3600 } }) // revalidate at most every hour
或者,要重新验证路由段中的所有请求,你可以使用"路由段配置选项
"。
arduino
// layout.js | page.js
export const revalidate = 3600 // revalidate at most every hour
按需再验证
在Next.js中,提供了两个API:revalidatePath
和revalidateTag
,用于实现这个功能。
revalidatePath
用于在服务端动作(Server Actions)或者路由处理器(Route Handler)中,对特定路由的数据进行再验证。比如在createPost
的函数中,数据被修改(Mutate data)后,可以调用revalidatePath('/posts')
来重新获取/posts
这个路由的数据。
javascript
import { revalidatePath } from 'next/cache'
export async function createPost() {
// Mutate data
revalidatePath('/posts')
}
revalidateTag
则用于对跨路由的抓取请求进行再验证。当使用fetch
请求数据时,可以选择给缓存项打上一个或多个标签。然后,你可以调用revalidateTag
来重新验证所有与该标签关联的条目。例如,在app/page.tsx
文件中的fetch
请求添加了collection
这个缓存标签。然后,在@/app/actions.ts
文件中的action
函数中,通过调用revalidateTag('collection')
来重新验证所有打上collection
标签的抓取请求。
这样,就可以在数据变化时,通过按需再验证,确保客户端获取到的数据总是最新的。这对于构建实时更新的应用来说非常有用。
javascript
// app/page.tsx
export default async function Page() {
const res = await fetch('https://...', { next: { tags: ['collection'] } })
const data = await res.json()
// ...
}
javascript
// @/app/actions.ts
'use server'
import { revalidateTag } from 'next/cache'
export async function action() {
revalidateTag('collection')
}
错误处理
如果在尝试重新验证数据时抛出错误,则将继续从缓存中提供最后成功生成的数据。在下一个后续请求中,Next.js 将重试重新验证数据。
服务端操作与数据变化
服务器操作是在服务器上执行的异步函数。可以在服务器和客户端组件中调用它们来处理 Next.js 应用程序中的表单提交和数据变化。
约定
可以使用 React"use server"指令定义服务器操作。你可以将该指令放置在异步函数的顶部以将该函数标记为服务器操作,或者放置在单独文件的顶部以将该文件的所有导出标记为服务器操作。
服务端组件
在 app/page.tsx
文件中定义了一个名为 Page
的默认导出函数组件。在这个组件中,定义了一个名为 create
的异步函数,这个函数的目的是用来进行数据变化或突变(Mutate data)。要将这个 create
函数标记为服务器操作,需要在函数体的开始部分添加 'use server'
指令。这样,Next.js就知道这个函数应该在服务器上执行,而不是在客户端。
javascript
export default function Page() {
// Server Action
async function create() {
'use server'
// Mutate data
}
return '...'
}
客户端组件
要在客户端组件中调用服务器操作,请创建一个新文件并在其顶部添加"use server"指令。文件中的所有函数将被标记为服务器操作,可以在客户端和服务器组件中重用:
javascript
// app/actions.ts
'use server'
export async function create() {}
javascript
// app/ui/button.tsx
'use client'
import { create } from '@/app/actions'
export function Button() {
return <Button onClick={create} />
}
你还可以将服务器操作作为道具传递给客户端组件:
ini
<ClientComponent updateItemAction={updateItem} />
javascript
// app/client-component.tsx
'use client'
export default function ClientComponent({
updateItemAction,
}: {
updateItemAction: (formData: FormData) => void
}) {
return <form action={updateItemAction}>{/* ... */}</form>
}
示例
表单
React 扩展了 HTML
元素,以允许使用 action 属性调用服务器操作。 当在表单中调用时,该操作会自动接收 FormData 对象。您不需要使用 React useState 来管理字段,而是可以使用原生的 FormData 方法提取数据:
javascript
export default function Page() {
async function createInvoice(formData: FormData) {
'use server'
const rawFormData = {
customerId: formData.get('customerId'),
amount: formData.get('amount'),
status: formData.get('status'),
}
// mutate data
// revalidate cache
}
return <form action={createInvoice}>...</form>
}
传递附加参数
通过bind方法向服务端action传递附加参数
javascript
// app/client-component.tsx
'use client'
import { updateUser } from './actions'
export function UserProfile({ userId }: { userId: string }) {
const updateUserWithId = updateUser.bind(null, userId)
return (
<form action={updateUserWithId}>
<input type="text" name="name" />
<button type="submit">Update User Name</button>
</form>
)
}
服务端操作会接受除了表单数据之外的userId参数
javascript
'use server'
export async function updateUser(userId, formData) {}
表单嵌套元素
您还可以在 内嵌套的元素中调用服务器操作,例如 、 和 。这些元素接受 formAction 属性或事件处理程序。
当您想要在表单中调用多个服务器操作时,这非常有用。例如,您可以创建一个特定的
元素来保存帖子草稿以及发布它。请参阅 React 文档
代码控制表单提交
javascript
'use client'
export function Entry() {
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (
(e.ctrlKey || e.metaKey) &&
(e.key === 'Enter' || e.key === 'NumpadEnter')
) {
e.preventDefault()
e.currentTarget.form?.requestSubmit()
}
}
return (
<div>
<textarea name="entry" rows={20} required onKeyDown={handleKeyDown} />
</div>
)
}
服务端表单验证
使用 HTML 属性(如 required 和 type="email")进行基本的客户端表单验证。
javascript
// app/actions.ts
'use server'
import { redirect } from 'next/navigation'
export async function createUser(prevState: any, formData: FormData) {
const res = await fetch('https://...')
const json = await res.json()
if (!res.ok) {
return { message: 'Please enter a valid email' }
}
redirect('/dashboard')
}
然后,您可以将操作传递给 useActionState 挂钩并使用返回的状态来显示错误消息。
javascript
// app/ui/signup.tsx
'use client'
import { useActionState } from 'react'
import { createUser } from '@/app/actions'
const initialState = {
message: '',
}
export function Signup() {
const [state, formAction] = useActionState(createUser, initialState)
return (
<form action={formAction}>
<label htmlFor="email">Email</label>
<input type="text" id="email" name="email" required />
{/* ... */}
<p aria-live="polite">{state?.message}</p>
<button>Sign up</button>
</form>
)
}
Pending状态
useActionState 提供一个挂起状态,可用于在执行操作时显示加载指示。
javascript
'use client'
import { useActionState } from 'react'
import { createUser } from '@/app/actions'
const initialState = {
message: '',
}
export function Signup() {
const [state, formAction, pending] = useActionState(createUser, initialState)
return (
<form action={formAction}>
<label htmlFor="email">Email</label>
<input type="text" id="email" name="email" required />
{/* ... */}
<p aria-live="polite" className="sr-only">
{state?.message}
</p>
<button aria-disabled={pending} type="submit">
{pending ? 'Submitting...' : 'Sign up'}
</button>
</form>
)
}
乐观更新
useOptimistic 可以使用这个钩子函数来对用户需要等待的操作进行乐观更新,而不是等待响应。
typescript
'use client'
import { useOptimistic } from 'react'
import { send } from './actions'
type Message = {
message: string
}
export function Thread({ messages }: { messages: Message[] }) {
const [optimisticMessages, addOptimisticMessage] = useOptimistic<
Message[],
string
>(messages, (state, newMessage) => [...state, { message: newMessage }])
const formAction = async (formData) => {
const message = formData.get('message') as string
addOptimisticMessage(message)
await send(message)
}
return (
<div>
{optimisticMessages.map((m, i) => (
<div key={i}>{m.message}</div>
))}
<form action={formAction}>
<input type="text" name="message" />
<button type="submit">Send</button>
</form>
</div>
)
}
事件处理
虽然在 元素中使用服务器操作很常见,但它们也可以通过事件处理程序(例如 onClick)来调用。例如,要增加相似计数:
javascript
// app/like-button.tsx
'use client'
import { incrementLike } from './actions'
import { useState } from 'react'
export default function LikeButton({ initialLikes }: { initialLikes: number }) {
const [likes, setLikes] = useState(initialLikes)
return (
<>
<p>Total Likes: {likes}</p>
<button
onClick={async () => {
const updatedLikes = await incrementLike()
setLikes(updatedLikes)
}}
>
Like
</button>
</>
)
}
您还可以向表单元素添加事件处理程序,例如,在 onChange 时保存表单字段:
javascript
// app/ui/edit-post.tsx
'use client'
import { publishPost, saveDraft } from './actions'
export default function EditPost() {
return (
<form action={publishPost}>
<textarea
name="content"
onChange={async (e) => {
await saveDraft(e.target.value)
}}
/>
<button type="submit">Publish</button>
</form>
)
}
useEffect
当组件安装或依赖项更改时,可以使用 React useEffect
挂钩来调用服务器操作。这对于依赖全局事件或需要自动触发的突变非常有用。例如,用于应用程序快捷方式的 onKeyDown
、用于无限滚动的交集观察者挂钩,或者当组件安装以更新视图计数时:
javascript
'use client'
import { incrementViews } from './actions'
import { useState, useEffect } from 'react'
export default function ViewCount({ initialViews }: { initialViews: number }) {
const [views, setViews] = useState(initialViews)
useEffect(() => {
const updateViews = async () => {
const updatedViews = await incrementViews()
setViews(updatedViews)
}
updateViews()
}, [])
return <p>Total Views: {views}</p>
}
错误处理
当抛出错误时,它将被客户端上最近的 error.js 或 边界捕获。我们建议使用 try/catch 返回错误以供 UI 处理。 例如,服务器操作可能会通过返回消息来处理创建新项目时出现的错误:
javascript
'use server'
export async function createTodo(prevState: any, formData: FormData) {
try {
// Mutate data
} catch (e) {
throw new Error('Failed to create task')
}
}
重定向
如果您想在服务器操作完成后将用户重定向到不同的路由,您可以使用重定向 API。需要在 try/catch 块之外调用重定向:
javascript
'use server'
import { redirect } from 'next/navigation'
import { revalidateTag } from 'next/cache'
export async function createPost(id: string) {
try {
// ...
} catch (error) {
// ...
}
revalidateTag('posts') // Update cached posts
redirect(`/post/${id}`) // Navigate to the new post page
}
安全
认证与授权
javascript
'use server'
import { auth } from './lib'
export function addItem() {
const { user } = auth()
if (!user) {
throw new Error('You must be signed in to perform this action')
}
// ...
}
闭包与加密
在组件内部定义服务器操作会创建一个闭包,其中该操作可以访问外部函数的范围。例如,publish可以访问publishVersion变量:
javascript
export default async function Page() {
const publishVersion = await getLatestVersion();
async function publish() {
"use server";
if (publishVersion !== await getLatestVersion()) {
throw new Error('The version has changed since pressing publish');
}
...
}
return (
<form>
<button formAction={publish}>Publish</button>
</form>
);
}
当需要在渲染时捕获数据快照(例如,publishVersion)以便稍后在调用操作时使用它时,闭包非常有用。
然而,为了实现这个功能,被捕获的变量需要在服务器和客户端之间进行传输。为了防止敏感数据在客户端被暴露,Next.js会自动对这些闭包变量进行加密。
服务器端操作(Server Action)在Next.js构建应用程序时,会生成一个与之关联的特定的私钥。私钥的作用是用来加密和解密与服务器端操作有关的数据,这些数据包括操作需要的参数以及操作的结果。
当服务器端操作被触发,例如用户点击一个按钮,这个操作的参数和其他相关数据会被这个私钥加密,然后发送到客户端。客户端在收到数据后,会再次用这个私钥对数据进行解密,然后执行相应的操作。
因为每次构建应用程序都会生成一个新的私钥,所以这个私钥仅对应于特定的构建版本。换句话说,只有在构建生成这个服务器端操作的特定版本的应用程序中,这个操作才能被成功调用。如果试图在一个不同的构建版本中调用这个操作,由于私钥不匹配,数据无法被正确解密,因此操作无法被执行。
这种设计主要是为了保护数据的安全,防止敏感数据在客户端被暴露,也确保了服务器端操作的调用是在一个可控和安全的环境中进行的。
我们不建议仅依靠加密来防止敏感值在客户端上泄露。相反,您应该使用 React taint API 主动阻止特定数据发送到客户端。
覆盖加密密钥(进阶)
当跨多个服务器自托管 Next.js 应用程序时,每个服务器实例最终可能会使用不同的加密密钥,从而导致潜在的不一致。
为了缓解这种情况,您可以使用 process.env.NEXT_SERVER_ACTIONS_ENCRYPTION_KEY 环境变量覆盖加密密钥。指定此变量可确保您的加密密钥在各个版本中保持不变,并且所有服务器实例都使用相同的密钥。
Allowed origins (进阶)
lua
// next.config.js
/** @type {import('next').NextConfig} */
module.exports = {
experimental: {
serverActions: {
allowedOrigins: ['my-proxy.com', '*.my-proxy.com'],
},
},
}
这段代码是Next.js的配置文件next.config.js
中的一部分,它的作用是配置服务器端行为(Server Actions)的允许来源(Allowed Origins)。
由于服务器端行为可以在HTML的<form>
元素中被调用,这可能使它们面临跨站请求伪造(CSRF)攻击。为了防止大多数现代浏览器中的CSRF漏洞,Next.js只允许使用POST方法来调用服务器端行为。
另外,Next.js还会将请求头中的Origin字段与Host字段(或X-Forwarded-Host字段)进行比较。如果这两个字段不匹配,那么请求将被终止。也就是说,服务器端行为只能在托管它的页面的同一主机上被调用。
对于那些使用反向代理或多层后端架构(即服务器API与生产域名不同)的大型应用来说,推荐使用serverActions.allowedOrigins
选项来指定一系列安全的来源。这个选项接受一个字符串数组。
Rendering
服务端组件
React Server Components是一种可以在服务器上渲染的UI编写方式,并且可以选择是否缓存。这种方式使得用户在访问网页时,服务器可以直接返回渲染好的HTML,而不是传统的客户端渲染方法,即在用户的浏览器上动态生成HTML。
在Next.js中,对React Server Components的渲染工作进一步进行了细分,按照路由段进行拆分,以实现流式和部分渲染。这意味着服务器可以按部分发送已渲染的HTML,而不是等待整个页面全部渲染完成。
next提供了三种服务端渲染策略。
-
静态渲染:预先生成和重复使用页面,适合内容不经常变化的网页。
-
动态渲染:每次请求时生成页面,适合内容经常变化或每个用户都有独特内容的网页。
-
流式渲染:在页面还在生成过程中就开始发送已经渲染好的部分,提高了首屏加载速度。
服务端渲染的好处
-
数据获取(Data Fetching):Server Components允许你将数据获取任务移到服务器上,这样可以更接近你的数据源。这有助于提升性能,因为可以减少获取渲染所需数据的时间,以及客户端需要发出的请求数量。
-
安全性(Security):Server Components使你可以将敏感数据和逻辑保留在服务器上,比如tokens和API密钥,从而避免将它们暴露给客户端。
-
缓存(Caching):通过在服务器上进行渲染,可以将结果缓存并在后续的请求或者不同的用户之间重复使用。这可以提升性能并降低成本,因为可以减少每个请求需要进行的渲染和数据获取的数量。
-
性能(Performance):Server Components提供了额外的工具来从基线开始优化性能。例如,如果你的应用完全由Client Components构成,那么将非交互性的UI部分移动到Server Components可以减少客户端所需的JavaScript数量。这对于网络速度较慢或设备性能较弱的用户来说很有益,因为浏览器需要下载、解析和执行的客户端JavaScript会减少。
-
初始页面加载和首次内容绘制(Initial Page Load and First Contentful Paint,FCP):在服务器上,我们可以生成HTML来让用户立即查看页面,无需等待客户端下载、解析和执行渲染页面所需的JavaScript。
-
搜索引擎优化和社交网络分享(Search Engine Optimization and Social Network Shareability):渲染出的HTML可以被搜索引擎机器人用来索引你的页面,也可以被社交网络机器人用来生成你的页面的社交卡片预览。
-
流式传输(Streaming):Server Components允许你将渲染工作分解成块,并在它们准备好时将它们流式传输到客户端。这使得用户无需等待服务器上整个页面的渲染就可以提前看到页面的部分内容。
服务端组件是如何渲染的
Next.js在服务器端使用React的API来编排渲染
在服务器端,Next.js将渲染工作分割成多个块,这些块是按照单独的路由段和Suspense Boundaries来划分的。每个块的渲染分为两个步骤:
- React将服务器组件渲染成一种特殊的数据格式,称为React Server Component Payload(RSC Payload)。
- Next.js使用RSC Payload和客户端组件的JavaScript指令来在服务器上渲染HTML。
然后,在客户端:
-
使用HTML立即展示路由的快速非交互式预览,这仅适用于初始页面加载。
-
使用React Server Components Payload来协调客户端和服务器组件树,并更新DOM。
-
使用JavaScript指令来"激活"客户端组件,使应用程序变得可交互。
RSC 是什么?
React Server Components 就是 React 的服务端组件,它们只在服务端运行,可以调用服务端的方法、访问数据库等。RSC 每次预渲染后把 HTML 发送到客户端,由客户端进行水合(hydrate)并正式渲染。这种做法的好处是,一部分原本要打包在客户端 JavaScript 文件里的代码,现在可以放在服务端运行了,从而减轻客户端的负担,提升应用的整体性能和响应速度。
服务端渲染策略
静态渲染(默认)
静态渲染是默认的渲染方式。在此模式下,路由在构建时或在数据重新验证后在后台进行渲染。渲染的结果会被缓存,并可以推送到内容分发网络(CDN)。这种优化允许您在用户和服务器请求之间共享渲染工作的结果。静态渲染在路由数据不针对特定用户并且可以在构建时知道的情况下非常有用,例如静态博客帖子或产品页面。
流式渲染
流式渲染则允许您从服务器逐步渲染用户界面。工作被分割成块,并在准备好后流式传输到客户端。这允许用户在整个内容完成渲染之前立即看到页面的部分内容。
流式渲染已经内置在Next.js的应用路由器中。这有助于提高初始页面加载性能,以及依赖于较慢数据获取的用户界面,这些数据获取会阻止渲染整个路由。例如,产品页面上的评论。
您可以使用loading.js和React Suspense开始流式传输路由段和UI组件。有关更多信息,请参阅加载UI和流式传输部分。
动态渲染
通过动态渲染,可以在请求时为每个用户渲染路由。
当路由具有针对用户的个性化数据或具有仅在请求时才能知道的信息(例如 cookie 或 URL 的搜索参数)时,动态渲染非常有用。
大多数网站的路由并不是完全静态或完全动态的,而是一个连续体。例如,你可以有一个电商页面,该页面使用缓存的产品数据(这些数据在一段时间内重新验证),但也有未缓存的,针对个人的客户数据。
在Next.js中,你可以有动态渲染的路由,这些路由既有缓存的数据,也有未缓存的数据。这是因为RSC负载和数据是分开缓存的。这让你可以选择动态渲染,而不用担心在请求时获取所有数据的性能影响。
在渲染过程中,如果发现动态函数或未缓存的数据请求,Next.js将切换到动态渲染整个路由。有一个表格总结了动态函数和数据缓存如何影响路由是静态渲染还是动态渲染。
在上表中,对于一个完全静态的路由,所有的数据都必须被缓存。然而,你可以有一个动态渲染的路由,它既使用缓存的数据获取,也使用未缓存的数据获取。
作为开发者,你无需在静态渲染和动态渲染之间做选择,因为Next.js会根据使用的功能和API自动为每个路由选择最佳的渲染策略。相反,你可以选择何时缓存或重新验证特定数据,你也可以选择流式传输你的UI的部分。
动态函数依赖于只能在请求时知道的信息,如用户的cookies,当前的请求头,或URL的搜索参数。在Next.js中,这些动态API包括:
cookies()
headers()
unstable_noStore()
unstable_after()
:searchParams prop
使用任何这些函数将在请求时将整个路由切换到动态渲染。
客户端组件
客户端组件允许您编写在服务器上预渲染的交互式 UI,并且可以使用客户端 JavaScript 在浏览器中运行。 本页将介绍客户端组件如何工作、如何呈现以及何时可以使用它们。
客户端渲染的优点:
-
可交互性:客户端可以使用state,effect,和事件侦听器。可以向用户提供即时反馈和更新ui
-
可使用浏览器ap:客户端组件可以访问浏览器 API,例如地理位置或本地存储。
要使用客户端组件,需要在文件顶部的导入上方添加 React"use client"指令。
"use client"用于区别服务器和客户端组件模块。这意味着通过在文件中定义"use client",导入到其中的所有其他模块(包括子组件)都被视为客户端包的一部分。
javascript
'use client'
import { useState } from 'react'
export default function Counter() {
const [count, setCount] = useState(0)
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>Click me</button>
</div>
)
}
下图显示,如果未定义"use client"指令,则在嵌套组件 (toggle.js) 中使用 onClick 和 useState 将导致错误。这是因为,默认情况下,App Router 中的所有组件都是服务器组件,服务器组件不可用这些 API 。通过在toggle.js中定义"use client"指令,React就会将这些组件识别为可以使用这些API的客户端组件。
客户端组件是如何渲染的?
在 Next.js 中,客户端组件的呈现方式有所不同,具体取决于请求是完整页面加载(对应用程序的初始访问还是由浏览器刷新触发的页面重新加载)或后续导航的一部分。
完整页面加载
next使用React的API,在服务器上为客户端和服务器端组件渲染静态HTML预览。这使得用户在首次访问应用程序时,可以立即看到页面内容,而不需要等待客户端下载、解析和执行客户端组件的JavaScript包。
在服务端:
-
react将服务端组件渲染成一种特殊的数据结构React Server Component Payload(RSC Payload),它包含了对客户端组件的引用。
-
next使用RSC Payload和客户端组件的js代码在服务器上呈现路由的html
然后,在客户端:
-
HTML 用于立即显示路由页面的快速非交互式初始预览。
-
RSC Payload用于协调客户端和服务器组件树,并更新 DOM。
-
JavaScript 指令用于补充客户端组件并使其 UI 具有交互性。
什么是hydration:
Hydration 是将事件侦听器附加到 DOM 的过程,以使静态 HTML 具有交互性。在幕后, hydrate是通过 HydroRoot 完成的
后续导航的一部分
在后续导航中,客户端组件完全在客户端上呈现,而不需要服务器呈现的 HTML。 这意味着客户端组件 JavaScript 包已下载并解析。一旦包准备好,React 将使用 RSC Payload 来协调客户端和服务器组件树,并更新 DOM。
服务端与客户端组件组合模式
什么时候使用服务端组件,什么时候使用客户端组件
服务端组件模式
在选择客户端渲染之前,您可能希望在服务器上执行一些工作,例如获取数据或访问数据库或后端服务。 以下是使用服务器组件时的一些常见模式:
组件之间共享数据
在服务器上获取数据时,可能存在需要跨不同组件共享数据的情况。例如,可能有一个依赖于相同数据的布局和页面。
服务器组件不可以使用 React Context(服务器上不可用)或将数据作为 props 传递,可以使用 fetch 或 React 的缓存功能来获取需要它的组件中的相同数据, 无需担心对相同数据发出重复请求。这是因为React扩展了fetch来自动记忆数据请求,并且当fetch不可用时可以使用缓存功能。
将仅服务端可用的代码排除在客户端环境之外
由于 JavaScript 模块可以在服务器和客户端组件模块之间共享,因此本来只打算在服务器上运行的代码可能会潜入客户端。
例如,采用以下数据获取函数:
javascript
export async function getData() {
const res = await fetch('https://external-service.com/data', {
headers: {
authorization: process.env.API_KEY,
},
})
return res.json()
}
getData
函数使用了环境变量 API_KEY
来进行身份验证。这个函数看起来可以在服务器和客户端上运行,但实际上,它是期望只在服务器上执行的。因为环境变量 API_KEY
并没有以 NEXT_PUBLIC
为前缀,所以它是一个私有变量,只能在服务器上访问。为了防止环境变量泄露到客户端,Next.js 会将私有环境变量替换为空字符串。因此,即使 getData()
可以在客户端被导入和执行,但它不会按预期工作。而将变量设置为公开的,虽然可以让函数在客户端工作,但你可能并不希望将敏感信息暴露给客户端。
为了防止这种无意中将服务器端代码用在客户端的情况,可以使用 server-only
包。当其他开发者错误地将这些模块导入到客户端组件中时,server-only
包会在构建时给出错误提示。
要使用 server-only
,首先要安装这个包,
sql
npm install server-only
然后在包含服务器端代码的模块中导入这个包。
javascript
import 'server-only'
export async function getData() {
const res = await fetch('https://external-service.com/data', {
headers: {
authorization: process.env.API_KEY,
},
})
return res.json()
}
现在,任何导入 getData()
的客户端组件都会在构建时收到一个错误提示,说明这个模块只能在服务器上使用。
使用第三方的包和依赖
目前,许多从 npm 包中引入并使用客户端特性的组件还没有添加这个指令。这些第三方组件在客户端组件中可以正常工作,因为它们有 "use client" 指令,但在服务器组件中则无法工作。
要解决这个问题,你可以将依赖客户端特性的第三方组件包装在你自己的客户端组件中。这样,你就可以直接在服务器组件中使用 Carousel
组件了。
在已声明的客户端组件中使用,OK
javascript
'use client'
import { useState } from 'react'
import { Carousel } from 'acme-carousel'
export default function Gallery() {
const [isOpen, setIsOpen] = useState(false)
return (
<div>
<button onClick={() => setIsOpen(true)}>View pictures</button>
{/* Works, since Carousel is used within a Client Component */}
{isOpen && <Carousel />}
</div>
)
}
直接使用,会报错:
javascript
import { Carousel } from 'acme-carousel'
export default function Page() {
return (
<div>
<p>View pictures</p>
{/* Error: `useState` can not be used within Server Components */}
<Carousel />
</div>
)
}
包装在客户端组件中,it will work
javascript
// app/carousel.tsx
'use client'
import { Carousel } from 'acme-carousel'
export default Carousel
javascript
// app/page.tsx
import Carousel from './carousel'
export default function Page() {
return (
<div>
<p>View pictures</p>
{/* Works, since Carousel is a Client Component */}
<Carousel />
</div>
)
}
使用 Context Providers
Context Providers通常在应用的根部渲染,用于共享全局关注点,例如当前主题。然而,由于 React 上下文在服务器组件中不被支持,如果试图在应用的根部创建上下文,将会导致错误。
javascript
import { createContext } from 'react'
// createContext is not supported in Server Components
export const ThemeContext = createContext({})
export default function RootLayout({ children }) {
return (
<html>
<body>
<ThemeContext.Provider value="dark">{children}</ThemeContext.Provider>
</body>
</html>
)
}
要解决这个问题的话,可以将它标记为客户端组件,你的服务端组件就可以直接渲染这个组件了。
javascript
// app/theme-provider.tsx
'use client'
import { createContext } from 'react'
export const ThemeContext = createContext({})
export default function ThemeProvider({
children,
}: {
children: React.ReactNode
}) {
return <ThemeContext.Provider value="dark">{children}</ThemeContext.Provider>
}
javascript
import ThemeProvider from './theme-provider'
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html>
<body>
<ThemeProvider>{children}</ThemeProvider>
</body>
</html>
)
}
由于提供者在根部渲染,应用中的所有其他客户端组件都能够使用这个上下文。
但要注意,应该尽可能在树的深处渲染提供者。因为将组件标记为客户端组件,那么它的所有子组件也将被视为客户端组件。客户端组件可以在服务器组件中渲染,但是服务器组件不能在客户端组件中渲染。因此,一旦将组件标记为客户端组件,它及其所有子组件都将被视为客户端组件,即使这些子组件在其他情况下可能会作为服务器组件渲染,这样会对性能有一定损耗
客户端组件模式
将客户端组件移到尽可能深的层级下
为了减少客户端 JavaScript 包的大小,我们建议将客户端组件移至尽可能深的层级下
例如,您可能有一个包含静态元素(例如徽标、链接等)的布局和一个使用状态的交互式搜索栏。不要将整个布局变成客户端组件,而是只将交互逻辑移到客户端组件(例如 ),其他的组件依然使用服务端组件。
javascript
// SearchBar is a Client Component
import SearchBar from './searchbar'
// Logo is a Server Component
import Logo from './logo'
// Layout is a Server Component by default
export default function Layout({ children }: { children: React.ReactNode }) {
return (
<>
<nav>
<Logo />
<SearchBar />
</nav>
<main>{children}</main>
</>
)
}
将props从服务端组件传递到客户端组件(序列化)
如果在服务器组件中获取数据,可能希望将数据作为 props 传递给客户端组件。从服务器传递到客户端组件的 Props 需要由 React 进行序列化。
客户端和服务端交错使用
需要注意的点:
- 在请求-响应生命周期中,您的代码从服务器移动到客户端。如果您需要在客户端上访问服务器上的数据或资源,您将向服务器发出新请求 - 而不是来回切换(发出新请求会有缓存,不会再次真实查询)。
- 当向服务器发出新请求时,首先呈现所有服务器组件,包括嵌套在客户端组件内的服务器组件。渲染结果(RSC 有效负载)将包含对客户端组件位置的引用。然后,在客户端,React 使用 RSC Payload 将服务器和客户端组件协调到单个树中。
- 由于客户端组件是在服务器组件之后呈现的,因此您无法将服务器组件导入到客户端组件模块中(因为它需要将新请求返回到服务器)。相反,您可以将服务器组件作为props传递给客户端组件。请参考下面的不支持的模式和支持的模式。
不支持的模式
无法将服务器组件导入客户端组件:
javascript
'use client'
// You cannot import a Server Component into a Client Component.
import ServerComponent from './Server-Component'
export default function ClientComponent({
children,
}: {
children: React.ReactNode
}) {
const [count, setCount] = useState(0)
return (
<>
<button onClick={() => setCount(count + 1)}>{count}</button>
<ServerComponent />
</>
)
}
支持的模式
可以将服务器组件作为props传递给客户端组件。
一种常见的模式是使用 React Children 属性在客户端组件中创建一个"slot"。
在下面的示例中, 接受一个 Children 属性:
javascript
'use client'
import { useState } from 'react'
export default function ClientComponent({
children,
}: {
children: React.ReactNode
}) {
const [count, setCount] = useState(0)
return (
<>
<button onClick={() => setCount(count + 1)}>{count}</button>
{children}
</>
)
}
javascript
// page.tsx
// This pattern works:
// You can pass a Server Component as a child or prop of a
// Client Component.
import ClientComponent from './client-component'
import ServerComponent from './server-component'
// Pages in Next.js are Server Components by default
export default function Page() {
return (
<ClientComponent>
<ServerComponent />
</ClientComponent>
)
}
通过这种方法, 和 被解耦并且可以独立渲染。在这种情况下,子 可以在 在客户端上呈现之前在服务器上呈现的样子。
部分预渲染(PPR)
部分预渲染 (PPR) 使您能够在同一路径中将静态和动态组件组合在一起。
部分预渲染是一项实验性功能,可能会发生变化。它尚未准备好用于生产用途。
运行时
Next.js 有两个可以在应用程序中使用的服务器运行时:
- Node.js 运行时(默认)可以访问生态系统中的所有 Node.js API 和兼容包。用于渲染您的应用程序。
- Edge Runtime 包含一组更有限的 API。用于中间件(路由规则,如重定向、重写和设置标头)。