前言
手写 Vue Router:从 0 到 1 构建一个生产级 Vue 路由库
第一阶段:重新理解 Vue Router 的本质
- Vue Router 不是 URL 映射表,它是“应用状态 + 浏览器历史 + 组件渲染”的桥梁
路由即状态,URL 是状态的持久化表示 - 响应式核心:
currentRoute是一个ref<string>,router-view响应其变化
基于 Vue3 的ref、watch、computed实现自动更新 - History 模式 vs Hash 模式:
/page/1vs#/page/1
本质是history.pushStatevslocation.hash的选择 - TypeScript 优先:类型安全的路由定义与参数推导
RouteRecord、RouteLocation、Router接口定义 - 模块化设计:
createRouter+createWebHistory+router-view+router-link
第二阶段:深入 History API 与路由模式
createWebHistory
- 基于原生
History API:pushState、replaceState、popstate
修改 URL 不刷新页面 pushState(state, title, url):如何添加历史记录?state可携带路由参数,url必须同源popstate事件:浏览器前进/后退时触发,如何同步currentRoute?window.addEventListener('popstate', ...)- 服务端配置:为何需要
index.htmlfallback?
避免 404,让所有路由都返回index.html - SEO 友好:
<link rel="canonical">如何避免重复内容?
createWebHashHistory
- 基于
location.hash:#后的内容不发送到服务器
兼容老浏览器,无需服务端配置 hashchange事件:监听#变化,同步currentRoutewindow.addEventListener('hashchange', ...)__vue_router__:如何避免与现有hashchange监听器冲突?
使用标记位或事件委托- URL 编码问题:
#后的?和&如何处理?
使用decodeURIComponent
createMemoryHistory(SSR 专用)
- 无浏览器环境:Node.js 中如何模拟路由?
维护一个内存中的stack数组 push、replace、go如何操作栈?stack.push(location),stack[currentIndex] = location- SSR 中
router如何初始化?
从请求的req.url创建初始location
第三阶段:掌握路由匹配与路径解析
路径匹配算法
path-to-regexp的替代:手写路径解析器
支持静态/home、动态/:id、正则/(\\d+)路径RouteRecord结构:path、component、name、children、meta
路由记录的标准化表示- 匹配优先级:最长前缀匹配 vs 注册顺序
/user/profile优先于/user/:id - 嵌套路由:
children如何生成嵌套的matched数组?router.currentRoute.matched包含所有匹配的记录 - 命名视图:
components: { default, sidebar }如何支持多router-view?
normalizeLocation 与 resolve
resolve(to):将string或Location对象标准化为RouteLocationNormalized
解析params、query、hash、nameparams填充:/user/:id+{ id: 123 }→/user/123
正则捕获组替换query序列化:{ a: 1, b: 2 }→?a=1&b=2
使用URLSearchParams
第四阶段:穿透导航流程与守卫机制
导航流程(Navigation Flow)
- 调用
router.push('/home') resolve生成目标Location- 执行
beforeEach守卫(全局前置守卫) - 匹配路由记录
- 执行
beforeEnter守卫(路由独享守卫) - 组件内
beforeRouteEnter/beforeRouteUpdate - 更新
currentRoute - 触发
router-view重新渲染 - 执行
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
router-link 组件
to属性:string或Location对象resolve(to)生成hrefactive-class:如何判断当前链接是否激活?route.path === to.path或route.matched包含toexact匹配:完全匹配 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选择器:#id或ref如何定位元素?document.querySelector(el)或ref.value.$el
第六阶段:SSR 与服务端集成
SSR 支持
- 问题:服务端渲染时,如何避免路由状态跨请求共享?
router实例必须是请求级别的 createMemoryHistory():在 Node.js 中创建内存路由
从req.url初始化- 状态同步:客户端如何接管服务端渲染的路由?
window.__ROUTE__ = serialize(route) - 脱水与注水:
router.state.value = initialState