VueRouter类型系统浅析

253 阅读7分钟

📋 核心类型分类与关系

重要说明

在深入了解各个类型之前,需要明确一些重要的类型关系:

  • RouteLocationNormalizedRouteLocationNormalizedLoaded继承自同一个基础接口
  • RouteLocationNormalizedLoadedRouteLocationNormalized加载完成版本,确保异步组件等已解析完毕。
  • RouteRecordNormalized独立的路由记录类型,代表路由配置的标准化形式,与路由位置类型不同。

1. 路由配置阶段

RouteRecordRaw

这是你在创建路由时使用的原始配置类型,用于定义路由的基本结构。这些原始记录在传递给 Vue Router 后会被标准化处理。

const routes: RouteRecordRaw[] = [
  {
    path: '/user/:id',
    name: 'User',
    component: UserComponent,
    meta: { requiresAuth: true },
    children: [
      {
        path: 'profile',
        component: UserProfile
      }
    ]
  }
];

RouteRecordNormalized

RouteRecordRaw 经过 Vue Router 内部标准化处理后的路由记录。它包含了完整的、规范化的路由配置信息。注意:这与 RouteLocationNormalized 是不同的类型! RouteLocationNormalizedmatched 属性会包含一个 RouteRecordNormalized 数组。

// 通过 to.matched 访问匹配的路由记录
router.beforeEach((to) => {
  // to.matched 是一个 RouteRecordNormalized 数组
  const matchedRecord: RouteRecordNormalized = to.matched[0];
  if (matchedRecord) {
    console.log(matchedRecord.path);      // 路由路径模板,如 "/user/:id"
    console.log(matchedRecord.name);      // 路由名称
    console.log(matchedRecord.components); // 组件配置 (可能是多个命名视图)
    console.log(matchedRecord.children);  // 子路由 (RouteRecordNormalized[])
    console.log(matchedRecord.meta);      // 路由元信息
  }
});

2. 路由导航阶段

RouteLocationRaw

用于编程式导航(如 router.pushrouter.replace)的参数类型,支持多种形式的路由描述。

// 字符串形式
const destination1: RouteLocationRaw = '/user/123';

// 对象形式
const destination2: RouteLocationRaw = {
  name: 'User',
  params: { id: '123' },
  query: { tab: 'profile' },
  hash: '#details'
};

// 使用
router.push(destination1);
router.replace(destination2);

RouteLocationNormalized

完整解析后的路由位置信息,包含所有标准化的路由数据。这是路由导航守卫(如 beforeEach, beforeResolve, afterEach)中 tofrom 参数的核心类型

// 在路由守卫中最常见
router.beforeEach((
  to: RouteLocationNormalized,
  from: RouteLocationNormalized
) => {
  console.log({
    path: to.path,           // "/user/123" (解析后的实际路径)
    name: to.name,           // "User"
    params: to.params,       // { id: "123" }
    query: to.query,         // { tab: "profile" }
    meta: to.meta,           // { requiresAuth: true }
    matched: to.matched,     // RouteRecordNormalized[] 匹配的路由记录数组
    fullPath: to.fullPath,   // "/user/123?tab=profile"
    hash: to.hash,           // "#details"
    redirectedFrom: to.redirectedFrom // 重定向来源 (RouteLocationNormalized)
  });
});

RouteLocationNormalizedLoaded

组件内通过 useRoute() 获取的当前路由信息,是响应式的路由对象。它继承自 RouteLocationNormalized,但关键区别在于它代表一个导航已完成且所有相关组件(包括异步组件)都已加载的路由状态。

// 在组件中使用
import { useRoute } from 'vue-router';
import { defineComponent, watchEffect } from 'vue';

export default defineComponent({
  setup() {
    const route: RouteLocationNormalizedLoaded = useRoute();

    // 响应式访问路由信息,此时所有组件都已加载
    watchEffect(() => {
      console.log('当前用户ID:', route.params.id);
      console.log('当前完整路径:', route.fullPath);
    });

    return { route };
  }
});

3. 实用类型

