Skip to content

页面过渡

Nuxt 支持页面切换和布局切换时的过渡动画。

为什么需要页面过渡?

没有过渡动画时,页面切换是"瞬间替换"——用户可能感觉突兀。加上淡入淡出或滑动效果后,切换更平滑、更有"应用感"。

但要注意

过渡动画不是必须的。如果你的应用追求极致性能,可以不用。它更多是 UX 增强。

页面过渡

全局配置

ts
// nuxt.config.ts
export default defineNuxtConfig({
  pageTransition: {
    name: 'page',    // CSS 类名前缀
    mode: 'out-in',  // 先离开再进入
  },
})
css
/* 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先进入,再离开可能同时显示两个页面
默认同时进行两个页面同时过渡

页面级配置

单个页面可以使用不同的过渡效果:

vue
<script setup>
definePageMeta({
  pageTransition: {
    name: 'slide',
    mode: 'out-in',
  },
})
</script>

全局 vs 页面级

页面级配置会覆盖全局配置。如果一个页面需要特殊的过渡效果(如模态页面用缩放),就在 definePageMeta 中单独设置。

过渡中访问新旧页面

过渡过程中,你可能需要根据目标页面决定动画方向(前进用右滑、后退用左滑):

vue
<script setup>
definePageMeta({
  pageTransition: {
    name: 'slide',
    mode: 'out-in',
    onBeforeEnter: (el) => {
      // 进入前设置初始状态
    },
    onAfterLeave: (el) => {
      // 离开后清理
    },
  },
})
</script>

JavaScript 钩子

pageTransition 支持 Vue <Transition> 的所有钩子:onBeforeEnteronEnteronAfterEnteronBeforeLeaveonLeaveonAfterLeave。当你需要 JavaScript 控制动画(如用 GSAP)时,使用这些钩子。

JavaScript 过渡钩子

当 CSS 过渡不够用(如需要使用动画库 GSAP、anime.js),可以在钩子中控制动画:

vue
<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 过渡永远不会结束,页面会卡住

布局过渡

ts
// nuxt.config.ts
export default defineNuxtConfig({
  layoutTransition: {
    name: 'layout',
    mode: 'out-in',
  },
})
css
.layout-enter-active,
.layout-leave-active {
  transition: opacity 0.3s;
}
.layout-enter-from,
.layout-leave-to {
  opacity: 0;
}

页面过渡 vs 布局过渡

  • 页面过渡:同一个布局内,不同页面切换时触发
  • 布局过渡:不同布局之间切换时触发(如从默认布局切换到管理后台布局)
  • 两者独立配置,可以有不同的动画

过渡与数据获取的交互

页面有 await useFetch() 时,过渡行为可能和预期不同:

阻塞式数据获取(默认)

vue
<script setup>
// 默认:useFetch 阻塞页面渲染
const { data } = await useFetch('/api/article')
</script>

过渡时序

text
1. 旧页面离开动画开始
2. 新页面开始获取数据(await 阻塞)
3. 等待数据返回...
4. 新页面渲染完成
5. 新页面进入动画开始

INFO

问题:如果 API 响应慢 旧页面已经消失了,新页面还在等数据,中间会有空白。用户看到的效果是:旧页面淡出 → 白屏等待 → 新页面淡入

解决方案:使用懒加载

vue
<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>

优化后的时序

text
1. 旧页面离开动画开始
2. 新页面获取核心数据
3. 新页面框架渲染完成,进入动画开始
4. 进入后,非关键数据在后台加载
5. 非关键数据加载完,更新显示

最佳实践

将数据分为"核心"和"非核心"。核心数据用 useFetch 阻塞渲染(确保有内容),非核心数据用 useLazyFetch 不阻塞(加载中显示占位符)。

过渡与 keepalive

使用 definePageMeta({ keepalive: true }) 缓存页面时,过渡动画行为不同:

vue
<script setup>
definePageMeta({
  keepalive: true,  // 页面被缓存,不销毁
})
</script>

keepalive 对过渡的影响

  • 没有 keepalive:每次导航都触发离开/进入动画
  • keepalive:页面被缓存而不是销毁,所以不会触发离开动画(因为不是真的离开)
  • 缓存的页面再次进入时,触发的是 onActivated 而非 onMounted

TIP

如果你需要缓存页面的同时也有过渡动画 使用 NuxtLoadingIndicator 提供视觉反馈

NuxtLoadingIndicator

页面切换时显示顶部加载条——这是过渡动画的最佳搭档:

vue
<!-- 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 过渡更强大的方案:

基本启用

ts
// 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+)

vue
<script setup>
definePageMeta({
  viewTransition: {
    types: {
      forward: 'slide-forward',
      backward: 'slide-backward',
    },
  },
})
</script>
css
::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 可以让同一个元素在两个页面之间"飞"过去——比如列表页的缩略图飞到详情页的大图:

css
/* 给图片加 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-1product-img-2),否则浏览器无法匹配

常见过渡效果

淡入淡出(最常用)

css
.fade-enter-active,
.fade-leave-active {
  transition: opacity 0.3s ease;
}
.fade-enter-from,
.fade-leave-to {
  opacity: 0;
}

滑动

css
.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%);
}

缩放

css
.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;
}

滑上

css
.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.tscss 数组中引入:

ts
css: ['~/app/assets/css/transitions.css']

性能建议

  1. 优先使用 transformopacity:这两个属性不触发重排(reflow),GPU 加速
  2. 避免过渡大量元素:只过渡必要的元素
  3. 使用 mode: 'out-in':避免同时渲染两个页面
  4. 控制过渡时间:0.2s ~ 0.4s 通常足够,太慢用户会烦
  5. 不要用 height/width/top/left 做过渡:这些会触发重排,性能很差

常见问题

过渡动画不生效

  1. 确认 CSS 类名和 name 配置匹配(如 name: 'page'.page-enter-active
  2. 确认 CSS 文件已通过 nuxt.config.tscss 选项引入
  3. 确认 app.vue 中有 <NuxtPage />(过渡应用在 NuxtPage 上)
  4. 清除浏览器缓存后重试

过渡时页面闪烁

可能是 Hydration 不匹配导致。在过渡期间,Vue 重新渲染页面,如果 SSR 和客户端结果不同,会闪烁。确保:

  • 没有在模板中使用 Date.now()Math.random()
  • 没有在顶层代码中使用浏览器 API
  • 使用 <ClientOnly> 包裹仅客户端内容

await useFetch 时过渡卡住

数据获取阻塞了页面渲染。解决方案:

  1. 非关键数据用 useLazyFetch
  2. 关键数据设置较短的超时
  3. 使用 NuxtLoadingIndicator 提供加载反馈

知识脉络

text
组件 → 你在这里:页面过渡

         ├─→ 相关:CSS 与 SCSS(过渡 CSS 写在哪)

         ├─→ 相关:数据获取(useFetch 与过渡的交互)

         └─→ 相关:内置组件速查(NuxtLoadingIndicator)

基于 Nuxt 4 官方文档整理编写