vue-router源码拾遗
但凡是使用过Vue-router的前端开发工作者,都知道Vue-router包括两种实现模式:hash和history。为了对这两种模式有更直观的认识,我选择简略阅读源码并在此记录。
vue-router是Vue.js框架的路由插件,下面从源码入手,边看代码边看原理,由浅入深学习vue-router实现两种路由模式的方法。
vue-router的模式参数
模式参数:mode
const router = new VueRouter({
mode: 'history',
routes: [...]
})
创建VueRouter实例的时候,直接将mode以构造函数参数的形式传入,在VueRouter类定义(src/index.js)中,使用如下
export default class VueRouter {
mode: string;//传入的string类型
history: HashHistory | HTML5History | AbstractHistory; //实际调用的对象属性
matcher: Matcher; // url正则匹配方法
fallback: boolean; // 如果浏览器不支持,history需要回滚为hash模式
...
let mode = options.mode || 'hash' //默认是hash模式
this.fallback = mode === 'history' && !supportsPushState && options.fallback !== false
if (this.fallback) {
mode = 'hash' // 传入模式为history,但是浏览器不支持,则回滚
}
if (!inBrowser) {
mode = 'abstract' // 不在浏览器环境下,强制设置为‘abstract’
}
this.mode = mode
// 根据mode的值,确定要使用的类,并实例化
switch(this.mode) {
case 'history':
this.history = new HTML5History(this, option.base)
break;
case 'hash:
this.history = new HashHistory(this, option.base, this.fallback)
break;
case 'abstract':
this.history = new AbstractHistory(this, option.base)
break;
default:
...
}
// 通过mode确定好history实例后,进行实例的初始化和监听
init (app: any /* Vue component instance */) {
const history = this.history
// 根据history的类别执行相应的初始化操作和监听
if (history instanceof HTML5History) {
// 'history'模式初始化
history.transitionTo(history.getCurrentLocation())
} else if (history instanceof HashHistory) {
const setupHashListener = () => {
history.setupListeners()
}
// 'hash'模式初始化
history.transitionTo(
history.getCurrentLocation(),
setupHashListener,
setupHashListener
)
}
// 添加监听
history.listen(route => {
this.apps.forEach((app) => {
app._route = route
})
})
}
}
自此,基本完成了对mode字段的前期校验和后期使用,history的实例也已经初始化完成。接下来就是路由的一些基本操作,比如push(),replace(),onReady()等。
接着以上源码往下看,以下代码只留存关键行
onReady (cb: Function, errorCb?: Function) {
this.history.onReady(cb, errorCb)
}
push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
// this.history会经过初始化操作,这里就会调用原生history的push方法
this.history.push(location, resolve, reject)
// this.history.push(location, onComplete, onAbort)
}
replace (location: RawLocation, onComplete?: Function, onAbort?: Function) {
// replace也是原生方法的调用
this.history.replace(location, resolve, reject)
// this.history.replace(location, onComplete, onAbort)
}
从以上代码可看出,VueRouter类中的方法可以认为是一个代理,实际是调用的具体history对象的对应方法,在init()方法中初始化时,会根据history对象具体的类别执行不同的操作。
说了这么多,什么时候调以及如何调用HTML5History和HashHistory有了大致的了解,接下来就看看这两个类是怎么实现的
HashHistory
源码文件路径:src/history/hash
原理回顾
hash("#")符号加在URL上只是用于指示网页中的位置,#符号本身及它后边的字符称为hash,可以通过window.location.hash属性读取。
- hash虽然加在URL中,但是并不会被包含在http请求中,它对服务器端无用,因此改变hash并不会重载页面。
- hash可以添加监听事件(window.addEventListener('hashchange', fn, false))。
- hash每次被改动后,都会在浏览器访问历史中增加一个记录。
hash的以上特点,就注定可以用来实现“更新视图但不重新请求页面”
代码解读
构造函数
// 继承History基类
export class HashHistory extends History {
constructor (router: Router, base: ?string, fallback: boolean) {
// 基类构造器
super(router, base)
// check history fallback deeplinking
if (fallback && checkFallback(this.base)) {
// 降级检查,如果降级了并且做了降级处理,直接返回
return
}
ensureSlash()
}
function checkFallback (base) {
// 得到除去base的真正的location值
const location = getLocation(base)
if (!/^/#/.test(location)) {
// 如果不是以/#开头,就降级处理,降级为hash模式下的/#开头
window.location.replace(cleanPath(base + '/#' + location))
return true
}
}
function ensureSlash (): boolean {
// 获取hash
const path = getHash()
if (path.charAt(0) === '/') {
// 如果是/开头,直接返回
return true
}
// 不是/开头,就手动增加开头/
replaceHash('/' + path)
return false
}
}
这里什么时候算降级呢?就是不支持history api的情况
push()
push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
// transitionTo():父类中定义用来处理路由变化的基础逻辑的方法
this.transitionTo(
location,
route => {
pushHash(route.fullPath)
handleScroll(this.router, route, fromRoute, false)
onComplete && onComplete(route)
},
onAbort
)
}
function pushHash (path) {
// supportsPushState:src/util/push-state.js中定义,浏览器环境并且支持pushState()方法
if (supportsPushState) {
pushState(getUrl(path))
} else {
// 不支持,直接赋值操作
window.location.hash = path
}
}
以上代码可看出push()方法只是对url中的hash进行了基本的赋值,那视图如何更新呢?继续看父类中的transitionTo()方法
源码路径:src/history/base.js
transitionTo (location: RawLocation, onComplete?: Function, onAbort?: Function) {
// 路由匹配过程,这个后边详解
const route = this.router.match(location, this.current)
this.confirmTransition(
route,
() => {
this.updateRoute(route)
...
}
)
}
updateRoute (route: Route) {
...
this.cb && this.cb(route)
...
}
在路由改变之后,调用了History中的this.cb方法,而this.cb的定义如下:
listen (cb: Function) {
this.cb = cb
}
listen方法作为基础类的方法,我们不难想到,在主类index.js中history初始化之后,必然有使用的痕迹
init (app: any /* Vue component instance */) {
this.apps.push(app)
history.listen(route => {
this.apps.forEach((app) => {
app._route = route
})
})
}
根据注释可以看出入参的app是Vue组件实例,但是我们知道Vue本身的组件定义中是没有有关路由内置属性_route的,VueRouter.install = install
如果组件中要有这个属性,应该是在插件加载的地方,即VueRouter的install()方法里,我们F12看下
export function install (Vue) {
// 混入
Vue.mixin({
beforeCreate () {
if (isDef(this.$options.router)) {
// 混入到哪个组件,this就指向哪个组件
this._routerRoot = this
// VueRouter实例
this._router = this.$options.router
// VueRouter中的init(),由于这是全局混入,所以this===Vue组件实例
this._router.init(this)
Vue.util.defineReactive(this, '_route', this._router.history.current)
} else {
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.mixin()全局注册了混入,影响注册后创建的每个Vue实例,在beforeCreate钩子函数中,使用Vue.util.defineReactive设置了响应式的_route属性,当路由当_route值变化,就会调用Vue实例的render()方法,更新视图。
replace()
replace()与push()略有不同,它不是将新路由添加到浏览器访问历史记录,而是直接替换当前路由
// index.js
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/hash.js
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
)
}
function replaceHash (path) {
if (supportsPushState) {
// 这个方法干啥的呢
replaceState(getUrl(path))
} else {
// 这里调用原生的replace方法
window.location.replace(getUrl(path))
}
}
// util/push-state.js
export function replaceState (url?: string) {
// 这里调的和push()一样,都是走pushState()方法
pushState(url, true)
}
export function pushState (url?: string, replace?: boolean) {
const history = window.history
if (replace) {
// 如果是replace,进这里
// 重写history.state进新对象,防止修改造成影响
const stateCopy = extend({}, history.state)
stateCopy.key = getStateKey()
// 调用原生replaceState
history.replaceState(stateCopy, '', url)
} else {
// 不是replace,调用原生的pushState
history.pushState({ key: setStateKey(genStateKey()) }, '', url)
}
}
地址栏监听
以上分析的push和replace方法,是在Vue组件的逻辑代码中调用,但是在实际场景中,还可以直接对地址栏进行修改,达到改变路由的目的,这在HashHistory是如何实现的,继续去HashHistory类代码中寻找答案。可以发现有这样一个方法setupListeners(),里边的window.addEventListener()跟上文提到的HashHistory的特性2不谋而合
setupListeners () {
const router = this.router
const expectScroll = router.options.scrollBehavior
const supportsScroll = supportsPushState && expectScroll
// 重点是这里面的hashchange监听事件
window.addEventListener(
supportsPushState ? 'popstate' : 'hashchange',
() => {
const current = this.current
if (!ensureSlash()) {
// 如果不符合hash要求
return
}
// 这里是基类中的视图更新
this.transitionTo(getHash(), route => {
if (supportsScroll) {
handleScroll(this.router, route, current, true)
}
if (!supportsPushState) {
// 直接修改地址栏,调用replaceHash方法
replaceHash(route.fullPath)
}
})
}
)
}
HTML5History
源码文件路径:src/history/html5
源码解读
History interface是浏览器历史记录栈提供的接口,通过back(),forward().go()等方法,实现读取浏览器历史记录栈信息的功能,进行跳转操作。从HTML5开始,提供两个新的方法:pushState()和replaceState(),这在上文已经接触过,直接打开源码src/html5.js
push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
const { current: fromRoute } = this
this.transitionTo(location, route => {
//这里直接调用基层的pushState
pushState(cleanPath(this.base + route.fullPath))
handleScroll(this.router, route, fromRoute, false)
onComplete && onComplete(route)
}, onAbort)
}
replace (location: RawLocation, onComplete?: Function, onAbort?: Function) {
const { current: fromRoute } = this
this.transitionTo(location, route => {
// 这里直接调用replaceState
replaceState(cleanPath(this.base + route.fullPath))
handleScroll(this.router, route, fromRoute, false)
onComplete && onComplete(route)
}, onAbort)
}
相较HashHistory方式,HTML5History的push和replace在route的处理上,少了赋值层,
// hash=>pushHash
window.location.hash = path
// hash=>replaceHash
window.location.replace(getUrl(path))
而是直接调用了history中的基层方法
地址栏监听
// 获取路径
const initLocation = getLocation(this.base)
// 添加监听方法
window.addEventListener('popstate', e => {
const current = this.current
// Avoiding first `popstate` event dispatched in some browsers but first
// history route not updated since async guard at the same time.
const location = getLocation(this.base)
if (this.current === START && location === initLocation) {
return
}
this.transitionTo(location, route => {
if (supportsScroll) {
handleScroll(router, route, current, true)
}
})
})
AbstractHistory
源码解读
export class AbstractHistory extends History {
index: number
// 用于存放路由记录的数组,仿浏览器历史记录栈
stack: Array<Route>
constructor (router: Router, base: ?string) {
super(router, base)
this.stack = []
this.index = -1
}
// 以push为例,任何路由操作都是操作stack数组
push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
this.transitionTo(
location,
route => {
this.stack = this.stack.slice(0, this.index + 1).concat(route)
this.index++
onComplete && onComplete(route)
},
onAbort
)
}
...
}
AbstractHistory的操作方式较为简单,初始化一个仿浏览器历史记录栈的路由数组,push(),replace()等操作,都对该数组进行操作。
路由匹配过程
VueRouter的基本工作流程大致就如上分析,这里有个很重要的点,需要单独分析一下,就是路由的匹配过程。
为了方便代码解读,需要先认清几个基本概念:
- Location:对url的结构化描述,例如:{path: '/main', query: {age: 25}, name: 'mainPage'}等
- rowLocation:type rowLocation = Location | string
- Route:表示一条路由
export interface Route {
path: string
name?: string | null
hash: string
query: Dictionary<string | (string | null)[]>
params: Dictionary<string>
fullPath: string
matched: RouteRecord[] //匹配的所有的RouteRecord对象
redirectedFrom?: string
meta?: any
}
- RouteRecord:表示路由记录对象
export interface RouteRecord {
path: string
regex: RegExp // 正则规则
components: Dictionary<Component>
instances: Dictionary<Vue> // vue实例
name?: string
parent?: RouteRecord
redirect?: RedirectOption
matchAs?: string
meta: any
beforeEnter?: (
route: Route,
redirect: (location: RawLocation) => void,
next: () => void
) => any // 钩子函数
props:
| boolean
| Object
| RoutePropsFunction
| Dictionary<boolean | Object | RoutePropsFunction>
}
对以上类型有基本认识后,来看下matcher实现代码
export function createMatcher (routes: Array<RouteConfig>, router: VueRouter): Matcher {
// 这个方法往下看
const { pathList, pathMap, nameMap } = createRouteMap(routes)
// 动态添加路由配置
function addRoutes (routes) {
createRouteMap(routes, pathList, pathMap, nameMap)
}
// match主函数,根据传入的raw和currentRoute 计算出新的路径并返回
function match (raw: RawLocation, currentRoute?: Route, redirectedFrom?: Location): Route {
// 获取路由path query hash等
const location = normalizeLocation(raw, currentRoute, false, router)
const { name } = location
if (name) {
// 如果传入了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)
// 检查获取的路由参数类型,保证是object
if (typeof location.params !== 'object') {
location.params = {}
}
// vueRouter的参数对应赋值给浏览器的路由参数
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) {
// 没有name参数,传了path
location.params = {}
// pathList
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)
}
}
在源码文件src/create-route-map
中找到createRouteMap()方法,入参传了路由配置routes,返回的是三个属性:
- pathList:存储所有的path值
- pathMap:path到RouteRecord的映射关系
- nameMap:name到RouteRecord的映射关系
其中的主要函数分析一下
function addRouteRecord (
pathList: Array<string>,
pathMap: Dictionary<RouteRecord>,
nameMap: Dictionary<RouteRecord>,
route: RouteConfig,
parent?: RouteRecord,
matchAs?: string
) {
const { path, name } = route
const pathToRegexpOptions: PathToRegexpOptions =
route.pathToRegexpOptions || {}
// path格式清理,确保格式正确
const normalizedPath = normalizePath(path, parent, pathToRegexpOptions.strict)
const record: RouteRecord = {
path: normalizedPath,
regex: compileRouteRegex(normalizedPath, pathToRegexpOptions),
components: route.components || { default: route.component },
instances: {},
name,
parent,
matchAs,
redirect: route.redirect,
beforeEnter: route.beforeEnter,
meta: route.meta || {},
props:
route.props == null
? {}
: route.components
? route.props
: { default: route.props }
}
// 嵌套路由递归调用
if (route.children) {
route.children.forEach(child => {
const childMatchAs = matchAs
? cleanPath(`${matchAs}/${child.path}`)
: undefined
addRouteRecord(pathList, pathMap, nameMap, child, record, childMatchAs)
})
}
// 记录path并且创建path到RouteRecord的映射
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
}
addRouteRecord(
pathList,
pathMap,
nameMap,
aliasRoute,
parent,
record.path || '/' // matchAs
)
}
}
// 创建name 到 RouteRecord 的映射
if (name) {
if (!nameMap[name]) {
nameMap[name] = record
}
}
}
Vue-router组件分析
router-link
routerLink作为点击跳转的组件,要实现的也是类似标签的能力
props: {
to: {
type: toTypes,
required: true
}, // 跳转路径
tag: {
type: String,
default: 'a'
}, // 渲染出的标签类型,默认是<a>
exact: Boolean,
append: Boolean,
replace: Boolean, // 调用replace还是push
activeClass: String, // 激活后的class
exactActiveClass: String,
ariaCurrentValue: {
type: String,
default: 'page'
},
event: {
type: eventTypes,
default: 'click'
} // 可以触发导航的事件,默认是click,还可以是'mouseover' 等
},
// 主要方法,render()
render (h: Function) {
const router = this.$router
const current = this.$route
const { location, route, href } = router.resolve(
this.to,
current,
this.append
)
const classes = {}
// ...这里省去一些赋值初始化语句
// handler方法用于绑定
// 绑定后,点击触发
const handler = e => {
if (guardEvent(e)) {
if (this.replace) {
//replace逻辑,触发VueRouter的replace()更新路由
router.replace(location, noop)
} else {
// 添加逻辑,触发VueRouter的push()更新路由
router.push(location, noop)
}
}
}
// 绑定click事件,忽略所有默认的点击事件
const on = { click: guardEvent }
if (Array.isArray(this.event)) {
// 如果传了event为数组类型
this.event.forEach(e => {
on[e] = handler
})
} else {
on[this.event] = handler
}
// 赋值初始化操作,创建元素需要附加的数据
const data: any = { class: classes }
if (this.tag === 'a') {
// 如果是<a>元素,直接绑定
data.on = on
data.attrs = { href, 'aria-current': ariaCurrentValue }
} else {
// 找到第一个<a>标签
const a = findAnchor(this.$slots.default)
if (a) {
// 为<a>标签绑定事件
a.isStatic = false
const aData = (a.data = extend({}, a.data))
aData.on = aData.on || {}
// transform existing events in both objects into arrays so we can push later
for (const event in aData.on) {
const handler = aData.on[event]
if (event in on) {
aData.on[event] = Array.isArray(handler) ? handler : [handler]
}
}
// append new listeners for router-link
for (const event in on) {
if (event in aData.on) {
// on[event] is always a function
aData.on[event].push(on[event])
} else {
aData.on[event] = handler
}
}
// 标签的所有属性=>attrs
const aAttrs = (a.data.attrs = extend({}, a.data.attrs))
// 赋值href属性
aAttrs.href = href
aAttrs['aria-current'] = ariaCurrentValue
} else {
// 没有<a>标签,就为当前元素绑定
data.on = on
}
}
// 创建Vnode $createElement
return h(this.tag, data, this.$slots.default)
}
}
// click点击事件自定义
function guardEvent (e) {
// 忽略带有功能键的点击
if (e.metaKey || e.altKey || e.ctrlKey || e.shiftKey) return
// 忽略点击引起的默认动作
if (e.defaultPrevented) return
// 忽略鼠标右键方法
if (e.button !== undefined && e.button !== 0) return
// 忽略 `target="_blank"` 属性
if (e.currentTarget && e.currentTarget.getAttribute) {
const target = e.currentTarget.getAttribute('target')
if (/_blank/i.test(target)) return
}
// 阻止默认行为,防止跳转
if (e.preventDefault) {
e.preventDefault()
}
return true
}
function findAnchor (children) {
if (children) {
let child
for (let i = 0; i < children.length; i++) {
child = children[i]
if (child.tag === 'a') {
return child
}
if (child.children && (child = findAnchor(child.children))) {
return child
}
}
}
}
router-view
作为视图显示组件,要根据routerLink的点击结果显示对应的Vue组件视图,还要可实现层级嵌套展示
render (_, { props, children, parent, data }) {
// props: props
// children: 所有子节点
// parent: 父组件的引用
// data: 要创建的组件的属性
data.routerView = true
// 父组件的$createElement函数
const h = parent.$createElement
const name = props.name
// 父组件(当前)的路由对象
const route = parent.$route
// 父组件(当前)的路由缓存信息
const cache = parent._routerViewCache || (parent._routerViewCache = {})
// 嵌套的层级深度
let depth = 0
// 是否keepAlive内
let inactive = false
while (parent && parent._routerRoot !== parent) {
// 如果父组件存在,并且父组件还存在父组件,就获取父组件的Vnode的data,没有就{}
const vnodeData = parent.$vnode ? parent.$vnode.data : {}
if (vnodeData.routerView) {
// 父组件routerView存在,层级+1
depth++
}
if (vnodeData.keepAlive && parent._directInactive && parent._inactive) {
// 如果设置了keepAlive
inactive = true
}
// 向上一级
parent = parent.$parent
}
// 赋值嵌套层级
data.routerViewDepth = depth
// 设置keepAlive
if (inactive) {
// 获取当前缓存
const cachedData = cache[name]
// 获取当前缓存的Vue组件
const cachedComponent = cachedData && cachedData.component
if (cachedComponent) {
// 创建缓存的组件
return h(cachedComponent, data, children)
} else {
// 没找到组件就创建空组件
return h()
}
}
// 根据嵌套层级获取当前要显示的路由对象
const matched = route.matched[depth]
// 要显示的组件
const component = matched && matched.components[name]
// 为空 将当前cache设为null,请创建空组件
if (!matched || !component) {
cache[name] = null
return h()
}
cache[name] = { component }
// attach instance registration hook
// this will be called in the instance's injected lifecycle hooks
data.registerRouteInstance = (vm, val) => {
// val could be undefined for unregistration
const current = matched.instances[name]
if (
(val && current !== vm) ||
(!val && current === vm)
) {
matched.instances[name] = val
}
}
// also register instance in prepatch hook
// in case the same component instance is reused across different routes
;(data.hook || (data.hook = {})).prepatch = (_, vnode) => {
matched.instances[name] = vnode.componentInstance
}
// register instance in init hook
// in case kept-alive component be actived when routes changed
data.hook.init = (vnode) => {
if (vnode.data.keepAlive &&
vnode.componentInstance &&
vnode.componentInstance !== matched.instances[name]
) {
matched.instances[name] = vnode.componentInstance
}
}
const configProps = matched.props && matched.props[name]
// save route and configProps in cachce
if (configProps) {
extend(cache[name], {
route,
configProps
})
fillPropsinData(component, data, route, configProps)
}
// 最后创建组件
return h(component, data, children)
}
总结
vue-router源码大致先记录在这里,仅作为学习记录,供以后回头查看。
如有不妥之处,还请指正。