Code前端首页关于Code前端联系我们

Vue Router TypeScript 基础配置咋做?

terry 9小时前 阅读数 12 #Vue

不少用 Vue 做项目的同学,现在都慢慢把技术栈切换到 TypeScript 了,毕竟 TS 能在编码阶段就提前发现类型错误,让代码更健壮,但碰到 Vue Router 的时候,总会犯嘀咕:路由配置咋写才能让 TS 发挥作用?动态路由参数咋做类型约束?导航守卫里的参数类型咋处理?今天就把 Vue Router 结合 TypeScript 的常见问题掰碎了讲,从基础到实战一步一步理清楚~

先解决最基础的安装和配置问题,首先得确保依赖装对,Vue Router 4.x 对 TS 的支持已经很友好了,所以先装包:

npm install vue-router@4

接下来创建路由实例,这时候要注意路由数组的类型,Vue Router 提供了 RouteRecordRaw 类型,用来定义单个路由记录的结构,比如这样写路由配置:

import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'
import HomeView from '../views/HomeView.vue'
// 定义路由数组,每个元素是 RouteRecordRaw 类型
const routes: RouteRecordRaw[] = [
  {
    path: '/',
    name: 'home',
    component: HomeView // 同步组件,直接引入
  },
  {
    path: '/about',
    name: 'about',
    component: () => import('../views/AboutView.vue') // 异步组件,懒加载
  }
]
const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes
})
export default router

这里 RouteRecordRaw 帮我们约束了每个路由对象的 pathnamecomponent 等字段的类型,要是写错字段(比如把 path 写成 paths),TS 会直接报错,提前拦截错误。

然后在 Vue 组件里用 useRouteruseRoute 的时候,TS 也能提供类型提示,比如在组件里导航:

import { useRouter, useRoute } from 'vue-router'
export default {
  setup() {
    const router = useRouter()
    const route = useRoute()
    // 跳转路由,router.push 的参数类型会被 TS 约束
    const goToAbout = () => {
      router.push({ name: 'about' }) // 这里如果 name 写错,TS 会提示
    }
    // 读取当前路由参数,route.params 等也有类型提示
    console.log(route.path) // 自动推断是 string 类型
  }
}

不过光这样还不够细,比如路由的 meta 信息、动态路由参数的类型,得进一步配置——这就需要往下深挖啦。

动态路由匹配,TS 咋约束参数类型?

动态路由比如 /user/:id 这种,参数 id 的类型咋保证?举个例子,路由配置里写:

const routes: RouteRecordRaw[] = [
  {
    path: '/user/:id',
    name: 'user',
    component: () => import('../views/UserView.vue')
  }
]

这时候在组件里用 useRoute() 拿到的 route.params.id 类型是 string | undefined(因为路由参数可能不存在),但实际业务中 id 应该是数字或者有明确格式,这时候得做类型约束。

类型断言

在组件里直接断言参数类型:

import { useRoute } from 'vue-router'
export default {
  setup() {
    const route = useRoute()
    // 断言 id 是 number 类型
    const userId = Number(route.params.id) as number
    // 后续用 userId 就有 number 类型提示了
  }
}

但这种方法有点“暴力”,如果参数格式不对(id 是字母),运行时会出错,所以更推荐在路由配置阶段就明确参数类型。

扩展路由参数类型

Vue Router 允许我们通过泛型来扩展 RouteParams,比如在项目的类型声明文件(如 env.d.tstypes/router.d.ts)里:

import 'vue-router'
declare module 'vue-router' {
  interface RouteParams {
    // 对应路由 name: 'user' 的 params 结构
    user: {
      id: string // 这里根据业务,也可以是 number,注意和 path 里的 :id 对应
    }
  }
}

这样配置后,当用 router.push({ name: 'user', params: { id: '123' } }) 时,TS 会检查 params.id 是否符合 string 类型;如果传错类型(比如传 id: 123 数字),就会报错。

在组件里用 useRoute() 时,route.params 也会有对应的类型提示:

const route = useRoute()
// route.params.id 会被推断为 string | undefined
const userId = route.params.id // 类型是 string | undefined,需处理 undefined 情况

要是业务里 id 必须存在,可以结合路由配置的 strict 模式,或者在导航守卫里做校验,但 TS 能提前帮我们把类型边界划清楚~

导航守卫里的 TS 类型咋搞?

导航守卫分全局守卫、路由独享守卫、组件内守卫,每个守卫的参数都有对应的类型,用 TS 可以避免参数用错。

全局守卫(如 router.beforeEach)

