36-API 篇 常用函数与方法(下)
前言
本篇我们讲解请求相关的常用方法,有:
- generateStaticParams
- generateViewport
- revalidatePath
- revalidateTag
- unstable_cache
- unstable_noStore
- useSelectedLayoutSegment
- useSelectedLayoutSegments
用到的时候到此篇查看具体的语法即可。
1. generateStaticParams
1.1. 介绍
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
会在对应的布局或页面生成之前运行 - 在 重新验证(ISR)的时候,
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
// ...
}
1.2. 参数
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
函数,直接一步到位,这种填充动态段的方式被称为“自下而上生成参数”。
1.3. 返回值
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[] }[] |
2. generateViewport
你可以自定义页面的初始 viewport,有两种方法:
- 使用静态的
viewport
对象 - 使用动态的
generateViewport
函数
使用的时候要注意:
viewport
对象和generateViewport
函数仅支持在服务端组件中导出- 不能在同一路由段中同时导出
viewport
对象和generateViewport
函数 - 如果视口不依赖运行时的一些信息,尽可能使用
viewport
对象的方式进行定义
2.1. viewport 对象
从 layout.js
或者 page.js
中导出一个名为 viewport
的对象:
// layout.js | page.js
export const viewport = {
themeColor: 'black',
}
export default function Page() {}
2.2. generateViewport
从 layout.js
或者 page.js
中导出一个名为 generateViewport
的函数,该函数返回包含一个或者多个viewport 字段的 Viewport 对象:
export function generateViewport({ params }) {
return {
themeColor: '...',
}
}
2.3. Viewport 字段
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
colorScheme,指定与当前文档兼容的一种或多种配色方案。 浏览器将优先采用此元数据的值,然后再使用用户的浏览器或设备设置,来确定页面上的各种默认颜色和元素外观,例如背景色、前景色、窗体控件和滚动条。<meta name="color-scheme">
的主要用途是指示当前页面与浅色模式和深色模式的兼容性,以及选用这两种模式时的优先顺序。它的值有 normal
、light
、dark
、only light
。
// layout.js | page.js
export const viewport = {
colorScheme: 'dark',
}
<meta name="color-scheme" content="dark" />
3. revalidatePath
3.1. 介绍
revalidatePath
用于按需清除特定路径上的缓存数据,可用于 Node.js 和 Edge Runtimes。
使用 revalidatePath
的时候要知道,在 Next.js 中,清除数据缓存并重新获取最新数据的过程就叫做重新验证(Revalidation),即便在动态路由段中调用了多次 revalidatePath
,也不会立即触发多次重新验证,只有当下次访问的时候才会重新获取数据并更新缓存。
3.2. 参数
revalidatePath(path: string, type?: 'page' | 'layout'): void;
path
可以是路由字符串(如/product/123
),也可以是文件系统地址字符串(如/product/[slug]/page
),必须少于 1024 个字符type
可选参数,要重新验证的地址类型,值为page
或layout
3.3. 返回值
revalidatePath
不返回任何值
3.4. 示例
重新验证特定 URL
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')
这会清除客户端路由缓存,并在下次访问时重新验证数据缓存。
Server Action
'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',
})
}
4. revalidateTag
4.1. 介绍
revalidateTag
用于按需清除特定标签的缓存数据,可用于 Node.js 和 Edge Runtimes。
使用 revalidateTag
的时候要知道,在 Next.js 中,清除数据缓存并重新获取最新数据的过程就叫做重新验证(Revalidation),即便在动态路由段中调用了多次 revalidateTag
,也不会立即触发多次重新验证,只有当下次访问的时候才会重新获取数据并更新缓存。
4.2. 参数
revalidateTag(tag: string): void;
- tag 表示要重新验证的标签,必须小于或等于 256 个字符。
添加标签的方式:
fetch(url, { next: { tags: [...] } });
4.3. 返回值
revalidateTag
不返回任何值
4.4. 示例
Server Action
// 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() })
}
5. unstable_cache
5.1. 介绍
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);
...
}
5.2. 参数
const data = unstable_cache(fetchData, keyParts, options)()
fetchData
:获取要缓存数据的异步函数,该函数返回一个 PromisekeyParts
:用于标识缓存键名的数组,必须包含全局唯一的值options
:用于控制缓存行为,具体包含:tags
: 用于控制缓存失效的标签数组revalidate
:缓存需要重新验证的秒数
5.3. 返回值
unstable_cache
返回一个函数,该函数调用时会返回一个解析为缓存数据的 Promise。如果数据不在缓存中,则会调用提供的函数,将结果缓存并返回。
6. unstable_noStore
6.1. 介绍
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'
更好的一点是它更细粒度,可以在每个组件的基础上使用。
6.2. 示例
如果你不想向 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(...);
...
}
7. useSelectedLayoutSegment
7.1. 介绍
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
。
7.2 参数
const segment = useSelectedLayoutSegment(parallelRoutesKey?: string)
useSelectedLayoutSegment
接收一个可选的 parallelRoutesKey 参数,用于读取平行路由中的激活路由段。
7.3 返回值
如果不存在,会返回 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' |
8. useSelectedLayoutSegments
8.1. 介绍
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>
)
}
8.2. 参数
const segments = useSelectedLayoutSegments(parallelRoutesKey?: string)
8.3. 返回值
以数组形式返回,如果没有,返回空数组。注意如果使用了路由组,也会返回,所以可以再用一个 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'] |