Vue Router

HCX大约 11 分钟Vue RouterVue Router

Vue-router V4

Vue Router 是官方路由库

快速起步

  • CDN:<script src="https://unpkg.com/vue-router@next">
  • NPM:npm install vue-router@next -S

在 App.vue 文件中配置路由出口

<template>
  <h1>Hello App!</h1>
  <!-- 在组件模板中使用 $route 来访问当前的路由对象 -->
  <p><strong>Current route path:</strong> {{ $route.fullPath }}</p>
  <nav>
    <!-- 创建链接 -->
    <RouterLink to="/">Go to Home</RouterLink>
    <RouterLink to="/about">Go to About</RouterLink>
  </nav>
  <main>
    <!-- 用于渲染当前 URL 路径对应的路由组件 -->
    <RouterView />
  </main>
</template>

定义路由配置,其中createWebHistory也可以是createWebHashHistory

import { createWebHistory, createRouter } from 'vue-router';

// 导入组件
import HomeView from './HomeView.vue';
import AboutView from './AboutView.vue';

// 定义路由规则
const routes = [
  { path: '/', component: HomeView },
  { path: '/about', component: AboutView },
];

// 创造路由器实例
export default createRouter({
  history: createWebHistory(),
  routes,
});

main.js中注册路由器插件

import router from './router/index';
createApp(App).use(router).mount('#app');

在插件内进行了以下工作:

  • 全局注册 RouterViewRouterLink 组件。
  • 添加全局 $router$route 属性。
  • 启用 useRouter()useRoute() 组合式函数。
  • 触发路由器解析初始路由。

访问路由:

在组合式 API 中通过

<script setup>
import { computed } from 'vue';
import { useRoute, useRouter } from 'vue-router';

// 通过 useRoute() 和 useRouter() 来访问路由器实例和当前路由。
const router = useRouter();
const route = useRoute();

const search = computed({
  get() {
    return route.query.search ?? '';
  },
  set(search) {
    router.replace({ query: { search } });
  },
});
</script>

在选项式 API 中通过

export default {
  methods: {
    goToAbout() {
      this.$router.push('/about');
    },
  },
};

相关信息

RouterView 和 RouterLink

组件 RouterView 和 RouterLink 都是全局注册的,因此它们不需要在组件模板中导入。但你也可以通过局部导入它们,例如 import { RouterLink } from 'vue-router'

在模板中,组件的名字可以是 PascalCase 风格或 kebab-case 风格的。Vue 的模板编译器支持两种格式,因此 <RouterView><router-view> 通常是等效的。此时应该遵循你自己项目中使用的约定。

如果使用 DOM 内模板,那么需要注意:组件名字必须使用 kebab-case 风格且不支持自闭合标签。因此你不能直接写 <RouterView />,而需要使用 <router-view></router-view>

动态路由匹配

基本用法

  • 路由配置: { path: "/course/:id", component: ComponentName}
  • 参数获取: this.$route.params.iduseRoute().params.id
import User from './User.vue';

// 这些都会传递给 `createRouter`
const routes = [
  // 动态字段以冒号开始
  { path: '/users/:id', component: User },
];

现在像 /users/johnny 和 /users/jolyne 这样的 URL 都会映射到同一个路由。

注意

使用带有参数的路由时需要注意的是,当用户从 /users/johnny 导航到 /users/jolyne 时,相同的组件实例将被重复使用。因为两个路由都渲染同个组件,比起销毁再创建,复用则显得更加高效。不过,这也意味着组件的生命周期钩子不会被调用。

要对同一个组件中参数的变化做出响应的话,你可以简单地 watch $route 对象上的任意属性

或者,使用 beforeRouteUpdate 导航守卫,它还允许你取消导航

通配或 404 处理

const routes = [
  // 将匹配所有内容并将其放在 `route.params.pathMatch` 下
  { path: '/:pathMatch(.*)*', name: 'NotFound', component: NotFound },
  // 将匹配以 `/user-` 开头的所有内容,并将其放在 `route.params.afterUser` 下
  { path: '/user-:afterUser(.*)', component: UserGeneric },
];

在括号之间使用了自定义正则表达式,并将 pathMatch 参数标记为可选可重复。也就意味着可以匹配到 /xxx/yyy/zzz 这样的路径

router.push({
  name: 'NotFound',
  // 保留当前路径并删除第一个字符,以避免目标 URL 以 `//` 开头。
  params: { pathMatch: this.$route.path.substring(1).split('/') },
  // 保留现有的查询和 hash 值,如果有的话
  query: route.query,
  hash: route.hash,
});

嵌套路由

组件之间的嵌套常常会用嵌套路由形式与之对应

const routes = [
  {
    path: '/user/:id',
    component: User,
    children: [
      {
        // 当 /user/:id/profile 匹配成功
        // UserProfile 将被渲染到 User 的 <router-view> 内部
        path: 'profile',
        component: UserProfile,
      },
      {
        // 当 /user/:id/posts 匹配成功
        // UserPosts 将被渲染到 User 的 <router-view> 内部
        path: 'posts',
        component: UserPosts,
      },
    ],
  },
];

注意此时 /user/:id 无匹配结果,将会白屏。需要提供一个空的嵌套路径:

const routes = [
  {
    path: '/user/:id',
    component: User,
    children: [
      // 当 /user/:id 匹配成功
      // UserHome 将被渲染到 User 的 <router-view> 内部
      { path: '', component: UserHome },

      // ...其他子路由
    ],
  },
];

如果需要导航 /user/:id 而不显示嵌套路由。那样的话,你还可以命名父路由,但请注意重新加载页面将始终显示嵌套的子路由,因为它被视为指向路径/users/:id 的导航,而不是命名路由

如果父组件不渲染任何东西,只想提供一个统一的 url 前缀,也可以省略父组件的声明

const routes = [
  {
    path: '/admin',
    children: [
      { path: '', component: AdminOverview },
      { path: 'users', component: AdminUserList },
      { path: 'users/:id', component: AdminUserDetails },
    ],
  },
];

相关信息

命名路由

当创建一个路由时,我们可以选择给路由一个 name

const routes = [
  {
    path: '/user/:username',
    name: 'profile',
    component: User,
  },
];
<router-link :to="{ name: 'profile', params: { username: 'erina' } }">
  User profile
</router-link>

使用 name 有很多优点:

  • 没有硬编码的 URL。
  • params 的自动编码/解码。
  • 防止你在 URL 中出现打字错误。
  • 绕过路径排序,例如展示一个匹配相同路径但排序较低的路由。
  • 所有路由的命名都必须是唯一的。如果为多条路由添加相同的命名,路由器只会保留最后那一条

编程式导航

借助 router 的实例方法,通过编写代码来实现路由跳转

在组件内部,你可以使用 $router 属性访问路由,例如 this.$router.push(...)。如果使用组合式 API,你可以通过调用 useRouter() 来访问路由器。

相关信息

router.push 方法。这个方法会向 history 栈添加一个新的记录,所以,当用户点击浏览器后退按钮时,会回到之前的 URL。

当你点击 <router-link> 时,内部会调用这个方法,所以点击 <router-link :to="..."> 相当于调用 router.push(...)

// 字符串路径
router.push('/users/eduardo');

// 带有路径的对象
router.push({ path: '/users/eduardo' });

// 命名的路由,并加上参数,让路由建立 url
router.push({ name: 'user', params: { username: 'eduardo' } });

// 带查询参数,结果是 /register?plan=private
router.push({ path: '/register', query: { plan: 'private' } });

// 带 hash,结果是 /about#team
router.push({ path: '/about', hash: '#team' });

注意:如果提供了 path,params 会被忽略,上述例子中的 query 并不属于这种情况。取而代之的是下面例子的做法,你需要提供路由的 name 或手写完整的带有参数的 path

const username = 'eduardo';
// 我们可以手动建立 url,但我们必须自己处理编码
router.push(`/user/${username}`); // -> /user/eduardo
// 同样
router.push({ path: `/user/${username}` }); // -> /user/eduardo
// 如果可能的话,使用 `name` 和 `params` 从自动 URL 编码中获益
router.push({ name: 'user', params: { username } }); // -> /user/eduardo
// `params` 不能与 `path` 一起使用
router.push({ path: '/user', params: { username } }); // -> /user

router.replace 也可以实现跳转,但不会新增历史记录,而是取代当前的历史。并且router.push也能实现

router.push({ path: '/home', replace: true });
// 相当于
router.replace({ path: '/home' });

路由守卫

vue-router 提供的导航守卫主要用来通过跳转或取消的方式守卫导航。这里有很多方式植入路由导航中:全局的,单个路由独享的,或者组件级的。

使用 router.beforeEach 注册一个全局前置守卫

const router = createRouter({ ... })

router.beforeEach((to, from) => {
  // ...
  // 返回 false 以取消导航
  return false
})

接收两个参数,to 表示即将导航进入的目标,from 表示从哪里导航来的

可以返回的值如下:

  • false: 取消当前的导航。如果浏览器的 URL 改变了(可能是用户手动或者浏览器后退按钮),那么 URL 地址会重置到 from 路由对应的地址。

  • 一个路由地址: 通过一个路由地址重定向到一个不同的地址,如同调用 router.push(),且可以传入诸如 replace: truename: 'home' 之类的选项。它会中断当前的导航,同时用相同的 from 创建一个新导航。

  • undefined 或返回 true,则导航是有效的,并调用下一个导航守卫

相关信息

在之前的 Vue Router 版本中,还可以使用 第三个参数 next 。这是一个常见的错误来源,Vue-router 经过 RFC 讨论将其移除。

然而,它仍然是被支持的,这意味着你可以向任何导航守卫传递第三个参数。在这种情况下,确保 next 在任何给定的导航守卫中都被严格调用一次。它可以出现多于一次,但是只能在所有的逻辑路径都不重叠的情况下,否则钩子永远都不会被解析或报错。

