上一篇,小编讲到在vue-router中是通过mode这一参数管制路由的实现模式的。明天就让咱们深刻去观摩vue-router源码是如何实现路由的
路由起源 – 后端路由
路由这个概念最先是后端呈现的。在以前用模板引擎开发页面时,常常会看到这样的地址
http://www.vueRouter.com/login
大抵流程能够看成这样:
- 浏览器发动申请
- 服务器监听到端口申请,如(80, 443)申请,解析URL门路
- 依据服务器的配置,返回相应信息(html字符,json数据,图片…)
- 浏览器依据数据包的Content-Type来决定如何解析数据
即:路由就是跟后端服务器的一种交互方式,通过不同的门路,来申请不同的资源,申请不同的页面是路由的其中一种性能
前端路由
随着前端利用的业务性能越来越简单、用户对于应用体验的要求越来越高,单页利用(SPA)成为前端利用的支流模式。大型单页利用最显著特点之一就是采纳前端路由零碎,通过扭转URL,在不从新申请页面的状况下,更新页面视图。
“更新视图但不从新申请页面“是前端路由原理的外围之一,目前在浏览器环境中这一性能的实现次要有两种形式:
- hash模式: 利用浏览器中#
- history 模式: 利用History interface在 HTML5中新增的办法
hash模式
hash示例:
http://www.vueRouter.com/login
hash模式:hash值是URL 的锚局部(从 # 号开始的局部)。hash值的变动并不会导致浏览器向服务器发动申请,浏览器不发动申请,从而不会刷新界面。另外每次 hash 值的变动,还会触发hashchange 这个事件,通过这个事件咱们就能够晓得 hash 值产生了哪些变动。而后咱们便能够监听hashchange来实现更新页面局部内容的操作:
function updateDom () {
// todo 匹配 hash 做 dom 更新操作
}
window.addEventListener('hashchange', updateDom)
history模式
如果不想要很丑的 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)
}
}
- 作为参数传入的字符串属性mode只是一个标记,用来批示理论起作用的对象属性history的实现类
- 在初始化对应的history之前,会对mode做一些校验:若浏览器不反对HTML5History形式(通过supportsPushState变量判断),则mode强制设为’hash’;若不是在浏览器环境下运行,则mode强制设为’abstract’
- VueRouter类中的onReady(), push()等办法只是一个代理,理论是调用的具体history对象的对应办法,在init()办法中初始化时,也是依据history对象具体的类别执行不同操作
在浏览器环境下的两种形式,别离就是在HTML5History,HashHistory两个类中实现的。他们都定义在src/history文件夹下,继承自同目录下base.js文件中定义的History类。History中定义的是专用和根底的办法,间接看会一头雾水,咱们先从HTML5History,HashHistory两个类中看着亲切的push(), replace()办法的说起。
HashHistory.push()
首先,咱们来看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()
HashHistory.replace()
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()办法
HTML5History
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次要有以下劣势:
- pushState设置的新URL能够是与以后URL同源的任意URL;而hash只可批改#前面的局部,故只可设置与以后同文档的URL
- pushState设置的新URL能够与以后URL截然不同,这样也会把记录增加到栈中;而hash设置的新值必须与原来不一样才会触发记录增加到栈中
- pushState通过stateObject能够增加任意类型的数据到记录中;而hash只可增加短字符串
- pushState可额定设置title属性供后续应用
AbstractHistory
形象模式是属于最简略的解决了,因为不波及和浏览器地址相干记录关联在一起;整体流程仍旧和 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更新时的具体逻辑不同。
欢送拍砖哈
参考资料
Vue-router
从vue-router看前端路由的两种实现
前端路由简介以及vue-router实现原理
发表回复