前言
前端的路由模式包括了 Hash 模式和 History 模式。
vue-router 在初始化的时候,会根据 mode
来判断使用不同的路由模式,从而 new 出了不同的对象实例。例如 history 模式就用 HTML5History
,hash 模式就用 HashHistory
。
init (app: any /* Vue component instance */) {
this.app = app
const { mode, options, fallback } = this
switch (mode) {
case 'history':
this.history = new HTML5History(this, options.base)
break
case 'hash':
this.history = new HashHistory(this, options.base, fallback)
break
case 'abstract':
this.history = new AbstractHistory(this)
break
default:
assert(false, `invalid mode: ${mode}`)
}
this.history.listen(route => {
this.app._route = route
})
}
本次重点来了解一下 HTML5History
和 HashHistory
的实现。
HashHistory
vue-router 通过 new 一个 HashHistory
来实现 Hash 模式路由。
this.history = new HashHistory(this, options.base, fallback)
三个参数分别代表:
- this:Router 实例
- base:应用的基路径
- fallback:History 模式,但不支持 History 而被转成 Hash 模式
HashHistory 继承 History 类,有一些属性与方法都来自于 History 类。先来看下 HashHistory 的构造函数 constructor。
constructor
构造函数主要做了四件事情。
- 通过 super 调用父类构造函数,这个先放一边。
- 处理 History 模式,但不支持 History 而被转成 Hash 模式的情况。
- 确保 # 后面有斜杠,没有则加上。
- 实现跳转到 hash 页面,并监听 hash 变化事件。
constructor (router: VueRouter, base: ?string, fallback: boolean) {
super(router, base)
// check history fallback deeplinking
if (fallback && this.checkFallback()) {
return
}
ensureSlash()
this.transitionTo(getHash(), () => {
window.addEventListener('hashchange', () => {
this.onHashChange()
})
})
}
下面细讲一下这几件事情的细节。
checkFallback
先来看构造函数做的第二件事情,fallback 为 true 的情况,一般是低版本的浏览器(IE9)不支持 History 模式,所以会被降级为 Hash 模式。
同时需要通过 checkFallback
方法来检测 url。
checkFallback () {
// 去掉 base 前缀
const location = getLocation(this.base)
// 如果不是以 /# 开头
if (!/^\/#/.test(location)) {
window.location.replace(
cleanPath(this.base + '/#' + location)
)
return true
}
}
先通过 getLocation 方法来去掉 base 前缀,接着正则判断 url 是否以 /# 为开头。如果不是,则将 url 替换成以 /# 为开头。最后跳出 constructor,因为在 IE9 下以 Hash 方式的 url 切换路由,它会使得整个页面进行刷新,后面的监听 hashchange 不会起作用,所以直接 return 跳出。
再来看看 checkFallback 里面调用的 getLocation
和 cleanPath
方法的实现。
getLocation
方法主要是去掉 base 前缀。在 vue-router 官方文档里搜索 base
,可以知道它是应用的基路径。
export function getLocation (base: string): string {
let path = window.location.pathname
if (base && path.indexOf(base) === 0) {
path = path.slice(base.length)
}
return (path || '/') + window.location.search + window.location.hash
}
cleanPath
方法则是将双斜杠替换成单斜杠,保证 url 路径正确。
export function cleanPath (path: string): string {
return path.replace(/\/\//g, '/')
}
ensureSlash
接下来来看看构造函数做的第三件事情。
ensureSlash
方法做的事情就是确保 url 根路径带上斜杠,没有的话则加上。
function ensureSlash (): boolean {
const path = getHash()
if (path.charAt(0) === '/') {
return true
}
replaceHash('/' + path)
return false
}
ensureSlash 通过 getHash
来获取 url 的 # 符号后面的路径,再通过 replaceHash
来替换路由。
function getHash (): string {
// We can't use window.location.hash here because it's not
// consistent across browsers - Firefox will pre-decode it!
const href = window.location.href
const index = href.indexOf('#')
return index === -1 ? '' : href.slice(index + 1)
}
由于 Firefox 浏览器的原因(源码注释里已经写出来了),所以不能通过 window.location.hash
来获取,而是通过 window.location.href
来获取。
function replaceHash (path) {
const i = window.location.href.indexOf('#')
window.location.replace(
window.location.href.slice(0, i >= 0 ? i : 0) + '#' + path
)
}
replaceHash
方法做的事情则是更换 # 符号后面的 hash 路由。
onHashChange
最后看看构造函数做的第四件事情。
this.transitionTo(getHash(), () => {
window.addEventListener('hashchange', () => {
this.onHashChange()
})
})
transitionTo
是父类 History 的一个方法,比较的复杂,主要是实现了 守卫导航 的功能。这里也暂时先放一放,以后再深入了解。
接下来的是监听 hashchange 事件,当 hash 路由发生的变化,会调用 onHashChange
方法。
onHashChange () {
if (!ensureSlash()) {
return
}
this.transitionTo(getHash(), route => {
replaceHash(route.fullPath)
})
}
当 hash 路由发生的变化,即页面发生了跳转时,首先取保路由是以斜杠开头的,然后触发守卫导航,最后更换新的 hash 路由。
HashHistory 还分别实现了 push
、replace
、go
等编程式导航,有兴趣可以直接看源码,这里就不一一讲解了,主要也是运用了上面的方法来实现。
HTML5History
vue-router 通过 new 一个 HTML5History
来实现 History 模式路由。
this.history = new HTML5History(this, options.base)
HTML5History 也是继承与 History 类。
constructor
HTML5History 的构造函数做了这么几件事情:
- 调用父类
transitionTo
方法,触发守卫导航,以后细讲。 - 监听
popstate
事件。 - 如果有滚动行为,则监听滚动条滚动。
constructor (router: VueRouter, base: ?string) {
super(router, base)
this.transitionTo(getLocation(this.base))
const expectScroll = router.options.scrollBehavior
window.addEventListener('popstate', e => {
_key = e.state && e.state.key
const current = this.current
this.transitionTo(getLocation(this.base), next => {
if (expectScroll) {
this.handleScroll(next, current, true)
}
})
})
if (expectScroll) {
window.addEventListener('scroll', () => {
saveScrollPosition(_key)
})
}
}
下面细讲一下这几件事情的细节。
scroll
先从监听滚动条滚动事件说起吧。
window.addEventListener('scroll', () => {
saveScrollPosition(_key)
})
滚动条滚动后,vue-router 就会保存滚动条的位置。这里有两个要了解的,一个是 saveScrollPosition
方法,一个是 _key
。
const genKey = () => String(Date.now())
let _key: string = genKey()
_key
是一个当前时间戳,每次浏览器的前进或后退,_key 都将作为参数传入,从而跳转的页面也能获取到。那么 _key 是做什么用呢。
来看看 saveScrollPosition
的实现就知道了:
export function saveScrollPosition (key: string) {
if (!key) return
window.sessionStorage.setItem(key, JSON.stringify({
x: window.pageXOffset,
y: window.pageYOffset
}))
}
vue-router 将滚动条位置保存在 sessionStorage,其中的键就是 _key
了。
所以每一次的浏览器滚动,滚动条的位置将会被保存在 sessionStorage 中,以便后面的取出使用。
popstate
浏览器的前进与后退会触发 popstate
事件。这时同样会调用 transitionTo 触发守卫导航,如果有滚动行为,则调用 handleScroll
方法。
handleScroll 方法代码比较多,我们先来看看是怎么使用滚动行为的。
scrollBehavior (to, from, savedPosition) {
if (savedPosition) {
return savedPosition
} else {
return { x: 0, y: 0 }
}
}
如果要模拟“滚动到锚点”的行为:
scrollBehavior (to, from, savedPosition) {
if (to.hash) {
return {
selector: to.hash
}
}
}
所以至少有三个要判断,一个是 savedPosition(即保存的滚动条位置),一个是 selector,还有一个就是 xy 坐标。
再来看 handleScroll(删掉一些判断):
handleScroll (to: Route, from: Route, isPop: boolean) {
const router = this.router
const behavior = router.options.scrollBehavior
// wait until re-render finishes before scrolling
router.app.$nextTick(() => {
let position = getScrollPosition(_key)
const shouldScroll = behavior(to, from, isPop ? position : null)
if (!shouldScroll) {
return
}
const isObject = typeof shouldScroll === 'object'
if (isObject && typeof shouldScroll.selector === 'string') {
const el = document.querySelector(shouldScroll.selector)
if (el) {
position = getElementPosition(el)
} else if (isValidPosition(shouldScroll)) {
position = normalizePosition(shouldScroll)
}
} else if (isObject && isValidPosition(shouldScroll)) {
position = normalizePosition(shouldScroll)
}
if (position) {
window.scrollTo(position.x, position.y)
}
})
}
从 if 判断开始,如果有 selector
,则获取对应的元素的坐标。
否则,则使用 scrollBehavior
返回的值作为坐标,其中有可能是 savedPosition 的坐标,也有可能是自定义的 xy 坐标。
通过一系列校验后,最终调用 window.scrollTo
方法来设置滚动条位置。
其中有三个方法用来对坐标进行处理的,分别是:
- getElementPosition:获取元素坐标
- isValidPosition:验证坐标是否有效
- normalizePosition:格式化坐标
代码量不大,具体的代码细节感兴趣的可以看一下。
同样,HTML5History 也分别实现了 push
、replace
、go
等编程式导航。
最后
至此,HashHistory 和 HTML5History 的实现就大致了解了。在阅读的过程中,我们不断地遇到了父类 History
与其 transitionTo
方法,下一篇就来对其进行深入了解吧。