RouteParams系列

  1. RouteParams

    • 经过解析的路由参数对象

    • 使用场景:获取当前路由的动态参数

      // 路由: /user/:id
      const params: RouteParams = route.params
      // { id: '123' }
      
  2. RouteParamsGeneric

    • 泛型路由参数,提供类型安全

    • 在TypeScript中定义具体的参数类型

      interface UserParams extends RouteParamsGeneric {
        id: string
        tab?: string
      }
      // 类型安全的参数访问
      const userParams = route.params as UserParams
      
  3. RouteParamsRaw

    • 原始路由参数对象

    • 编程式导航时传入参数

      router.push({
        name: 'user',
        params: { id: 123 } as RouteParamsRaw
      })
      
  4. RouteParamsRawGeneric

    • 泛型原始路由参数

    • 提供类型安全的原始参数设置

      interface UserParamsRaw extends RouteParamsRawGeneric {
        id: string | number
      }
      const params: UserParamsRaw = { id: 123 }
      
  5. RouteParamValue

    • 单个路由参数的值

    • 当你确定参数只有一个值时使用

      const userId: RouteParamValue = route.params.id // string
      
  6. 应用场景

    // 列表页面的分页和筛选
    const handleSearch = (filters: any) => {
      const query: LocationQueryRaw = {
        page: 1,
        pageSize: 20,
        keyword: filters.keyword,
        category: filters.categories,
        sortBy: 'created_at'
      }
      
      router.push({ query })
    }
    
    // 获取查询参数
    const currentQuery: LocationQuery = route.query
    const page = Number(currentQuery.page) || 1
    
    // 用户详情页路由定义
    // /user/:id/profile/:tab?
    
    // 访问参数
    const params: RouteParams = route.params
    const userId = params.id          // string
    const activeTab = params.tab      // string | undefined
    
    // 编程式导航
    router.push({
      name: 'UserProfile',
      params: { 
        id: '123', 
        tab: 'settings' 
      } as RouteParamsRaw
    })
    

RouteQuery系列

  1. LocationQuery

    • 经过解析的查询参数对象,值已被规范化

    • 当你需要访问已解析的查询参数时,比如在组件中获取当前路由的查询参数

      // 获取当前路由的查询参数
      const query: LocationQuery = route.query
      // { page: '1', sort: 'name', tags: ['vue', 'router'] }
      
  2. LocationQueryRaw

    • 原始的查询参数对象,值未经处理

    • 当你需要设置路由参数时,可以传入原始值,Vue Router会自动处理

      // 导航时传入原始查询参数
      router.push({
        path: '/users',
        query: { page: 1, active: true } as LocationQueryRaw
      })
      
  3. LocationQueryValue

    • 单个查询参数的值(已解析)

    • 当你确定某个查询参数只有一个值时使用

      const pageParam: LocationQueryValue = route.query.page // string | null
      
  4. 单个查询参数的原始值

    • 单个查询参数的原始值

    • 设置查询参数时,可以传入多种类型的值

      const queryParams = {
        page: 1,           // number
        search: 'vue',     // string
        active: null       // null
      } as Record<string, LocationQueryValueRaw>
      
  5. RouteQueryAndHash

    • 包含查询参数和hash的对象

    • 当你需要同时设置查询参数和hash时

      router.push({
        path: '/docs',
        ...{ 
          query: { section: 'api' },
          hash: '#methods'
        } as RouteQueryAndHash
      })
      

查询参数的类型定义。查询参数的值可以是字符串或字符串数组 (当同一参数名出现多次时)。

// 查询参数总是字符串或字符串数组
const query: RouteQuery = {
  page: '1',
  size: '10',
  tags: ['vue', 'router'] // 例如 ?tags=vue&tags=router
};

🎯 选择指南

1. 使用 RouteRecordRaw 的场景

✅ 适用于:

  • 定义初始路由配置数组时 (createRouter 中的 routes 选项)
  • 使用 router.addRoute() 动态添加单个路由或子路由时
  • 构建路由表的原始结构数据时
// 定义路由配置
const userRoutes: RouteRecordRaw[] = [
  {
    path: '/profile',
    name: 'Profile',
    component: Profile,
    meta: { title: '个人资料' }
  },
  {
    path: '/settings',
    name: 'Settings',
    component: Settings,
    meta: { title: '设置' }
  }
];

// 动态添加路由 (作为顶级路由)
router.addRoute(userRoutes[0]);

// 动态添加子路由
router.addRoute('User', { path: 'preferences', name: 'UserPreferences', component: UserPreferences });

// 批量添加 (通常在初始化时作为 routes 数组传递)
// userRoutes.forEach(route => {
//   router.addRoute(route); // 如果要添加为顶级路由
// });

2. 使用 RouteLocationNormalized 的场景

✅ 适用于:

  • 路由守卫 (beforeEach, beforeResolve, afterEach) 中处理 tofrom 路由信息
  • 需要访问完整、解析后的路由信息(包括 matched 记录、fullPath 等)进行逻辑判断或数据提取
  • 开发路由中间件或插件,分析和操作路由导航
// 全局前置守卫
router.beforeEach((to: RouteLocationNormalized, from: RouteLocationNormalized,next:NavigationGuardNext) => {
  // 权限检查
  if (to.meta?.requiresAuth && !isAuthenticated()) {
    return { name: 'Login', query: { redirect: to.fullPath }, replace: true };
  }

  // 页面标题设置
  if (to.meta?.title && typeof to.meta.title === 'string') {
    document.title = to.meta.title;
  }
  next();
});

// 路由解析守卫
router.beforeResolve(async (to: RouteLocationNormalized) => {
  // 在导航被确认之前,同时在所有组件内守卫和异步路由组件被解析之后调用
  // 适合执行一些最终的检查或数据预取
  console.log('即将导航到 (已解析异步组件):', to.fullPath);
  if (to.meta.needsData) {
    // await fetchDataForRoute(to);
  }
});

3. 使用 RouteLocationNormalizedLoaded 的场景

✅ 适用于:

  • 在 Vue 组件内部通过 useRoute() (Composition API) 或 this.$route (Options API) 获取当前激活的路由信息
  • 当需要响应式地访问当前路由的 params, query, meta, path 等,并确保这些信息是导航完成后的最终状态
  • 组件内依赖路由数据进行渲染、计算属性或侦听变化
// 组合式 API
import { defineComponent, computed, watch } from 'vue';
import { useRoute, useRouter, type RouteLocationNormalizedLoaded } from 'vue-router';

export default defineComponent({
  setup() {
    const route: RouteLocationNormalizedLoaded = useRoute(); // 关键!
    const router = useRouter();

    // 响应式计算属性
    const userId = computed(() => route.params.id as string);
    const currentTab = computed(() => (route.query.tab as string) || 'info');

    // 监听路由参数变化
    watch(() => route.params.id, (newId, oldId) => {
      if (newId && newId !== oldId) {
        // loadUserData(newId);
      }
    });
    
    // 监听整个路由对象变化 (更通用,但需注意性能)
    // watch(route, (newRoute, oldRoute) => { ... });

    return { userId, currentTab };
  }
});

// 选项式 API
export default {
  computed: {
    userId(): string {
      // this.$route 的类型是 RouteLocationNormalizedLoaded
      return this.$route.params.id as string;
    },
    currentTab(): string {
      return (this.$route.query.tab as string) || 'info';
    }
  },
  watch: {
    '$route.params.id': {
      handler(newId: string, oldId: string) {
        if (newId && newId !== oldId) {
          // this.loadUserData(newId);
        }
      },
      immediate: true // 可选,组件创建时立即执行
    }
  }
};

4. 使用 RouteLocationRaw 的场景

✅ 适用于:

  • 编程式导航 (router.push(), router.replace()) 的参数
  • 在代码中动态构建导航链接或目标
  • 定义 <router-link>to prop (虽然通常直接写对象或字符串,但其内部也接受 RouteLocationRaw)
// 编程式导航函数
const navigateToUser = (userId: string, tab?: string) => {
  const destination: RouteLocationRaw = {
    name: 'User', // 推荐使用 name 进行导航
    params: { id: userId },
    query: tab ? { tab } : undefined // query 可以是 undefined
  };
  router.push(destination);
};

// 条件导航
const handleNavigation = (pathOrName: string, replace = false) => {
  const destination: RouteLocationRaw = pathOrName.startsWith('/') ? pathOrName : { name: pathOrName };

  if (replace) {
    router.replace(destination);
  } else {
    router.push(destination);
  }
};

// 复杂路由构建
const buildProductRoute = (categoryId: string, productId: string): RouteLocationRaw => {
  return {
    name: 'ProductDetail',
    params: { categoryId, productId },
    query: { from: 'search' },
    hash: '#reviews'
  };
};
// router.push(buildProductRoute('electronics', 'tv-123'));

关键区别说明

