前言
前端的路由模式包括了 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
方法,下一篇就来对其进行深入了解吧。