Vue Router 5:手写路由与类型安全的文件路由

系列导航:Vue 3 现代开发指南
上一篇:跨层通信与模板引用
下一篇:Pinia 4 状态管理

Vue Router 5 是 Vue 的官方路由方案。它保留了 Vue Router 4 的手写路由 API,并把原 unplugin-vue-router 的文件路由和类型生成能力合并进核心包。普通 Router 4 项目若没有使用该插件,升级到 5 通常不需要修改路由代码。

本文先讲所有项目都适用的核心 API,再介绍 Router 5 的文件路由。两种模式选一种即可,不需要同时维护两套路由表。

一、安装和注册

使用 create-vue 时可以直接选择 Vue Router。手工安装:

npm install vue-router@5

创建 src/router/index.ts

import {
  createRouter,
  createWebHistory,
  type RouteRecordRaw,
} from 'vue-router'

const routes: RouteRecordRaw[] = [
  {
    path: '/',
    redirect: { name: 'home' },
  },
  {
    path: '/home',
    name: 'home',
    component: () => import('@/views/HomeView.vue'),
  },
  {
    path: '/about',
    name: 'about',
    component: () => import('@/views/AboutView.vue'),
  },
]

const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes,
  scrollBehavior: () => ({ top: 0 }),
})

export default router

路由页面使用动态导入,可以在生产构建中按路由拆包。入口文件注册路由:

import { createApp } from 'vue'
import App from './App.vue'
import router from './router'

createApp(App)
  .use(router)
  .mount('#app')

根组件放置导航和出口:

<script setup lang="ts">
import { RouterLink, RouterView } from 'vue-router'
</script>

<template>
  <nav aria-label="主导航">
    <RouterLink :to="{ name: 'home' }">首页</RouterLink>
    <RouterLink :to="{ name: 'about' }">关于</RouterLink>
  </nav>

  <RouterView />
</template>

RouterLink 最终会渲染可访问的链接;普通站内跳转应优先使用它,而不是给按钮绑定 router.push()

二、History 与 Hash 模式

createWebHistory() 生成没有 # 的常规 URL:

history: createWebHistory(import.meta.env.BASE_URL)

生产服务器必须把未知前端路径回退到 index.html,同时不要错误吞掉真正的静态资源和 API 404。Nginx 常见配置思路是:

location / {
  try_files $uri $uri/ /index.html;
}

无法配置服务器回退时,可以使用 Hash:

import { createWebHashHistory } from 'vue-router'

history: createWebHashHistory(import.meta.env.BASE_URL)

Hash 后的内容不会发送给服务器,部署简单,但 URL 中会出现 #

三、动态参数与 Props

声明用户详情路由:

import type { RouteRecordRaw } from 'vue-router'

const userDetailRoute = {
  path: '/users/:id',
  name: 'user-detail',
  component: () => import('@/views/UserDetailView.vue'),
  props: (route) => ({ id: String(route.params.id) }),
} satisfies RouteRecordRaw

通过命名路由跳转:

<RouterLink
  :to="{
    name: 'user-detail',
    params: { id: user.id },
  }"
>
  {{ user.name }}
</RouterLink>

页面把路由参数当普通 Prop 接收:

<script setup lang="ts">
defineProps<{ id: string }>()
</script>

这种方式让页面组件更容易测试,也减少组件对 useRoute() 的直接依赖。

使用对象跳转并携带 params 时,应使用 name;如果提供 path,额外的 params 会被忽略。未在路径中声明的临时信息应放在 query,需要长期存在的数据则应由 API 或 Store 管理。

四、Query 参数和编程式导航

<script setup lang="ts">
import { computed } from 'vue'
import { useRoute, useRouter } from 'vue-router'

const route = useRoute()
const router = useRouter()

const keyword = computed(() => {
  const value = route.query.q
  return typeof value === 'string' ? value : ''
})

function search(q: string) {
  router.push({
    name: 'search',
    query: { q, page: '1' },
  })
}

function closeModal() {
  router.back()
}
</script>

Query 值可能是字符串、字符串数组、null 或缺失值,使用前要做类型收窄。URL 参数最终都是文本,不要假设 route.params.idroute.query.page 自动成为数字。

常用导航方法:

  • router.push():增加一条历史记录;
  • router.replace():替换当前记录;
  • router.back() / router.forward():前进后退;
  • <RouterLink replace>:声明式替换当前记录。

五、嵌套路由

父页面必须包含自己的 <RouterView>

import type { RouteRecordRaw } from 'vue-router'

const settingsRoute = {
  path: '/settings',
  component: () => import('@/views/settings/SettingsLayout.vue'),
  children: [
    {
      path: '',
      name: 'settings-profile',
      component: () => import('@/views/settings/ProfileSettings.vue'),
    },
    {
      path: 'security',
      name: 'settings-security',
      component: () => import('@/views/settings/SecuritySettings.vue'),
    },
  ],
} satisfies RouteRecordRaw

子路由的 path 不以 / 开头,最终路径分别是 /settings/settings/security

六、导航守卫

导航守卫可以返回目标位置或 false,不必调用旧式 next()

router.beforeEach(async (to) => {
  const requiresAuth = to.meta.requiresAuth === true
  const signedIn = await checkSession()

  if (requiresAuth && !signedIn) {
    return {
      name: 'login',
      query: { redirect: to.fullPath },
    }
  }
})

避免在模块顶层、Pinia 尚未安装前创建 Store。需要在守卫中访问 Store 时,在守卫回调里调用 useXxxStore(),或显式传入已创建的 Pinia 实例。

元信息可以通过模块扩展类型化:

import 'vue-router'

declare module 'vue-router' {
  interface RouteMeta {
    requiresAuth?: boolean
    title?: string
  }
}

七、Router 5 的文件路由

Router 5 内置文件路由插件,可以根据 src/pages 自动生成路由和类型。配置 vite.config.ts

import { fileURLToPath, URL } from 'node:url'
import vue from '@vitejs/plugin-vue'
import { defineConfig } from 'vite'
import VueRouter from 'vue-router/vite'

export default defineConfig({
  plugins: [
    VueRouter({
      dts: 'src/route-map.d.ts',
    }),
    // Vue 插件必须放在 VueRouter 后面
    vue(),
  ],
  resolve: {
    alias: {
      '@': fileURLToPath(new URL('./src', import.meta.url)),
    },
  },
})

创建文件:

src/pages/
├── index.vue          # /
├── about.vue          # /about
└── users/
    └── [id].vue       # /users/:id

入口使用生成的路由:

import { createApp } from 'vue'
import { createRouter, createWebHistory } from 'vue-router'
import { handleHotUpdate, routes } from 'vue-router/auto-routes'
import App from './App.vue'

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

if (import.meta.hot) {
  handleHotUpdate(router)
}

createApp(App).use(router).mount('#app')

启动开发服务器后会生成类型文件。把生成文件提交到仓库,并确保它包含在 TypeScript 配置中。官方还提供 Vue 语言工具插件,让页面内的 useRoute() 根据当前文件推断参数类型。

文件路由适合路由较多、重视参数类型和约定式目录的项目;路由很少或有大量动态配置时,手写 routes 数组依然简单可靠。

八、从 Router 4 升级

Router 5 是过渡版本:

  • 未使用 unplugin-vue-router 的 Router 4 项目没有业务 API 破坏性变化;
  • 使用文件路由插件的项目主要需要把导入路径迁到 vue-router/vitevue-router/auto-routes 等新入口;
  • Router 5 为未来 ESM-only 的 Router 6 提供迁移窗口,应逐步清理弃用 API。

小结

核心路由 API 仍围绕 createRouter()、History、路由记录、RouterLinkRouterView。页面组件优先懒加载,动态参数优先通过 Props 接收。Router 5 的新增价值主要是内置类型安全的文件路由;小项目继续手写路由完全没有问题。

官方资料

系列导航:目录 · 上一篇 · 下一篇