Skip to content

第一章:重新理解 Vue Router 的本质

在 Vue 生态系统中,Vue Router 是官方的路由管理器,它深度集成了 Vue 的响应式系统。然而,很多开发者只是停留在如何使用 Vue Router 的层面,很少深入理解其设计思想和实现原理。

在本章中,我们将重新审视 Vue Router 的本质,理解它为什么不仅仅是 URL 映射表,而是"应用状态 + 浏览器历史 + 组件渲染"的桥梁。

Vue Router 不是 URL 映射表,它是"应用状态 + 浏览器历史 + 组件渲染"的桥梁

传统的路由概念通常被理解为 URL 到页面的映射关系,但在 Vue Router 中,路由的概念更加丰富和深入。Vue Router 将路由视为应用状态的一部分,URL 只是这种状态的持久化表示:

javascript
// 传统路由概念
// URL -> 页面文件
// /home -> home.html
// /about -> about.html

// Vue Router 概念
// URL -> 应用状态 -> 组件渲染
// /home -> { path: '/home', name: 'Home', params: {} } -> <HomeComponent />
// /user/123 -> { path: '/user/123', name: 'User', params: { id: '123' } } -> <UserComponent :id="123" />

这种设计使得路由成为了应用状态管理的一部分,URL 的变化会直接反映为应用状态的变化,进而驱动组件的重新渲染。

响应式核心:currentRoute 是一个 ref<string>router-view 响应其变化

Vue Router 的核心是响应式系统。它使用 Vue 3 的响应式 API 来管理当前路由状态:

javascript
// 简化的 Vue Router 响应式实现
import { ref, computed, watch } from 'vue'

// 当前路由作为响应式引用
const currentRoute = ref({
  path: '/',
  name: null,
  params: {},
  query: {},
  hash: '',
  meta: {}
})

// 路由匹配结果
const matchedRoute = computed(() => {
  return matchRoute(currentRoute.value.path)
})

// 监听路由变化
watch(currentRoute, (newRoute, oldRoute) => {
  // 执行导航守卫
  // 触发组件更新
  console.log('Route changed:', newRoute)
})

// 路由跳转函数
function push(location) {
  const resolved = resolveLocation(location)
  currentRoute.value = resolved
}

// 在组件中使用
export default {
  setup() {
    // currentRoute 是响应式的,变化时会触发组件更新
    return { currentRoute }
  }
}

这种响应式设计使得 router-view 组件能够自动响应路由变化并重新渲染对应的组件。

History 模式 vs Hash 模式:/page/1 vs #/page/1

Vue Router 提供了两种主要的路由模式,它们在实现上有本质的区别:

javascript
// HTML5 History 模式
// URL: https://example.com/page/1
// 使用 History API 实现
class HTML5History {
  constructor(router) {
    this.router = router
    // 监听浏览器前进后退
    window.addEventListener('popstate', this.onPopState.bind(this))
  }
  
  push(location) {
    // 使用 pushState 修改 URL 而不刷新页面
    history.pushState({}, '', location)
    this.transitionTo(location)
  }
  
  onPopState(e) {
    // 处理浏览器前进后退
    this.transitionTo(location)
  }
}

// Hash 模式
// URL: https://example.com/#/page/1
// 使用 location.hash 实现
class HashHistory {
  constructor(router) {
    this.router = router
    // 监听 hash 变化
    window.addEventListener('hashchange', this.onHashChange.bind(this))
  }
  
  push(location) {
    // 修改 hash 值
    window.location.hash = location
    // transitionTo 会在 onHashChange 中触发
  }
  
  onHashChange() {
    const path = getHash()
    this.transitionTo(path)
  }
}

两种模式的主要区别:

  1. History 模式需要服务器配置支持,提供更干净的 URL
  2. Hash 模式兼容性更好,无需服务器配置,但 URL 中带有 #

TypeScript 优先:类型安全的路由定义与参数推导

