从零设计一个Vue路由系统:揭秘SPA导航的核心原理
想深入理解Vue路由?自己动手实现一个!本文将带你从零设计完整的路由系统,彻底掌握前端路由的核心原理。
前言:为什么需要前端路由?
在传统的多页面应用中,每次页面跳转都需要向服务器请求新页面,用户体验存在明显的中断感。而现代单页面应用(SPA)使用前端路由,实现了无刷新页面切换,大大提升了用户体验。
今天,我们就来亲手实现一个完整的Vue路由系统,深入理解其工作原理!
一、路由系统核心概念
1.1 路由系统三大核心
- 路由器(Router):管理所有路由规则和状态
- 路由表(Routes):定义路径与组件的映射关系
- 路由视图(RouterView):动态渲染匹配的组件
1.2 两种路由模式
Hash模式:使用URL的hash部分(#后的内容)
示例:http://example.com/#/home
优点:兼容性好,无需服务器配置
History模式:使用HTML5 History API
示例:http://example.com/home
优点:URL更美观,更符合传统URL习惯
二、路由系统架构设计
2.1 系统架构图
graph TD
A[URL变化] --> B{路由模式}
B -->|Hash模式| C[监听hashchange事件]
B -->|History模式| D[监听popstate事件]
C --> E[解析当前路径]
D --> E
E --> F[匹配路由规则]
F --> G[执行导航守卫]
G --> H[更新路由状态]
H --> I[渲染对应组件]
I --> J[RouterView更新]
2.2 核心类设计
class VueRouter {
constructor(options) {
this.mode = options.mode || 'hash'
this.routes = options.routes || []
this.current = { path: '/', matched: [] }
this.routeMap = this.createRouteMap()
this.init()
}
}
三、完整实现步骤
3.1 创建路由映射表
class VueRouter {
constructor(options) {
this.options = options
this.routeMap = {}
this.current = {
path: '/',
query: {},
params: {},
fullPath: '/',
matched: []
}
// 创建路由映射表
this.createRouteMap(options.routes || [])
// 初始化路由
this.init()
}
createRouteMap(routes, parentPath = '') {
routes.forEach(route => {
const record = {
path: parentPath + route.path,
component: route.component,
parent: parentPath,
meta: route.meta || {}
}
// 存储路由记录
const normalizedPath = this.normalizePath(record.path)
this.routeMap[normalizedPath] = record
// 递归处理嵌套路由
if (route.children) {
this.createRouteMap(route.children, record.path + '/')
}
})
}
normalizePath(path) {
// 处理路径格式:确保以/开头,不以/结尾(除了根路径)
let normalized = path.replace(/\/+$/, '') || '/'
if (!normalized.startsWith('/')) {
normalized = '/' + normalized
}
return normalized
}
}
3.2 实现路由模式
class VueRouter {
init() {
if (this.options.mode === 'history') {
this.initHistoryMode()
} else {
this.initHashMode()
}
}
initHashMode() {
// 确保hash以/#/开头
if (!location.hash) {
location.hash = '/'
}
// 初始加载
window.addEventListener('load', () => {
this.transitionTo(this.getHash())
})
// 监听hash变化
window.addEventListener('hashchange', () => {
this.transitionTo(this.getHash())
})
}
initHistoryMode() {
// 初始加载
window.addEventListener('load', () => {
this.transitionTo(this.getPath())
})
// 监听popstate事件(浏览器前进后退)
window.addEventListener('popstate', () => {
this.transitionTo(this.getPath())
})
}
getHash() {
const hash = location.hash.slice(1)
return hash || '/'
}
getPath() {
const path = location.pathname + location.search
return path || '/'
}
}
3.3 实现路由匹配算法
class VueRouter {
match(path) {
const matched = []
const params = {}
// 查找匹配的路由记录
let routeRecord = this.findRouteRecord(path)
// 收集所有匹配的路由记录(包括父路由)
while (routeRecord) {
matched.unshift(routeRecord)
routeRecord = this.routeMap[routeRecord.parent] || null
}
// 解析路径参数(动态路由)
if (path.includes(':')) {
this.extractParams(path, matched[matched.length - 1], params)
}
// 解析查询参数
const query = this.extractQuery(path)
return {
path: this.normalizePath(path.split('?')[0]),
fullPath: path,
matched,
params,
query
}
}
findRouteRecord(path) {
const pathWithoutQuery = path.split('?')[0]
const normalizedPath = this.normalizePath(pathWithoutQuery)
// 精确匹配
if (this.routeMap[normalizedPath]) {
return this.routeMap[normalizedPath]
}
// 动态路由匹配(如 /user/:id)
for (const routePath in this.routeMap) {
if (this.isDynamicRoute(routePath)) {
const pattern = this.pathToRegexp(routePath)
if (pattern.test(normalizedPath)) {
return this.routeMap[routePath]
}
}
}
return null
}
isDynamicRoute(path) {
return path.includes(':')
}
pathToRegexp(path) {
// 将路径模式转换为正则表达式
const keys = []
const pattern = path
.replace(/\/:(\w+)/g, (_, key) => {
keys.push(key)
return '/([^/]+)'
})
.replace(/\//g, '\\/')
return new RegExp(`^${pattern}$`)
}
extractParams(path, routeRecord, params) {
const pathParts = path.split('/')
const routeParts = routeRecord.path.split('/')
routeParts.forEach((part, index) => {
if (part.startsWith(':')) {
const key = part.slice(1)
params[key] = pathParts[index] || ''
}
})
}
extractQuery(path) {
const query = {}
const queryString = path.split('?')[1]
if (queryString) {
queryString.split('&').forEach(pair => {
const [key, value] = pair.split('=')
if (key) {
query[decodeURIComponent(key)] = decodeURIComponent(value || '')
}
})
}
return query
}
}
3.4 实现路由导航
class VueRouter {
transitionTo(path, onComplete) {
const route = this.match(path)
// 导航守卫(简化版)
const guards = this.runQueue(this.beforeHooks, route)
guards.then(() => {
// 更新当前路由
this.current = route
// 触发路由变化
this.cb && this.cb(route)
// 更新URL
this.ensureURL()
// 完成回调
onComplete && onComplete()
}).catch(() => {
// 导航取消
console.log('Navigation cancelled')
})
}
push(location) {
if (this.options.mode === 'history') {
window.history.pushState({}, '', location)
this.transitionTo(location)
} else {
window.location.hash = location
}
}
replace(location) {
if (this.options.mode === 'history') {
window.history.replaceState({}, '', location)
this.transitionTo(location)
} else {
const hash = location.startsWith('#') ? location : '#' + location
window.location.replace(
window.location.pathname + window.location.search + hash
)
}
}
go(n) {
window.history.go(n)
}
back() {
this.go(-1)
}
forward() {
this.go(1)
}
ensureURL() {
if (this.options.mode === 'history') {
if (window.location.pathname !== this.current.path) {
window.history.replaceState({}, '', this.current.fullPath)
}
} else {
const currentHash = '#' + this.current.path
if (window.location.hash !== currentHash) {
window.location.replace(
window.location.pathname + window.location.search + currentHash
)
}
}
}
}
3.5 实现RouterView组件
// RouterView组件实现
const RouterView = {
name: 'RouterView',
functional: true,
render(_, { props, children, parent, data }) {
// 标记为路由组件
data.routerView = true
// 获取当前路由匹配的组件
const route = parent.$route
const matchedComponents = route.matched.map(record => record.component)
// 计算当前渲染深度(处理嵌套路由)
let depth = 0
let parentNode = parent
while (parentNode && parentNode !== parent.$root) {
if (parentNode.$vnode && parentNode.$vnode.data.routerView) {
depth++
}
parentNode = parentNode.$parent
}
// 获取对应层级的组件
const component = matchedComponents[depth]
if (!component) {
return children || []
}
// 渲染组件
return createElement(component, data)
}
}
3.6 实现RouterLink组件
// RouterLink组件实现
const RouterLink = {
name: 'RouterLink',
props: {
to: {
type: [String, Object],
required: true
},
tag: {
type: String,
default: 'a'
},
exact: Boolean,
activeClass: {
type: String,
default: 'router-link-active'
},
exactActiveClass: {
type: String,
default: 'router-link-exact-active'
},
replace: Boolean
},
render(h) {
// 解析目标路由
const router = this.$router
const current = this.$route
const { location, route } = router.resolve(this.to, current)
// 生成href
const href = router.options.mode === 'hash'
? '#' + route.fullPath
: route.fullPath
// 判断是否激活
const isExact = current.path === route.path
const isActive = this.exact ? isExact : current.path.startsWith(route.path)
// 类名处理
const classObj = {}
if (this.activeClass) {
classObj[this.activeClass] = isActive
}
if (this.exactActiveClass) {
classObj[this.exactActiveClass] = isExact
}
// 点击处理
const handler = e => {
if (e.metaKey || e.ctrlKey || e.shiftKey) return
if (e.defaultPrevented) return
e.preventDefault()
if (this.replace) {
router.replace(location)
} else {
router.push(location)
}
}
// 创建子元素
const children = this.$slots.default || [this.to]
const data = {
class: classObj,
attrs: {
href
},
on: {
click: handler
}
}
return h(this.tag, data, children)
}
}
3.7 Vue插件集成
// Vue插件安装
VueRouter.install = function(Vue) {
// 混入$router和$route
Vue.mixin({
beforeCreate() {
if (this.$options.router) {
// 根实例
this._routerRoot = this
this._router = this.$options.router
this._router.init(this)
// 响应式定义$route
Vue.util.defineReactive(this, '_route', this._router.current)
} else {
// 子组件
this._routerRoot = (this.$parent && this.$parent._routerRoot) || this
}
}
})
// 定义$router和$route属性
Object.defineProperty(Vue.prototype, '$router', {
get() {
return this._routerRoot._router
}
})
Object.defineProperty(Vue.prototype, '$route', {
get() {
return this._routerRoot._route
}
})
// 注册全局组件
Vue.component('RouterView', RouterView)
Vue.component('RouterLink', RouterLink)
}
四、使用示例
4.1 基本使用
// 1. 定义路由组件
const Home = { template: '<div>Home Page</div>' }
const About = { template: '<div>About Page</div>' }
const User = {
template: `
<div>
<h2>User {{ $route.params.id }}</h2>
<router-view></router-view>
</div>
`
}
const Profile = { template: '<div>User Profile</div>' }
// 2. 创建路由实例
const router = new VueRouter({
mode: 'history',
routes: [
{ path: '/', component: Home },
{ path: '/about', component: About },
{
path: '/user/:id',
component: User,
children: [
{ path: 'profile', component: Profile }
]
}
]
})
// 3. 创建Vue实例
const app = new Vue({
router,
template: `
<div id="app">
<nav>
<router-link to="/">Home</router-link>
<router-link to="/about">About</router-link>
<router-link to="/user/123">User 123</router-link>
</nav>
<router-view></router-view>
</div>
`
}).$mount('#app')
4.2 导航守卫示例
// 全局前置守卫
router.beforeEach((to, from, next) => {
console.log(`Navigating from ${from.path} to ${to.path}`)
// 检查是否需要登录
if (to.meta.requiresAuth && !isAuthenticated()) {
next('/login')
} else {
next()
}
})
// 全局后置钩子
router.afterEach((to, from) => {
// 页面标题
document.title = to.meta.title || 'My App'
// 发送页面浏览统计
trackPageView(to.path)
})
五、性能优化与高级特性
5.1 路由懒加载
// 动态导入组件(Webpack代码分割)
const User = () => import('./views/User.vue')
const router = new VueRouter({
routes: [
{
path: '/user/:id',
component: User,
meta: {
preload: true // 自定义预加载策略
}
}
]
})
// 实现预加载策略
router.onReady(() => {
// 预加载匹配的路由组件
router.getMatchedComponents().forEach(component => {
if (component && component.preload) {
component()
}
})
})
5.2 滚动行为控制
const router = new VueRouter({
scrollBehavior(to, from, savedPosition) {
// 返回滚动位置
if (savedPosition) {
return savedPosition
}
// 锚点导航
if (to.hash) {
return {
selector: to.hash,
behavior: 'smooth'
}
}
// 页面顶部
return { x: 0, y: 0 }
}
})
六、完整流程图
graph TB
subgraph "初始化阶段"
A[创建VueRouter实例] --> B[创建路由映射表]
B --> C[选择路由模式]
C --> D[初始化事件监听]
end
subgraph "导航过程"
E[触发导航] --> F{路由模式}
F -->|Hash| G[hashchange事件]
F -->|History| H[popstate/API调用]
G --> I[解析目标路径]
H --> I
I --> J[路由匹配]
J --> K[执行导航守卫]
K --> L{守卫结果}
L -->|通过| M[更新路由状态]
L -->|取消| N[导航中止]
M --> O[触发响应式更新]
O --> P[RouterView重新渲染]
P --> Q[完成导航]
end
subgraph "组件渲染"
R[RouterView组件] --> S[计算渲染深度]
S --> T[获取匹配组件]
T --> U[渲染组件]
end
七、总结
通过自己动手实现一个Vue路由系统,我们可以深入理解:
- 路由的核心原理:URL与组件的映射关系
- 两种模式的区别:Hash与History的实现差异
- 导航的生命周期:从触发到渲染的完整流程
- 组件的渲染机制:RouterView如何处理嵌套路由
这个实现虽然简化了官方Vue Router的一些复杂特性,但涵盖了最核心的功能。理解了这些基本原理后,无论是使用Vue Router还是排查相关问题时,都会更加得心应手。