Files
2026-07-13 21:35:52 +08:00

184 lines
6.1 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
name: nextjs-view-transitions
description: Next.js 中 View Transitions API 的使用指南
metadata:
type: reference
---
# Next.js 中的 View Transitions
## 设置
`<ViewTransition>` 开箱即用,支持 `startTransition`/`Suspense` 更新。若要对 `<Link>` 导航也启用动画:
```js
// next.config.js
const nextConfig = {
experimental: { viewTransition: true },
};
module.exports = nextConfig;
```
这会为每个 `<Link>` 导航包裹 `document.startViewTransition`。任何 `default="auto"` 的 VT 会在**每次**链接点击时触发——请使用 `default="none"` 来防止动画冲突。
**不要**安装 `react@canary`——详见 SKILL.md 的"可用性"章节。
---
## Next.js 实现补充
在遵循 `implementation.md` 时,应用以下补充内容:
**步骤 2 之后:** 启用上述实验性标志。
**步骤 4**`<Link>` 上使用 `transitionTypes`——详见下文"`transitionTypes` Prop"章节的用法和可用性说明。
**步骤 6 之后:** 对于相同路由的动态段(例如 `/collection/[slug]`),使用 `key` + `name` + `share` 模式——详见下文"相同路由动态段过渡"章节。
---
## 布局级别的 ViewTransition
**如果页面有自己的 VT,不要在布局中添加包裹 `{children}` 的布局级 VT。** 嵌套 VT 在父级 VT 内部时,enter/exit 永远不会触发——页面级的 enter/exit 会静默失效。请完全移除布局级 VT。
一个裸露的 `<ViewTransition>` 在布局中只有在页面**没有**自己的 VT 时才能正常工作。
**布局在导航间持久存在**——`enter`/`exit` 仅在初始挂载时触发,路由变化时不触发。不要在布局中使用类型键映射。
---
## `next/link` 上的 `transitionTypes` Prop
无需包装组件,在服务端组件中即可工作:
```tsx
<Link href="/products/1" transitionTypes={['transition-to-detail']}>查看产品</Link>
```
这替代了手动 `onNavigate` + `startTransition` + `addTransitionType` + `router.push()` 的模式。仅在非链接交互(按钮、表单)时保留手动 `startTransition`
**可用性:** `transitionTypes` 需要启用 `experimental.viewTransition: true`,在 Next.js 15+ canary 构建版本和 Next.js 16+ 中可用。如果不可用,请使用 `startTransition` + `addTransitionType` + `router.push()`(参见下面的"程序化导航"章节)。检查方法:`grep -r "transitionTypes" node_modules/next/dist/`——如果没有结果,则回退到程序化导航。
---
## 程序化导航
```tsx
'use client';
import { useRouter } from 'next/navigation';
import { startTransition, addTransitionType } from 'react';
function handleNavigate(href: string) {
const router = useRouter();
startTransition(() => {
addTransitionType('nav-forward');
router.push(href);
});
}
```
---
## 使用 `router.replace` 实现服务端筛选
对于通过 URL 参数在服务端重新渲染的搜索/排序/筛选,使用 `startTransition` + `router.replace`。VT 会生效,因为状态更新位于 `startTransition` 内部:
```tsx
'use client';
import { useRouter } from 'next/navigation';
import { startTransition } from 'react';
function handleSort(sort: string) {
const router = useRouter();
startTransition(() => {
router.replace(`?sort=${sort}`);
});
}
```
包裹在 `<ViewTransition key={item.id}>` 中的列表项会以动画方式重新排序。这是 `patterns.md` 中客户端 `useDeferredValue` 模式的服务端组件替代方案。
---
## 双层模式(方向动画 + Suspense)
方向滑动动画与 Suspense 展现动画可以共存,因为它们在各自不同的时机触发。请将方向 VT 放在**页面组件**中(而非布局):
```tsx
<ViewTransition
enter={{ "nav-forward": "slide-from-right", default: "none" }}
exit={{ "nav-forward": "slide-to-left", default: "none" }}
default="none"
>
<div>
<Suspense fallback={<ViewTransition exit="slide-down"><骨架屏 /></ViewTransition>}>
<ViewTransition enter="slide-up" default="none"><内容 /></ViewTransition>
</Suspense>
</div>
</ViewTransition>
```
---
## 将 `loading.tsx` 作为 Suspense 边界
Next.js 的 `loading.tsx` 是一个隐式的 `<Suspense>` 边界。在 `loading.tsx` 中,将骨架屏包裹在 `<ViewTransition exit="...">` 中;在页面中,将内容包裹在 `<ViewTransition enter="..." default="none">` 中:
```tsx
// loading.tsx
<ViewTransition exit="slide-down"><PhotoGridSkeleton /></ViewTransition>
// page.tsx
<ViewTransition enter="slide-up" default="none"><PhotoGrid photos={photos} /></ViewTransition>
```
与显式 `<Suspense>` 规则相同:使用简单字符串 prop(而非类型映射),因为 Suspense 展现动画触发时不带过渡类型。
---
## 跨路由共享元素
```tsx
// 列表页
{products.map((product) => (
<Link key={product.id} href={`/products/${product.id}`} transitionTypes={['nav-forward']}>
<ViewTransition name={`product-${product.id}`}>
<Image src={product.image} alt={product.name} width={400} height={300} />
</ViewTransition>
</Link>
))}
// 详情页——相同的 name
<ViewTransition name={`product-${product.id}`}>
<Image src={product.image} alt={product.name} width={800} height={600} />
</ViewTransition>
```
---
## 相同路由动态段过渡
当在同一路由的不同动态段之间导航时(例如 `/collection/[slug]`),页面保持挂载状态——enter/exit 永远不会触发。请使用 `key` + `name` + `share`
```tsx
<Suspense fallback={<骨架屏 />}>
<ViewTransition key={slug} name={`collection-${slug}`} share="auto" default="none">
<Content slug={slug} />
</ViewTransition>
</Suspense>
```
- `key={slug}` 在变化时强制卸载/重新挂载
- `name` + `share="auto"` 创建共享元素的交叉淡入淡出效果
- 位于 `<Suspense>` 内部的 VT(不对 Suspense 设置 key)在加载期间保持旧内容可见
---
## 服务端组件
- `<ViewTransition>` 在服务端组件和客户端组件中均可使用
- `<Link transitionTypes>` 在服务端组件中可用——无需 `'use client'`
- 程序化导航的 `addTransitionType``startTransition` 需要客户端组件