前言
前端的路由模式包括了 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 的一个方法, 比较的复杂, 主要是实现了 守卫导航 https://router.vuejs.org/zh/guide/advanced/navigation-guards.html 的功能. 这里也暂时先放一放, 以后再深入了解.
接下来的是监听 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()
复制代码
- export function saveScrollPosition (key: string) {
- if (!key) return
- window.sessionStorage.setItem(key, JSON.stringify({
- x: window.pageXOffset,
- y: window.pageYOffset
- }))
- }
- 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
- }
- }
- }
- 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)
- }
- })
- }
来源: https://juejin.im/post/5b739d8cf265da280f3ad1fa