浅析vue-router实现原理及两种模式


Posted in Javascript onFebruary 11, 2020

之前用Vue开发单页应用,发现不管路由怎么变化,浏览器地址栏总是会有一个'#'号。

浅析vue-router实现原理及两种模式
浅析vue-router实现原理及两种模式

当时检查自己的代码,没有发现请求的地址带'#',当时也很纳闷,但是由于没有影响页面的渲染以及向后台发送请求,当时也没有在意。最近看了一下vue-router的实现原理,才逐渐揭开了这个谜题。

vue-router 的两种方式(浏览器环境下)

1. Hash (对应HashHistory)

hash(“#”)符号的本来作用是加在URL中指示网页中的位置:

http://www.example.com/index.html#print

#符号本身以及它后面的字符称之为hash(也就是我之前为什么地址栏都会有一个‘#'),可通过window.location.hash属性读取。它具有如下特点:

hash虽然出现在URL中,但不会被包括在HTTP请求中。它是用来指导浏览器动作的,对服务器端完全无用,因此,改变hash不会重新加载页面

2.可以为hash的改变添加监听事件:

window.addEventListener("hashchange", funcRef, false)

每一次改变hash(window.location.hash),都会在浏览器的访问历史中增加一个记录

利用hash的以上特点,就可以来实现前端路由“更新视图但不重新请求页面”的功能了。

2. History (对应HTML5History)

History接口 是浏览器历史记录栈提供的接口,通过back(), forward(), go()等方法,我们可以读取浏览器历史记录栈的信息,进行各种跳转操作。

从HTML5开始,History interface提供了两个新的方法:pushState(), replaceState()使得我们可以对浏览器历史记录栈进行修改:

window.history.pushState(stateObject, title, URL)
window.history.replaceState(stateObject, title, URL)

stateObject: 当浏览器跳转到新的状态时,将触发popState事件,该事件将携带这个stateObject参数的副本 title: 所添加记录的标题 URL: 所添加记录的URL

这两个方法有个共同的特点:当调用他们修改浏览器历史记录栈后,虽然当前URL改变了,但浏览器不会刷新页面,这就为单页应用前端路由“更新视图但不重新请求页面”提供了基础。 浏览器历史记录可以看作一个「栈」。栈是一种后进先出的结构,可以把它想象成一摞盘子,用户每点开一个新网页,都会在上面加一个新盘子,叫「入栈」。用户每次点击「后退」按钮都会取走最上面的那个盘子,叫做「出栈」。而每次浏览器显示的自然是最顶端的盘子的内容。

vue-router 的作用

vue-router的作用就是通过改变URL,在不重新请求页面的情况下,更新页面视图。简单的说就是,虽然地址栏的地址改变了,但是并不是一个全新的页面,而是之前的页面某些部分进行了修改。

export default new Router({
 // mode: 'history', //后端支持可开
 routes: constantRouterMap
})

这是Vue项目中常见的一段初始化vue-router的代码,之前没仔细研究过vue-router,不知道还有一个mode属性,后来看了相关文章后了解到,mode属性用来指定vue-router使用哪一种模式。在没有指定mode的值,则使用hash模式。

源码分析

首先看一下vue-router的构造函数

constructor (options: RouterOptions = {}) {
 this.app = null
 this.apps = []
 this.options = options
 this.beforeHooks = []
 this.resolveHooks = []
 this.afterHooks = []
 this.matcher = createMatcher(options.routes || [], this)

 let mode = options.mode || 'hash'
 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}`)
 }
 }
 }

主要是先获取mode的值,如果mode的值为 history 但是浏览器不支持 history 模式,那么就强制设置mode值为 hash 。如果支持则为 history 。接下来,根据mode的值,来选择vue-router使用哪种模式。

case 'history':
 this.history = new HTML5History(this, options.base)
 break
case 'hash':
 this.history = new HashHistory(this, options.base, this.fallback)
 break

这样就有了两种模式。确定好了vue-router使用哪种模式后,就到了init。 先来看看router 的 init 方法就干了哪些事情,在 src/index.js 中

init (app: any /* Vue component instance */) {
// ....
 const history = this.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,则执行

history.transitionTo(history.getCurrentLocation())

如果是Hash模式,则执行

const setupHashListener = () => {
 history.setupListeners()
 }
 history.transitionTo(
 history.getCurrentLocation(),
 setupHashListener,
 setupHashListener
 )

可以看出,两种模式都执行了transitionTo( )函数。 接下来看一下两种模式分别是怎么执行的,首先看一下Hash模式

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) {
 // 调用 match 得到匹配的 route 对象
 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的实例,._route本不是本身的组件中定义的内置属性,而是在Vue.use(Router)加载vue-router插件的时候,通过Vue.mixin()方法,全局注册一个混合,影响注册之后所有创建的每个 Vue 实例,该混合在beforeCreate钩子中通过Vue.util.defineReactive()定义了响应式的_route。所谓响应式属性,即当_route值改变时,会自动调用Vue实例的render()方法,更新视图。vm.render()是根据当前的 _route 的path,name等属性,来将路由对应的组件渲染到. 所以总结下来,从路由改变到视图的更新流程如下:

this.$router.push(path)
 --> 
HashHistory.push() 
--> 
History.transitionTo() 
--> 
const route = this.router.match(location, this.current)会进行地址匹配,得到一个对应当前地址的route(路由信息对象)
-->
History.updateRoute(route) 
 -->
 app._route=route (Vue实例的_route改变) 由于_route属性是采用vue的数据劫持,当_route的值改变时,会执行响应的render( )
-- >
vm.render() 具体是在<router-view></router-view> 中render
 -->
window.location.hash = route.fullpath (浏览器地址栏显示新的路由的path)

HashHistory.replace()

说完了HashHistory.push(),该说HashHistory.replace()了。

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
 )
}

可以看出来,HashHistory.replace它与push()的实现结构上基本相似,不同点在于它不是直接对window.location.hash进行赋值,而是调用window.location.replace方法将路由进行替换。这样不会将新路由添加到浏览器访问历史的栈顶,而是替换掉当前的路由。

监听地址栏

可以看出来,上面的过程都是在代码内部进行路由的改变的,比如项目中常见的this.$router.push(), 等方法。然后将浏览器的地址栏置为新的hash值。那么如果直接在地址栏中输入URL从而改变路由呢,例如

浅析vue-router实现原理及两种模式

我将dashboadr删除,然后置为article/hotSpot,然后回车,vue又是如何处理的呢?

setupListeners () {
 window.addEventListener('hashchange', () => {
 if (!ensureSlash()) {
 return
 }
 this.transitionTo(getHash(), route => {
 replaceHash(route.fullPath)
 })
 })
}

该方法设置监听了浏览器事件hashchange,调用的函数为replaceHash,即在浏览器地址栏中直接输入路由相当于代码调用了replace()方法.后面的步骤自然与HashHistory.replace()相同,一样实现页面渲染。

HTML5History

HTML5History模式的vue-router 代码结构以及更新视图的逻辑与hash模式基本类似,和HashHistory的步骤基本一致,只是HashHistory的push和replace()变成了HTML5History.pushState()和HTML5History.replaceState()

在HTML5History中添加对修改浏览器地址栏URL的监听是直接在构造函数中执行的,对HTML5History的popstate 事件进行监听:

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)
 }
 })
 })
}

以上就是vue-router hash模式与history模式不同模式下处理逻辑的分析了。

总结

以上所述是小编给大家介绍的vue-router实现原理及两种模式分析,希望对大家有所帮助!

Javascript 相关文章推荐
jQuery 版元素拖拽原型代码
Apr 25 Javascript
Javascript学习笔记-详解in运算符
Sep 13 Javascript
jQuery探测位置的提示弹窗(toolTip box)详细解析
Nov 14 Javascript
js为空或不是对象问题的快速解决方法
Dec 11 Javascript
使用原生js写的一个简单slider
Apr 29 Javascript
javascript学习笔记之函数定义
Jun 25 Javascript
jQuery实现仿腾讯视频列表分页效果的方法
Aug 07 Javascript
Vue.js组件使用开发实例教程
Nov 01 Javascript
基于bootstrap的文件上传控件bootstrap fileinput
Dec 23 Javascript
JS实现二叉查找树的建立以及一些遍历方法实现
Apr 17 Javascript
jQuery remove()过滤被删除的元素(推荐)
Jul 18 jQuery
JAVA面试题 static关键字详解
Jul 16 Javascript
vue-socket.io跨域问题有效解决方法
Feb 11 #Javascript
Vue开发中遇到的跨域问题及解决方法
Feb 11 #Javascript
Vue data的数据响应式到底是如何实现的
Feb 11 #Javascript
JS实现TITLE悬停长久显示效果完整示例
Feb 11 #Javascript
vue.config.js中配置Vue的路径别名的方法
Feb 11 #Javascript
vue-resourc发起异步请求的方法
Feb 11 #Javascript
js实现圆形显示鼠标单击位置
Feb 11 #Javascript
You might like
输出控制类
2006/10/09 PHP
php auth_http类库进行身份效验
2009/03/19 PHP
spl_autoload_register与autoload的区别详解
2013/06/03 PHP
php内嵌函数用法实例
2015/03/20 PHP
csdn 博客的css样式 v3
2009/02/24 Javascript
jquery异步请求实例代码
2011/06/21 Javascript
jQuery function的正确书写方法
2013/08/02 Javascript
js实现背景图片感应鼠标变化的方法
2015/02/28 Javascript
JavaScript位置与大小(1)之正确理解和运用与尺寸大小相关的DOM属性
2015/12/26 Javascript
jQuery实现div拖拽效果实例分析
2016/02/20 Javascript
深入理解setTimeout函数和setInterval函数
2016/05/20 Javascript
plupload+artdialog实现多平台上传文件
2016/07/19 Javascript
jQuery 自定义下拉框(DropDown)附源码下载
2016/07/22 Javascript
深入理解Javascript中的作用域链和闭包
2017/04/25 Javascript
浅谈sass在vue注意的地方
2017/08/10 Javascript
详解基于Vue+Koa的pm2配置
2017/10/24 Javascript
Vue中mintui的field实现blur和focus事件的方法
2018/08/25 Javascript
微信小程序多音频播放进度条问题
2018/08/28 Javascript
jquery.pagination.js分页使用教程
2018/10/23 jQuery
node和vue实现商城用户地址模块
2018/12/05 Javascript
使用python编写批量卸载手机中安装的android应用脚本
2014/07/21 Python
Python 中的with关键字使用详解
2016/09/11 Python
Python数据结构之顺序表的实现代码示例
2017/11/15 Python
python实现图片中文字分割效果
2019/07/22 Python
Python numpy数组转置与轴变换
2019/11/15 Python
python异步Web框架sanic的实现
2020/04/27 Python
Python调用shell命令常用方法(4种)
2020/05/11 Python
使用Keras预训练模型ResNet50进行图像分类方式
2020/05/23 Python
Python提取视频中图片的示例(按帧、按秒)
2020/10/22 Python
食堂员工工作职责
2013/12/18 职场文书
2016应届毕业生实习评语
2015/12/01 职场文书
美甲店的创业计划书模板
2019/08/23 职场文书
90行Python代码开发个人云盘应用
2021/04/20 Python
详解Spring事件发布与监听机制
2021/06/30 Java/Android
「女孩的钓鱼慢活」全新版权绘公布
2022/03/21 日漫
2022年显卡天梯图(6月更新)
2022/06/17 数码科技