浅析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 相关文章推荐
Mootools 1.2教程 输入过滤第一部分(数字)
Sep 15 Javascript
Javascript实现仿WebQQ界面的“浮云”兼容 IE7以上版本及FF
Apr 27 Javascript
Jquery仿IGoogle实现可拖动窗口示例代码
Aug 22 Javascript
60个很实用的jQuery代码开发技巧收集
Dec 15 Javascript
javascript继承的六大模式小结
Apr 13 Javascript
jQuery对象与DOM对象转换方法详解
May 10 Javascript
JavaScript函数绑定用法实例分析
Nov 14 Javascript
在react-router4中进行代码拆分的方法(基于webpack)
Mar 08 Javascript
Vue实现6位数密码效果
Aug 18 Javascript
D3.js(v3)+react 实现带坐标与比例尺的柱形图 (V3版本)
May 09 Javascript
解决layui 三级联动下拉框更新时回显的问题
Sep 03 Javascript
使用原生javascript开发计算器实例代码
Feb 21 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
form表单只提交数据而不进行页面跳转的解决方案
2013/09/18 Javascript
bootstrap——bootstrapTable实现隐藏列的示例
2017/01/14 Javascript
canvas绘制万花筒效果(代码分享)
2017/01/20 Javascript
JavaScript中Promise的使用详解
2017/02/26 Javascript
JavaScript 字符串数字左补位,右补位,取固定长度,截位扩展函数代码
2017/03/25 Javascript
angular4中关于表单的校验示例
2017/10/16 Javascript
JavaScript控制浏览器全屏显示简单示例
2018/07/05 Javascript
vue3实现v-model原理详解
2019/10/09 Javascript
ES6扩展运算符和rest运算符用法实例分析
2020/05/23 Javascript
vue 解决兄弟组件、跨组件深层次的通信操作
2020/07/27 Javascript
[38:27]完美世界DOTA2联赛PWL S2 Forest vs FTD.C 第二场 11.26
2020/11/30 DOTA
Python 列表list使用介绍
2014/11/30 Python
Python中的ceil()方法使用教程
2015/05/14 Python
初步讲解Python中的元组概念
2015/05/21 Python
Python 类与元类的深度挖掘 I【经验】
2016/05/06 Python
python读写json文件的简单实现
2017/04/11 Python
Python实现定时备份mysql数据库并把备份数据库邮件发送
2018/03/08 Python
使用Python抓取豆瓣影评数据的方法
2018/10/17 Python
在unittest中使用 logging 模块记录测试数据的方法
2018/11/30 Python
Python WEB应用部署的实现方法
2019/01/02 Python
python ddt数据驱动最简实例代码
2019/02/22 Python
python高级特性简介
2020/08/13 Python
CSS3 实现雷达扫描图的示例代码
2020/09/21 HTML / CSS
英国百安居装饰建材网上超市:B&Q
2016/09/13 全球购物
卡西欧G-SHOCK英国官网: 防水防震手表
2018/01/08 全球购物
Kneipp克奈圃美国官网:德国百年精油配方的传承
2018/02/07 全球购物
父亲追悼会答谢词
2014/01/17 职场文书
初级会计求职信范文
2014/02/15 职场文书
金融管理应届生求职信
2014/02/20 职场文书
放飞梦想演讲稿
2014/05/05 职场文书
全国优秀教师事迹材料
2014/08/26 职场文书
工作经常出错的检讨书
2014/09/13 职场文书
请假条应该怎么写?
2019/06/24 职场文书
RestTemplate如何通过HTTP Basic Auth认证示例说明
2022/03/17 Java/Android
pycharm安装深度学习pytorch的d2l包失败问题解决
2022/03/25 Python
零基础学java之方法的定义与调用详解
2022/04/10 Java/Android