上一篇,小编讲到在vue-router中是经过mode这一参数控制路由的实现模式的。今天就让咱们深刻去观摩vue-router源码是如何实现路由的html
路由这个概念最早是后端出现的。在之前用模板引擎开发页面时,常常会看到这样的地址前端
http://www.vueRouter.com/login
大体流程能够当作这样:vue
即:路由就是跟后端服务器的一种交互方式,经过不一样的路径,来请求不一样的资源,请求不一样的页面是路由的其中一种功能vue-router
随着前端应用的业务功能愈来愈复杂、用户对于使用体验的要求愈来愈高,单页应用(SPA)成为前端应用的主流形式。大型单页应用最显著特色之一就是采用前端路由系统,经过改变URL,在不从新请求页面的状况下,更新页面视图。json
"更新视图但不从新请求页面"是前端路由原理的核心之一,目前在浏览器环境中这一功能的实现主要有两种方式:后端
hash示例:数组
http://www.vueRouter.com/login
hash模式:hash值是URL 的锚部分(从 # 号开始的部分)。hash值的变化并不会致使浏览器向服务器发起请求,浏览器不发起请求,从而不会刷新界面。另外每次 hash 值的变化,还会触发hashchange 这个事件,经过这个事件咱们就能够知道 hash 值发生了哪些变化。而后咱们即可以监听hashchange来实现更新页面部份内容的操做:浏览器
function updateDom () { // todo 匹配 hash 作 dom 更新操做 } window.addEventListener('hashchange', updateDom)
若是不想要很丑的 hash,咱们能够用路由的 history 模式,这种模式充分利用 history.pushState API 来完成 URL 跳转而无须从新加载页面。前端框架
const router = new VueRouter({ mode: 'history', routes: [...] })
咱们找到VueRouter类的定义,摘录与mode参数有关的部分以下:服务器
export default class VueRouter { mode: string; // 传入的字符串参数,指示history类别 history: HashHistory | HTML5History | AbstractHistory; // 实际起做用的对象属性,必须是以上三个类的枚举 fallback: boolean; // 如浏览器不支持,'history'模式需回滚为'hash'模式 constructor (options: RouterOptions = {}) { let mode = options.mode || 'hash' // 默认为'hash'模式 this.fallback = mode === 'history' && !supportsPushState // 经过supportsPushState判断浏览器是否支持'history'模式 if (this.fallback) { mode = 'hash' } if (!inBrowser) { mode = 'abstract' // 不在浏览器环境下运行需强制为'abstract'模式 } this.mode = mode // 根据mode肯定history实际的类并实例化 switch (mode) { case 'history': this.history = new HTML5History(this, options.base) break case 'hash': 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}`) } } } init (app: any /* Vue component instance */) { const history = this.history // 根据history的类别执行相应的初始化操做和监听 if (history instanceof HTML5History) { history.transitionTo(history.getCurrentLocation()) } else if (history instanceof HashHistory) { const setupHashListener = () => { history.setupListeners() } history.transitionTo( history.getCurrentLocation(), setupHashListener, setupHashListener ) } history.listen(route => { this.apps.forEach((app) => { app._route = route }) }) } // VueRouter类暴露的如下方法实际是调用具体history对象的方法 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) } }
在浏览器环境下的两种方式,分别就是在HTML5History,HashHistory两个类中实现的。他们都定义在src/history文件夹下,继承自同目录下base.js文件中定义的History类。History中定义的是公用和基础的方法,直接看会一头雾水,咱们先从HTML5History,HashHistory两个类中看着亲切的push(), replace()方法的提及。
首先,咱们来看HashHistory中的push()方法:
push (location: RawLocation, onComplete?: Function, onAbort?: Function) { this.transitionTo(location, route => { pushHash(route.fullPath) onComplete && onComplete(route) }, onAbort) } function pushHash (path) { window.location.hash = path }
transitionTo()方法是父类中定义的是用来处理路由变化中的基础逻辑的,push()方法最主要的是对window的hash进行了直接赋值:
window.location.hash = route.fullPath
hash的改变会自动添加到浏览器的访问历史记录中。
那么视图的更新是怎么实现的呢,咱们来看父类History中transitionTo()方法的这么一段:
transitionTo (location: RawLocation, onComplete?: Function, onAbort?: Function) { const route = this.router.match(location, this.current) this.confirmTransition(route, () => { this.updateRoute(route) ... }) } updateRoute (route: Route) { this.cb && this.cb(route) } listen (cb: Function) { this.cb = cb }
能够看到,当路由变化时,调用了History中的this.cb方法,而this.cb方法是经过History.listen(cb)进行设置的。回到VueRouter类定义中,找到了在init()方法中对其进行了设置:
init (app: any /* Vue component instance */) { this.apps.push(app) history.listen(route => { this.apps.forEach((app) => { app._route = route }) }) }
根据注释,app为Vue组件实例,但咱们知道Vue做为渐进式的前端框架,自己的组件定义中应该是没有有关路由内置属性_route,若是组件中要有这个属性,应该是在插件加载的地方,即VueRouter的install()方法中混合入Vue对象的,查看install.js源码,有以下一段:
export function install (Vue) { Vue.mixin({ beforeCreate () { if (isDef(this.$options.router)) { this._router = this.$options.router this._router.init(this) Vue.util.defineReactive(this, '_route', this._router.history.current) } registerInstance(this, this) }, }) }
经过Vue.mixin()方法,全局注册一个混合,影响注册以后全部建立的每一个 Vue 实例,该混合在beforeCreate钩子中经过Vue.util.defineReactive()定义了响应式的_route属性。所谓响应式属性,即当_route值改变时,会自动调用Vue实例的render()方法,更新视图。
总结,从设置路由改变到视图更新的流程以下:
$router.push() --> HashHistory.push() --> History.transitionTo() --> History.updateRoute() --> {app._route = route} --> vm.render()
replace()方法与push()方法不一样之处在于,它并非将新路由添加到浏览器访问历史的栈顶,而是替换掉当前的路由:
replace (location: RawLocation, onComplete?: Function, onAbort?: Function) { this.transitionTo(location, route => { replaceHash(route.fullPath) onComplete && onComplete(route) }, onAbort) } function replaceHash (path) { const i = window.location.href.indexOf('#') window.location.replace( window.location.href.slice(0, i >= 0 ? i : 0) + '#' + path ) }
以上讨论的VueRouter.push()和VueRouter.replace()是能够在vue组件的逻辑代码中直接调用的,除此以外在浏览器中,用户还能够直接在浏览器地址栏中输入改变路由,所以VueRouter还须要能监听浏览器地址栏中路由的变化,并具备与经过代码调用相同的响应行为。在HashHistory中这一功能经过setupListeners实现:
setupListeners () { window.addEventListener('hashchange', () => { if (!ensureSlash()) { return } this.transitionTo(getHash(), route => { replaceHash(route.fullPath) }) }) }
该方法设置监听了浏览器事件hashchange,调用的函数为replaceHash,即在浏览器地址栏中直接输入路由至关于代码调用了replace()方法
History interface是浏览器历史记录栈提供的接口,经过back(), forward(), go()等方法,咱们能够读取浏览器历史记录栈的信息,进行各类跳转操做。
window.history.pushState(stateObject, title, URL) window.history.replaceState(stateObject, title, URL)
咱们来看vue-router中的源码:
push (location: RawLocation, onComplete?: Function, onAbort?: Function) { const { current: fromRoute } = this this.transitionTo(location, route => { pushState(cleanPath(this.base + route.fullPath)) handleScroll(this.router, route, fromRoute, false) onComplete && onComplete(route) }, onAbort) } 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) } // src/util/push-state.js export 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 { if (replace) { history.replaceState({ key: _key }, '', url) } else { _key = genKey() history.pushState({ key: _key }, '', url) } } catch (e) { window.location[replace ? 'replace' : 'assign'](url) } } export function replaceState (url?: string) { pushState(url, true) }
在HTML5History中添加对修改浏览器地址栏URL的监听是直接在构造函数中执行的:
constructor (router: Router, base: ?string) { window.addEventListener('popstate', e => { const current = this.current this.transitionTo(getLocation(this.base), route => { if (expectScroll) { handleScroll(router, route, current, true) } }) }) }
固然了HTML5History用到了HTML5的新特特性,是须要特定浏览器版本的支持的,前文已经知道,浏览器是否支持是经过变量supportsPushState来检查的:
// src/util/push-state.js export const supportsPushState = inBrowser && (function () { const ua = window.navigator.userAgent if ( (ua.indexOf('Android 2.') !== -1 || ua.indexOf('Android 4.0') !== -1) && ua.indexOf('Mobile Safari') !== -1 && ua.indexOf('Chrome') === -1 && ua.indexOf('Windows Phone') === -1 ) { return false } return window.history && 'pushState' in window.history })()
根据MDN的介绍,调用history.pushState()相比于直接修改hash主要有如下优点:
抽象模式是属于最简单的处理了,由于不涉及和浏览器地址相关记录关联在一块儿;总体流程依旧和 HashHistory 是同样的,只是这里经过数组来模拟浏览器历史记录堆栈信息。
export class AbstractHistory extends History { index: number; stack: Array<Route>; // ... push (location: RawLocation) { this.transitionTo(location, route => { // 更新历史堆栈信息 this.stack = this.stack.slice(0, this.index + 1).concat(route) // 更新当前所处位置 this.index++ }) } replace (location: RawLocation) { this.transitionTo(location, route => { // 更新历史堆栈信息 位置则不用更新 由于是 replace 操做 // 在堆栈中也是直接 replace 掉的 this.stack = this.stack.slice(0, this.index).concat(route) }) } // 对于 go 的模拟 go (n: number) { // 新的历史记录位置 const targetIndex = this.index + n // 超出返回了 if (targetIndex < 0 || targetIndex >= this.stack.length) { return } // 取得新的 route 对象 // 由于是和浏览器无关的 这里获得的必定是已经访问过的 const route = this.stack[targetIndex] // 因此这里直接调用 confirmTransition 了 // 而不是调用 transitionTo 还要走一遍 match 逻辑 this.confirmTransition(route, () => { // 更新 this.index = targetIndex this.updateRoute(route) }) } ensureURL () { // noop } }
整个的和 history 相关的代码到这里已经分析完毕了,虽然有三种模式,可是总体执行过程仍是同样的,惟一差别的就是在处理location更新时的具体逻辑不一样。
欢迎拍砖哈