vue-router源码解析系列。这是第八篇。本篇介绍源码中的html5.js
,它其实比较简单,是vue-router
在mode:history
模式下的History
子类实现,Histor
类是路由跳转的核心类,在上上篇博客中已有详细的解析。本系列解析的是官方git库中3.1.6的版本源码。
源码链接:html5.js。源码里面用的是typescript,但是不影响阅读。
1 | // 声明HTML5History 继承自History |
其中pushState
和replaceState
是在其它源码中定义的:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26export function pushState (url?: string, replace?: boolean) {
// 保存滚动位置 后面的博客再来解析
saveScrollPosition()
// 注意下面的注释
// try...catch the pushState call to get around Safari
// DOM Exception 18 where it limits to 100 pushState calls
const history = window.history
try {
// stateKey也是跟滚动行为有关的,都会在后面的博客来解析
if (replace) {
// preserve existing history state as it could be overriden by the user
const stateCopy = extend({}, history.state)
stateCopy.key = getStateKey()
history.replaceState(stateCopy, '', url)
} else {
history.pushState({ key: setStateKey(genStateKey()) }, '', url)
}
} catch (e) {
window.location[replace ? 'replace' : 'assign'](url)
}
}
export function replaceState (url?: string) {
pushState(url, true)
}
从上面这段代码可以看到pushState
本质上就是在利用history pushstate
api来处理浏览器的地址修改和浏览器历史记录的修改,只不过做了一些额外的处理,来应对safari bug和与滚动行为相关的功能。
ensureURL的作用
在base.js
中,一共有三处ensureURL
的调用:
一处是在confirmTransition
的成功回调中,位于updateRoute
之后:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17this.confirmTransition(
route,
() => {//成功
this.updateRoute(route)
onComplete && onComplete(route)
this.ensureURL() // 这个!
// fire ready cbs once
if (!this.ready) {
this.ready = true
this.readyCbs.forEach(cb => {
cb(route)
})
}
},
// 省略
一处是检测到相同路由跳转的时候:1
2
3
4
5
6
7
8if (
isSameRoute(route, current) &&
// in the case the route map has been dynamically appended to
route.matched.length === current.matched.length
) {
this.ensureURL()
return abort(new NavigationDuplicated(route))
}
第三处是在iterator
函数中调用hook
的时候:1
2
3
4
5
6hook(route, current, (to: any) => {
if (to === false || isError(to)) {
// next(false) -> abort navigation, ensure current URL
this.ensureURL(true)
abort(to)
}
为什么在这几个场景里面需要调用ensureURL
呢,观看ensureURL
的源码可以看到它是从window.location
中提取地址与this.current.fullPath
比较,不同的话,就以this.current.fullPath
去覆盖window.location
,有这个必要性吗?
先来看第三处为什么有这个必要,第三处的调用ensureURL
传入了true
,代表执行pushState
的逻辑。什么场景下会触发这样的情况呢?假设你有一个beforeRouteLeave
的钩子:1
2
3beforeRouteLeave(to, from, next) {
next(false)
}
这个钩子将会阻止浏览器离开当前页面。那么浏览器离开当前页面的方式有几种?其实就是两种,一是通过this.push
和this.replace
这两个途径主动前往其他页面;二是通过浏览器的前进或后退(用户通过浏览器交互或者是window.history.go)。第一种离开方式,最终对浏览器历史记录的修改,是受代码控制的,并且是在路由完成之后才修改,所以beforeRouteLeave
中的next(false)
执行时,浏览器历史记录并未发生变化,this.updateRoute
也没有执行,所以this.ensureURL(true)
等于没做什么事情;但是第二种离开方式,就不一样了,是浏览器的行为先触发,也就是说历史记录是先被浏览器修改了,然后再借助popstate
事件通知vue-router
,那么当beforeRouteLeave
触发时,浏览器历史记录已经被修改了,此时next(false)
中断了路由,如果不做this.ensureURL(true)
的处理,就会导致浏览器的历史记录,与vue-router
中的路由页面不一致,而借助this.ensureURL(true)
可以在这种情况,自动把浏览器回退的那条记录添加回来,就保证浏览器记录与vue-router
的hook
控制的一致性。
那么前两个ensureURL
调用有什么作用呢?我目前理解的是为了保证浏览器访问地址与this.current.fullPath
的一致性,因为在路由过程中,不排除有其它js代码他通过history.replaceState
修改了浏览器地址,导致浏览器访问地址出现与this.current.fullPath
不一致的情况,从正常角度来说,如果一个app内只有vue-router
在使用history api
,那前两处ensureURL
的调用就是多余的,但是加了也没什么关系,因为它大部分情况下,都不会执行,有这个if逻辑在呢:1
2
3
4if (getLocation(this.base) !== this.current.fullPath) {
const current = cleanPath(this.base + this.current.fullPath)
push ? pushState(current) : replaceState(current)
}
ensureURL
的作用就如ensure
的含义一样,是为了保证浏览器访问地址与vue-router
内的状态保持一致,所在vue-route
在3处意味着路由终止的位置都有加入这个调用。
特殊的isSameRoute场景
有一个场景,一定会触发isSameRoute
。先push
3条,/detail/1, /detail/2, /detail/3
,此时浏览器访问位于/detail/3
,然后点击2次后退按钮,此时浏览器访问位于/detail/1
,然后在这个页面内触发replace('/detail/2')
,那么此时的浏览器历史记录就变为了:/detail/2, /detail/2, /detail/3
,并且浏览器访问位于第1条/detail/2
,此时在页面内如果触发push('/detail/2)
,就会触发same route
。