页面过渡
Nuxt 支持页面切换和布局切换时的过渡动画。
为什么需要页面过渡?
没有过渡动画时,页面切换是"瞬间替换"——用户可能感觉突兀。加上淡入淡出或滑动效果后,切换更平滑、更有"应用感"。
但要注意
过渡动画不是必须的。如果你的应用追求极致性能,可以不用。它更多是 UX 增强。
页面过渡
全局配置
// nuxt.config.ts
export default defineNuxtConfig({
pageTransition: {
name: 'page', // CSS 类名前缀
mode: 'out-in', // 先离开再进入
},
})/* app/assets/css/main.css */
.page-enter-active,
.page-leave-active {
transition: opacity 0.3s;
}
.page-enter-from,
.page-leave-to {
opacity: 0;
}name: 'page' 和 CSS 类名的关系
| 阶段 | CSS 类名 | 说明 |
|---|---|---|
| 进入前 | .page-enter-from | 初始状态(如 opacity: 0) |
| 进入中 | .page-enter-active | 进入过渡(定义 transition) |
| 进入后 | .page-enter-to | 最终状态(如 opacity: 1) |
| 离开前 | .page-leave-from | 初始状态 |
| 离开中 | .page-leave-active | 离开过渡 |
| 离开后 | .page-leave-to | 最终状态 |
mode: 'out-in' vs mode: 'in-out' vs 默认
| 模式 | 行为 | 效果 |
|---|---|---|
out-in | 先离开,再进入 | ✅ 推荐,避免重叠 |
in-out | 先进入,再离开 | 可能同时显示两个页面 |
| 默认 | 同时进行 | 两个页面同时过渡 |
页面级配置
单个页面可以使用不同的过渡效果:
<script setup>
definePageMeta({
pageTransition: {
name: 'slide',
mode: 'out-in',
},
})
</script>全局 vs 页面级
页面级配置会覆盖全局配置。如果一个页面需要特殊的过渡效果(如模态页面用缩放),就在 definePageMeta 中单独设置。
过渡中访问新旧页面
过渡过程中,你可能需要根据目标页面决定动画方向(前进用右滑、后退用左滑):
<script setup>
definePageMeta({
pageTransition: {
name: 'slide',
mode: 'out-in',
onBeforeEnter: (el) => {
// 进入前设置初始状态
},
onAfterLeave: (el) => {
// 离开后清理
},
},
})
</script>JavaScript 钩子
pageTransition 支持 Vue <Transition> 的所有钩子:onBeforeEnter、onEnter、onAfterEnter、onBeforeLeave、onLeave、onAfterLeave。当你需要 JavaScript 控制动画(如用 GSAP)时,使用这些钩子。
JavaScript 过渡钩子
当 CSS 过渡不够用(如需要使用动画库 GSAP、anime.js),可以在钩子中控制动画:
<script setup>
definePageMeta({
pageTransition: {
mode: 'out-in',
css: false, // 禁用 CSS 过渡,完全用 JS 控制
onEnter: (el, done) => {
// el 是进入的 DOM 元素
// done 是回调,动画结束后必须调用
gsap.from(el, {
opacity: 0,
y: 20,
duration: 0.3,
onComplete: done,
})
},
onLeave: (el, done) => {
gsap.to(el, {
opacity: 0,
y: -20,
duration: 0.3,
onComplete: done,
})
},
},
})
</script>INFO
️ done 回调必须调用:如果不调用 done 过渡永远不会结束,页面会卡住
布局过渡
// nuxt.config.ts
export default defineNuxtConfig({
layoutTransition: {
name: 'layout',
mode: 'out-in',
},
}).layout-enter-active,
.layout-leave-active {
transition: opacity 0.3s;
}
.layout-enter-from,
.layout-leave-to {
opacity: 0;
}页面过渡 vs 布局过渡
- 页面过渡:同一个布局内,不同页面切换时触发
- 布局过渡:不同布局之间切换时触发(如从默认布局切换到管理后台布局)
- 两者独立配置,可以有不同的动画
过渡与数据获取的交互
页面有 await useFetch() 时,过渡行为可能和预期不同:
阻塞式数据获取(默认)
<script setup>
// 默认:useFetch 阻塞页面渲染
const { data } = await useFetch('/api/article')
</script>过渡时序:
1. 旧页面离开动画开始
2. 新页面开始获取数据(await 阻塞)
3. 等待数据返回...
4. 新页面渲染完成
5. 新页面进入动画开始INFO
️ 问题:如果 API 响应慢 旧页面已经消失了,新页面还在等数据,中间会有空白。用户看到的效果是:旧页面淡出 → 白屏等待 → 新页面淡入
解决方案:使用懒加载
<script setup>
// 核心数据阻塞,确保页面框架先渲染
const { data: article } = await useFetch('/api/article')
// 非关键数据懒加载,不阻塞页面过渡
const { data: comments, pending } = useLazyFetch('/api/comments', {
default: () => [],
})
</script>
<template>
<article>{{ article?.content }}</article>
<div v-if="pending">评论加载中...</div>
<div v-else>{{ comments }}</div>
</template>优化后的时序:
1. 旧页面离开动画开始
2. 新页面获取核心数据
3. 新页面框架渲染完成,进入动画开始
4. 进入后,非关键数据在后台加载
5. 非关键数据加载完,更新显示最佳实践
将数据分为"核心"和"非核心"。核心数据用 useFetch 阻塞渲染(确保有内容),非核心数据用 useLazyFetch 不阻塞(加载中显示占位符)。
过渡与 keepalive
使用 definePageMeta({ keepalive: true }) 缓存页面时,过渡动画行为不同:
<script setup>
definePageMeta({
keepalive: true, // 页面被缓存,不销毁
})
</script>keepalive 对过渡的影响
- 没有
keepalive:每次导航都触发离开/进入动画 - 有
keepalive:页面被缓存而不是销毁,所以不会触发离开动画(因为不是真的离开) - 缓存的页面再次进入时,触发的是
onActivated而非onMounted
TIP
如果你需要缓存页面的同时也有过渡动画 使用 NuxtLoadingIndicator 提供视觉反馈
NuxtLoadingIndicator
页面切换时显示顶部加载条——这是过渡动画的最佳搭档:
<!-- app/app.vue -->
<template>
<NuxtLoadingIndicator color="#00DC82" :height="3" />
<NuxtLayout>
<NuxtPage />
</NuxtLayout>
</template>Props
| Prop | 类型 | 默认值 | 说明 | |---|---|---| | color | string | #00DC82 | 加载条颜色 | | height | number | 3 | 加载条高度(px) | | duration | number | 2000 | 动画持续时间 | | throttle | number | 200 | 节流时间 |
NuxtLoadingIndicator 做了什么?
- 页面开始加载时,顶部出现一个进度条
- 进度条逐渐填充
- 加载完成后消失
推荐添加
它给了用户"页面正在加载"的反馈,比页面空白好得多。尤其是客户端导航时,用户看不到浏览器原生的加载指示器。
和页面过渡搭配
NuxtLoadingIndicator 在数据加载时显示进度条,pageTransition 在页面切换时显示过渡动画。两者互补——进度条告诉用户"正在加载",过渡动画让切换更平滑。
View Transitions API
Nuxt 实验性支持浏览器原生的 View Transitions API——这是比 CSS 过渡更强大的方案:
基本启用
// nuxt.config.ts
export default defineNuxtConfig({
experimental: {
viewTransition: true,
},
})View Transitions API vs CSS 过渡
| 特性 | CSS 过渡(Transition) | View Transitions API |
|---|---|---|
| 动画范围 | 只对 Vue 组件的 DOM 节点 | 浏览器级别,可跨 DOM |
| 共享元素动画 | ❌ 不支持 | ✅ 如图片从列表"飞"到详情 |
| 浏览器支持 | ✅ 所有浏览器 | ⚠️ Chrome/Edge ✅ Safari 部分支持 Firefox ❌ |
| 降级 | — | ✅ 自动降级到 CSS 过渡 |
| 性能 | 好 | 更好(浏览器原生优化) |
定义过渡类型(v4.4+)
<script setup>
definePageMeta({
viewTransition: {
types: {
forward: 'slide-forward',
backward: 'slide-backward',
},
},
})
</script>::view-transition-old(slide-forward) {
animation: slide-out-left 0.3s;
}
::view-transition-new(slide-forward) {
animation: slide-in-right 0.3s;
}
::view-transition-old(slide-backward) {
animation: slide-out-right 0.3s;
}
::view-transition-new(slide-backward) {
animation: slide-in-left 0.3s;
}types 是什么?
它让你为不同的导航方向定义不同的过渡动画。比如前进用右滑、后退用左滑,模拟原生 App 的导航体验。
INFO
️ View Transitions 的浏览器兼容性:不支持的浏览器会自动降级为无动画或 CSS 过渡 你可以放心使用,不会影响功能
共享元素动画(最强大的特性)
View Transitions API 可以让同一个元素在两个页面之间"飞"过去——比如列表页的缩略图飞到详情页的大图:
/* 给图片加 view-transition-name */
.product-image {
view-transition-name: product-img;
}
/* 浏览器自动在两个页面的同名元素之间做动画 */
::view-transition-old(product-img) {
animation: fade-out 0.3s;
}
::view-transition-new(product-img) {
animation: fade-in 0.3s;
}INFO
️ view-transition-name 必须在页面中唯一 如果有多个商品图片,需要动态设置不同的名称(如 product-img-1、product-img-2),否则浏览器无法匹配
常见过渡效果
淡入淡出(最常用)
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.3s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}滑动
.slide-left-enter-active,
.slide-left-leave-active {
transition: transform 0.3s ease;
}
.slide-left-enter-from {
transform: translateX(100%);
}
.slide-left-leave-to {
transform: translateX(-100%);
}
.slide-right-enter-active,
.slide-right-leave-active {
transition: transform 0.3s ease;
}
.slide-right-enter-from {
transform: translateX(-100%);
}
.slide-right-leave-to {
transform: translateX(100%);
}缩放
.scale-enter-active,
.scale-leave-active {
transition: transform 0.3s ease, opacity 0.3s ease;
}
.scale-enter-from,
.scale-leave-to {
transform: scale(0.95);
opacity: 0;
}滑上
.slide-up-enter-active,
.slide-up-leave-active {
transition: transform 0.3s ease, opacity 0.3s ease;
}
.slide-up-enter-from,
.slide-up-leave-to {
transform: translateY(20px);
opacity: 0;
}CSS 文件放在哪里?
过渡 CSS 放在 app/assets/css/transitions.css 中,然后在 nuxt.config.ts 的 css 数组中引入:
css: ['~/app/assets/css/transitions.css']性能建议
- 优先使用
transform和opacity:这两个属性不触发重排(reflow),GPU 加速 - 避免过渡大量元素:只过渡必要的元素
- 使用
mode: 'out-in':避免同时渲染两个页面 - 控制过渡时间:0.2s ~ 0.4s 通常足够,太慢用户会烦
- 不要用
height/width/top/left做过渡:这些会触发重排,性能很差
常见问题
过渡动画不生效
- 确认 CSS 类名和
name配置匹配(如name: 'page'→.page-enter-active) - 确认 CSS 文件已通过
nuxt.config.ts的css选项引入 - 确认
app.vue中有<NuxtPage />(过渡应用在 NuxtPage 上) - 清除浏览器缓存后重试
过渡时页面闪烁
可能是 Hydration 不匹配导致。在过渡期间,Vue 重新渲染页面,如果 SSR 和客户端结果不同,会闪烁。确保:
- 没有在模板中使用
Date.now()或Math.random() - 没有在顶层代码中使用浏览器 API
- 使用
<ClientOnly>包裹仅客户端内容
有 await useFetch 时过渡卡住
数据获取阻塞了页面渲染。解决方案:
- 非关键数据用
useLazyFetch - 关键数据设置较短的超时
- 使用
NuxtLoadingIndicator提供加载反馈
知识脉络
组件 → 你在这里:页面过渡
│
├─→ 相关:CSS 与 SCSS(过渡 CSS 写在哪)
│
├─→ 相关:数据获取(useFetch 与过渡的交互)
│
└─→ 相关:内置组件速查(NuxtLoadingIndicator)