1. RouteRecordNormalized vs RouteLocationNormalized

  • RouteRecordNormalized: 代表一个路由配置记录的标准化形式。它是关于路由“应该如何”匹配和渲染的定义。它包含 path (通常带参数占位符,如 /user/:id)、componentsmetachildren 等配置信息。
  • RouteLocationNormalized: 代表一个实际发生的、具体化的路由位置。它是关于“当前在哪里”或“要去哪里”的信息。它包含解析后的 path (如 /user/123)、具体的 params (如 { id: '123' })、queryhash,以及一个 matched 数组(其中包含与当前位置匹配的 RouteRecordNormalized 对象)。
// RouteRecordNormalized - 路由配置记录 (定义)
const userRouteRecord: RouteRecordNormalized = {
  path: '/user/:id',        // 路径模板
  name: 'User',
  components: { default: UserComponent }, // 或单个组件
  children: [], // 子路由记录 (RouteRecordNormalized[])
  meta: { requiresAuth: true },
  // ... 其他配置属性如 props, beforeEnter 等
  leaveGuards: new Set(),
  updateGuards: new Set(),
  enterCallbacks: {},
  instances: {}, // 运行时组件实例
  aliasOf: undefined,
  pathToRegexpOptions: undefined, // 路径匹配选项
  redirect: undefined
};

// RouteLocationNormalized - 当前或目标路由位置信息 (实例)
const userRouteLocation: RouteLocationNormalized = {
  path: '/user/123',        // 解析后的实际路径
  fullPath: '/user/123?tab=profile#info',
  name: 'User',
  params: { id: '123' },    // 解析后的参数
  query: { tab: 'profile' },
  hash: '#info',
  meta: { requiresAuth: true }, // 从 matched 记录中合并而来
  matched: [/* ..., userRouteRecord */], // 包含匹配的 RouteRecordNormalized
  redirectedFrom: undefined,
};

2. RouteLocationNormalized vs RouteLocationNormalizedLoaded

  • RouteLocationNormalized: 这是在路由守卫 (beforeEach, beforeResolve, afterEach) 中 tofrom 参数的类型。在这个阶段,异步组件可能尚未加载完成,导航也可能还在进行中或即将被取消/重定向。
  • RouteLocationNormalizedLoaded: 这是在组件内部通过 useRoute()this.$route 获取的当前路由对象类型。它保证了导航已经完成,所有相关的异步组件和路由层面的 beforeResolve 守卫已经执行完毕。它是 RouteLocationNormalized 的一个“稳定”版本,可以安全地用于组件渲染和响应式数据。
// 在路由守卫中 - 可能还在加载中,导航可能未最终确认
router.beforeEach((to: RouteLocationNormalized) => {
  // 此时 to.matched 中的异步组件可能还未解析
  console.log('导航目标:', to.path);
});

// 在组件 setup 中 - 确保已加载完成
// const route: RouteLocationNormalizedLoaded = useRoute();
// 此时 route 对象代表的是一个稳定和完全解析的路由状态。

3. 实际使用中的差异 (常见误区)

// ❌ 不准确的理解或类型标注
router.beforeEach((to) => { // to 默认类型是 RouteLocationNormalized
  // to.matched[0] 不是 RouteLocationNormalized 类型!
  // const record: RouteLocationNormalized = to.matched[0]; // 类型错误,且逻辑上不通
});

// ✅ 正确的理解和类型使用
router.beforeEach((to: RouteLocationNormalized) => {
  // 'to' 是 RouteLocationNormalized,代表目标路由位置
  const location: RouteLocationNormalized = to;

  // 'to.matched' 是一个 RouteRecordNormalized 数组
  // 'to.matched[0]' (如果存在) 是一个 RouteRecordNormalized 对象,代表匹配到的路由配置记录
  if (to.matched.length > 0) {
    const record: RouteRecordNormalized = to.matched[0];
    console.log('匹配到的路由记录路径模板:', record.path); // e.g., /user/:id
    console.log('当前实际路径:', location.path);      // e.g., /user/123
  }
});

📝 快速记忆口诀

记住这个简单的规则,帮助区分核心类型:

  • 配置用 Raw (原始) - RouteRecordRaw (定义路由配置时)
  • 导航用 Raw (原始) - RouteLocationRaw (调用 router.push/replace 时)
  • 守卫用 Normalized (标准化) - RouteLocationNormalized (导航守卫中的 to/from)
  • 组件用 Loaded (已加载) - RouteLocationNormalizedLoaded (组件内 useRoute()this.$route)
  • 记录是 Record (标准化配置) - RouteRecordNormalized (标准化后的单个路由配置对象, 存在于 matched 数组中)

🔗 相关资源