路由独享的守卫

const routes = [
  {
    path: '/users/:id',
    component: UserDetails,
    beforeEnter: (to, from) => {
      // reject the navigation
      return false;
    },
  },
];

beforeEnter 守卫 只在进入路由时触发,不会在 params、query 或 hash 改变时触发。例如,从 /users/2 进入到 /users/3 或者从 /users/2#info 进入到 /users/2#projects。它们只有在从一个不同的路由导航时,才会被触发。

在组件内部定义路由守卫

在组件内可以使用以下钩子定义路由守卫:

  • beforeRouteEnter
  • beforeRouteUpdate
  • beforeRouteLeave
<script>
export default {
  beforeRouteEnter(to, from) {
    // 在渲染该组件的对应路由被验证前调用
    // 不能获取组件实例 `this` !
    // 因为当守卫执行时,组件实例还没被创建!
  },
  beforeRouteUpdate(to, from) {
    // 在当前路由改变,但是该组件被复用时调用
    // 举例来说,对于一个带有动态参数的路径 `/users/:id`,在 `/users/1` 和 `/users/2` 之间跳转的时候,
    // 由于会渲染同样的 `UserDetails` 组件,因此组件实例会被复用。而这个钩子就会在这个情况下被调用。
    // 因为在这种情况发生的时候,组件已经挂载好了,导航守卫可以访问组件实例 `this`
  },
  beforeRouteLeave(to, from) {
    // 在导航离开渲染该组件的对应路由时调用
    // 与 `beforeRouteUpdate` 一样,它可以访问组件实例 `this`
  },
};
</script>

路由元数据

有时需要在定义路由时附带额外信息,此时可以利用路由源数据,这样导航时就可以访问到这些信息

{
  path: '/about',
  component: About,
  meta: {foo: 'bar'}
}

如何访问到? 通过 meta 属性

一个路由匹配到的所有路由记录会暴露为 route 对象(还有在导航守卫中的路由对象)的 route.matched 数组。我们需要遍历这个数组来检查路由记录中的 meta 字段,Vue Router 还提供了一个 route.meta 方法,它是一个非递归合并所有 meta 字段(从父字段到子字段)的方法。

路由懒加载

打包时将单个路由组件分片打包,访问时才异步加载,可以有效降低 app 尺寸和加载空间

定义异步路由,component (和 components) 配置接收一个返回 Promise 组件的函数,Vue Router 只会在第一次进入页面时才会获取这个函数,然后使用缓存数据。

const Test = () => import('./Test');
const router = createRouter({
  //..
  routes: [{ path: '/users/:id', component: Test }],
});

分块打包,有时候我们想把某个路由下的所有组件都打包在同个异步块 (chunk) 中

使用 webpack,只需要使用命名 chunk,一个特殊的注释语法来提供 chunk name (需要 Webpack > 2.4):

const UserDetails = () => import(/* webpackChunkName: "group-user" */ './UserDetails.vue');
const UserDashboard = () => import(/* webpackChunkName: "group-user" */ './UserDashboard.vue');
const UserProfileEdit = () => import(/* webpackChunkName: "group-user" */ './UserProfileEdit.vue');

使用 Vite ,在 rollupOptions 下定义分块:

// vite.config.js
export default defineConfig({
  build: {
    rollupOptions: {
      // https://rollupjs.org/guide/en/#outputmanualchunks
      output: {
        manualChunks: {
          'group-user': ['./src/UserDetails', './src/UserDashboard', './src/UserProfileEdit'],
        },
      },
    },
  },
});

缓存和过渡动画

RotuerView 组件暴露了一个插槽,可以用来渲染路由组件:

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

上面的代码等价于不带插槽的 ,但是当我们想要获得其他功能时,插槽提供了额外的扩展性。

如缓存的实现

当在处理 KeepAlive 组件时,我们通常想要保持路由组件活跃,而不是 RouterView 本身。为了实现这个目的,我们可以将 KeepAlive 组件放置在插槽内:

<router-view v-slot="{ Component }">
  <keep-alive>
    <component :is="Component" />
  </keep-alive>
</router-view>

类似的过渡动画

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

动态路由的添加、删除

有时希望在 app 正在运行时动态添加路由到 router,vue-router 提供如下 api

新增路由router.addRoute({ path: '/about', component: About })

移除路由

  • 通过 name 删除:router.removeRoute('about')
  • 通过addRoute()返回的回调
const removeRoute = router.addRoute(routeRecord);
removeRoute();
  • 通过添加一个 name 冲突的路由
router.addRoute({ path: '/about', name: 'about', component: About });
router.addRoute({ path: '/other', name: 'about', component: Other });

添加嵌套路由

通过参数 1 传递父组件 name 即可 router.addRoute('parentRouteName',{...})

查看已存在的路由

  • router.hasRoute() 判断是否存在某个路由
  • router.getRoutes() 获取所有路由数组

实际场景:用户登录后动态添加权限路由