阿里云主机折上折
  • 微信号
您当前的位置:网站首页 > 路由元信息类型推断

路由元信息类型推断

作者:陈川 阅读数:64474人阅读 分类: Vue.js

路由元信息类型推断

Vue Router 允许开发者为路由配置添加自定义元信息(meta),这些信息通常用于权限控制、页面标题设置等场景。随着 TypeScript 在 Vue 项目中的普及,如何为这些元信息提供类型支持成为提升开发体验的关键点。

基础类型定义

最简单的元信息类型可以通过接口直接定义:

interface RouteMeta {
  requiresAuth: boolean
  title: string
}

const router = createRouter({
  routes: [
    {
      path: '/dashboard',
      meta: {
        requiresAuth: true,
        title: '控制面板'
      }
    }
  ]
})

这种方式的缺点是类型定义与路由配置分离,当元信息结构变化时需要同步修改两处代码。

扩展 RouteMeta 接口

更推荐的做法是扩展 Vue Router 的 RouteMeta 接口:

// types/router.d.ts
import 'vue-router'

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

这样所有路由的元信息都会自动获得类型检查,且可以通过模块声明实现全局类型共享。

嵌套元信息结构

对于复杂的应用场景,元信息可能需要嵌套结构:

interface Permission {
  roles: string[]
  permissions: string[]
}

declare module 'vue-router' {
  interface RouteMeta {
    auth: {
      required: boolean
      redirect?: string
    }
    page: {
      title: string
      transition?: string
    }
    permission?: Permission
  }
}

// 使用示例
const adminRoute = {
  path: '/admin',
  meta: {
    auth: {
      required: true,
      redirect: '/login'
    },
    page: {
      title: '管理后台',
      transition: 'fade'
    },
    permission: {
      roles: ['admin'],
      permissions: ['user:manage']
    }
  }
}

基于泛型的类型推断

对于需要动态生成路由配置的场景,可以创建路由配置工厂函数:

function defineRoute<T extends RouteMeta>(config: {
  path: string
  meta: T
}): RouteRecordRaw {
  return {
    ...config,
    component: () => import(`@/views${config.path}.vue`)
  }
}

const routes = [
  defineRoute({
    path: '/profile',
    meta: {
      requiresAuth: true,
      title: '个人资料',
      // 这里会检查是否缺少 requiredAuth 或 title
    }
  })
]

路由守卫中的类型推断

在导航守卫中也能获得完整的类型提示:

router.beforeEach((to) => {
  if (to.meta.requiresAuth && !isAuthenticated()) {
    // to.meta.redirect 会有自动补全
    return to.meta.auth?.redirect || '/login'
  }
  
  document.title = to.meta.title // 这里会报错,提示拼写错误
})

动态元信息合并

有时需要根据条件动态合并元信息:

function withAnalytics<T extends RouteMeta>(meta: T): T {
  return {
    ...meta,
    analytics: {
      trackPageView: true,
      category: meta.title
    }
  }
}

const routes = [
  {
    path: '/products',
    meta: withAnalytics({
      title: '产品列表',
      requiresAuth: false
    })
  }
]

需要相应扩展 RouteMeta 接口来包含 analytics 属性。

类型安全的元信息访问器

可以创建工具函数来安全访问元信息:

function getRouteTitle(route: RouteLocationNormalized): string {
  if (typeof route.meta.title !== 'string') {
    throw new Error('Missing required title meta field')
  }
  return route.meta.title
}

// 或者使用类型谓词
function hasRequiredMeta(
  meta: RouteMeta
): meta is RouteMeta & { title: string; requiresAuth: boolean } {
  return typeof meta.title === 'string' && typeof meta.requiresAuth === 'boolean'
}

元信息与组合式 API

在 setup 函数中访问路由元信息:

import { useRoute } from 'vue-router'

export default {
  setup() {
    const route = useRoute()
    
    watch(() => route.meta.title, (title) => {
      document.title = title || '默认标题'
    })

    // 需要类型断言时
    const analyticsMeta = route.meta as { analytics?: { enabled: boolean } }
  }
}

高级模式匹配

对于动态路由,可以使用映射类型来定义元信息:

type DynamicRouteMeta = {
  [key: string]: {
    title: string
    params: Record<string, string>
  }
}

declare module 'vue-router' {
  interface RouteMeta {
    dynamic?: DynamicRouteMeta
  }
}

// 使用示例
const routes = [{
  path: '/user/:id',
  meta: {
    dynamic: {
      user: {
        title: '用户详情',
        params: { id: '' } // 必须包含 id 参数
      }
    }
  }
}]

元信息与路由配置分离

对于大型项目,可以将元信息单独管理:

// meta-configs.ts
export const metaConfigs = {
  dashboard: {
    requiresAuth: true,
    title: '仪表盘',
    icon: 'mdi-gauge'
  },
  settings: {
    requiresAuth: true,
    title: '系统设置',
    permissionLevel: 2
  }
} as const

// router.ts
const routes = [
  {
    path: '/dashboard',
    meta: metaConfigs.dashboard
  }
]

需要扩展 RouteMeta 来包含所有可能的元信息类型。

类型测试与验证

可以编写类型测试来确保元信息结构正确:

type AssertExtends<T, U extends T> = true

// 测试元信息类型
type TestMeta = AssertExtends<RouteMeta, {
  title: string
  requiresAuth: boolean
}>

与 Pinia 集成

当路由元信息需要与状态管理交互时:

export const useAuthStore = defineStore('auth', {
  actions: {
    checkPermission(route: RouteLocationNormalized) {
      if (route.meta.requiresAuth && !this.isLoggedIn) {
        throw new Error('Unauthorized')
      }
      
      // 可以访问扩展的 meta 类型
      if (route.meta.permission?.roles.includes('admin')) {
        // ...
      }
    }
  }
})

自动生成类型文档

利用 TypeScript 类型可以自动生成元信息文档:

type MetaFieldDescription = {
  [K in keyof RouteMeta]: {
    type: string
    required: boolean
    description: string
  }
}

const metaDocs: MetaFieldDescription = {
  title: {
    type: 'string',
    required: true,
    description: '页面标题'
  },
  requiresAuth: {
    type: 'boolean',
    required: false,
    description: '是否需要认证'
  }
}

多级路由类型合并

对于嵌套路由,子路由会继承父路由的元信息类型:

const routes = [
  {
    path: '/admin',
    meta: { accessLevel: 2 },
    children: [
      {
        path: 'users',
        meta: { 
          // 必须包含 accessLevel
          title: '用户管理' 
        }
      }
    ]
  }
]

可以通过类型工具实现部分覆盖:

type PartialRouteMeta = Partial<RouteMeta> & {
  title: string // 确保 title 始终存在
}

运行时类型验证

虽然 TypeScript 在编译时提供类型检查,但有时需要运行时验证:

import { is } from 'typescript-is'

router.beforeEach((to) => {
  if (!is<RouteMeta>(to.meta)) {
    console.warn('Invalid route meta', to.meta)
  }
  
  // 或者使用 Zod 等验证库
  const metaSchema = z.object({
    title: z.string(),
    requiresAuth: z.boolean().optional()
  })
  
  const result = metaSchema.safeParse(to.meta)
})

元信息类型演变

随着项目发展,元信息类型可能需要版本控制:

type LegacyMeta = {
  needAuth?: boolean
  pageTitle?: string
}

type CurrentMeta = {
  requiresAuth: boolean
  title: string
}

type RouteMeta = CurrentMeta | (LegacyMeta & {
  __isLegacy: true
})

// 使用时需要类型守卫
function normalizeMeta(meta: RouteMeta): CurrentMeta {
  if ('__isLegacy' in meta) {
    return {
      requiresAuth: meta.needAuth ?? false,
      title: meta.pageTitle || '默认标题'
    }
  }
  return meta
}

本站部分内容来自互联网,一切版权均归源网站或源作者所有。

如果侵犯了你的权益请来信告知我们删除。邮箱:cc@cccx.cn

前端川

前端川,陈川的代码茶馆🍵,专治各种不服的Bug退散符💻,日常贩卖秃头警告级的开发心得🛠️,附赠一行代码笑十年的摸鱼宝典🐟,偶尔掉落咖啡杯里泡开的像素级浪漫☕。‌