原由是由于咱们团队内部在进行发布系统迁移的时候,遇到了个路由相关的基础问题。当时隐约知道是为何,可是对于路由 由于咱们平时过于熟悉,以致于忘了其不少基础特性,并无第一时间快速的排查问题。对此深感惭愧。javascript
因而就找个时间补一补路由相关的基础知识,而且查看了下 vue-router 的源码,从大致上先讲一下 vue-router 是怎么和 vue 结合作路由的管理的。后续还会有更详细的源码分析~~~html
本篇文章主要内容以下前端
咱们既然是要聊一下前端路由,那么首先应该知道什么是路由。vue
路由这个概念原本是后端提出来的。很早的时候,都是服务端渲染,那时候先后端尚未分离,服务端将整个页面返回,响应过程基本都是这样的:java
那么什么是路由呢?咱们能够简单理解为和服务器交互的一种方式,经过不一样的路由咱们去请求不一样的资源(HTML 资源只是其中的一种方式)node
咱们上面介绍的其实就是后端路由。ajax
后端路由又可称之为服务器端路由,由于对于服务器来讲,当接收到客户端发来的HTTP请求,就会根据所请求的URL,来找到相应的映射函数,而后执行该函数,并将函数的返回值发送给客户端。vue-router
对于最简单的静态资源服务器,能够认为,全部URL的映射函数就是一个文件读取操做。 对于动态资源,映射函数多是一个数据库读取操做,也多是进行一些数据的处理,等等。数据库
而后根据这些读取的数据,在服务器端就使用相应的模板来对页面进行渲染后,再返回渲染完毕的页面。后端
前端路由是因为 ajax 的崛起而诞生的,咱们你们都知道 ajax 是浏览器为了实现异步加载的一种技术方案,刚刚也介绍了,在先后端没有分离的时候,服务端都是直接将整个 HTML 返回,用户每次一个很小的操做都会引发页面的整个刷新(再加上以前的网速还很慢,因此用户体验可想而知)
在 90年代末的时候,微软首先实现了 ajax(Asynchronous JavaScript And XML) 这个技术,这样用户每次的操做就能够不用刷新整个页面了,用户体验就大大提高了。
又随着技术的发展,慢慢三大框架称霸了前端圈,成为前端开发的主力军。前端也能够作更多的事情了,陆陆续续也有了模块化和组件化的概念。
固然还有单页应用、MVVM也陆陆续续出如今了前端er的视野。
至此,前端开发者可以开发出更加大型的应用,职能也变得更增强大了,那么这和前端路由有什么关系呢?
异步交互体验的更高级版本就是 SPA —— 单页应用。单页应用不只仅是在页面交互是无刷新的,连页面跳转都是无刷新的。既然页面的跳转是无刷新的,也就是再也不向后端请求返回 html。
那么,一个大型应用一般会有几十个页面(url 地址)相互跳转,怎么前端怎么知道 url 对应展现什么内容呢?
答案就是 —— 前端路由
能够理解为,前端路由就是将以前服务端根据 url 的不一样返回不一样的页面的任务交给前端来作。
优势:用户体验好,不须要每次都从服务器所有获取,快速展示给用户 缺点:使用浏览器的前进,后退键的时候会从新发送请求,没有合理地利用缓存,单页面没法记住以前滚动的位置,没法在前进,后退的时候记住滚动的位置。
在了解了什么是前端路由和前端路由解决了什么问题以后,咱们再来深刻了解下前端路由实现的原理
前端路由的实现原理其实很简单,本质上就是检测 URL 的变化,经过拦截 URL而后解析匹配路由规则。
以前,你们都是经过 hash 来实现实现路由的,hash 路由的方式就和 <a>
连接的锚点是同样的,在地址后面增长 #
,例如个人我的博客 https://cherryblog.site/#/
#
及后面的内容,咱们称之为 location 的 hash
而后咱们再点开其余的 tab 页面,发现虽然浏览器地址栏的 url 改变了,可是页面却没有刷新。打开控制台,咱们能够看到切换 tab 只是向服务端发送了请求接口数据的接口,并无从新请求 html 的资源。 这是由于 hash 的变化不会致使浏览器向服务端发送请求,因此也就不会刷新页面。可是每次 hash 的变化,都会触发
haschange
事件。因此咱们就能够经过监听 haschange
的变化来作出响应。
在咱们如今(2021)的前端开发中,一般都是会有一个根节点 <div id="root"></div>
,而后将所要展现的内容插入到这个根节点之中。而后根据路由的不一样,更换插入的内容组件。
hash 路由有一个问题就是由于有 #
因此不是那么“好看”
14年后,由于 HTML5 标准发布。多了两个 API, pushState
和 replaceState
,经过这两个 API 能够改变 url 地址且不会发送请求。同时还有 onpopstate
事件。经过这些就能用另外一种方式来实现前端路由了,但原理都是跟 hash 实现相同的。
用了 HTML5 的实现,单页路由的 url 就不会多出一个 #
,变得更加美观。但由于没有 #
号,因此当用户刷新页面之类的操做时,浏览器仍是会给服务器发送请求。为了不出现这种状况,因此这个实现须要服务器的支持,须要把全部路由都重定向到根页面。具体能够见:[HTML5 histroy 模式](HTML5 History 模式)
注意,直接调用 history.popState()
和 history.poshState()
并不会触发 popState
。只有在作出浏览器的行为才会调用 popState
,好比点击浏览器的前进后退按钮或者JS调用 history.back()
或者 history.forward()
那咱们来看一下 vue-router 是怎么结合 vue 一块儿实现前端路由的。
总的来讲就是使用 Vue.util.defineReactive 将实例的 _route 设置为响应式对象。而 push, replace 方法会主动更新属性 _route。而 go,back,或者点击前进后退的按钮则会在 onhashchange 或者 onpopstate 的回调中更新 _route。_route 的更新会触发 RoterView 的从新渲染。
而后咱们就在具体的看下是怎么实现的
Vue提供了插件注册机制是,每一个插件都须要实现一个静态的 install
方法,当执行 Vue.use
注册插件的时候,就会执行 install
方法,该方法执行的时候第一个参数强制是 Vue
对象。
在 vue-router 中,install 方法以下。
import View from './components/view'
import Link from './components/link'
// 导出 vue 实例
export let _Vue
// install 方法 当 Vue.use(vueRouter)时 至关于 Vue.use(vueRouter.install())
export function install (Vue) {
// 若是已经注册过了而且已经有了 vue 实例,那么直接返回
if (install.installed && _Vue === Vue) return
install.installed = true
// 保存Vue实例,方便其它插件文件使用
_Vue = Vue
const isDef = v => v !== undefined
// 递归注册实例的方法
const registerInstance = (vm, callVal) => {
let i = vm.$options._parentVnode
if (isDef(i) && isDef(i = i.data) && isDef(i = i.registerRouteInstance)) {
i(vm, callVal)
}
}
/** * 递归的将全部的 vue 组件混入两个生命周期 beforeCreate 和 destroyed * 在 beforeCreated 中初始化 vue-router,并将_route响应式 */
Vue.mixin({
beforeCreate () {
// 初始化 vue-router
if (isDef(this.$options.router)) {
this._routerRoot = this
this._router = this.$options.router
this._router.init(this)
Vue.util.defineReactive(this, '_route', this._router.history.current)
} else {
// 将 _route 变成响应式对象
this._routerRoot = (this.$parent && this.$parent._routerRoot) || this
}
registerInstance(this, this)
},
destroyed () {
registerInstance(this)
}
})
/** * 给Vue添加实例对象 $router 和 $route * $router为router实例 * $route为当前的route */
Object.defineProperty(Vue.prototype, '$router', {
get () { return this._routerRoot._router }
})
Object.defineProperty(Vue.prototype, '$route', {
get () { return this._routerRoot._route }
})
/** * 注入两个全局组件 * <router-view> * <router-link> */
Vue.component('RouterView', View)
Vue.component('RouterLink', Link)
/** * Vue.config 是一个对象,包含了Vue的全局配置 * 将vue-router的hook进行Vue的合并策略 */
const strats = Vue.config.optionMergeStrategies
// use the same hook merging strategy for route hooks
strats.beforeRouteEnter = strats.beforeRouteLeave = strats.beforeRouteUpdate = strats.created
}
复制代码
为了保证 VueRouter
只执行一次,当执行 install
逻辑的时候添加一个标识 installed
。用一个全局变量保存 Vue,方便插件对 Vue 的使用。
VueRouter 安装的核心是经过 mixin
,向 Vue app 的全部组件混入 beforeCreate
和 destroyed
钩子函数。
而且还在 Vue 添加实例对象
在 Vue 的 prototype 上初始化了一些 getter
Vue.util.defineReactive, 这是Vue里面观察者劫持数据的方法,劫持 _route,当 _route 触发 setter 方法的时候,则会通知到依赖的组件。
后面经过 Vue.component
方法定义了全局的 <router-link>
和 <router-view>
两个组件。<router-link>
相似于a标签,<router-view>
是路由出口,在 <router-view>
切换路由渲染不一样Vue组件。 最后定义了路由守卫的合并策略,采用了Vue的合并策略。
刚刚咱们提到了在 install 的时候会执行 VueRouter 的 init 方法( this._router.init(this)
),那么接下来咱们就来看一下 init 方法作了什么。简单来讲就是将 Vue 实例挂载到当前 router 的实例上。
而后 install 的时候会执行执行 VueRouter 的 init 方法( this._router.init(this)
)。init 执行的时候经过 history.transitionTo
作路由过渡。matcher
路由匹配器是后面路由切换,路由和组件匹配的核心函数。
init (app: any /* Vue component instance */) {
this.apps.push(app)
// main app previously initialized
// return as we don't need to set up new history listener
if (this.app) {
return
}
// 在 VueRouter 上挂载 Vue 实例
this.app = app
const history = this.history
// setupListeners 里会对 hashchange 事件进行监听
// transitionTo 是进行路由导航的函数
if (history instanceof HTML5History || history instanceof HashHistory) {
const setupListeners = routeOrError => {
history.setupListeners()
}
history.transitionTo(
history.getCurrentLocation(),
setupListeners,
setupListeners
)
}
// 路由全局监听,维护当前的route
// 由于 _route 在 install 执行时定义为响应式属性,
// 当 route 变动时 _route 更新,后面的视图更新渲染就是依赖于 _route
history.listen(route => {
this.apps.forEach(app => {
app._route = route
})
})
}
复制代码
VueRouter 的 constructor 相对而言比较简单
constructor (options: RouterOptions = {}) {
this.app = null
this.apps = []
this.options = options
this.beforeHooks = []
this.resolveHooks = []
this.afterHooks = []
// 建立 matcher 匹配函数
this.matcher = createMatcher(options.routes || [], this)
// 默认使用 哈希路由
let mode = options.mode || 'hash'
// h5的history有兼容性 对history作降级处理
this.fallback =
mode === 'history' && !supportsPushState && options.fallback !== false
if (this.fallback) {
mode = 'hash'
}
if (!inBrowser) {
mode = 'abstract'
}
this.mode = mode
// 分发处理
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}`)
}
}
}
复制代码
在实例化 vueRouter 的时候,vueRouter 仿照 history 定义了一些api:push
、replace
、back
、go
、forward
,还定义了路由匹配器、添加router动态更新方法等。
那么 VueRouter 是如何作路由的跳转的呢?也就是说咱们在使用 _this_.$router.push('/foo', increment)
的时候,怎么让渲染的视图展现 Foo 组件。
const router = new VueRouter({
mode: 'history',
base: __dirname,
routes: [
{ path: '/', component: Home },
{ path: '/foo', component: Foo },
{ path: '/bar', component: Bar },
{ path: encodeURI('/é'), component: Unicode },
{ path: '/query/:q', component: Query }
]
})
复制代码
还记得咱们刚刚在 vue-router 的 constructor 中作了什么吗?我来帮你们回忆一下。在 constructor 中,咱们根据不一样的 mode 选择不一样类型的 history 进行实例化(h5 history 仍是 hash history 仍是 abstract ),而后在 init 的时候调用 history.transitionTo 进行路由初始化匹配,也就是完成第一次路由导航。
咱们在 history/base.js
文件中能够找到 transitionTo
方法。transitionTo
能够接收三个参数 location
、onComplete
、onAbort
,分别是目标路径、路经切换成功的回调、路径切换失败的回调。
首先在 router 中找到传入的 location ,而后更新当前的 route,接着就执行路经切换成功的回调函数(在这个函数中,不一样模式的 history 的实现是不同的)。
回调中会调用 replaceHash 或者 pushHash 方法。它们会更新 location 的 hash 值。若是兼容 historyAPI,会使用 history.replaceState 或者 history.pushState。若是不兼容 historyAPI 会使用 window.location.replace 或者window.location.hash。
而handleScroll方法则是会更新咱们的滚动条的位置。
transitionTo (
location: RawLocation,
onComplete?: Function,
onAbort?: Function
) {
// 调用 match方法获得匹配的 route对象
const route = this.router.match(location, this.current)
// 过渡处理
this.confirmTransition(
route,
() => {
// 更新当前的 route 对象
this.updateRoute(route)
// 更新url地址 hash模式更新hash值 history模式经过pushState/replaceState来更新
onComplete && onComplete(route)
this.ensureURL()
// fire ready cbs once
if (!this.ready) {
this.ready = true
this.readyCbs.forEach(cb => {
cb(route)
})
}
},
err => {
if (onAbort) {
onAbort(err)
}
if (err && !this.ready) {
this.ready = true
this.readyErrorCbs.forEach(cb => {
cb(err)
})
}
}
)
}
复制代码
到此为止,已经可让不一样模式下的 history 对象拥有了表现相同的 push
replace
功能(详细能够看下面的实现部分)
那么路由更换以后怎么进行正确的渲染呢。
记得咱们前面说过的 vue 的响应式原理了吗?咱们在 install 的时候已经将 _router 设置为响应式的了。只要 _router 进行了改变,那么就会触发 RouterView 的渲染。(咱们在 transitionTo 的回调中更新了 _route)
在 VueRouter 上定义的 go,forward,back方法都是调用 history 的属性的 go 方法。
而hash上go方法调用的是history.go,它是如何更新RouteView的呢?答案是hash对象在setupListeners方法中添加了对popstate或者hashchange事件的监听。在事件的回调中会触发RoterView的更新
咱们在经过点击后退, 前进按钮或者调用 back, forward, go 方法的时候。咱们没有主动更新 _app.route 和current。咱们该如何触发 RouterView 的更新呢?经过在 window 上监听 popstate,或者 hashchange 事件。在事件的回调中,调用 transitionTo 方法完成对 _route 和 current 的更新。
或者能够这样说,在使用 push,replace 方法的时候,hash的更新在 _route 更新的后面。而使用 go, back 时,hash 的更新在 _route 更新的前面。
setupListeners () {
const router = this.router
const expectScroll = router.options.scrollBehavior
const supportsScroll = supportsPushState && expectScroll
if (supportsScroll) {
setupScroll()
}
window.addEventListener(supportsPushState ? 'popstate' : 'hashchange', () => {
const current = this.current
if (!ensureSlash()) {
return
}
this.transitionTo(getHash(), route => {
if (supportsScroll) {
handleScroll(this.router, route, current, true)
}
if (!supportsPushState) {
replaceHash(route.fullPath)
}
})
})
}
复制代码
export class HashHistory extends History {
constructor (router: Router, base: ?string, fallback: boolean) {
super(router, base)
// check history fallback deeplinking
if (fallback && checkFallback(this.base)) {
return
}
ensureSlash()
}
// this is delayed until the app mounts
// to avoid the hashchange listener being fired too early
setupListeners () {
if (this.listeners.length > 0) {
return
}
const router = this.router
const expectScroll = router.options.scrollBehavior
const supportsScroll = supportsPushState && expectScroll
if (supportsScroll) {
this.listeners.push(setupScroll())
}
// 添加 hashchange 事件监听
window.addEventListener(
hashchange,
() => {
const current = this.current
// 获取 hash 的内容并经过路由配置,把新的页面 render 到 ui-view 的节点
this.transitionTo(getHash(), route => {
if (supportsScroll) {
handleScroll(this.router, route, current, true)
}
if (!supportsPushState) {
replaceHash(route.fullPath)
}
})
}
)
this.listeners.push(() => {
window.removeEventListener(eventType, handleRoutingEvent)
})
}
push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
const { current: fromRoute } = this
this.transitionTo(
location,
route => {
pushHash(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 => {
replaceHash(route.fullPath)
handleScroll(this.router, route, fromRoute, false)
onComplete && onComplete(route)
},
onAbort
)
}
go (n: number) {
window.history.go(n)
}
}
function pushHash (path) {
if (supportsPushState) {
pushState(getUrl(path))
} else {
window.location.hash = path
}
}
function replaceHash (path) {
if (supportsPushState) {
replaceState(getUrl(path))
} else {
window.location.replace(getUrl(path))
}
}
复制代码
其实和 hash 的实现方式是基本相似的,区别点主要在于
export class HTML5History extends History {
_startLocation: string
constructor (router: Router, base: ?string) {
super(router, base)
this._startLocation = getLocation(this.base)
}
setupListeners () {
if (this.listeners.length > 0) {
return
}
const router = this.router
const expectScroll = router.options.scrollBehavior
const supportsScroll = supportsPushState && expectScroll
if (supportsScroll) {
this.listeners.push(setupScroll())
}
// 经过监听 popstate 事件
window.addEventListener('popstate', () => {
const current = this.current
// Avoiding first `popstate` event dispatched in some browsers but first
// history route not updated since async guard at the same time.
const location = getLocation(this.base)
if (this.current === START && location === this._startLocation) {
return
}
this.transitionTo(location, route => {
if (supportsScroll) {
handleScroll(router, route, current, true)
}
})
})
this.listeners.push(() => {
window.removeEventListener('popstate', handleRoutingEvent)
})
}
go (n: number) {
window.history.go(n)
}
push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
const { current: fromRoute } = this
this.transitionTo(location, route => {
// 使用 pushState 更新 url,不会致使浏览器发送请求,从而不会刷新页面
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 跟 pushState 的区别在于,不会记录到历史栈
replaceState(cleanPath(this.base + route.fullPath))
handleScroll(this.router, route, fromRoute, false)
onComplete && onComplete(route)
}, onAbort)
}
ensureURL (push?: boolean) {
if (getLocation(this.base) !== this.current.fullPath) {
const current = cleanPath(this.base + this.current.fullPath)
push ? pushState(current) : replaceState(current)
}
}
getCurrentLocation (): string {
return getLocation(this.base)
}
}
复制代码
能读到这里的同窗真的很感谢你们~~ 这是我第一次写源码相关的内容,尚未研究的很透彻,其中难免会有一些错误的地方,但愿你们多多指正~~~