Vue Router 3.0 开始就提供了完整的 TypeScript 支持,Vue Router 4 更是完全用 TypeScript 重写:

typescript
// 路由记录类型定义
interface RouteRecord {
  path: string
  name?: string | symbol
  component?: Component
  components?: Record<string, Component>
  redirect?: RouteRecordRedirectOption
  children?: RouteRecordRaw[]
  meta?: RouteMeta
  beforeEnter?: NavigationGuard | NavigationGuard[]
  props?: boolean | Record<string, any> | RoutePropsFunction
}

// 路由位置类型定义
interface RouteLocation {
  name: string | symbol | null
  path: string
  hash: string
  query: Record<string, string | string[]>
  params: Record<string, string | string[]>
  meta: RouteMeta
  matched: RouteRecord[]
}

// 路由器接口定义
interface Router {
  // 当前路由(响应式)
  readonly currentRoute: Ref<RouteLocationNormalized>
  
  // 导航方法
  push(to: RouteLocationRaw): Promise<NavigationFailure | void | undefined>
  replace(to: RouteLocationRaw): Promise<NavigationFailure | void | undefined>
  go(delta: number): void
  back(): void
  forward(): void
  
  // 路由匹配
  resolve(to: RouteLocationRaw, currentLocation?: RouteLocationNormalized): RouteLocation & { href: string }
  
  // 导航守卫
  beforeEach(guard: NavigationGuardWithThis<undefined>): () => void
  beforeResolve(guard: NavigationGuardWithThis<undefined>): () => void
  afterEach(guard: NavigationHookAfter): () => void
}

这种类型定义确保了在开发过程中能够获得完整的类型检查和智能提示。

Vue Router 4 采用了更加模块化的设计,各个功能模块可以独立使用:

javascript
// 路由创建
import { createRouter, createWebHistory } from 'vue-router'

const router = createRouter({
  history: createWebHistory(),
  routes: [
    { path: '/', component: Home },
    { path: '/about', component: About }
  ]
})

// 在应用中使用
import { createApp } from 'vue'
import App from './App.vue'

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

主要模块包括:

  1. createRouter - 路由器创建函数
  2. createWebHistory - History 模式历史管理器
  3. createWebHashHistory - Hash 模式历史管理器
  4. createMemoryHistory - 内存历史管理器(用于 SSR)
  5. router-view - 路由出口组件
  6. router-link - 路由链接组件

Vue Router 与 Vuex/Pinia 的关系

Vue Router 与状态管理库(Vuex/ Pinia)有着密切的关系,但它们解决的是不同层面的问题:

javascript
// Vue Router 管理路由状态
const currentRoute = router.currentRoute.value
console.log(currentRoute.path) // /user/123
console.log(currentRoute.params) // { id: '123' }

// Vuex/Pinia 管理应用状态
const userStore = useUserStore()
console.log(userStore.currentUser) // { id: 123, name: 'John' }

// 两者可以协同工作
router.beforeEach(async (to, from) => {
  // 根据路由参数加载用户数据
  if (to.params.id) {
    await userStore.fetchUser(to.params.id)
  }
})

小结

在本章中,我们深入探讨了 Vue Router 的本质和设计哲学:

  1. Vue Router 不仅仅是 URL 映射表,而是应用状态、浏览器历史和组件渲染的桥梁
  2. 它基于 Vue 3 的响应式系统,currentRoute 是一个响应式引用
  3. 提供多种路由模式(History、Hash、Memory)以适应不同场景
  4. 完整的 TypeScript 支持确保类型安全
  5. 模块化设计使得各部分功能可以独立使用

在下一章中,我们将深入探讨 History API 与路由模式的实现细节,包括 createWebHistorycreateWebHashHistory 的底层差异。


思考题

  1. 你在项目中使用 Vue Router 时,有没有遇到过路由状态与应用状态不一致的情况?是如何解决的?
  2. 对于 History 模式和 Hash 模式,你通常如何选择?考虑了哪些因素?