项目文件夹

文件
2026-07-13 21:35:52 +08:00

184 行
6.1 KiB
Markdown

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
---
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` 需要客户端组件