Skip to content
New issue

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 #55

Open
dwqs opened this issue Jun 30, 2017 · 0 comments
Open

vue-router 源码分析-History #55

dwqs opened this issue Jun 30, 2017 · 0 comments

Comments

@dwqs
Copy link
Owner

dwqs commented Jun 30, 2017

在前两篇文章中, 分别介绍了 vue-router整体流程组件, 对 history 的细节没有具体分析, 这一篇就具体来分析下 history 的实现.

本文分析的 vue-router 的版本为 2.6.0

History 实例化

整体流程一文中有提到, VueRouter 提供了 HTML5HistoryHashHistory 以及 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(默认)、historyabstract, 三者的区别见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 不会触发该事件
        // https://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 模式是被所有浏览器支持的. 在 [email protected] 中, 提供了 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 的相关的事件: hashchangepopstate.

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 触发时, 会调用 routerpush/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 改变时的具体处理逻辑不同.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant