自我介绍:大家好,我是吉帅振的网络日志(其他平台账号名字相同),互联网前端开发工程师,工作5年,去过上海和北京,经历创业公司,加入过阿里本地生活团队,现在郑州北游教育从事编程培训。
一、前言
相信对有一定基础的前端开发工程师来说,路由并不陌生,它最初源于服务端,在服务端中路由描述的是 URL 与处理函数之间的映射关系。而在 Web 前端单页应用 SPA 中,路由描述的是 URL 与视图之间的映射关系,这种映射是单向的,即 URL 变化会引起视图的更新。相比于后端路由,前端路由的好处是无须刷新页面,减轻了服务器的压力,提升了用户体验。目前主流支持单页应用的前端框架,基本都有配套的或第三方的路由系统。相应的,Vue.js 也提供了官方前端路由实现 Vue Router,那么这节课我们就来学习它的实现原理。
二、路由的基本用法
<div id="app">
<h1>Hello App!</h1>
<p>
<router-link to="/">Go to Home</router-link>
<router-link to="/about">Go to About</router-link>
</p>
<router-view></router-view>
</div>
RouterLink 表示路由的导航组件,我们可以配置 to 属性来指定它跳转的链接,它最终会在页面上渲染生成 a 标签。RouterView 表示路由的视图组件,它会渲染路径对应的 Vue 组件,也支持嵌套。
import { createApp } from 'vue'
import { createRouter, createWebHashHistory } from 'vue-router'
// 1. 定义路由组件
const Home = { template: '<div>Home</div>' }
const About = { template: '<div>About</div>' }
// 2. 定义路由配置,每个路径映射一个路由视图组件
const routes = [
{ path: '/', component: Home },
{ path: '/about', component: About },
]
// 3. 创建路由实例,可以指定路由模式,传入路由配置对象
const router = createRouter({
history: createWebHistory(),
routes
})
// 4. 创建 app 实例
const app = createApp({
})
// 5. 在挂载页面 之前先安装路由
app.use(router)
// 6. 挂载页面
app.mount('#app')
首先需要定义一个路由配置,这个配置主要用于描述路径和组件的映射关系,即什么路径下 RouterView 应该渲染什么路由组件。接着创建路由对象实例,传入路由配置对象,并且也可以指定路由模式,Vue Router 目前支持三种模式,hash 模式,HTML5 模式和 memory 模式,我们常用的是前两种模式。最后在挂载页面前,我们需要安装路由,这样我们就可以在各个组件中访问路由对象以及使用路由的内置组件 RouterLink 和 RouterView 了。
三、路由对象的创建
function createRouter(options) {
// 定义一些辅助方法和变量
// ...
// 创建 router 对象
const router = {
// 当前路径
currentRoute,
addRoute,
removeRoute,
hasRoute,
getRoutes,
resolve,
options,
push,
replace,
go,
back: () => go(-1),
forward: () => go(1),
beforeEach: beforeGuards.add,
beforeResolve: beforeResolveGuards.add,
afterEach: afterGuards.add,
onError: errorHandlers.add,
isReady,
install(app) {
// 安装路由函数
}
}
return router
}
我们省略了大部分代码,只保留了路由对象相关的代码,可以看到路由对象 router 就是一个对象,它维护了当前路径 currentRoute,且拥有很多辅助方法。
四、路由的安装
Vue Router 作为 Vue 的插件,当我们执行 app.use(router) 的时候,实际上就是在执行 router 的 install 方法来安装路由,并把 app 作为参数传入,来看它的定义:
const router = {
install(app) {
const router = this
// 注册路由组件
app.component('RouterLink', RouterLink)
app.component('RouterView', RouterView)
// 全局配置定义 $router 和 $route
app.config.globalProperties.$router = router
Object.defineProperty(app.config.globalProperties, '$route', {
get: () => unref(currentRoute),
})
// 在浏览器端初始化导航
if (isBrowser &&
!started &&
currentRoute.value === START_LOCATION_NORMALIZED) {
// see above
started = true
push(routerHistory.location).catch(err => {
warn('Unexpected error when starting the router:', err)
})
}
// 路径变成响应式
const reactiveRoute = {}
for (let key in START_LOCATION_NORMALIZED) {
reactiveRoute[key] = computed(() => currentRoute.value[key])
}
// 全局注入 router 和 reactiveRoute
app.provide(routerKey, router)
app.provide(routeLocationKey, reactive(reactiveRoute))
let unmountApp = app.unmount
installedApps.add(app)
// 应用卸载的时候,需要做一些路由清理工作
app.unmount = function () {
installedApps.delete(app)
if (installedApps.size < 1) {
removeHistoryListener()
currentRoute.value = START_LOCATION_NORMALIZED
started = false
ready = false
}
unmountApp.call(this, arguments)
}
}
}
全局注册 RouterView 和 RouterLink 组件——这是你安装了路由后,可以在任何组件中去使用这俩个组件的原因,如果你使用 RouterView 或者 RouterLink 的时候收到提示不能解析 router-link 和 router-view,这说明你压根就没有安装路由。通过 provide 方式全局注入 router 对象和 reactiveRoute 对象,其中 router 表示用户通过 createRouter 创建的路由对象,我们可以通过它去动态操作路由,reactiveRoute 表示响应式的路径对象,它维护着路径的相关信息。
五、路径的管理
const START_LOCATION_NORMALIZED = {
path: '/',
name: undefined,
params: {},
query: {},
hash: '',
fullPath: '/',
matched: [],
meta: {},
redirectedFrom: undefined
}
路由想要发生变化,就是通过改变路径完成的,路由对象提供了很多改变路径的方法,比如 router.push、router.replace,它们的底层最终都是通过 pushWithRedirect 完成路径的切换,我们来看一下它的实现:
function pushWithRedirect(to, redirectedFrom) {
const targetLocation = (pendingLocation = resolve(to))
const from = currentRoute.value
const data = to.state
const force = to.force
const replace = to.replace === true
const toLocation = targetLocation
toLocation.redirectedFrom = redirectedFrom
let failure
if (!force && isSameRouteLocation(stringifyQuery$1, from, targetLocation)) {
failure = createRouterError(16 /* NAVIGATION_DUPLICATED */, { to: toLocation, from })
handleScroll(from, from, true, false)
}
return (failure ? Promise.resolve(failure) : navigate(toLocation, from))
.catch((error) => {
if (isNavigationFailure(error, 4 /* NAVIGATION_ABORTED */ |
8 /* NAVIGATION_CANCELLED */ |
2 /* NAVIGATION_GUARD_REDIRECT */)) {
return error
}
return triggerError(error)
})
.then((failure) => {
if (failure) {
// 处理错误
}
else {
failure = finalizeNavigation(toLocation, from, true, replace, data)
}
triggerAfterEach(toLocation, from, failure)
return failure
})
}
我省略了一部分代码的实现,这里主要来看 pushWithRedirect 的核心思路,首先参数 to 可能有多种情况,可以是一个表示路径的字符串,也可以是一个路径对象,所以要先经过一层 resolve 返回一个新的路径对象,它比前面提到的路径对象多了一个 matched 属性,它的作用我们后续会介绍。得到新的目标路径后&#x