前端的路由模式包括了 Hash 模式和 History 模式。html
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
的实现。vue
vue-router 经过 new 一个 HashHistory
来实现 Hash 模式路由。vue-router
this.history = new HashHistory(this, options.base, fallback)
复制代码
三个参数分别表明:编程
HashHistory 继承 History 类,有一些属性与方法都来自于 History 类。先来看下 HashHistory 的构造函数 constructor。浏览器
构造函数主要作了四件事情。session
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()
})
})
}
复制代码
下面细讲一下这几件事情的细节。app
先来看构造函数作的第二件事情,fallback 为 true 的状况,通常是低版本的浏览器(IE9)不支持 History 模式,因此会被降级为 Hash 模式。ide
同时须要经过 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
方法作的事情就是确保 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 路由。
最后看看构造函数作的第四件事情。
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
等编程式导航,有兴趣能够直接看源码,这里就不一一讲解了,主要也是运用了上面的方法来实现。
vue-router 经过 new 一个 HTML5History
来实现 History 模式路由。
this.history = new HTML5History(this, options.base)
复制代码
HTML5History 也是继承与 History 类。
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)
})
}
}
复制代码
下面细讲一下这几件事情的细节。
先从监听滚动条滚动事件提及吧。
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
事件。这时一样会调用 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
方法来设置滚动条位置。
其中有三个方法用来对坐标进行处理的,分别是:
代码量不大,具体的代码细节感兴趣的能够看一下。
一样,HTML5History 也分别实现了 push
、replace
、go
等编程式导航。
至此,HashHistory 和 HTML5History 的实现就大体了解了。在阅读的过程当中,咱们不断地遇到了父类 History
与其 transitionTo
方法,下一篇就来对其进行深刻了解吧。