We read every piece of feedback, and take your input very seriously.
To see all available qualifiers, see our documentation.
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
在前两篇文章中, 分别介绍了 vue-router 的整体流程和组件, 对 history 的细节没有具体分析, 这一篇就具体来分析下 history 的实现.
vue-router
history
本文分析的 vue-router 的版本为 2.6.0
在整体流程一文中有提到, VueRouter 提供了 HTML5History、HashHistory 以及 AbstractHistory 三种方式. 在 VueRouter 实例化的同时, 会对 History 实例化, 源码在 src/index.js:
HTML5History
HashHistory
AbstractHistory
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.
mode
abstract
在整体流程和组件一文中有提到, 所有的 History 类都是在 src/history/ 目录下, 并且都继承自 src/history/base.js. 下面会分别作具体分析.
src/history/
src/history/base.js
HTML5History 是利用 HTML5 History 的 API pushState/repaceState 来完成 URL 跳转而无须重新加载页面; 源码在 src/history/html5.js 中:
pushState/repaceState
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
hash 模式是一种降级方案, 也是默认模式. history 模式存在兼容性问题, 但 hash 模式是被所有浏览器支持的. 在 [email protected] 中, 提供了 fallback 属性用于 history 模式下的降级处理, 详情见tag#v2.6.0 源码在 src/history/hash.js 中:
hash
[email protected]
fallback
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 方法中调用的:
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 被调用两次.
route
beforeEnter
此外, 我们都知道可以通过 window.location.hash 来获取 url 的 hash 部分, 但在 getHash() 方法却没有使用, 这样处理的原因是低版本的 Firefox 会对 hash 进行编码, 具体见 Firefox automatically decoding encoded parameter in url, does not happen in IE.
window.location.hash
getHash()
这种模式和浏览器无关, 一般用于 Node 端测试, 其实现也是最简单的:
// ... export class AbstractHistory extends History { constructor(router: Router, base: ?string) { // 调用基类构造函数 super(router, base) // 初始化记录栈 this.stack = [] // 记录的当前位置 this.index = -1 } // replace/go/push 的模拟... }
该模式比较抽象, 仅用一个数组来模拟浏览器的历史记录, 通过位置变量来获取当前的记录.
三种模式的初始化就大致介绍完了, 现在看看浏览器的 history 改变会发生什么?
有两种方式可以改变浏览器的 history:
router-link
浏览器的 history 发生改变时, 会触发 window 的相关的事件: hashchange 和 popstate.
window
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:
event
router
push/replace
// ... 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 的版本虽然不一样, 但整个过程大致是一样的.
transitionTo
vue-router 虽然提供了三种模式, 但是执行的整体流程差异不大, 最大的差异是在 history 改变时的具体处理逻辑不同.
The text was updated successfully, but these errors were encountered:
No branches or pull requests
在前两篇文章中, 分别介绍了
vue-router
的整体流程和组件, 对history
的细节没有具体分析, 这一篇就具体来分析下history
的实现.History 实例化
在整体流程一文中有提到, VueRouter 提供了
HTML5History
、HashHistory
以及AbstractHistory
三种方式. 在 VueRouter 实例化的同时, 会对 History 实例化, 源码在src/index.js
:从上述代码可以看出,
vue-router
提供了三种模式:mode
(默认)、history
和abstract
, 三者的区别见mode.在整体流程和组件一文中有提到, 所有的 History 类都是在
src/history/
目录下, 并且都继承自src/history/base.js
. 下面会分别作具体分析.HTML5History
HTML5History
是利用 HTML5 History 的 APIpushState/repaceState
来完成 URL 跳转而无须重新加载页面; 源码在src/history/html5.js
中:从上述代码可以看到,
history
模式是比较简单的:popstate
HashHistory
hash
模式是一种降级方案, 也是默认模式.history
模式存在兼容性问题, 但hash
模式是被所有浏览器支持的. 在[email protected]
中, 提供了fallback
属性用于history
模式下的降级处理, 详情见tag#v2.6.0 源码在src/history/hash.js
中:从代码可知, 与
HTML5History
不同, 并没有在constructor
中作hashchange
的监听,setupListeners
是在router.init
方法中调用的:是在
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 端测试, 其实现也是最简单的:
该模式比较抽象, 仅用一个数组来模拟浏览器的历史记录, 通过位置变量来获取当前的记录.
三种模式的初始化就大致介绍完了, 现在看看浏览器的
history
改变会发生什么?history 改变
有两种方式可以改变浏览器的
history
:router-link
组件浏览器的
history
发生改变时, 会触发window
的相关的事件:hashchange
和popstate
.hash
模式下:history
模式下:在vue-router 源码分析-组件一文中, 已经介绍过
router-link
组件, 其事件绑定如下:当
event
触发时, 会调用router
的push/replace
来更新路由, 其实现在src/index.js
:这里可以看出, 会去调用各子类的对应实现.
在整体流程一文中大致介绍了
transitionTo
的处理流程, 但忽略了很多细节. 如果想了解更多细节, 请移步到 vue-router 源码分析-history,vue-router
的版本虽然不一样, 但整个过程大致是一样的.小结
vue-router
虽然提供了三种模式, 但是执行的整体流程差异不大, 最大的差异是在history
改变时的具体处理逻辑不同.The text was updated successfully, but these errors were encountered: