手写 Mini Vue Router:150 行代码还原路由核心逻辑
理解一个库最好的方式就是手写一个简化版。本文将用约 150 行 TypeScript 实现一个 Mini Vue Router,涵盖路由匹配、历史管理、导航守卫、<router-view> 和 <router-link> 等核心功能。读完本文,你会对 Vue Router 的内部运行机制有清晰直觉。
目标功能
我们要实现的 Mini Router 支持:
- ✅ 路由配置与匹配(含动态参数
:id) - ✅ Hash 和 HTML5 两种历史模式
- ✅ 导航守卫(
beforeEach/afterEach) - ✅
<router-view>组件 - ✅
<router-link>组件 - ✅ 编程式导航(
push/replace/back)
第一步:类型定义
import { ref, reactive, computed, h, defineComponent, inject, provide } from 'vue'
// 路由配置
type RouteRecord = {
path: string
component: any
children?: RouteRecord[]
beforeEnter?: (to: RouteLocation, from: RouteLocation) => any
}
// 当前路由状态
type RouteLocation = {
path: string
params: Record<string, string>
query: Record<string, string>
matched: RouteRecord[]
}
// 路由匹配结果
type Matcher = {
re: RegExp
keys: string[]
record: RouteRecord
parent: Matcher | null
score: number
}
第二步:路由匹配器
将路径模式编译为正则表达式,并构建匹配树:
function compilePath(path: string): { re: RegExp; keys: string[]; score: number } {
const keys: string[] = []
let score = 0
// /users/:id/posts/:postId → /users/([^/]+)/posts/([^/]+)
const pattern = path.replace(/:([^/]+)/g, (_, key) => {
keys.push(key)
score += 4 // 动态参数分值 4
return '([^/]+'
})
// 静态段分值
const staticSegments = path.split('/').filter(s => s && !s.startsWith(':'))
score += staticSegments.length * 10
// 编译正则
const re = new RegExp(`^${pattern}/?$`, 'i')
return { re, keys, score }
}
function createMatcher(routes: RouteRecord[]): Matcher[] {
const matchers: Matcher[] = []
function addRoute(route: RouteRecord, parent: Matcher | null) {
const fullPath = parent
? parent.record.path + route.path
: route.path
const { re, keys, score } = compilePath(fullPath)
const matcher: Matcher = {
re,
keys,
record: route,
parent,
score: parent ? parent.score + score : score
}
matchers.push(matcher)
// 递归处理子路由
if (route.children) {
route.children.forEach(child => addRoute(child, matcher))
}
}
routes.forEach(route => addRoute(route, null))
// 按分值降序排列
matchers.sort((a, b) => b.score - a.score)
return matchers
}
function matchRoute(
path: string,
matchers: Matcher[]
): { matched: RouteRecord[]; params: Record<string, string> } | null {
for (const matcher of matchers) {
const result = matcher.re.exec(path)
if (result) {
// 提取参数
const params: Record<string, string> = {}
matcher.keys.forEach((key, i) => {
params[key] = decodeURIComponent(result[i + 1])
})
// 构建 matched 数组(含所有父级)
const matched: RouteRecord[] = []
let m: Matcher | null = matcher
while (m) {
matched.unshift(m.record)
m = m.parent
}
return { matched, params }
}
}
return null
}
第三步:History 管理器
抽象出通用的 History 接口,Hash 和 HTML5 模式各自实现:
type Listener = (to: RouteLocation, from: RouteLocation) => void
abstract class RouterHistory {
listeners: Listener[] = []
current = ref<RouteLocation>({
path: '/',
params: {},
query: {},
matched: []
})
abstract push(path: string): void
abstract replace(path: string): void
abstract destroy(): void
listen(listener: Listener) {
this.listeners.push(listener)
}
notify(to: RouteLocation, from: RouteLocation) {
this.listeners.forEach(l => l(to, from))
this.current.value = to
}
}
class HashHistory extends RouterHistory {
constructor() {
super()
window.addEventListener('hashchange', this.onHashChange)
// 初始化
if (!window.location.hash) {
window.location.hash = '#/'
}
}
private onHashChange = () => {
const path = window.location.hash.slice(1) || '/'
// 过渡到新路由(由 Router 处理守卫)
this.transitionTo(path)
}
transitionTo(path: string) {
// 由 Router 调用,实际触发导航
// 这里简化处理:直接通知
}
push(path: string) {
window.location.hash = '#' + path
}
replace(path: string) {
const url = window.location.href.split('#')[0] + '#' + path
window.location.replace(url)
}
destroy() {
window.removeEventListener('hashchange', this.onHashChange)
}
}
class HTML5History extends RouterHistory {
constructor() {
super()
window.addEventListener('popstate', this.onPopState)
}
private onPopState = (e: PopStateEvent) => {
this.transitionTo(window.location.pathname + window.location.search)
}
transitionTo(path: string) {}
push(path: string) {
window.history.pushState({ path }, '', path)
this.transitionTo(path)
}
replace(path: string) {
window.history.replaceState({ path }, '', path)
this.transitionTo(path)
}
destroy() {
window.removeEventListener('popstate', this.onPopState)
}
}
第四步:Router 核心
整合匹配器、历史管理器和导航守卫:
type NavigationGuard = (to: RouteLocation, from: RouteLocation) => boolean | string | void | Promise<boolean | string | void>
class MiniRouter {
matchers: Matcher[]
history: RouterHistory
beforeEachGuards: NavigationGuard[] = []
afterEachHooks: ((to: RouteLocation, from: RouteLocation) => void)[] = []
constructor(routes: RouteRecord[], mode: 'hash' | 'html5' = 'hash') {
this.matchers = createMatcher(routes)
this.history = mode === 'hash'
? new HashHistory()
: new HTML5History()
// 初始导航
this.history.transitionTo = (path: string) => this.transitionTo(path)
// 初始化当前路由
const initialPath = mode === 'hash'
? window.location.hash.slice(1) || '/'
: window.location.pathname
this.transitionTo(initialPath)
}
private async transitionTo(path: string) {
const from = this.history.current.value
// 解析路径(分离 query)
const [purePath, queryString] = path.split('?')
const query: Record<string, string> = {}
if (queryString) {
new URLSearchParams(queryString).forEach((v, k) => query[k] = v)
}
// 匹配路由
const match = matchRoute(purePath, this.matchers)
if (!match) {
console.warn(`[MiniRouter] No route matched: ${purePath}`)
return
}
const to: RouteLocation = {
path: purePath,
params: match.params,
query,
matched: match.matched
}
// 执行前置守卫
for (const guard of this.beforeEachGuards) {
const result = await guard(to, from)
if (result === false) return // 取消导航
if (typeof result === 'string') { // 重定向
this.push(result)
return
}
}
// 执行路由独享守卫
for (const record of to.matched) {
if (record.beforeEnter) {
const result = await record.beforeEnter(to, from)
if (result === false) return
if (typeof result === 'string') {
this.push(result)
return
}
}
}
// 更新当前路由
this.history.notify(to, from)
// 执行后置钩子
this.afterEachHooks.forEach(hook => hook(to, from))
}
// API
push(path: string) {
this.history.push(path)
if (this.history instanceof HTML5History) {
this.transitionTo(path)
}
}
replace(path: string) {
this.history.replace(path)
}
back() {
window.history.back()
}
beforeEach(guard: NavigationGuard) {
this.beforeEachGuards.push(guard)
return () => {
const i = this.beforeEachGuards.indexOf(guard)
if (i > -1) this.beforeEachGuards.splice(i, 1)
}
}
afterEach(hook: (to: RouteLocation, from: RouteLocation) => void) {
this.afterEachHooks.push(hook)
}
install(app: any) {
app.provide('router', this)
app.provide('route', this.history.current)
app.component('RouterView', RouterView)
app.component('RouterLink', RouterLink)
}
}
第五步:RouterView 与 RouterLink 组件
const RouterView = defineComponent({
name: 'RouterView',
setup() {
const router = inject<MiniRouter>('router')!
const route = inject<ReturnType<typeof ref<RouteLocation>>>('route')!
// 深度索引,用于嵌套路由
const depth = inject('routerViewDepth', 0)
provide('routerViewDepth', depth + 1)
// 根据当前匹配层级渲染对应组件
const component = computed(() => {
const record = route.value.matched[depth]
return record?.component
})
return () => {
const comp = component.value
if (!comp) return null
return h(comp, {
key: route.value.path // 路径变化时强制更新
})
}
}
})
const RouterLink = defineComponent({
name: 'RouterLink',
props: {
to: { type: [String, Object], required: true }
},
setup(props, { slots }) {
const router = inject<MiniRouter>('router')!
const href = computed(() => {
if (typeof props.to === 'string') return props.to
let path = props.to.path || ''
if (props.to.params) {
Object.entries(props.to.params).forEach(([k, v]) => {
path = path.replace(`:${k}`, v as string)
})
}
if (props.to.query) {
const qs = new URLSearchParams(props.to.query).toString()
path += `?${qs}`
}
return path
})
function navigate(e: Event) {
e.preventDefault()
router.push(href.value)
}
return () => {
return h('a', {
href: '#' + href.value, // hash 模式
onClick: navigate
}, slots.default?.())
}
}
})
第六步:组合成 createRouter API
对外暴露与 Vue Router 4 一致的 API:
function createRouter(options: {
routes: RouteRecord[]
history?: 'hash' | 'html5'
}): MiniRouter {
const router = new MiniRouter(options.routes, options.history)
return router
}
// 导出
export { createRouter, RouterView, RouterLink, useRouter, useRoute }
export type { RouteRecord, RouteLocation }
function useRouter() {
return inject<MiniRouter>('router')!
}
function useRoute() {
return inject<ReturnType<typeof ref<RouteLocation>>>('route')!
}
第七步:实际使用
import { createApp, createRouter } from './mini-router'
const router = createRouter({
routes: [
{
path: '/',
component: {
template: '<h1>首页</h1><RouterView />'
},
children: [
{ path: 'users', component: { template: '<h2>用户列表</h2>' } },
{ path: 'users/:id', component: { template: '<h2>用户详情:{{ $route.params.id }}</h2>' } }
]
}
],
history: 'hash'
})
// 导航守卫
router.beforeEach((to, from) => {
console.log('导航:', from.path, '→', to.path)
if (to.path.startsWith('/admin') && !isLoggedIn()) {
return '/login'
}
})
router.afterEach((to) => {
document.title = `App - ${to.path}`
})
const app = createApp({
template: '<RouterView />'
})
app.use(router)
app.mount('#app')
与真实 Vue Router 的区别
| 功能 | Mini Router | Vue Router 4 |
|---|---|---|
| 路由匹配 | ✅ 基础 :param |
✅ + 自定义正则、可选参数、重复参数 |
| 路由评分 | ✅ 简化版 | ✅ 更精细的评分系统 |
| 嵌套路由 | ✅ | ✅ + children 配置继承 |
| 导航守卫 | ✅ beforeEach/afterEach | ✅ + beforeResolve、组件内守卫 |
| Composition API | ✅ useRouter/useRoute | ✅ + useLink、onBeforeRouteLeave |
| 动态路由 | ❌ | ✅ addRoute/removeRoute |
| 过渡动画 | ❌ | ✅ 编程式 transition |
| 错误处理 | ❌ | ✅ NavigationFailureType |
总结
150 行代码,我们实现了一个能用的 Mini Vue Router。核心就是三件事:
- 编译匹配:把路径模式编译成正则 → 按分值排序 → 逐个匹配
- 历史管理:封装
hashchange/popstate→ 统一 push/replace 接口 - 守卫管线:洋葱式执行 beforeEach → 路由守卫 → 组件守卫 → afterEach
理解了这个骨架后,再看 Vue Router 4 的源码,你会发现核心逻辑就是在这个基础上增加了更多的边界处理、类型推导和功能扩展。
Comments | 0条评论