Skip to content

前言

手写 Vue Router:从 0 到 1 构建一个生产级 Vue 路由库

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

  • Vue Router 不是 URL 映射表,它是“应用状态 + 浏览器历史 + 组件渲染”的桥梁
    路由即状态,URL 是状态的持久化表示
  • 响应式核心:currentRoute 是一个 ref<string>router-view 响应其变化
    基于 Vue3 的 refwatchcomputed 实现自动更新
  • History 模式 vs Hash 模式:/page/1 vs #/page/1
    本质是 history.pushState vs location.hash 的选择
  • TypeScript 优先:类型安全的路由定义与参数推导
    RouteRecordRouteLocationRouter 接口定义
  • 模块化设计:createRouter + createWebHistory + router-view + router-link

第二阶段:深入 History API 与路由模式

createWebHistory

  • 基于原生 History APIpushStatereplaceStatepopstate
    修改 URL 不刷新页面
  • pushState(state, title, url):如何添加历史记录?
    state 可携带路由参数,url 必须同源
  • popstate 事件:浏览器前进/后退时触发,如何同步 currentRoute
    window.addEventListener('popstate', ...)
  • 服务端配置:为何需要 index.html fallback?
    避免 404,让所有路由都返回 index.html
  • SEO 友好:<link rel="canonical"> 如何避免重复内容?

createWebHashHistory

  • 基于 location.hash# 后的内容不发送到服务器
    兼容老浏览器,无需服务端配置
  • hashchange 事件:监听 # 变化,同步 currentRoute
    window.addEventListener('hashchange', ...)
  • __vue_router__:如何避免与现有 hashchange 监听器冲突?
    使用标记位或事件委托
  • URL 编码问题:# 后的 ?& 如何处理?
    使用 decodeURIComponent

createMemoryHistory(SSR 专用)

  • 无浏览器环境:Node.js 中如何模拟路由?
    维护一个内存中的 stack 数组
  • pushreplacego 如何操作栈?
    stack.push(location)stack[currentIndex] = location
  • SSR 中 router 如何初始化?
    从请求的 req.url 创建初始 location

第三阶段:掌握路由匹配与路径解析

路径匹配算法

  • path-to-regexp 的替代:手写路径解析器
    支持静态 /home、动态 /:id、正则 /(\\d+) 路径
  • RouteRecord 结构:pathcomponentnamechildrenmeta
    路由记录的标准化表示
  • 匹配优先级:最长前缀匹配 vs 注册顺序
    /user/profile 优先于 /user/:id
  • 嵌套路由:children 如何生成嵌套的 matched 数组?
    router.currentRoute.matched 包含所有匹配的记录
  • 命名视图:components: { default, sidebar } 如何支持多 router-view

normalizeLocationresolve

  • resolve(to):将 stringLocation 对象标准化为 RouteLocationNormalized
    解析 paramsqueryhashname
  • params 填充:/user/:id + { id: 123 }/user/123
    正则捕获组替换
  • query 序列化:{ a: 1, b: 2 }?a=1&b=2
    使用 URLSearchParams

第四阶段:穿透导航流程与守卫机制

导航流程(Navigation Flow)

  1. 调用 router.push('/home')
  2. resolve 生成目标 Location
  3. 执行 beforeEach 守卫(全局前置守卫)
  4. 匹配路由记录
  5. 执行 beforeEnter 守卫(路由独享守卫)
  6. 组件内 beforeRouteEnter / beforeRouteUpdate
  7. 更新 currentRoute
  8. 触发 router-view 重新渲染
  9. 执行 afterEach 钩子

导航守卫(Guards)

  • beforeEach(to, from, next):全局前置守卫
    next() 继续,next(false) 中断,next('/login') 重定向
  • 异步守卫:如何支持 async 函数?
    使用 Promise 链或 runQueue
  • beforeResolve:在导航确认前执行
    用于权限检查、数据预取
  • afterEach(to, from):全局后置钩子,无 next
    用于分析、修改页面标题
  • 组件内守卫:beforeRouteEnter(无法访问 this)、beforeRouteUpdate

runQueue 与流程控制

  • runQueue(guards, iterator, callback):串行执行守卫函数
    iterator(guard, done) 调用守卫,done 回调推进流程
  • 如何实现“任意守卫中断则停止后续执行”?
    done(false) 或抛错
  • 错误处理:router.onError 如何捕获导航错误?

第五阶段:生产级功能实现

router-view 组件

  • 如何根据 matched 数组递归渲染组件?
    h(route.component) + slot 传递 Component
  • name 属性:支持命名视图
    <router-view name="sidebar" />
  • route 注入:如何让子组件访问 $route
    provide('route', currentRoute)
  • 缓存与激活:<keep-alive> 如何与 router-view 协同?
    include 匹配 meta.keepAlive
  • to 属性:stringLocation 对象
    resolve(to) 生成 href
  • active-class:如何判断当前链接是否激活?
    route.path === to.pathroute.matched 包含 to
  • exact 匹配:完全匹配 vs 前缀匹配
  • @click 拦截:阻止默认跳转,调用 router.push
    支持 Ctrl+Click 新标签页打开

懒加载(Lazy Loading)

  • component: () => import('./Home.vue')
    返回 Promise<Component>
  • 代码分割:Webpack/Vite 如何生成独立 chunk?
    import() 语法触发分割
  • 加载状态:如何显示 loading 组件?
    defineAsyncComponent + loadingComponent
  • 错误处理:chunk 加载失败如何降级?
    errorComponent + 重试机制

scrollBehavior

  • scrollBehavior(to, from, savedPosition):控制滚动位置
    返回 { x, y }{ el, behavior }
  • savedPosition:浏览器前进/后退时的恢复位置
    popstate 导航时可用
  • el 选择器:#idref 如何定位元素?
    document.querySelector(el)ref.value.$el

第六阶段:SSR 与服务端集成

SSR 支持

  • 问题:服务端渲染时,如何避免路由状态跨请求共享?
    router 实例必须是请求级别的
  • createMemoryHistory():在 Node.js 中创建内存路由
    req.url 初始化
  • 状态同步:客户端如何接管服务端渲染的路由?
    window.__ROUTE__ = serialize(route)
  • 脱水与注水:router.state.value = initialState