1 前言
用过Vue的各位小伙伴,想必或多或少都会接触过Vue Router。
不知道你们是否也会跟我一样好奇Vue Router是如何实现的呢?
如果是的话,那么就跟随我来一起阅读Vue Router的源码,了解这款官方路由器是如何设计与实现的吧。
提示:本文讲解的是
Vue Router3版本的源码。
2 Vue Router
想要读懂Vue Router,我们需要先思考Vue Router提供了哪些什么功能给我们?
接着才会考虑这些功能是如何实现的?然后从代码里找答案。
2.1 Vue Router功能
回想一下我们使用Vue Router,可以总结出来它其实就提供了两个功能:
- 路由组件渲染:通过
router-view渲染我们配置的组件。 - 路由路径导航:通过
router-link或者this.$router的方法进行导航
提前预告一下:
router-view渲染的组件是从this.$router中找到匹配的路由的component里获取的。router-link的导航功能底层也是调用this.$router的方法,
所以只要搞懂this.$router从何而来,如何实现,也就基本上等于搞懂了Vue Router的工作原理了。
那么接下来就先看看VueRouter类提供了什么东西吧!
2.2 VueRouter类静态属性
静态属性里最重要的就是install方法,该方法将$router和$route注入到Vue的原型上。
inNavigationFailure可以用来判断路由导航过程中抛出的异常,进行个性化处理,但是较少用到。
2.3 VueRouter类的实例属性
VueRouter实例属性中最重要的就是history对象,后面会重点解读。
实例属性中提供了添加导航守卫、路由导航、路由添加这三类方法,我们主要用到的也就是这些方法。
route-view里会调用VueRouter的match方法返回匹配到的路由进行渲染。
至此,基本理清楚了路由组件渲染和路由路径导航两个功能是从何而来,接下来就是看代码咯~~
2.4 导航守卫
在路由路径导航的时候,会伴随调用很多的导航守卫函数,这也是Vue Router重点功能之一。
由于Vue Router的导航守卫较多,这里列出来导航流程给大家回忆一下,看代码会更有思路一点:
- 导航被触发。
- 在失活的组件里调用
beforeRouteLeave守卫。 - 调用全局的
beforeEach守卫。 - 在重用的组件里调用
beforeRouteUpdate守卫。 - 在路由配置里调用
beforeEnter。 - 解析异步路由组件。
- 在被激活的组件里调用
beforeRouteEnter。 - 调用全局的
beforeResolve守卫。 - 导航被确认。
- 调用全局的
afterEach钩子。 - 触发 DOM 更新。
- 调用
beforeRouteEnter守卫中传给next的回调函数,创建好的组件实例会作为回调函数的参数传入。
3 源码文件结构
Vue Router的源码文件结构如下所示:
重点文件我已经用*号标注出来。
src
│ create-matcher.js // *创建matcher对象,提供match,addRoute等方法
│ create-route-map.js // *创建routeMap,便于根据名称或路径找到路由记录
│ index.js // *入口文件
│ install.js // *vue插件安装
│ router.js // *VueRouter类定义
│
├─components // 组件包
│ link.js // *router-link
│ view.js // *router-view
│
├─composables // 函数方法
│ globals.js
│ guards.js
│ index.js
│ useLink.js
│ utils.js
│
├─entries // 入口
│ cjs.js
│ esm.js
│
├─history // history相关
│ abstract.js // node环境的history实现类
│ base.js // *history基类
│ hash.js // *hash模式的history实现类
│ html5.js // *history模式的history实现类
│
└─util // 工具函数
async.js
dom.js
errors.js
location.js
misc.js
params.js
path.js
push-state.js
query.js
resolve-components.js
route.js
scroll.js
state-key.js
warn.js
打开入口文件可以看到其实就是引用router.js。
所以我们直接从router.js开始阅读,但是在这之前先看看Vue.use(VueRouter),也就是install.js做了什么。
4 install.js
import View from './components/view'
import Link from './components/link'
export let _Vue
export function install (Vue) {
// 确保只注册一次
if (install.installed && _Vue === Vue) return
install.installed = true
_Vue = Vue
const isDef = v => v !== undefined
// 将vue实例注册到route实例上
const registerInstance = (vm, callVal) => {
// 获取当前组件实例的父虚拟节点(VNode)
let i = vm.$options._parentVnode
// 赋值表达式会返回等号右边的值
// registerRouteInstance是route-view组件注入的方法
// 作用是:将vue实例注册到route实例上
if (isDef(i) && isDef(i = i.data) && isDef(i = i.registerRouteInstance)) {
i(vm, callVal)
}
}
Vue.mixin({
// 给vue组件混入beforeCreate方法
beforeCreate () {
if (isDef(this.$options.router)) {
this._routerRoot = this
this._router = this.$options.router
// 调用router的init方法,后面会讲到
this._router.init(this)
// 设置响应性
Vue.util.defineReactive(this, '_route', this._router.history.current)
} else {
// 子组件从父组件中拿到$routers
this._routerRoot = (this.$parent && this.$parent._routerRoot) || this
}
// 将当前实例注册到路由实例中
registerInstance(this, this)
},
// 取消注册
destroyed () {
registerInstance(this)
}
})
// Vue原型增加$router
Object.defineProperty(Vue.prototype, '$router', {
get () { return this._routerRoot._router }
})
// Vue原型增加$route
Object.defineProperty(Vue.prototype, '$route', {
get () { return this._routerRoot._route }
})
// Vue全局注册两个组件
Vue.component('RouterView', View)
Vue.component('RouterLink', Link)
// 获取Vue的合并策略
const strats = Vue.config.optionMergeStrategies
// beforeRouteEnter、beforeRouteLeave、beforeRouteUpdate
// 与created的合并策略保持一致
strats.beforeRouteEnter = strats.beforeRouteLeave = strats.beforeRouteUpdate = strats.created
}
可以看到,插件安装主要做了四件事:
Vue原型注入$router和$route。Vue全局注册组件route-view和route-link。Vue全组件混入beforeCreate生命周期钩子函数,用于将vue组件实例注册到route实例中。- 设置了
beforeRouteEnter、beforeRouteLeave、beforeRouteUpdate钩子函数的合并策略。
5 router.js(重点)
export default class VueRouter {
// 静态属性
// vue插件install函数
static install: () => void
// 版本
static version: string
// 是否跳转导航错误
static isNavigationFailure: Function
// 导航错误类型
static NavigationFailureType: any
// 起始路径,默认是"/"
static START_LOCATION: Route
// Vue根节点
app: any
apps: Array<any>
// 保存new VueRouter是传入的配置
options: RouterOptions
// 模式
mode: string
// history对象
history: HashHistory | HTML5History | AbstractHistory
// 包含这四个函数:match,addRoutes,addRoute,getRoutes
matcher: Matcher
// 是否回退history
fallback: boolean
// 全局导航守卫函数
beforeHooks: Array<?NavigationGuard>
resolveHooks: Array<?NavigationGuard>
afterHooks: Array<?AfterNavigationHook>
// 构造函数
constructor (options: RouterOptions = {}) {
this.app = null
this.apps = []
// 记录传入的配置
this.options = options
this.beforeHooks = []
this.resolveHooks = []
this.afterHooks = []
// 返回这四个函数:match,addRoutes,addRoute,getRoutes
// 入参分别是:我们传入的routes和router实例
this.matcher = createMatcher(options.routes || [], this)
// 默认hash模式
let mode = options.mode || 'hash'
this.fallback =
mode === 'history' && !supportsPushState && options.fallback !== false
// 不支持history,回退为hash模式
if (this.fallback) {
mode = 'hash'
}
// 不是浏览器环境,自动切换成abstract
if (!inBrowser) {
mode = 'abstract'
}
this.mode = mode
// 创建history实例
switch (mode) {
case 'history':
this.history = new HTML5History(this, options.base)
break
case 'hash':
this.history = new HashHistory(this, options.base, this.fallback)
break
case 'abstract':
this.history = new AbstractHistory(this, options.base)
break
default:
if (process.env.NODE_ENV !== 'production') {
assert(false, `invalid mode: ${mode}`)
}
}
}
// 实际是调用matcher.match
// 作用:根据路径返回路由实例
match (raw: RawLocation, current?: Route, redirectedFrom?: Location): Route {
return this.matcher.match(raw, current, redirectedFrom)
}
// 获取当前路由,实际是从history中取的
get currentRoute (): ?Route {
return this.history && this.history.current
}
// 初始化方法,
// 在组件beforeCreate的时候会调用
// 入参为当前组件实例
init (app: any) {
// 保存到apps数组中
this.apps.push(app)
// 设置实例的摧毁钩子函数
app.$once('hook:destroyed', () => {
// 清除当前实例
const index = this.apps.indexOf(app)
if (index > -1) this.apps.splice(index, 1)
if (this.app === app) this.app = this.apps[0] || null
// 如果实例全部都被清除了,那么也清除history
if (!this.app) this.history.teardown()
})
// 说明之前已经初始化过主应用,
// 直接返回,不需要设置新的历史记录监听器。
if (this.app) {
return
}
this.app = app
const history = this.history
// 设置history的监听器
if (history instanceof HTML5History || history instanceof HashHistory) {
// 如果有配置且支持滚动行为
// 执行配置的滚动行为操作
const handleInitialScroll = routeOrError => {
const from = history.current
const expectScroll = this.options.scrollBehavior
const supportsScroll = supportsPushState && expectScroll
if (supportsScroll && 'fullPath' in routeOrError) {
handleScroll(this, routeOrError, from, false)
}
}
const setupListeners = routeOrError => {
history.setupListeners()
handleInitialScroll(routeOrError)
}
// 第一次加载跳转到首页
// 设置
history.transitionTo(
history.getCurrentLocation(),
setupListeners,
setupListeners
)
}
// 给history设置监听事件
history.listen(route => {
// 更新组件实例中的route实例
// 也就是$router
this.apps.forEach(app => {
app._route = route
})
})
}
// 添加前置导航守卫
beforeEach (fn: Function): Function {
return registerHook(this.beforeHooks, fn)
}
// 添加解析导航守卫
beforeResolve (fn: Function): Function {
return registerHook(this.resolveHooks, fn)
}
// 添加后置导航守卫
afterEach (fn: Function): Function {
return registerHook(this.afterHooks, fn)
}
// 给history添加ready回调
onReady (cb: Function, errorCb?: Function) {
this.history.onReady(cb, errorCb)
}
// 给history添加error回调
onError (errorCb: Function) {
this.history.onError(errorCb)
}
// 跳转页面
// 调用的其实是history的push方法
push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
if (!onComplete && !onAbort && typeof Promise !== 'undefined') {
return new Promise((resolve, reject) => {
this.history.push(location, resolve, reject)
})
} else {
this.history.push(location, onComplete, onAbort)
}
}
// replcae 页面
// 调用的其实是history的replace方法
replace (location: RawLocation, onComplete?: Function, onAbort?: Function) {
if (!onComplete && !onAbort && typeof Promise !== 'undefined') {
return new Promise((resolve, reject) => {
this.history.replace(location, resolve, reject)
})
} else {
this.history.replace(location, onComplete, onAbort)
}
}
// 跳转
// 调用的也是history里的go
go (n: number) {
this.history.go(n)
}
// 返回上一个页面
back () {
this.go(-1)
}
// 前进
forward () {
this.go(1)
}
// 获取匹配路径到的组件
getMatchedComponents (to?: RawLocation | Route): Array<any> {
const route: any = to
? to.matched
? to
: this.resolve(to).route
: this.currentRoute
if (!route) {
return []
}
return [].concat.apply(
[],
route.matched.map(m => {
return Object.keys(m.components).map(key => {
return m.components[key]
})
})
)
}
// 解析路径
// 主要是返回三个东西
// location,route,href
resolve (
to: RawLocation,
current?: Route,
append?: boolean
): {
location: Location,
route: Route,
href: string,
normalizedTo: Location,
resolved: Route
} {
current = current || this.history.current
const location = normalizeLocation(to, current, append, this)
const route = this.match(location, current)
const fullPath = route.redirectedFrom || route.fullPath
const base = this.history.base
const href = createHref(base, fullPath, this.mode)
return {
location,
route,
href,
normalizedTo: location,
resolved: route
}
}
// 获取所有路由记录
getRoutes () {
return this.matcher.getRoutes()
}
// 添加路由,调用的是matcher.addRoute
addRoute (parentOrRoute: string | RouteConfig, route?: RouteConfig) {
this.matcher.addRoute(parentOrRoute, route)
if (this.history.current !== START) {
this.history.transitionTo(this.history.getCurrentLocation())
}
}
// 批量添加路由,调用的是matcher.addRoutes
// Vue Router4已废弃该方法
addRoutes (routes: Array<RouteConfig>) {
this.matcher.addRoutes(routes)
if (this.history.current !== START) {
this.history.transitionTo(this.history.getCurrentLocation())
}
}
}
// 返回一个函数,用来移除钩子
function registerHook (list: Array<any>, fn: Function): Function {
list.push(fn)
return () => {
const i = list.indexOf(fn)
if (i > -1) list.splice(i, 1)
}
}
// 创建href
function createHref (base: string, fullPath: string, mode) {
var path = mode === 'hash' ? '#' + fullPath : fullPath
return base ? cleanPath(base + '/' + path) : path
}
// 设置静态属性
VueRouter.install = install
VueRouter.version = '__VERSION__'
VueRouter.isNavigationFailure = isNavigationFailure
VueRouter.NavigationFailureType = NavigationFailureType
VueRouter.START_LOCATION = START
// 浏览器环境下安装VueRouter插件
if (inBrowser && window.Vue) {
window.Vue.use(VueRouter)
}
看完VueRouter类的代码,我们会发现其中有两个很重要的东西:
matcher:match、addRoute都是调用matcher里的方法history:push、replace、go等方法以及记录当前路由的current都是从history里获取的。
所以接下来我们就需要看看matcher和history是如何实现的。
6 create-matcher.js
回顾上文的代码:
// 入参分别是:我们传入的routes和router实例
this.matcher = createMatcher(options.routes || [], this)
接下来看createMatcher的源码:
// 可以看出来Matcher返回了四个函数
export type Matcher = {
match: (raw: RawLocation, current?: Route, redirectedFrom?: Location) => Route;
addRoutes: (routes: Array<RouteConfig>) => void;
addRoute: (parentNameOrRoute: string | RouteConfig, route?: RouteConfig) => void;
getRoutes: () => Array<RouteRecord>;
};
// 返回值是4个函数
// 入参是routes和router
export function createMatcher (
routes: Array<RouteConfig>,
router: VueRouter
): Matcher {
// createRouteMap的作用是根据传入的routes
// 生成pathList, pathMap, nameMap三个对象
// pathList:路径数组,按路径优先级排序
// pathMap:path和RouteRecord的映射
// nameMap:name和RouteRecord的映射
const { pathList, pathMap, nameMap } = createRouteMap(routes)
// 返回的第一个函数:addRoutes
function addRoutes (routes) {
// 在原有的基础上添加新传入的routes
createRouteMap(routes, pathList, pathMap, nameMap)
}
// 返回的第二个函数:addRoute
// 如果传了两个参数,会把第二个参数,
// 的路由添加到第一个参数的路由下
// 如果只传一个参数,就直接添加单个路由
function addRoute (parentOrRoute, route) {
const parent = (typeof parentOrRoute !== 'object') ? nameMap[parentOrRoute] : undefined
// 第五个参数代表添加到该路由下
createRouteMap([route || parentOrRoute], pathList, pathMap, nameMap, parent)
// 如果父路由有别名
// 则复制一份别名路由
// 并添加
if (parent && parent.alias.length) {
createRouteMap(
parent.alias.map(alias => ({ path: alias, children: [route] })),
pathList,
pathMap,
nameMap,
parent
)
}
}
// 返回的第三个函数:getRoutes
// 比较简单,返回所有路由
function getRoutes () {
return pathList.map(path => pathMap[path])
}
// 返回的最后一个函数:match
// 作用:根据传入的Location返回Route对象
function match (
raw: RawLocation,
currentRoute?: Route,
redirectedFrom?: Location
): Route {
// 返回标准化路径
const location = normalizeLocation(raw, currentRoute, false, router)
const { name } = location
// 有传路由name的情况
// 从nameMap里找路由
if (name) {
// 根据name找到路由记录
const record = nameMap[name]
// 如果没找到,直接创建一个路由
// 因为不然的话就没法跳转
// 有了路由就可以跳转过去,但是内容是空的
if (!record) return _createRoute(null, location)
// 找出动态路由的必填参数
const paramNames = record.regex.keys
.filter(key => !key.optional)
.map(key => key.name)
// 如果params不是对象,初始化为空对象
if (typeof location.params !== 'object') {
location.params = {}
}
// 如果有传currentRoute
// 从currentRoute获取params信息
if (currentRoute && typeof currentRoute.params === 'object') {
for (const key in currentRoute.params) {
if (!(key in location.params) && paramNames.indexOf(key) > -1) {
location.params[key] = currentRoute.params[key]
}
}
}
// 填充动态参数
location.path = fillParams(record.path, location.params, `named route "${name}"`)
// 创建路由
return _createRoute(record, location, redirectedFrom)
} else if (location.path) {
// 根据路径匹配
location.params = {}
for (let i = 0; i < pathList.length; i++) {
const path = pathList[i]
const record = pathMap[path]
// 如果根据路径可以匹配的到就创建路由
if (matchRoute(record.regex, location.path, location.params)) {
return _createRoute(record, location, redirectedFrom)
}
}
}
// 没匹配到,也要创建一个新的路由
// 不然就跳转不过去了
return _createRoute(null, location)
}
// 创建路由的方法
function _createRoute (
record: ?RouteRecord,
location: Location,
redirectedFrom?: Location
): Route {
if (record && record.redirect) {
return redirect(record, redirectedFrom || location)
}
if (record && record.matchAs) {
return alias(record, location, record.matchAs)
}
// 实际上调用的是该方法
return createRoute(record, location, redirectedFrom, router)
}
// 最后返回这四个函数
return {
match,
addRoute,
getRoutes,
addRoutes
}
}
再补充一下_createRoute里调用的createRoute方法:
export function createRoute (
record: ?RouteRecord,
location: Location,
redirectedFrom?: ?Location,
router?: VueRouter
): Route {
const stringifyQuery = router && router.options.stringifyQuery
let query: any = location.query || {}
try {
query = clone(query)
} catch (e) {}
// 可以看到每次match到的路由是在这里创建的
// 有些信息是来自location,
// 有些信息是来自record
const route: Route = {
name: location.name || (record && record.name),
meta: (record && record.meta) || {},
path: location.path || '/',
hash: location.hash || '',
query,
params: location.params || {},
fullPath: getFullPath(location, stringifyQuery),
// 我们打印this.$route可以看到
// 里面有matched数组,
// 就是匹配到的一个个嵌套路由
matched: record ? formatMatch(record) : []
}
if (redirectedFrom) {
route.redirectedFrom = getFullPath(redirectedFrom, stringifyQuery)
}
return Object.freeze(route)
}
总结一下,create-matcher其实内容并不复杂,match函数的作用是根据传入的location返回一个路由记录。
另外三个方法都是来自于createRouteMap这个方法,所以接下来我们再来看一下它做了什么吧~
7 create-route-map.js
// 该方法支持五个参数
export function createRouteMap (
routes: Array<RouteConfig>, //要添加的路由
oldPathList?: Array<string>, //路径数组
oldPathMap?: Dictionary<RouteRecord>, //路径与路由记录的映射
oldNameMap?: Dictionary<RouteRecord>, //名称与路由记录的映射
parentRoute?: RouteRecord //父路由
): {
pathList: Array<string>,
pathMap: Dictionary<RouteRecord>,
nameMap: Dictionary<RouteRecord>
} {
// 用来控制路径匹配优先级
// 类似这样的数组['/dashboard','/about']
const pathList: Array<string> = oldPathList || []
// 路径与路由记录的映射
const pathMap: Dictionary<RouteRecord> = oldPathMap || Object.create(null)
// 名称与路由记录的映射
const nameMap: Dictionary<RouteRecord> = oldNameMap || Object.create(null)
routes.forEach(route => {
// 遍历,添加route,会发现
// 所有的逻辑都在这个方法里
// 作用就是获取route数据,
// 保存到pathList, pathMap, nameMap这三个对象里
addRouteRecord(pathList, pathMap, nameMap, route, parentRoute)
})
// 如果存在通配符路由*,将它移动到最后面
for (let i = 0, l = pathList.length; i < l; i++) {
if (pathList[i] === '*') {
pathList.push(pathList.splice(i, 1)[0])
l--
i--
}
}
return {
pathList,
pathMap,
nameMap
}
}
function addRouteRecord (
pathList: Array<string>,
pathMap: Dictionary<RouteRecord>,
nameMap: Dictionary<RouteRecord>,
route: RouteConfig,
parent?: RouteRecord,
matchAs?: string
) {
const { path, name } = route
const pathToRegexpOptions = route.pathToRegexpOptions || {}
// 标准化路径
// 比如
// 去除路径末尾的斜杆
// 连续的斜杆替换为1个
const normalizedPath = normalizePath(path, parent, pathToRegexpOptions.strict)
// 是否大小写敏感
if (typeof route.caseSensitive === 'boolean') {
pathToRegexpOptions.sensitive = route.caseSensitive
}
// 要保存在nameMap和pathMap里的路由记录
const record: RouteRecord = {
path: normalizedPath,
// 解析路由正则表达,
// 底层使用的是'path-to-regexp'插件
regex: compileRouteRegex(normalizedPath, pathToRegexpOptions),
// component都会被放到components里,默认是default
components: route.components || { default: route.component },
// 说明别名可以设置为数组,也就是可以设置多个
alias: route.alias
? typeof route.alias === 'string'
? [route.alias]
: route.alias
: [],
// 保存路由对应组件实例
// 在history里会用到
instances: {},
enteredCbs: {},
name,
parent,
matchAs,
redirect: route.redirect,
beforeEnter: route.beforeEnter,
meta: route.meta || {},
// 如果设置了components,
// 那么props也要用对象的形式设置
props:
route.props == null
? {}
: route.components
? route.props
: { default: route.props }
}
if (route.children) {
// 递归添加子路由
// 父路由有别名情况添加子路由,
// 实际匹配的时候还是用原有名称
// matchAs含义就是实际匹配用的路径
route.children.forEach(child => {
const childMatchAs = matchAs
? cleanPath(`${matchAs}/${child.path}`)
: undefined
addRouteRecord(pathList, pathMap, nameMap, child, record, childMatchAs)
})
}
// 如果路径不存在,添加
if (!pathMap[record.path]) {
pathList.push(record.path)
pathMap[record.path] = record
}
// 如果配置了路由别名
if (route.alias !== undefined) {
// 转为数组
const aliases = Array.isArray(route.alias) ? route.alias : [route.alias]
for (let i = 0; i < aliases.length; ++i) {
const alias = aliases[i]
const aliasRoute = {
path: alias,
children: route.children
}
// 添加别名路由
// 但是实际匹配(matchAs)还是用原路由
// 最后一个参数就是matchAs
addRouteRecord(
pathList,
pathMap,
nameMap,
aliasRoute,
parent,
record.path || '/'
)
}
}
// 如果路由配置了名称
// 并不会再添加,而是用原来的
if (name) {
if (!nameMap[name]) {
nameMap[name] = record
}
}
}
// 路径转为正则表达是对象
// 使用'path-to-regexp'插件转换
function compileRouteRegex (
path: string,
pathToRegexpOptions: PathToRegexpOptions
): RouteRegExp {
const regex = Regexp(path, [], pathToRegexpOptions)
// regex.keys里保存了动态参数的信息
if (process.env.NODE_ENV !== 'production') {
const keys: any = Object.create(null)
regex.keys.forEach(key => {
warn(
!keys[key.name],
`Duplicate param keys in route with path: "${path}"`
)
keys[key.name] = true
})
}
return regex
}
// 标准化path
function normalizePath (
path: string,
parent?: RouteRecord,
strict?: boolean
): string {
// 非严格模式下,去除path末尾的/返回
if (!strict) path = path.replace(/\/$/, '')
if (path[0] === '/') return path
if (parent == null) return path
// 严格模式下,且路径不是/开头,会拼接父路由的path
// cleanPath是用来清除连续多个斜杆为一个斜杆
return cleanPath(`${parent.path}/${path}`)
}
至此,我们理清楚了matcher里的addRoute是如何实现的了!!
各位看官,如果能看到这里,可以给自己鼓个掌,真的很不容易!
8 history
理清楚了matcher中的函数是怎么来的,接下来就剩下history是如何实现的了。
由于Vue Router提供了三种history,分别是hash,history,abstract,那么肯定是有三种具体的实现方法,就像axios底层发送请求提供了xhr,http两种模式。
但是代码肯定不可能写三套,分析history的作用,可以发现无论是哪一种history,我们都是使用它进行页面跳转,调用push,replace,go这样的方法。
另外由于Vue Router提供了多种导航守卫,导航守卫肯定是和页面跳转紧密关联。
以push函数为例,我们在调用push之前要调用前置导航守卫,push之后要调用后置导航守卫,当然还有其他的导航守卫,这些导航守卫的调用无论是哪一类history,逻辑都是一样的。
所以只要定义一个公共的基类,此处叫做base类,实现可以共用调用导航守卫的逻辑,再让hash,history,abstract各自实现push,replace,go这些方法就可以了。
所以先让我们看一下base.js的实现吧~
8.1 base.js
export class History {
// 路由器
router: Router
// 基础路径
base: string
// 当前路由对象
current: Route
// 跳转中的路由
// 如果有多个跳转中的路由
// 会把上一个跳转中的取消掉
// 这就是cancelled导航故障
pending: ?Route
cb: (r: Route) => void
// 是否准备完毕
ready: boolean
// 准备完毕回调
readyCbs: Array<Function>
// 准备异常回调
readyErrorCbs: Array<Function>
// history异常回调
errorCbs: Array<Function>
// 监听器
listeners: Array<Function>
// 清空监听器
cleanupListeners: Function
// 实现类需要实现以下方法
// 因为hash,history的跳转原理不一样
// 这些方法交给具体的history来实现
+go: (n: number) => void
+push: (loc: RawLocation, onComplete?: Function, onAbort?: Function) => void
+replace: (
loc: RawLocation,
onComplete?: Function,
onAbort?: Function
) => void
+ensureURL: (push?: boolean) => void
+getCurrentLocation: () => string
+setupListeners: Function
constructor (router: Router, base: ?string) {
// 路由器
this.router = router
// 基础路径
this.base = normalizeBase(base)
// 当前路径,初始为"/"
this.current = START
// 初始化基础的信息
this.pending = null
this.ready = false
this.readyCbs = []
this.readyErrorCbs = []
this.errorCbs = []
this.listeners = []
}
// 设置回调函数
listen (cb: Function) {
this.cb = cb
}
// 添加ready回调函数
onReady (cb: Function, errorCb: ?Function) {
// 如果history状态已经ready
if (this.ready) {
// 执行传入的回调函数
cb()
} else {
// 否则将回调函数保存到数组中
this.readyCbs.push(cb)
if (errorCb) {
this.readyErrorCbs.push(errorCb)
}
}
}
// 添加错误回调函数
onError (errorCb: Function) {
this.errorCbs.push(errorCb)
}
// 跳转方法,重点
// 传参为:
// 1.要跳转的路径
// 2.跳转成功回调函数
// 3.跳转失败回调函数
transitionTo (
location: RawLocation,
onComplete?: Function,
onAbort?: Function
) {
let route
try {
// 获取匹配到的路由,
// 前面讲到的match方法
route = this.router.match(location, this.current)
} catch (e) {
// 抛出错误前执行异常回调
this.errorCbs.forEach(cb => {
cb(e)
})
// 继续抛出
throw e
}
// 跳转后current就是prev了
// 所以命名prev记录
const prev = this.current
// 调用确认跳转方法,
// 此处又嵌套了一层
// 该方法也是三个参数:
// 1.跳转到的路由
// 2.跳转成功的回调
// 3.跳转失败的回调
// 先看看这两个回调函数做了什么
this.confirmTransition(
// 跳转到的路由
route,
// 跳转成功的回调
() => {
// 更新当前路由
this.updateRoute(route)
// 执行transitionTo传入的回调函数
onComplete && onComplete(route)
// 确保路由正确
this.ensureURL()
// 执行全局后置导航守卫
this.router.afterHooks.forEach(hook => {
hook && hook(route, prev)
})
// 只会执行一次
if (!this.ready) {
// 修改ready状态
this.ready = true
// 执行ready回调函数
this.readyCbs.forEach(cb => {
cb(route)
})
}
},
err => {
// 调用transitionTo传入的
// 异常回调函数
if (onAbort) {
onAbort(err)
}
if (err && !this.ready) {
// 切换状态并且执行ready异常回调函数
if (!isNavigationFailure(err, NavigationFailureType.redirected) || prev !== START) {
this.ready = true
this.readyErrorCbs.forEach(cb => {
cb(err)
})
}
}
}
)
}
// 真正跳转的方法实现
confirmTransition (route: Route, onComplete: Function, onAbort?: Function) {
// 记录当前路由
const current = this.current
// 记录当前正在跳转的路由
this.pending = route
// 执行错误回调
const abort = err => {
// 非导航故障,执行
// errorCbs回调函数
if (!isNavigationFailure(err) && isError(err)) {
if (this.errorCbs.length) {
this.errorCbs.forEach(cb => {
cb(err)
})
}
}
// 执行传入的异常回调函数
onAbort && onAbort(err)
}
// 获取路由深度
const lastRouteIndex = route.matched.length - 1
const lastCurrentIndex = current.matched.length - 1
// 判断是否同一个路由
if (
isSameRoute(route, current) &&
lastRouteIndex === lastCurrentIndex &&
route.matched[lastRouteIndex] === current.matched[lastCurrentIndex]
) {
this.ensureURL()
if (route.hash) {
// 如果有hash,执行滚动行为
handleScroll(this.router, current, route, false)
}
// 返回duplicated导航故障
return abort(createNavigationDuplicatedError(current, route))
}
// 获取更新、激活、失活的路由
// 假设有路由嵌套两层,
// 从/app/route1跳转到/app/route2
// updated 就是/app对应的路由
// deactivated 就是/app/route1对应的路由
// activated 就是/app/route2对应的路由
const { updated, deactivated, activated } = resolveQueue(
this.current.matched,
route.matched
)
// 拼接回调执行队列
const queue: Array<?NavigationGuard> = [].concat(
// 组件内leave守卫
extractLeaveGuards(deactivated),
// 全局前置守卫
this.router.beforeHooks,
// 组件内update守卫
extractUpdateHooks(updated),
// 路由独享守卫
activated.map(m => m.beforeEnter),
// 解析异步组件
// 这样在路由的components就可以取到组件
resolveAsyncComponents(activated)
)
// 第一个参数是路由守卫函数
// 第二个参数就是next函数
const iterator = (hook: NavigationGuard, next) => {
// 一个路由还没导航完,又有新的路由导航,
// 执行上面定义的错误回调,会抛出导航故障:cancelled
if (this.pending !== route) {
return abort(createNavigationCancelledError(current, route))
}
try {
// 执行守卫函数
// to,from,next方法
// 注意看next的实现
hook(route, current, (to: any) => {
// next(false)
// 执行上面定义的错误回调,会抛出abort导航故障
if (to === false) {
this.ensureURL(true)
abort(createNavigationAbortedError(current, route))
} else if (isError(to)) {
// next(Error)
// 执行上面定义的错误回调
this.ensureURL(true)
abort(to)
} else if (
typeof to === 'string' ||
(typeof to === 'object' &&
(typeof to.path === 'string' || typeof to.name === 'string'))
) {
// next('/') or next({ path: '/' }) -> redirect
// 执行上面定义的错误回调,会抛出重定向导航故障
abort(createNavigationRedirectedError(current, route))
// 抛出异常后会继续跳转到新的路径
if (typeof to === 'object' && to.replace) {
this.replace(to)
} else {
this.push(to)
}
} else {
// 调用iterator中传入的next方法
// 作用其实是索引加1,
// 接着执行下一个守卫函数
next(to)
}
})
} catch (e) {
abort(e)
}
}
// 顺序执行队列(queue)里的守卫函数
// 全部执行完后执行第三个参数传入的回调函数
runQueue(queue, iterator, () => {
// 守卫函数全部执行完
// 这个时候异步组件都已经加载好了
// 提取activated组件内的beforeRouteEnter守卫函数
const enterGuards = extractEnterGuards(activated)
// 拼接上全局解析守卫
const queue = enterGuards.concat(this.router.resolveHooks)
runQueue(queue, iterator, () => {
// 调用完组件内的beforeRouteEnter之后
if (this.pending !== route) {
return abort(createNavigationCancelledError(current, route))
}
this.pending = null
// confirmTransition完成的回调函数
onComplete(route)
})
})
}
// 如果看到这里还没迷糊的同学
// 可能会发现我看了半天,执行了很多守卫,
// 但是没看到页面跳转(push)
// 这是因为页面跳转是在onComplete回调函数里传进来的
// 更新当前路由
// 并执行监听函数
updateRoute (route: Route) {
this.current = route
this.cb && this.cb(route)
}
// 默认是空的
setupListeners () {
// Default implementation is empty
}
// 清除函数
teardown () {
// 监听函数全部执行一遍
this.listeners.forEach(cleanupListener => {
cleanupListener()
})
// 清空监听函数
this.listeners = []
// 还原初始设置
this.current = START
this.pending = null
}
}
function resolveQueue (
current: Array<RouteRecord>,
next: Array<RouteRecord>
): {
// updated的路由
updated: Array<RouteRecord>,
// activated的路由
activated: Array<RouteRecord>,
// deactivated的路由
deactivated: Array<RouteRecord>
} {
let i
const max = Math.max(current.length, next.length)
for (i = 0; i < max; i++) {
if (current[i] !== next[i]) {
break
}
}
return {
updated: next.slice(0, i),
activated: next.slice(i),
deactivated: current.slice(i)
}
}
// 提取组件内部的守卫
// beforeRouteEnter、beforeRouteLeave、beforeRouteUpdate
// 在这些守卫函数里this指向当前组件
// 是因为第三个参数bind进行了绑定
function extractGuards (
records: Array<RouteRecord>,
name: string,
bind: Function,
reverse?: boolean
): Array<?Function> {
// 扁平化多层组件,返回执行回调函数后的结果
// def:组件,instance:vue实例,match:路由记录,key:路由组件名称
const guards = flatMapComponents(records, (def, instance, match, key) => {
// 获取组件里的守卫函数,
// def是组件,name是守卫函数名称
const guard = extractGuard(def, name)
if (guard) {
// 给守卫函数绑定this
return Array.isArray(guard)
? guard.map(guard => bind(guard, instance, match, key))
: bind(guard, instance, match, key)
}
})
// 多层数组扁平化
return flatten(reverse ? guards.reverse() : guards)
}
function extractGuard (
def: Object | Function,
key: string
): NavigationGuard | Array<NavigationGuard> {
if (typeof def !== 'function') {
def = _Vue.extend(def)
}
// 返回对应的守卫函数
return def.options[key]
}
// 给守卫绑定this为vue组件实例
function bindGuard (guard: NavigationGuard, instance: ?_Vue): ?NavigationGuard {
if (instance) {
return function boundRouteGuard () {
return guard.apply(instance, arguments)
}
}
}
// 提取beforeRouteLeave函数
function extractLeaveGuards (deactivated: Array<RouteRecord>): Array<?Function> {
// 最后一个参数为false,
// 将所有组件反序排列
// 获取deactivated路由里的所有beforeRouteLeave守卫函数
// 并且给函数绑定this指向组件实例
return extractGuards(deactivated, 'beforeRouteLeave', bindGuard, true)
}
// 提取beforeRouteUpdate函数
function extractUpdateHooks (updated: Array<RouteRecord>): Array<?Function> {
return extractGuards(updated, 'beforeRouteUpdate', bindGuard)
}
// 提取beforeRouteEnter函数
function extractEnterGuards (
activated: Array<RouteRecord>
): Array<?Function> {
return extractGuards(
activated,
'beforeRouteEnter',
(guard, _, match, key) => {
return bindEnterGuard(guard, match, key)
}
)
}
// beforeRouteEnter守卫里,
// 组件实例还没加载,所以this取不到
// 用回调的方式
function bindEnterGuard (
guard: NavigationGuard,
match: RouteRecord,
key: string
): NavigationGuard {
return function routeEnterGuard (to, from, next) {
return guard(to, from, cb => {
if (typeof cb === 'function') {
if (!match.enteredCbs[key]) {
match.enteredCbs[key] = []
}
match.enteredCbs[key].push(cb)
}
next(cb)
})
}
}
8.2 hash.js
最后以hash模式的history为例子,看它的具体实现:
export class HashHistory extends History {
constructor (router: Router, base: ?string, fallback: boolean) {
// 调用base的构造函数
super(router, base)
// 补充hash模式中间的#号
if (fallback && checkFallback(this.base)) {
return
}
// 调整路径的斜杆
ensureSlash()
}
// 构建初始的listeners
setupListeners () {
// 已经有了就不再创建
if (this.listeners.length > 0) {
return
}
const router = this.router
const expectScroll = router.options.scrollBehavior
const supportsScroll = supportsPushState && expectScroll
// 是否支持滚动条
if (supportsScroll) {
// 添加设置滚动行为的回调
this.listeners.push(setupScroll())
}
// 处理路由导航
const handleRoutingEvent = () => {
const current = this.current
if (!ensureSlash()) {
return
}
this.transitionTo(getHash(), route => {
if (supportsScroll) {
handleScroll(this.router, route, current, true)
}
if (!supportsPushState) {
replaceHash(route.fullPath)
}
})
}
// 判断是监听history变化还是hash变化
// 添加window的原生监听事件
const eventType = supportsPushState ? 'popstate' : 'hashchange'
window.addEventListener(
eventType,
handleRoutingEvent
)
// 移除window监听事件
this.listeners.push(() => {
window.removeEventListener(eventType, handleRoutingEvent)
})
}
push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
const { current: fromRoute } = this
this.transitionTo(
location,
// 成功回调
route => {
// 处理完各种导航守卫后,
// 页面调整
pushHash(route.fullPath)
// 处理滚动行为
handleScroll(this.router, route, fromRoute, false)
// 执行用户传入的成功回调
onComplete && onComplete(route)
},
// 执行用户传入的失败回调
onAbort
)
}
// 同push
replace (location: RawLocation, onComplete?: Function, onAbort?: Function) {
const { current: fromRoute } = this
this.transitionTo(
location,
route => {
replaceHash(route.fullPath)
handleScroll(this.router, route, fromRoute, false)
onComplete && onComplete(route)
},
onAbort
)
}
go (n: number) {
window.history.go(n)
}
// 确保url正确
// hash和fullPath要一致
ensureURL (push?: boolean) {
const current = this.current.fullPath
if (getHash() !== current) {
push ? pushHash(current) : replaceHash(current)
}
}
// 获取当前Location
getCurrentLocation () {
return getHash()
}
}
// 添加hash模式路径的#
function checkFallback (base) {
const location = getLocation(base)
if (!/^\/#/.test(location)) {
window.location.replace(cleanPath(base + '/#' + location))
return true
}
}
// 调整路径的斜杆
function ensureSlash (): boolean {
const path = getHash()
if (path.charAt(0) === '/') {
return true
}
replaceHash('/' + path)
return false
}
// 获取url#之后的内容
export function getHash (): string {
let href = window.location.href
const index = href.indexOf('#')
if (index < 0) return ''
href = href.slice(index + 1)
return href
}
// 根据path返回完整的url
// /dashboard => https://test.xx.com/xxx/#/dashboard
function getUrl (path) {
const href = window.location.href
const i = href.indexOf('#')
const base = i >= 0 ? href.slice(0, i) : href
return `${base}#${path}`
}
// 优先使用history.pushState
// 这个是浏览器的原生方法
function pushHash (path) {
if (supportsPushState) {
pushState(getUrl(path))
} else {
window.location.hash = path
}
}
// 优先使用history.replaceState
// 这个是浏览器的原生方法
function replaceHash (path) {
if (supportsPushState) {
replaceState(getUrl(path))
} else {
window.location.replace(getUrl(path))
}
}
至此,history的实现也讲解结束!!!!
9 结束
本文篇幅很长,本来想要拆成多篇来写,但是感觉逻辑关系十分紧密,不好拆分,就干脆一篇讲完。
还剩下router-view和router-link的实现还没讲到,会在下一篇文章中讲解。
如果您觉得本文对您有所帮助,记得点赞、收藏、关注支持一下,我会分享更多的源码解读文章。
您也可以在本专栏查看其他源码阅读的文章,比如vuex、axios、dayjs。