全局前置守卫的参数是 tofromnext,它们的类型分别是 RouteLocationRouteLocationNavigationGuardNext,Vue Router 已经内置了这些类型,所以直接写:

router.beforeEach((to, from, next) => {
  // to 和 from 都有完整的路由信息类型
  if (to.meta.requiresAuth && !isAuthenticated()) {
    next({ name: 'login' })
  } else {
    next()
  }
})

但这里 to.meta.requiresAuth 可能会报“属性不存在”的错,因为默认 RouteMeta 是个空接口,这时候需要扩展 RouteMeta 类型(前面提过的方法):

declare module 'vue-router' {
  interface RouteMeta {
    requiresAuth?: boolean // 标记是否需要授权 string // 页面标题,必传
  }
}

这样在路由配置里,每个路由的 meta 必须包含 title,可选 requiresAuth

const routes: RouteRecordRaw[] = [
  {
    path: '/',
    name: 'home',
    component: HomeView,
    meta: {
      title: '首页', // 必须传,否则 TS 报错
      requiresAuth: false
    }
  }
]

现在在导航守卫里,to.meta.title 就有类型提示了,也不会报错~

路由独享守卫(beforeEnter)

路由独享守卫写在单个路由配置里,参数类型和全局守卫类似:

const routes: RouteRecordRaw[] = [
  {
    path: '/profile',
    name: 'profile',
    component: () => import('../views/ProfileView.vue'),
    beforeEnter: (to, from, next) => {
      // 这里 to 和 from 的类型同样受约束
      if (from.name === 'home') {
        next()
      } else {
        next({ name: 'home' })
      }
    }
  }
]

组件内守卫(如 onBeforeRouteEnter)

在 Composition API 里,组件内守卫用 onBeforeRouteUpdateonBeforeRouteLeave 这些函数,它们的参数类型也会被 TS 识别:

import { onBeforeRouteUpdate } from 'vue-router'
export default {
  setup() {
    onBeforeRouteUpdate((to, from) => {
      // to 和 from 类型正确,能拿到 meta、params 等信息
      console.log(to.meta.title, from.params.id)
    })
  }
}

通过给守卫参数加上类型约束,能避免很多“传错参数”“读错属性”的低级错误,这也是 TS 结合路由的核心优势之一~

路由懒加载时,TS 咋处理异步组件类型?

路由懒加载用 () => import('../views/AboutView.vue') 这种写法,Webpack 之类的打包工具能自动拆分代码,但 TS 对这种动态导入的组件类型会不会有丢失?

Vue Router 会自动推断异步组件的类型,只要组件本身是用 TS 写的,类型就会传递过来。AboutView.vue 是这样的:

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

当路由配置里用 component: () => import('../views/AboutView.vue') 时,TS 能识别这个异步组件的 props 类型,如果在路由跳转时传参:

router.push({
  name: 'about',
  params: {}, // 如果有 params
  query: {
    msg: 'hello' // 这里如果 AboutView 需要 msg 是 string,TS 会检查
  }
})

不过有时候为了更明确,也可以手动给异步组件指定类型,比如用 defineAsyncComponent 结合泛型:

import { defineAsyncComponent } from 'vue'
const AboutView = defineAsyncComponent<{ msg: string }>(() => import('../views/AboutView.vue'))
// 然后在路由配置里用 component: AboutView

这样就算组件在异步加载,TS 也能提前知道它的 props 类型,避免传参错误~

懒加载时还可以加 Webpack 的魔法注释来控制 chunk 名字,

component: () => import(/* webpackChunkName: "about" */ '../views/AboutView.vue')

TS 对这种注释是友好的,不会报错,还能配合打包工具优化~

怎么给路由做更细的类型强化?

前面讲了扩展 RouteMetaRouteParams,但实际项目中,我们还可以用泛型让 useRoute()useRouter() 返回更精确的类型。

给 useRoute 加泛型

useRoute() 可以传入泛型,指定当前路由的 paramsquerymeta 结构,比如我们有个用户路由,params{ id: string }metatitlerequiresAuth

import { useRoute, RouteLocationNormalizedLoaded } from 'vue-router'
// 定义当前路由的类型
type UserRoute = RouteLocationNormalizedLoaded & {
  params: { id: string }
  meta: { title: string; requiresAuth: boolean }
}
export default {
  setup() {
    const route = useRoute<UserRoute>()
    // route.params.id 是 string 类型,route.meta.title 是 string 类型
    console.log(route.params.id, route.meta.title)
  }
}

