浅析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之超简单的div显示和隐藏特效demo(分享)
Jul 09 Javascript
Jquery chosen动态设置值实例介绍
Aug 08 Javascript
jQuery on()方法使用技巧详解
Apr 16 Javascript
JS+DIV+CSS实现仿表单下拉列表效果
Aug 18 Javascript
jquery实现的用户注册表单提示操作效果代码分享
Aug 28 Javascript
基于BootStarp的Dailog
Apr 28 Javascript
详细讲解vue2+vuex+axios
May 27 Javascript
浅谈JS对象添加getter与setter的5种方法
Jun 09 Javascript
vue中子组件传递数据给父组件的讲解
Jan 27 Javascript
vue  elementUI 表单嵌套验证的实例代码
Nov 06 Javascript
sharp.js安装过程中遇到的问题总结
Apr 02 Javascript
javascript实现评分功能
Jun 24 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远程采集图片详细教程
2014/07/01 PHP
yii实现级联下拉菜单的方法
2014/07/31 PHP
php创建类并调用的实例方法
2019/09/25 PHP
php设计模式之正面模式实例分析【星际争霸游戏案例】
2020/03/24 PHP
javascript读取RSS数据
2007/01/20 Javascript
捕获浏览器关闭、刷新事件不同情况下的处理方法
2013/06/02 Javascript
非常好用的JsonToString 方法 简单实例
2013/07/18 Javascript
js Dialog 去掉右上角的X关闭功能
2014/04/23 Javascript
js防止页面被iframe调用的方法
2014/10/30 Javascript
javascript实现给定半径求出圆的面积
2015/06/26 Javascript
微信小程序 动态绑定事件并实现事件修改样式
2017/04/13 Javascript
laydate日历控件使用方法详解
2017/11/20 Javascript
详解webpack编译多页面vue项目的配置问题
2017/12/11 Javascript
BootStrap中的模态框(modal,弹出层)功能示例代码
2018/11/02 Javascript
javascript的this关键字详解
2019/05/20 Javascript
JavaScript面向对象中接口实现方法详解
2019/07/24 Javascript
javascript 原型与原型链的理解及实例分析
2019/11/23 Javascript
Vue程序化的事件监听器(实例方案详解)
2020/01/07 Javascript
用JS实现选项卡
2020/03/23 Javascript
vue单文件组件无法获取$refs的问题
2020/06/24 Javascript
javascript实现拼图游戏
2021/01/29 Javascript
wxPython中文教程入门实例
2014/06/09 Python
使用Python神器对付12306变态验证码
2016/01/05 Python
解决python2.7 查询mysql时出现中文乱码
2016/10/09 Python
Python使用中文正则表达式匹配指定中文字符串的方法示例
2017/01/20 Python
如何基于Python + requests实现发送HTTP请求
2020/01/13 Python
使用Python获取爱奇艺电视剧弹幕数据的示例代码
2021/01/12 Python
乐天旅游台湾网站:Rakuten Travel TW
2017/06/01 全球购物
高级Java程序员面试题
2016/06/23 面试题
母亲节感恩活动记录
2014/03/16 职场文书
2014年办公室文员工作总结
2014/11/12 职场文书
幼儿园六一儿童节主持词
2015/06/30 职场文书
代码解析React中setState同步和异步问题
2021/06/03 Javascript
python使用matplotlib绘制图片时x轴的刻度处理
2021/08/30 Python
《英雄联盟》2022日蚀、月蚀皮肤演示 黑潮亚索曝光
2022/04/13 其他游戏
Python 数据可视化工具 Pyecharts 安装及应用
2022/04/20 Python