vue-router 源码分析 - History
在前两篇文章中,分别介绍了 vue-router
的 整体流程 和 组件 , 对 history
的细节没有具体分析,这一篇就具体来分析下 history
的实现。
本文分析的 vue-router 的版本为 2.6.0。
History 实例化
在 整体流程 一文中有提到, VueRouter 提供了 HTML5History
、 HashHistory
以及 AbstractHistory
三种方式. 在 VueRouter 实例化的同时,会对 History 实例化,源码在 src/index.js
:
//...
import {HashHistory} from './history/hash'
import {HTML5History} from './history/html5'
import {AbstractHistory} from './history/abstract'
//...
export default class VueRouter{
// ...
constructor(options: RouterOptions = {}){
// ...
// 对 mode 作检测
// options.fallback 是 2.6.0 新增,表示是否对不支持 HTML5 history 的浏览器采用降级处理
// https://github.com/vuejs/vue-router/releases/tag/v2.6.0
let mode = options.mode || 'hash'
this.fallback = mode === 'history' && !supportsPushState && options.fallback !== false
if (this.fallback) {
// 兼容不支持 history 的浏览器
mode = 'hash'
}
if (!inBrowser) {
// 非浏览器环境
mode = 'abstract'
}
this.mode = mode
// 根据 mode 创建 history 实例
switch (mode) {
case 'history':
this.history = new HTML5History(this, options.base)
break
case 'hash':
// 传入 fallback
this.history = new HashHistory(this, options.base, this.fallback)
break
case 'abstract':
this.history = new AbstractHistory(this, options.base)
break
default:
if (process.env.NODE_ENV !== 'production') {
assert(false, `invalid mode: ${mode}`)
}
}
}
// ...
}
// ...
从上述代码可以看出, vue-router
提供了三种模式: mode
(默认)、 history
和 abstract
, 三者的区别见 mode .
在 整体流程 和 组件 一文中有提到,所有的 History 类都是在 src/history/
目录下,并且都继承自 src/history/base.js
. 下面会分别作具体分析。
HTML5History
HTML5History
是利用 HTML5 History 的 API pushState/repaceState
来完成 URL 跳转而无须重新加载页面; 源码在 src/history/html5.js
中:
// ...
import {History} from './base'
import {cleanPath} from '../util/path'
import {setupScroll, handleScroll} from '../util/scroll'
import {pushState, replaceState} from '../util/push-state'
export class HTML5History extends History {
constructor(router: Router, base: ?string) {
// 调用基类构造函数
super(router, base)
// 获取路由的滚动行为
const expectScroll = router.options.scrollBehavior
// 处理滚动
if (expectScroll) {
setupScroll()
}
// 监听 popstate 事件
// 点击浏览器前进后退 或者调用 history api 时触发
// pushState/replaceState 不会触发该事件
// http://javascript.ruanyifeng.com/bom/history.html#toc4
window.addEventListener('popstate', e => {
// 当前 route
const current = this.current
// 导航过渡
this.transitionTo(getLocation(this.base), route => {
if (expectScroll) {
// 处理滚动
handleScroll(router, route, current, true)
}
})
})
}
// html5 history api
go(n: number) {
window.history.go(n)
}
push(location: RawLocation, onComplete?: Function, onAbort?: Function) {
}
replace(location: RawLocation, onComplete?: Function, onAbort?: Function) {
const {current: fromRoute} = this
this.transitionTo(location, route => {
replaceState(cleanPath(this.base + route.fullPath))
handleScroll(this.router, route, fromRoute, false)
onComplete && onComplete(route)
}, onAbort)
}
// ...
}
// 获取 location
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
}
从上述代码可以看到, history
模式是比较简单的:
- 调用基类的构造函数进行初始化
- 监听
popstate
- history api 调用
HashHistory
hash
模式是一种降级方案,也是默认模式. history
模式存在兼容性问题,但 hash
模式是被所有浏览器支持的. 在 vue-router@2.6.0
中,提供了 fallback
属性用于 history
模式下的降级处理,详情见 tag#v2.6.0 源码在 src/history/hash.js
中:
// ...
import {History} from './base'
import {cleanPath} from '../util/path'
import {getLocation} from './html5'
export class HashHistory extends History {
constructor(router: Router, base: ?string, fallback: boolean) {
// 调用基类构造函数
super(router, base)
// 降级检查
if (fallback && checkFallback(this.base)) {
return
}
// 保证 hash 是以 / 开头
ensureSlash()
}
// 等到 app mount 之后才设置 hashchange 的监听
// https://github.com/vuejs/vue-router/issues/725
setupListeners() {
window.addEventListener('hashchange', () => {
if (!ensureSlash()) {
return
}
this.transitionTo(getHash(), route => {
// hash 替换 route 中的 path
replaceHash(route.fullPath)
})
})
}
push(location: RawLocation, onComplete?: Function, onAbort?: Function) {
// 在回调中调用 pushHash
this.transitionTo(...)
}
replace(location: RawLocation, onComplete?: Function, onAbort?: Function) {
// 在回调中调用 replaceHash
this.transitionTo(...)
}
go(n: number) {
window.history.go(n)
}
// ...
}
function checkFallback(base) {
// 得到不含 base 的 location 值
// hash 模式下的导航以 /# 开始的
const location = getLocation(base)
if (!/^\/#/.test(location)) {
// 如果说此时的地址不是以 /# 开头的
// 需要做一次 url 替换处理
window.location.replace(
cleanPath(base + '/#' + location)
)
return true
}
}
function ensureSlash(): boolean {
// 获取当前 url 的 hash 值
const path = getHash()
// 以 / 开头 直接返回
if (path.charAt(0) === '/') {
return true
}
// 否则替换 hash 值
replaceHash('/' + path)
return false
}
export function getHash(): string {
const href = window.location.href
const index = href.indexOf('#')
// 如果此时没有 # 则返回 ''
// 否则 取得 # 后的所有内容
return index === -1 ? '' : href.slice(index + 1)
}
// transitionTo 的回调里调用
function pushHash(path) {
window.location.hash = path
}
// transitionTo 的回调里调用
function replaceHash(path) {
const href = window.location.href
const i = href.indexOf('#')
const base = i >= 0 ? href.slice(0, i) : href
window.location.replace(`${base}#${path}`)
}
从代码可知,与 HTML5History
不同,并没有在 constructor
中作 hashchange
的监听, setupListeners
是在 router.init
方法中调用的:
// ...
// Router 初始化
init(app: any /* Vue component instance */){
if (history instanceof HTML5History) {
history.transitionTo(history.getCurrentLocation())
} else if (history instanceof HashHistory) {
const setupHashListener = () => {
// 设置 hashchange 监听
history.setupListeners()
}
history.transitionTo(
history.getCurrentLocation(),
setupHashListener,
setupHashListener
)
}
}
// ...
是在 route
切换完成后的回调中设置的,这是为了修复 vuejs/vue-router#725 , 避免 beforeEnter
是异步的情况下, beforeEnter
被调用两次。
此外,我们都知道可以通过 window.location.hash
来获取 url 的 hash
部分,但在 getHash()
方法却没有使用,这样处理的原因是低版本的 Firefox 会对 hash 进行编码,具体见 Firefox automatically decoding encoded parameter in url, does not happen in IE .
AbstractHistory
这种模式和浏览器无关,一般用于 Node 端测试,其实现也是最简单的:
// ...
export class AbstractHistory extends History {
constructor(router: Router, base: ?string) {
// 调用基类构造函数
super(router, base)
// 初始化记录栈
this.stack = []
// 记录的当前位置
this.index = -1
}
// replace/go/push 的模拟...
}
该模式比较抽象,仅用一个数组来模拟浏览器的历史记录, 通过位置变量来获取当前的记录。
三种模式的初始化就大致介绍完了,现在看看浏览器的 history
改变会发生什么?
history 改变
有两种方式可以改变浏览器的 history
:
- 点击
router-link
组件 - 点击浏览器的前进后退按钮
浏览器的 history
发生改变时,会触发 window
的相关的事件: hashchange
和 popstate
.
hash
模式下:
// ...
window.addEventListener('hashchange', () => {
// ...
this.transitionTo(getHash(), route => {
// 回调处理
})
})
// ...
history
模式下:
// ...
window.addEventListener('popstate', e => {
const current = this.current
this.transitionTo(getLocation(this.base), route => {
if (expectScroll) {
// 处理滚动
handleScroll(router, route, current, true)
}
})
})
// ...
在 vue-router 源码分析-组件 一文中,已经介绍过 router-link
组件,其事件绑定如下:
// ...
// router-link 的 event 绑定
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 (/\b_blank\b/i.test(target)) return
}
// 阻止默认行为
if (e.preventDefault) {
e.preventDefault()
}
return true
}
//...
当 event
触发时,会调用 router
的 push/replace
来更新路由,其实现在 src/index.js
:
// ...
export default class VueRouter{
// ...
constructor(options: RouterOptions = {}) {
// ...
}
// ...
push(location: RawLocation, onComplete?: Function, onAbort?: Function) {
this.history.push(location, onComplete, onAbort)
}
replace(location: RawLocation, onComplete?: Function, onAbort?: Function) {
this.history.replace(location, onComplete, onAbort)
}
// ...
}
这里可以看出,会去调用各子类的对应实现。
// ...
push(location: RawLocation, onComplete?: Function, onAbort?: Function) {
this.transitionTo(// ...)
}
replace(location: RawLocation, onComplete?: Function, onAbort?: Function) {
this.transitionTo(// ...)
}
//...
在 整体流程 一文中大致介绍了 transitionTo
的处理流程,但忽略了很多细节. 如果想了解更多细节,请移步到 vue-router 源码分析 - history , vue-router
的版本虽然不一样,但整个过程大致是一样的。
小结
vue-router
虽然提供了三种模式,但是执行的整体流程差异不大, 最大的差异是在 history
改变时的具体处理逻辑不同。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
下一篇: 不要相信一个熬夜的人说的每一句话
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论