这样就不用依赖全局的 RouteParamsRouteMeta 扩展,在组件内更精确地约束类型,适合局部特殊的路由场景。

统一管理路由类型

可以把所有路由的 name 和对应的参数类型集中到一个文件里,router-types.ts

export type RouteNames = 'home' | 'user' | 'about'
export type RouteParamsMap = {
  home: never // 没有参数
  user: { id: string }
  about: { tab?: string } // 可选参数
}
export type RouteMetaMap = {
  home: { title: '首页'; requiresAuth: false }
  user: { title: '用户页'; requiresAuth: true }
  about: { title: '关于页'; requiresAuth: false }
}

然后在路由配置和组件里引用这些类型:

// 路由配置
import { RouteNames, RouteParamsMap, RouteMetaMap } from './router-types'
const routes: RouteRecordRaw[] = [
  {
    path: '/',
    name: 'home' as RouteNames, // 断言 name 类型
    component: HomeView,
    meta: {
      title: '首页',
      requiresAuth: false
    } as RouteMetaMap['home'] // 断言 meta 类型
  }
  // 其他路由类似
]
// 组件内用 router.push
import { useRouter } from 'vue-router'
import { RouteNames, RouteParamsMap } from './router-types'
const router = useRouter()
router.push({
  name: 'user' as RouteNames,
  params: { id: '123' } as RouteParamsMap['user']
})

这种集中管理的方式,在团队协作时能保证路由名称、参数、元信息的类型统一,减少沟通成本~

实战中 TS 类型错误咋解决?

用 TS 结合 Vue Router 时,最头疼的就是各种类型报错,这里列几个常见场景和解决方法。

Property 'xxx' does not exist on type 'RouteParams'

比如访问 route.params.id 时,TS 报错说 id 不存在,原因是 RouteParams 默认是个宽泛的类型,没包含我们的自定义参数。

解决方法:扩展 RouteParams 接口(前面讲过的),或者在组件内用类型断言:

const userId = (route.params as { id: string }).id

更推荐全局扩展 RouteParams,一劳永逸:

declare module 'vue-router' {
  interface RouteParams {
    user: { id: string }
    // 其他路由参数
  }
}

导航时 params 类型不匹配

比如路由 user 需要 id: string,但跳转时传了 id: 123(数字),TS 没报错,但运行时可能出错。

解决方法:给 router.push 的参数加类型约束,用泛型指定路由名称和参数:

import { Router, RouteLocationNamedRaw } from 'vue-router'
type PushParams<Name extends RouteNames> = RouteLocationNamedRaw & {
  name: Name
  params: RouteParamsMap[Name]
}
const push = <Name extends RouteNames>(router: Router, params: PushParams<Name>) => {
  router.push(params)
}
// 使用时
push(router, {
  name: 'user',
  params: { id: '123' } // 这里如果传数字,TS 会报错
})

meta 字段类型丢失

访问 to.meta.title 时,TS 说 title 不存在,因为默认 RouteMeta 是空接口。

解决方法:扩展 RouteMeta 接口,在项目的类型声明文件里:

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

这样所有路由的 meta 都得符合这个结构,TS 会自动检查~

异步组件导入类型错误

动态导入组件时,TS 报“模块没有默认导出”之类的错,通常是因为组件用了 export default 还是 export const 不一致。

解决方法:确保组件用 export default 导出,或者在导入时处理:

component: () => import('../views/AboutView.vue').then(m => m.AboutView)

如果组件是用 <script setup> 写的,默认会有 default 导出,所以直接用 () => import('../views/AboutView.vue') 即可~

TS 让 Vue Router 更“聪明”了!

把 Vue Router 和 TypeScript 结合起来,核心是利用 TS 的类型系统,给路由配置、参数传递、导航守卫这些环节加上“安全锁”,从基础配置时的 RouteRecordRaw,到动态路由的参数约束,再到导航守卫的类型提示,TS 能在编码阶段就帮我们发现大部分错误,减少运行时 Bug。

而且通过扩展 RouteMetaRouteParams 这些接口,还能让团队内的路由规则更统一,新人接手项目时也能通过类型提示快速理解路由逻辑,现在前端项目越来越复杂,TS + Vue Router 这种组合,既能享受 Vue 的开发效率,又能借助 TS 保证代码质量,未来肯定是主流玩法~

要是你在实际开发中还有其他关于 Vue Router + TS 的疑问,比如和 Pinia 结合时的路由类型处理,或者 SSR 场景下的路由类型,评论区随时交流呀~

版权声明

本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。

发表评论:

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

热门