浅析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 相关文章推荐
Array栈方法和队列方法的特点说明
Jan 24 Javascript
JS 作用域与作用域链详解
Apr 07 Javascript
Vue.js 父子组件通讯开发实例
Sep 06 Javascript
详解javascript事件绑定使用方法
Oct 20 Javascript
js实现自定义进度条效果
Mar 15 Javascript
JS实现禁止高频率连续点击的方法【基于ES6语法】
Apr 25 Javascript
js实现放大镜特效
May 18 Javascript
浅谈Koa2框架利用CORS完成跨域ajax请求
Mar 06 Javascript
vue+axios+promise实际开发用法详解
Oct 15 Javascript
vue配置文件实现代理v2版本的方法
Jun 21 Javascript
微信小程序如何自定义table组件
Jun 29 Javascript
layer.alert回调函数执行关闭弹窗的实例
Sep 11 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
php 注释规范
2012/03/29 PHP
探讨php define()函数及defined()函数使用详解
2013/06/09 PHP
初识通用数据库操作类――前端easyui-datagrid,form(php)
2015/07/31 PHP
PHP实现用户登录的案例代码
2018/05/10 PHP
安装PHP扩展时解压官方 tgz 文件后没有configure文件无法进行配置编译的问题
2020/08/26 PHP
js中有关IE版本检测
2012/01/04 Javascript
javascript文件加载管理简单实现方法
2015/07/25 Javascript
jq实现左侧显示图片右侧文字滑动切换效果
2015/08/04 Javascript
JS获取数组最大值、最小值及长度的方法
2015/11/24 Javascript
Node.JS中事件轮询(Event Loop)的解析
2017/02/25 Javascript
原生javascript上传图片带进度条【实例分享】
2017/04/06 Javascript
web前端vue之vuex单独一文件使用方式实例详解
2018/01/11 Javascript
深入解析vue 源码目录及构建过程分析
2019/04/24 Javascript
VUE前后端学习tab写法实例
2019/08/06 Javascript
JavaScript使用表单元素验证表单的示例代码
2019/08/20 Javascript
p5.js码绘“跳动的小正方形”的实现代码
2019/10/22 Javascript
微信小程序 scroll-view 实现锚点跳转功能
2019/12/12 Javascript
JavaScript实现省份城市的三级联动
2020/02/11 Javascript
ES6函数实现排它两种写法解析
2020/05/13 Javascript
nuxt.js 在middleware(中间件)中实现路由鉴权操作
2020/11/06 Javascript
[38:39]KG vs Mineski 2019国际邀请赛小组赛 BO2 第一场 8.15
2019/08/16 DOTA
python错误处理详解
2014/09/28 Python
Python+tkinter使用80行代码实现一个计算器实例
2018/01/16 Python
Django教程笔记之中间件middleware详解
2018/08/01 Python
Python使用try except处理程序异常的三种常用方法分析
2018/09/05 Python
Python实现的多进程拷贝文件并显示百分比功能示例
2019/04/09 Python
浅谈python之自动化运维(Paramiko)
2020/01/31 Python
Python flask框架如何显示图像到web页面
2020/06/03 Python
详解Tensorflow不同版本要求与CUDA及CUDNN版本对应关系
2020/08/04 Python
Python collections.deque双边队列原理详解
2020/10/05 Python
创业资金计划书
2014/02/06 职场文书
培训协议书范本
2014/04/22 职场文书
看看如何用Python绘制小米新版天价logo
2021/04/20 Python
MYSQL(电话号码,身份证)数据脱敏的实现
2021/05/28 MySQL
ubuntu安装jupyter并设置远程访问的实现
2022/03/31 Python
Win11如何查看显卡型号 Win11查看显卡型号的方法
2022/08/14 数码科技