本篇我们讲解请求相关的常用方法,有:
用到的时候到此篇查看具体的语法即可。
generateStaticParams
和动态路由一起使用,用于在构建时静态生成路由:
// app/product/[id]/page.js
export function generateStaticParams() {
return [{ id: '1' }, { id: '2' }, { id: '3' }]
}
// 对应会生成 3 个静态路由:
// - /product/1
// - /product/2
// - /product/3
export default function Page({ params }) {
const { id } = params
// ...
}
可以在 generateStaticParams
使用 fetch 请求,这个例子更贴近实际的开发场景:
// app/blog/[slug]/page.js
export async function generateStaticParams() {
const posts = await fetch('https://.../posts').then((res) => res.json())
return posts.map((post) => ({
slug: post.slug,
}))
}
export default function Page({ params }) {
const { slug } = params
// ...
}
关于 generateStaticParams
:
dynamicParams
路由段配置控制当访问不是由 generateStaticParams
生成的动态段时发生的情况next dev
的时候,当你导航到路由时,generateStaticParams
才会被调用next build
的时候,generateStaticParams
会在对应的布局或页面生成之前运行generateStaticParams
不会再次被调用generateStaticParams
替代了 Pages Router 下的 getStaticPaths
函数的功能上面这个例子是处理单个动态段,generateStaticParams
也可以处理多个动态段:
// app/products/[category]/[product]/page.js
export function generateStaticParams() {
return [
{ category: 'a', product: '1' },
{ category: 'b', product: '2' },
{ category: 'c', product: '3' },
]
}
// 对应会生成 3 个静态路由:
// - /products/a/1
// - /products/b/2
// - /products/c/3
export default function Page({ params }) {
const { category, product } = params
// ...
}
也可以处理 Catch-all 动态段:
// app/product/[...slug]/page.js
export function generateStaticParams() {
return [{ slug: ['a', '1'] }, { slug: ['b', '2'] }, { slug: ['c', '3'] }]
}
// 对应会生成 3 个静态路由:
// - /product/a/1
// - /product/b/2
// - /product/c/3
export default function Page({ params }) {
const { slug } = params
// ...
}
generateStaticParams
支持传入一个可选 options.params
参数。如果一个路由中的多个动态段都使用了 generateStaticParams
,子 generateStaticParams
函数会为每一个父 generateStaticParams
生成的 params
执行一次。
这句话是什么意思呢?举个例子,现在我们有这样一个 /products/[category]/[product]
路由地址,这个路由里有两个动态段 [category]
和 [product]
,[product]
依赖于 [category]
,毕竟要先知道类目才能该类目下知道有哪些产品。为了解决这个问题:
首先生成父段:
// app/products/[category]/layout.js
export async function generateStaticParams() {
const products = await fetch('https://.../products').then((res) => res.json())
return products.map((product) => ({
category: product.category.slug,
}))
}
export default function Layout({ params }) {
// ...
}
然后子 generateStaticParams
函数就可以使用父 generateStaticParams
函数返回的 params
参数动态生成自己的段:
// app/products/[category]/[product]/page.js
export async function generateStaticParams({ params: { category } }) {
const products = await fetch(
`https://.../products?category=${category}`
).then((res) => res.json())
return products.map((product) => ({
product: product.id,
}))
}
export default function Page({ params }) {
// ...
}
在这个例子中,params
对象就包含了从父 generateStaticParams
生成的 params
,可以用此生成子段的 params
。
这种填充动态段的方式被称为“自上而下生成参数”,子段依赖于父段的数据。但如果不依赖,就比如提供一个接口,直接返回所有的产品和对应的目录信息,完全可以直接生成,示例代码如下:
// app/products/[category]/[product]/page.js
export async function generateStaticParams() {
const products = await fetch('https://.../products').then((res) => res.json())
return products.map((product) => ({
category: product.category.slug,
product: product.id,
}))
}
export default function Page({ params }) {
// ...
}
不需要再写父 generateStaticParams
函数,直接一步到位,这种填充动态段的方式被称为“自下而上生成参数”。
generateStaticParams
应该返回一个对象数组,其中每个对象表示单个路由的填充动态段:
直接描述反而有些复杂,其实很简单,比如:
/product/[id]
这种动态路由,generateStaticParams
应该返回一个类似于 [{id: xxx}, {id: xxx}, ...]
的对象。
对于 /products/[category]/[product]
这种动态路由,generateStaticParams
应该返回一个类似于 [{category: xxx, product: xxx}, {category: xxx, product: xxx}, ...]
的对象。
对于 /products/[...slug]
这种动态路由,generateStaticParams
应该返回一个类似于[{slug: [xxx, xxx, ...]}, {slug: [xxx, xxx, ...]}, ...]
的对象。
返回类型描述如下:
示例路由 | generateStaticParams 返回类型 |
---|---|
/product/[id] |
{ id: string }[] |
/products/[category]/[product] |
{ category: string, product: string }[] |
/products/[...slug] |
{ slug: string[] }[] |
你可以自定义页面的初始 viewport,有两种方法:
viewport
对象generateViewport
函数使用的时候要注意:
viewport
对象和 generateViewport
函数仅支持在服务端组件中导出viewport
对象和 generateViewport
函数viewport
对象的方式进行定义从 layout.js
或者 page.js
中导出一个名为 viewport
的对象:
// layout.js | page.js
export const viewport = {
themeColor: 'black',
}
export default function Page() {}
从 layout.js
或者 page.js
中导出一个名为 generateViewport
的函数,该函数返回包含一个或者多个viewport 字段的 Viewport 对象:
export function generateViewport({ params }) {
return {
themeColor: '...',
}
}
theme-color,用户的浏览器将根据所设定的建议颜色来改变用户界面,比如在 Android 上的 Chrome 设定颜色后:
支持简单的主题颜色设置:
// layout.js | page.js
export const viewport = {
themeColor: 'black',
}
对应输出为:
<meta name="theme-color" content="black" />
也支持带 media 属性的主题颜色设置:
export const viewport = {
themeColor: [
{ media: '(prefers-color-scheme: light)', color: 'cyan' },
{ media: '(prefers-color-scheme: dark)', color: 'black' },
],
}
对应输出为:
<meta name="theme-color" media="(prefers-color-scheme: light)" content="cyan" />
<meta name="theme-color" media="(prefers-color-scheme: dark)" content="black" />
width
, initialScale
, 和 maximumScale
这其实是 viewport
元标签的默认设置值,通常不需要手动设置:
// layout.js | page.js
export const viewport = {
width: 'device-width',
initialScale: 1,
maximumScale: 1,
// 也支持
// interactiveWidget: 'resizes-visual',
}
对应输出为:
<meta
name="viewport"
content="width=device-width, initial-scale=1, maximum-scale=1"
/>
colorScheme,指定与当前文档兼容的一种或多种配色方案。 浏览器将优先采用此元数据的值,然后再使用用户的浏览器或设备设置,来确定页面上的各种默认颜色和元素外观,例如背景色、前景色、窗体控件和滚动条。 的主要用途是指示当前页面与浅色模式和深色模式的兼容性,以及选用这两种模式时的优先顺序。它的值有 normal
、light
、dark
、only light
。
// layout.js | page.js
export const viewport = {
colorScheme: 'dark',
}
<meta name="color-scheme" content="dark" />
revalidatePath
用于按需清除特定路径上的缓存数据,可用于 Node.js 和 Edge Runtimes。
使用 revalidatePath
的时候要知道,在 Next.js 中,清除数据缓存并重新获取最新数据的过程就叫做重新验证(Revalidation),即便在动态路由段中调用了多次 revalidatePath
,也不会立即触发多次重新验证,只有当下次访问的时候才会重新获取数据并更新缓存。
revalidatePath(path: string, type?: 'page' | 'layout'): void;
path
可以是路由字符串(如 /product/123
),也可以是文件系统地址字符串(如 /product/[slug]/page
),必须少于 1024 个字符type
可选参数,要重新验证的地址类型,值为 page
或 layout
revalidatePath
不返回任何值
import { revalidatePath } from 'next/cache'
revalidatePath('/blog/post-1')
import { revalidatePath } from 'next/cache'
revalidatePath('/blog/[slug]', 'page')
// 带路由组也可以
revalidatePath('/(main)/post/[slug]', 'page')
注意在这个例子中,仅重新验证与所提供的 page 文件对应的 URL,也就是说,不会重新验证在这之下的页面,比如 /blog/[slug]
不会让 /blog/[slug]/[author]
也失效
import { revalidatePath } from 'next/cache'
revalidatePath('/blog/[slug]', 'layout')
// 带路由组也可以
revalidatePath('/(main)/post/[slug]', 'layout')
在这个例子中,这会何重新验证任何使用这个布局的页面,也就是说, /blog/[slug]
也会让 /blog/[slug]/[author]
失效
import { revalidatePath } from 'next/cache'
revalidatePath('/', 'layout')
这会清除客户端路由缓存,并在下次访问时重新验证数据缓存。
'use server'
// app/actions.js
import { revalidatePath } from 'next/cache'
export default async function submit() {
await submitForm()
revalidatePath('/')
}
// app/api/revalidate/route.js
import { revalidatePath } from 'next/cache'
export async function GET(request) {
const path = request.nextUrl.searchParams.get('path')
if (path) {
revalidatePath(path)
return Response.json({ revalidated: true, now: Date.now() })
}
return Response.json({
revalidated: false,
now: Date.now(),
message: 'Missing path to revalidate',
})
}
revalidateTag
用于按需清除特定标签的缓存数据,可用于 Node.js 和 Edge Runtimes。
使用 revalidateTag
的时候要知道,在 Next.js 中,清除数据缓存并重新获取最新数据的过程就叫做重新验证(Revalidation),即便在动态路由段中调用了多次 revalidateTag
,也不会立即触发多次重新验证,只有当下次访问的时候才会重新获取数据并更新缓存。
revalidateTag(tag: string): void;
添加标签的方式:
fetch(url, { next: { tags: [...] } });
revalidateTag
不返回任何值
// app/actions.js
import { revalidateTag } from 'next/cache'
export async function GET(request) {
const tag = request.nextUrl.searchParams.get('tag')
revalidateTag(tag)
return Response.json({ revalidated: true, now: Date.now() })
}
// app/api/revalidate/route.js
import { revalidateTag } from 'next/cache'
export async function GET(request) {
const tag = request.nextUrl.searchParams.get('tag')
revalidateTag(tag)
return Response.json({ revalidated: true, now: Date.now() })
}
unstable_cache
用于缓存昂贵操作的结果(如数据库查询)并在之后的请求中复用结果,使用示例如下:
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);
...
}
const data = unstable_cache(fetchData, keyParts, options)()
fetchData
:获取要缓存数据的异步函数,该函数返回一个 PromisekeyParts
:用于标识缓存键名的数组,必须包含全局唯一的值options
:用于控制缓存行为,具体包含:
tags
: 用于控制缓存失效的标签数组revalidate
:缓存需要重新验证的秒数unstable_cache
返回一个函数,该函数调用时会返回一个解析为缓存数据的 Promise。如果数据不在缓存中,则会调用提供的函数,将结果缓存并返回。
unstable_noStore
用于声明退出静态渲染和表明该组件不应缓存,使用示例如下:
import { unstable_noStore as noStore } from 'next/cache';
export default async function Component() {
noStore();
const result = await db.query(...);
...
}
unstable_noStore
相当于在 fetch
上添加了 cache: 'no-store'
。unstable_noStore
比 export const dynamic = 'force-dynamic'
更好的一点是它更细粒度,可以在每个组件的基础上使用。
如果你不想向 fetch
传递额外的选项如 cache: 'no-store'
或 next: { revalidate: 0 }
,你可以使用 noStore()
作为替代。
import { unstable_noStore as noStore } from 'next/cache';
export default async function Component() {
noStore();
const result = await db.query(...);
...
}
useSelectedLayoutSegment
是一个客户端组件 hook,用于读取比调用该方法所在的布局低一级的激活路由段。这个功能对于导航 UI 非常有用,比如父布局中的选项卡,需要根据当前所处的路由段来更改样式,基础使用示例代码如下:
'use client'
// app/example-client-component.js
import { useSelectedLayoutSegment } from 'next/navigation'
export default function ExampleClientComponent() {
const segment = useSelectedLayoutSegment()
return <p>Active segment: {segment}</p>
}
为了解释这个 hook 的作用和用法,我们来写一个 demo,demo 效果如下:
这个 demo 模拟的是侧边栏点击切换当前文章,你可以看到,随着路由的切换,对应链接的样式也发生了变化。代码如下:
// app/blog/layout.js
import BlogNavLink from './blog-nav-link'
import getFeaturedPosts from './get-featured-posts'
export default async function Layout({ children }) {
const featuredPosts = await getFeaturedPosts()
return (
<div>
{featuredPosts.map((post) => (
<div key={post.id}>
<BlogNavLink slug={post.slug}>{post.title}</BlogNavLink>
</div>
))}
<div>{children}</div>
</div>
)
}
'use client'
// app/blog/blog-nav-link.js
import Link from 'next/link'
import { useSelectedLayoutSegment } from 'next/navigation'
export default function BlogNavLink({ slug, children }) {
const segment = useSelectedLayoutSegment()
const isActive = slug === segment
return (
<Link
href={`/blog/${slug}`}
style={{ fontWeight: isActive ? 'bold' : 'normal' }}
>
{children}
</Link>
)
}
// app/blog/get-featured-posts.js
export default async function getFeaturedPosts() {
await new Promise((resolve) => setTimeout(resolve, 3000))
return [
{ id: '1', slug: 'article1', title: '文章 1'},
{ id: '2', slug: 'article2', title: '文章 2'},
{ id: '3', slug: 'article3', title: '文章 3'}
]
}
// app/blog/[slug]/page.js
export default function Page({ params }) {
return <div>当前 slug: {params.slug}</div>
}
在这个例子中,useSelectedLayoutSegment
是在 app/blog/layout.js
这个布局中调用的,所以访问 /blog/article1
的时候,返回的是比这个布局低一级的路由段,也就是会返回 article1
,然后我们在 blog-nav-link.js
中根据该返回值和当前 slug 进行判断,从而实现了当前所处链接加粗功能。
useSelectedLayoutSegment
返回比调用该方法所在的布局低一级的激活路由段,也就是说,即使你访问 blog/article1/about
,因为调用该方法的布局依然是 app/blog/layout.js
,所以返回的值依然是 article1
。
const segment = useSelectedLayoutSegment(parallelRoutesKey?: string)
useSelectedLayoutSegment
接收一个可选的 parallelRoutesKey 参数,用于读取平行路由中的激活路由段。
如果不存在,会返回 null
,让我们再看几个例子:
Layout | 访问 URL | 返回值 |
---|---|---|
app/layout.js |
/ |
null |
app/layout.js |
/dashboard |
'dashboard' |
app/dashboard/layout.js |
/dashboard |
null |
app/dashboard/layout.js |
/dashboard/settings |
'settings' |
app/dashboard/layout.js |
/dashboard/analytics |
'analytics' |
app/dashboard/layout.js |
/dashboard/analytics/monthly |
'analytics' |
useSelectedLayoutSegments
是一个客户端组件 hook,用于读取调用该方法所在的布局以下所有的激活路由段。
useSelectedLayoutSegments
与 useSelectedLayoutSegment
的区别是:
useSelectedLayoutSegment
返回的是布局下一级的激活路由段useSelectedLayoutSegments
返回的是布局下所有的激活路由段以上节的 demo 为例,当在 app/blog/layout.js
布局中调用这两个方法:
访问 /blog/article1
,useSelectedLayoutSegment
返回'article1'
,useSelectedLayoutSegments
返回
['article1']``。
访问 /blog/article1/about
,useSelectedLayoutSegment
返回 'article1'
,useSelectedLayoutSegments
返回 ['article1', 'about']
。
useSelectedLayoutSegments
可以用于实现如面包屑功能,基础使用示例代码如下:
'use client'
// app/example-client-component.js
import { useSelectedLayoutSegments } from 'next/navigation'
export default function ExampleClientComponent() {
const segments = useSelectedLayoutSegments()
return (
<ul>
{segments.map((segment, index) => (
<li key={index}>{segment}</li>
))}
</ul>
)
}
const segments = useSelectedLayoutSegments(parallelRoutesKey?: string)
以数组形式返回,如果没有,返回空数组。注意如果使用了路由组,也会返回,所以可以再用一个 filter()
排除掉以括号为开头的条目。让我们再看几个例子:
Layout | 访问 URL | 返回值 |
---|---|---|
app/layout.js |
/ |
[] |
app/layout.js |
/dashboard |
['dashboard'] |
app/layout.js |
/dashboard/settings |
['dashboard', 'settings'] |
app/dashboard/layout.js |
/dashboard |
[] |
app/dashboard/layout.js |
/dashboard/settings |
['settings'] |