Skip to content

Vue Router 路由切换动效

1. 核心思想:利用 <transition> 组件

Vue Router 的路由切换本质上是组件的销毁和创建。我们可以利用 Vue 内置的 <transition> 组件来包裹 <router-view>,从而在路由组件切换时自动应用过渡效果。

这并不是新知识,而是将 Vue 的过渡动画知识点应用于路由场景。

html
<router-view v-slot="{ Component }">
  <transition name="fade">
    <component :is="Component" />
  </transition>
</router-view>

2. 基础实现:单向滑动动画

我们可以实现一个常见的动效:旧组件向左滑出,新组件从右侧滑入。

2.1. 包裹 <router-view> 并命名

在根组件 (如 App.vue) 中,用 <transition> 包裹 <router-view>,并为其提供一个 name,例如 left

html
<router-view v-slot="{ Component }">
  <transition name="left">
    <component :is="Component" />
  </transition>
</router-view>

2.2. 定义 CSS 过渡类

根据 name="left",我们需要定义相应的 CSS 类来描述动画过程。Vue 会在过渡的不同阶段自动添加/删除这些类。

  • .left-enter-from: 进入动画的起始状态。
  • .left-leave-to: 离开动画的结束状态。
  • .left-enter-active, .left-leave-active: 定义进入和离开动画的持续时间、延迟和曲线函数。
vue
<style scoped>
/* 进入动画的起始状态:组件在屏幕右侧外边,完全透明 */
.left-enter-from {
  transform: translateX(100%);
  opacity: 0;
}

/* 离开动画的结束状态:组件移动到屏幕左侧外边,完全透明 */
.left-leave-to {
  transform: translateX(-100%);
  opacity: 0;
}

/* 进入和离开动画的过渡效果:持续 0.5 秒 */
.left-enter-active,
.left-leave-active {
  transition: all 0.5s ease;
}
</style>

3. 问题修复:解决组件共存时的跳动问题

3.1. 问题分析

在过渡期间,新旧两个组件会同时存在于 DOM 中。由于它们都是块级元素,会遵循正常的文档流布局,导致新组件被“挤”到旧组件的下方,动画结束后再“跳”回正确位置。

3.2. 解决方案:绝对定位

为了解决跳动问题,我们可以给 正在离开的组件 添加 position: absolute。这样它就会脱离文档流,不再影响新进入组件的布局。

css
<style scoped>
/* ... 其他样式 ... */

/* 离开动画激活时,让旧组件脱离文档流 */
.left-leave-active {
  transition: all 0.5s ease;
  position: absolute; /* 关键!脱离文档流 */
  width: 100%;       /* 保持宽度 */
  left: 0;           /* 保持水平位置 */
}
</style>

4. 进阶实现:动态过渡方向 (前进/后退)

在实际应用中,我们希望根据导航的层级关系决定动画方向。例如:

  • Home -> About (前进):向左滑动。
  • About -> Home (后退):向右滑动。

这就要求 <transition>name 属性是动态的。

4.1. 实现思路

整个逻辑链条如下:

  1. 路由 (Router):通过导航守卫 beforeEach 比较目标路由和来源路由的层级。
  2. 判断方向:根据层级比较结果,确定应该是 left 还是 right 动画。
  3. 状态仓库 (Store):将判断出的方向('left'/'right')存储在一个全局共享的状态中(如 Vuex)。
  4. 组件 (Component)<transition> 组件的 name 属性动态绑定到仓库中的方向数据。
  5. 渲染:当仓库数据变化时,组件自动重新渲染,应用正确的 name,从而触发对应的 CSS 动画。

4.2. 步骤 1: 为路由添加元数据 (meta)

在路由配置中,为每个路由添加一个 meta 对象,并定义一个 index 来表示其层级或顺序。

javascript
// router/index.js
const routes = [
  {
    path: '/home',
    name: 'Home',
    component: Home,
    meta: { index: 0 } // 首页,层级为 0
  },
  {
    path: '/about',
    name: 'About',
    component: About,
    meta: { index: 1 } // 关于页,层级为 1
  },
  {
    path: '/user',
    name: 'User',
    component: User,
    meta: { index: 2 } // 用户页,层级为 2
  }
];

4.3. 步骤 2: 创建状态仓库 (Store) 管理方向

使用 Vuex 或其他状态管理工具来存储全局的过渡方向。

javascript
// store/index.js (以 Vuex 为例)
import { createStore } from 'vuex';

export default createStore({
  state: {
    // 默认过渡方向
    transitionName: 'left'
  },
  mutations: {
    // 修改过渡方向的方法
    setTransitionName(state, name) {
      state.transitionName = name;
    }
  },
  actions: {},
  modules: {}
});

4.4. 步骤 3: 使用导航守卫 (beforeEach) 判断方向

在路由配置文件中,添加一个全局前置守卫,根据 meta.index 判断导航方向,并提交 mutation 更改仓库状态。

javascript
// router/index.js
import { createRouter, createWebHistory } from 'vue-router';
import store from '../store'; // 引入仓库

// ... routes 定义 ...

const router = createRouter({
  history: createWebHistory(process.env.BASE_URL),
  routes
});

router.beforeEach((to, from, next) => {
  // 只有当 to 和 from 都有 meta.index 时才进行比较
  if (to.meta.index && from.meta.index) {
    // from.index < to.index,判定为前进,使用 left 动画
    // from.index > to.index,判定为后退,使用 right 动画
    const transitionName = to.meta.index < from.meta.index ? 'right' : 'left';
    store.commit('setTransitionName', transitionName);
  }
  next();
});

export default router;

4.5. 步骤 4: 动态绑定 <transition>name

最后,在 App.vue 中,将 <transition>name 属性与仓库中的 transitionName 状态进行绑定。同时,准备好 right 过渡方向的 CSS 类。

vue
<template>
  <router-view v-slot="{ Component }">
    <transition :name="$store.state.transitionName">
      <component :is="Component" />
    </transition>
  </router-view>
</template>

<style scoped>
/* 向左滑动 (前进) */
.left-enter-from { transform: translateX(100%); opacity: 0; }
.left-leave-to { transform: translateX(-100%); opacity: 0; }
.left-enter-active,
.left-leave-active {
  transition: all 0.5s ease;
  position: absolute;
  width: 100%;
  left: 0;
}

/* 向右滑动 (后退) */
.right-enter-from { transform: translateX(-100%); opacity: 0; }
.right-leave-to { transform: translateX(100%); opacity: 0; }
.right-enter-active,
.right-leave-active {
  transition: all 0.5s ease;
  position: absolute;
  width: 100%;
  left: 0;
}
</style>