183 lines
10 KiB
Markdown
183 lines
10 KiB
Markdown
# 实施工作流
|
|
|
|
为应用添加视图过渡时,请按以下步骤顺序操作。每一步都建立在之前步骤的基础之上。
|
|
|
|
## 步骤 1:审查应用
|
|
|
|
在编写任何代码之前,彻底扫描代码库。搜索:
|
|
|
|
- **每一个 `<Link>` 和 `router.push`** —— 这些是你的导航触发器。打开包含它们的每个文件。
|
|
- **每一个 `<Suspense>` 边界** —— 每一个都是展示动画的候选对象。检查其 fallback 渲染了什么。
|
|
- **每一个页面/路由组件** —— 将它们全部列出。每个页面都需要一个 VT 放置决策。
|
|
- **持久化元素** —— 在导航间保持停留在屏幕上的 header、navbar、sidebar、粘性控件。这些需要 `viewTransitionName` 隔离。
|
|
- **共享视觉元素** —— 在源视图和目标视图中都出现的图像、卡片或头像(例如列表中的缩略图和详情页上的同一张图片)。
|
|
- **骨架屏与内容控件对** —— 如果 Suspense fallback 渲染了一个控件(搜索输入框、标签栏),而实际内容中也有同样的控件,那么两者都需要一个匹配的 `viewTransitionName`。
|
|
|
|
然后对每个导航进行分类,并生成一份导航映射表:
|
|
|
|
```
|
|
| 路由 | 导航至 | 方向 | VT 模式 |
|
|
|---------------------|----------------------|-------------|------------------------|
|
|
| / | /detail/[id] | 前进 | 定向滑动 (directional slide) |
|
|
| /detail/[id] | / | 后退 | 定向滑动 (directional slide) |
|
|
| /detail/[id] | /detail/[other] | 顺序切换 | 定向滑动 (有序 prev/next) 或 key+share 交叉淡变 |
|
|
| /tab/[a] | /tab/[b] | 横向切换 | key+share 交叉淡变 |
|
|
| (Suspense) | (内容加载) | — | 上滑展示 (slide-up reveal) |
|
|
```
|
|
|
|
对于每个共享元素(`name` 属性),记录哪些导航会形成配对、哪些不会——这决定了你是否需要将 `enter`/`exit` 作为 `share` 的备用方案。
|
|
|
|
## 步骤 2:添加 CSS 配方
|
|
|
|
将 `css-recipes.md` 中的**完整** CSS 配方集复制到你的全局样式表中。这包括计时变量、共享关键帧、淡入淡出、滑动(垂直)、定向导航(前进/后退)、共享元素变形、持久化元素隔离以及减弱动效。
|
|
|
|
不要自行编写动画 CSS——这些配方已经处理了错开计时、变形时的运动模糊以及容易出错的减弱动效。你可以在初始设置之后自定义计时变量(`--duration-exit`、`--duration-enter`、`--duration-move`)。
|
|
|
|
## 步骤 3:隔离持久化元素
|
|
|
|
对于步骤 1 中识别出的每个持久化元素,添加 `viewTransitionName` 样式以将其从页面内容的过渡快照中提取出来:
|
|
|
|
```jsx
|
|
<header style={{ viewTransitionName: "site-header" }}>...</header>
|
|
```
|
|
|
|
然后添加 `css-recipes.md` 中的持久化元素隔离 CSS(防止该元素在页面过渡期间产生动画)。如果该元素使用了 `backdrop-blur` 或 `backdrop-filter`,请改用 `css-recipes.md` 中的 backdrop-blur 变通方案。
|
|
|
|
如果 Suspense fallback 镜像了一个持久化控件(例如骨架搜索输入框),请为实际控件和骨架屏设置相同的 `viewTransitionName`,使它们在原位变形。
|
|
|
|
## 步骤 4:添加定向页面过渡
|
|
|
|
对于步骤 1 中识别出的层级导航,使用 `startTransition` 内部的 `addTransitionType` 标记导航方向:
|
|
|
|
```jsx
|
|
startTransition(() => {
|
|
addTransitionType('nav-forward');
|
|
router.push('/detail/1');
|
|
});
|
|
```
|
|
|
|
然后将每个**页面组件**(而非布局)包裹在类型键控的 `<ViewTransition>` 中:
|
|
|
|
```jsx
|
|
<ViewTransition
|
|
enter={{
|
|
"nav-forward": "nav-forward",
|
|
"nav-back": "nav-back",
|
|
default: "none",
|
|
}}
|
|
exit={{
|
|
"nav-forward": "nav-forward",
|
|
"nav-back": "nav-back",
|
|
default: "none",
|
|
}}
|
|
default="none"
|
|
>
|
|
<div>...页面内容...</div>
|
|
</ViewTransition>
|
|
```
|
|
|
|
`css-recipes.md` 中的 `nav-forward` 和 `nav-back` CSS 类会产生水平滑动效果。对于不需要定向动效的简单应用,一个空的 `<ViewTransition default="none">` 包裹搭配 `enter="fade-in"` / `exit="fade-out"` 也同样可行。
|
|
|
|
将其提取为可复用组件,这样每个页面就不必重复冗长的类型映射:
|
|
|
|
```jsx
|
|
export function DirectionalTransition({ children }: { children: React.ReactNode }) {
|
|
return (
|
|
<ViewTransition
|
|
enter={{ 'nav-forward': 'nav-forward', 'nav-back': 'nav-back', default: 'none' }}
|
|
exit={{ 'nav-forward': 'nav-forward', 'nav-back': 'nav-back', default: 'none' }}
|
|
default="none"
|
|
>
|
|
{children}
|
|
</ViewTransition>
|
|
);
|
|
}
|
|
```
|
|
|
|
这也成为了将来添加新过渡类型时进行修改的唯一入口点。
|
|
|
|
**规则:**
|
|
- 始终将 `enter` 与 `exit` 配对使用——没有退出动画,旧页面会瞬间消失,而新页面则动画进入。
|
|
- 始终在类型映射对象中包含 `default: "none"`,并在组件上设置 `default="none"`——否则它会在每次过渡时触发。
|
|
- 将定向的 `<ViewTransition>` 放置在**每个页面组件**中,而不是布局中。布局在导航间持久存在,永远不会触发 enter/exit。
|
|
- 仅对层级导航或有序序列(prev/next)使用定向滑动。横向/同级导航(tab 到 tab)应使用空的 `<ViewTransition>`(交叉淡变)或 `default="none"`。
|
|
|
|
## 步骤 5:添加 Suspense 展示
|
|
|
|
对于步骤 1 中识别出的每个 `<Suspense>` 边界,将 fallback 和内容分别包裹在各自的 `<ViewTransition>` 中:
|
|
|
|
```jsx
|
|
<Suspense
|
|
fallback={
|
|
<ViewTransition exit="slide-down">
|
|
<Skeleton />
|
|
</ViewTransition>
|
|
}
|
|
>
|
|
<ViewTransition enter="slide-up" default="none">
|
|
<AsyncContent />
|
|
</ViewTransition>
|
|
</Suspense>
|
|
```
|
|
|
|
此示例使用 `slide-down` / `slide-up` 实现定向垂直动效。对于更简单的展示,一个包裹 `<Suspense>` 的空 `<ViewTransition>` 即可实现零配置的交叉淡变。根据空间含义进行选择——请查阅主技能文件中的"选择正确的动画样式"表格。
|
|
|
|
**规则:**
|
|
- 始终在内容 `<ViewTransition>` 上使用 `default="none"`,以防止在重新验证或不相关的过渡中重新触发动画。
|
|
- 在 Suspense `<ViewTransition>` 上使用简单的字符串属性(而非类型映射)——Suspense 的解析作为独立的、没有类型的过渡触发,因此类型键控属性无法匹配。
|
|
|
|
## 步骤 6:添加共享元素过渡
|
|
|
|
对于步骤 1 中识别出的每个共享视觉元素,在源视图和目标视图上添加匹配的命名 `<ViewTransition>` 包裹:
|
|
|
|
```jsx
|
|
// 在源视图上(例如列表/网格页面)
|
|
<ViewTransition name={`photo-${photo.id}`} share="morph" default="none">
|
|
<Image src={photo.src} ... />
|
|
</ViewTransition>
|
|
|
|
// 在目标视图上(例如详情页)——相同的 name
|
|
<ViewTransition name={`photo-${photo.id}`} share="morph">
|
|
<Image src={photo.src} ... />
|
|
</ViewTransition>
|
|
```
|
|
|
|
`share="morph"` 类使用 `css-recipes.md` 中的变形配方(受控时长 + 运动模糊)。对于更简单的交叉淡变,使用 `share="auto"`(浏览器默认行为)。
|
|
|
|
当列表项包含共享元素时,使用两个嵌套的 `<ViewTransition>` 层组合两种模式——请参阅 `SKILL.md` 中的"将共享元素与列表标识组合使用"。
|
|
|
|
**规则:**
|
|
- 名称必须是全局唯一的——使用 `photo-${id}` 等前缀。
|
|
- 在列表侧的共享元素上添加 `default="none"`,以防止在过滤/搜索更新时每个项目都产生交叉淡变。
|
|
|
|
## 步骤 7:验证每个导航路径
|
|
|
|
对照步骤 1 中的导航映射表,逐行确认:
|
|
|
|
- VT 在此导航中是挂载/卸载,还是保持挂载状态(相同路由)?
|
|
- 对于命名 VT:是否形成了共享配对?如果没有,`enter`/`exit` 是否提供了备用方案?
|
|
- `default="none"` 是否阻止了你实际想要的动画?
|
|
- 持久化元素是否保持静态(不随页面内容滑动)?
|
|
- Suspense 展示动画是否独立于定向导航?
|
|
|
|
如果任何路径没有产生动画或产生了冲突的动画,请重新审视相关步骤。
|
|
|
|
---
|
|
|
|
## 常见错误
|
|
|
|
- **不带属性的空 `<ViewTransition>`** ——没有 `default="none"`,它会在每次过渡(每次导航、每次 Suspense 解析、每次重新验证)时触发浏览器的默认交叉淡变。始终设置 `default="none"` 并仅显式启用你想要的触发器。
|
|
- **在布局中使用定向 `<ViewTransition>`** ——布局在导航间持久存在,永远不会卸载/重新挂载。`enter`/`exit` 属性不会在路由变化时触发。将外层的类型键控 `<ViewTransition>` 放置在**每个页面组件**中。
|
|
- **淡出退出与共享元素变形共存** ——页面溶解与变形冲突。请改用定向滑动退出。
|
|
- **自行编写自定义动画 CSS** ——`css-recipes.md` 中的配方已经处理了错开计时、变形时的运动模糊以及减弱动效。直接复制它们,不要重新发明。
|
|
- **类型键控对象中缺少 `default: "none"`** ——TypeScript 要求有 `default` 键,没有它时回退值为 `"auto"`,会在每次过渡时触发。
|
|
- **在 Suspense 展示中使用类型映射** ——Suspense 的解析作为独立的、没有类型的过渡触发。类型键控属性无法匹配——请改用简单的字符串属性。
|
|
- **使用原生的 `viewTransitionName` CSS 触发动画** ——只有当 `<ViewTransition>` 组件存在于渲染树中时,React 才会调用 `document.startViewTransition`。单纯的 `viewTransitionName` 样式用于将元素从父级快照中隔离出来,而非用于触发动画。
|
|
- **同路由导航的 `update` 触发器** ——内容内部嵌套的 VT 会劫持父级的突变,因此 `update` 永远不会在外层 VT 上触发。请改用 `key` + `name` + `share`。
|
|
- **在可复用组件中使用命名 VT** ——如果一个带有命名 VT 的组件同时在模态框/弹出层**和**页面中渲染,两者会同时挂载,破坏变形效果。请使 name 变为条件性,或将其移至具体的消费者处。
|
|
- **使用 `router.back()` 进行后退导航** ——`router.back()` 会触发同步的 `popstate`,与视图过渡不兼容。请使用带显式 URL 的 `router.push()`。
|
|
|
|
---
|
|
|
|
关于 Next.js 特定的实施步骤(配置标志、`<Link>` 上的 `transitionTypes`、同路由动态段),请参阅 `nextjs.md`。
|