简化版的vue-router实现思路详解


Posted in Javascript onOctober 19, 2018

本文旨在介绍 vue-router 的实现思路,并动手实现一个简化版的 vue-router 。我们先来看一下一般项目中对 vue-router 最基本的一个使用,可以看到,这里定义了四个路由组件,我们只要在根 vue 实例中注入该 router 对象就可以使用了.

import VueRouter from 'vue-router';
import Home from '@/components/Home';
import A from '@/components/A';
import B from '@/components/B'
import C from '@/components/C'
Vue.use(VueRouter)
export default new VueRouter.Router({
 // mode: 'history',
 routes: [
 {
  path: '/',
  component: Home
 },
 {
  path: '/a',
  component: A
 },
 {
  path: '/b',
  component: B
 },
 {
  path: '/c',
  component: C
 }
 ]
})

vue-router 提供两个全局组件, router-view router-link ,前者是用于路由组件的占位,后者用于点击时跳转到指定路由。此外组件内部可以通过 this.$router.push , this.$rouer.replace 等api实现路由跳转。本文将实现上述两个全局组件以及 push 和 replace 两个api,调用的时候支持 params 传参,并且支持 hash 和 history 两种模式,忽略其余api、嵌套路由、异步路由、 abstract 路由以及导航守卫等高级功能的实现,这样有助于理解 vue-router 的核心原理。本文的最终代码不建议在生产环境使用,只做一个学习用途,下面我们就来一步步实现它。

install实现

任何一个 vue 插件都要实现一个 install 方法,通过 Vue.use 调用插件的时候就是在调用插件的 install 方法,那么路由的 install 要做哪些事情呢?首先我们知道 我们会用 new 关键字生成一个 router 实例,就像前面的代码实例一样,然后将其挂载到根 vue 实例上,那么作为一个全局路由,我们当然需要在各个组件中都可以拿到这个 router 实例。另外我们使用了全局组件 router-view 和 router-link ,由于 install 会接收到 Vue 构造函数作为实参,方便我们调用 Vue.component 来注册全局组件。因此,在 install 中主要就做两件事,给各个组件都挂载 router 实例,以及实现 router-view router-link 两个全局组件。下面是代码:

const install = (Vue) => {
 if (this._Vue) {
 return;
 };
 Vue.mixin({
 beforeCreate() {
  if (this.$options && this.$options.router) {
  this._routerRoot = this;
  this._router = this.$options.router;
  Vue.util.defineReactive(this, '_routeHistory', this._router.history)
  } else {
  this._routerRoot = (this.$parent && this.$parent._routerRoot) || this
  }

  Object.defineProperty(this, '$router', {
  get() {
   return this._routerRoot._router;
  }
  })

  Object.defineProperty(this, '$route', {
  get() {
   return {
   current: this._routerRoot._routeHistory.current,
   ...this._routerRoot._router.route
   };
  }
  })
 }
 });

 Vue.component('router-view', {
 render(h) { ... }
 })

 Vue.component('router-link', { 
 props: {
  to: String,
  tag: String,
 },
 render(h) { ... }
 })
 this._Vue = Vue;
}

这里的 this 代表的就是 vue-router 对象,它有两个属性暴露出来供外界调用,一个是 install ,一个是 Router 构造函数,这样可以保证插件的正确安装以及路由实例化。我们先忽略 Router 构造函数,来看 install ,上面代码中的 this._Vue 是个开始没有定义的属性,他的目的是防止多次安装。我们使用 Vue.mixin 对每个组件的 beforeCreate 钩子做全局混入,目的是让每个组件实例共享 router 实例,即通过 this.$router 拿到路由实例,通过 this.$route 拿到路由状态。需要重点关注的是这行代码:

Vue.util.defineReactive(this, '_routeHistory', this._router.history)

这行代码利用 vue 的响应式原理,对根 vue 实例注册了一个 _routeHistory 属性,指向路由实例的 history 对象,这样 history 也变成了响应式的。因此一旦路由的 history 发生变化,用到这个值的组件就会触发 render 函数重新渲染,这里的组件就是 router-view 。从这里可以窥察到 vue-router 实现的一个基本思路。上述的代码中对于两个全局组件的 render 函数的实现,因为会依赖于 router 对象,我们先放一放,稍后再来实现它们,下面我们分析一下 Router 构造函数。

Router构造函数

经过刚才的分析,我们知道 router 实例需要有一个 history 对象,需要一个保存当前路由状态的对象 route ,另外很显然还需要接受路由配置表 routes ,根据 routes 需要一个路由映射表 routerMap 来实现组件搜索,还需要一个变量 mode 判断是什么模式下的路由,需要实现 push 和 replace 两个api,代码如下:

const Router = function (options) {
 this.routes = options.routes; // 存放路由配置
 this.mode = options.mode || 'hash';
 this.route = Object.create(null), // 生成路由状态
 this.routerMap = createMap(this.routes) // 生成路由表
 this.history = new RouterHistory(); // 实例化路由历史对象
 this.init(); // 初始化
}
Router.prototype.push = (options) => { ... }
Router.prototype.replace = (options) => { ... }
Router.prototype.init = () => { ... }

我们看一下路由表 routerMap 的实现,由于不考虑嵌套等其他情况,实现很简单,如下:

const createMap = (routes) => {
 let resMap = Object.create(null);
 routes.forEach(route => {
 resMap[route['path']] = route['component'];
 })
 return resMap;
}

RouterHistory 的实现也很简单,根据前面分析,我们只需要一个 current 属性就可以,如下:

const RouterHistory = function (mode) {
 this.current = null; 
}

有了路由表和 history , router-view 的实现就很容易了,如下:

Vue.component('router-view', {
 render(h) {
  let routerMap = this._self.$router.routerMap;
  return h(routerMap[this._self.$route.current])
 }
 })

这里的 this 是一个 renderProxy 实例,他有一个属性 _self 可以拿到当前的组件实例,进而访问到 routerMap ,可以看到路由实例 history 的 current 本质上就是我们配置的路由表中的 path 。

接下来我们看一下 Router 要做哪些初始化工作。对于 hash 路由而言,url上 hash 值的改变不会引起页面刷新,但是可以触发一个 hashchange 事件。由于路由 history.current 初始为 null ,因此匹配不到任何一个路由,所以会导致页面刷新加载不出任何路由组件。基于这两点,在 init 方法中,我们需要实现对页面加载完成的监听,以及 hash 变化的监听。对于 history 路由,为了实现浏览器前进后退时准确渲染对应组件,还要监听一个 popstate 事件。代码如下:

Router.prototype.init = function () {

 if (this.mode === 'hash') {
 fixHash()
 window.addEventListener('hashchange', () => {
  this.history.current = getHash();
 })
 window.addEventListener('load', () => {
  this.history.current = getHash();
 })
 }

 if (this.mode === 'history') {
 removeHash(this);
 window.addEventListener('load', () => {
  this.history.current = location.pathname;
 })
 window.addEventListener('popstate', (e) => {
  if (e.state) {
  this.history.current = e.state.path;
  }
 })
 }

}

当启用 hash 模式的时候,我们要检测url上是否存在 hash 值,没有的话强制赋值一个默认 path , hash 路由时会根据 hash 值作为 key 来查找路由表。 fixHash 和 getHash 实现如下:

const fixHash = () => {
 if (!location.hash) {
 location.hash = '/';
 }
}
const getHash = () => {
 return location.hash.slice(1) || '/';
}

这样在刷新页面和 hash 改变的时候, current 可以得到赋值和更新,页面能根据 hash 值准确渲染路由。 history 模式也是一样的道理,只是它通过 location.pathname 作为 key 搜索路由组件,另外 history 模式需要去除url上可能存在的 hash , removeHash 实现如下:

const removeHash = (route) => {
 let url = location.href.split('#')[1]
 if (url) {
 route.current = url;
 history.replaceState({}, null, url)
 }
}

我们可以看到当浏览器后退的时候, history 模式会触发 popstate 事件,这个时候是通过 state 状态去获取 path 的,那么 state 状态从哪里来呢,答案是从 window.history 对象的 pushState 和 replaceState 而来,这两个方法正好可以用来实现 router 的 push 方法和 replace 方法,我们看一下这里它们的实现:

Router.prototype.push = (options) => {
 this.history.current = options.path;
 if (this.mode === 'history') {
 history.pushState({
  path: options.path
 }, null, options.path);
 } else if (this.mode === 'hash') {
 location.hash = options.path;
 }
 this.route.params = {
 ...options.params
 }
}

Router.prototype.replace = (options) => {
 this.history.current = options.path;
 if (this.mode === 'history') {
 history.replaceState({
  path: options.path
 }, null, options.path);
 } else if (this.mode === 'hash') {
 location.replace(`#${options.path}`)
 }
 this.route.params = {
 ...options.params
 }
}

pushState 和 replaceState 能够实现改变url的值但不引起页面刷新,从而不会导致新请求发生, pushState 会生成一条历史记录而 replaceState 不会,后者只是替换当前url。在这两个方法执行的时候将 path 存入 state ,这就使得 popstate 触发的时候可以拿到路径从而触发组件渲染了。我们在组件内按照如下方式调用,会将 params 写入 router 实例的 route 属性中,从而在跳转后的组件 B 内通过 this.$route.params 可以访问到传参。

this.$router.push({
 path: '/b',
 params: {
  id: 55
 }
 });

router-link实现

router-view 的实现很简单,前面已经说过。最后,我们来看一下 router-link 的实现,先放上代码:

Vue.component('router-link', { 
 props: {
  to: String,
  tag: String,
 },

 render(h) {
  let mode = this._self.$router.mode;
  let tag = this.tag || 'a';
  let routerHistory = this._self.$router.history;
  return h(tag, {
  attrs: tag === 'a' ? {
   href: mode === 'hash' ? '#' + this.to : this.to,

  } : {},
  on: {
   click: (e) => {
   if (this.to === routerHistory.current) {
    e.preventDefault();
    return;
   }
   routerHistory.current = this.to;
   switch (mode) {
    case 'hash':
    if (tag === 'a') return;
    location.hash = this.to;
    break;
    case 'history':
    history.pushState({
     path: this.to
    }, null, this.to);
    break;
    default:
   }
   e.preventDefault();
   }
  },
  style: {
   cursor: 'pointer'
  }
  }, this.$slots.default)
 }
 })

router-link 可以接受两个属性, to 表示要跳转的路由路径, tag 表示 router-link 要渲染的标签名,默认为标签。如果是 a 标签,我们为其添加一个 href 属性。我们给标签绑定 click 事件,如果检测到本次跳转为当前路由的话什么都不做直接返回,并且阻止默认行为,否者根据 to 更换路由。 hash 模式下并且是 a 标签时候可以直接利用浏览器的默认行为完成url上 hash 的替换,否者重新为 location.hash 赋值。 history 模式下则利用 pushState 去更新url。

以上实现就是一个简单的vue-router,完整代码参见vue-router-simple 。

总结

以上所述是小编给大家介绍的简化版的vue-router实现思路详解,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对三水点靠木网站的支持!

Javascript 相关文章推荐
DWR Ext 加载数据
Mar 22 Javascript
JavaScript中:表达式和语句的区别[译]
Sep 17 Javascript
JS中不为人知的五种声明Number的方式简要概述
Feb 22 Javascript
jQuery form 表单验证插件(fieldValue)校验表单
Jan 24 Javascript
Avalonjs 实现简单购物车功能(实例代码)
Feb 07 Javascript
浅谈angular2的http请求返回结果的subcribe注意事项
Mar 01 Javascript
详解A标签中href=""的几种用法
Aug 20 Javascript
vue 计时器组件的实现代码
Sep 14 Javascript
JavaScript实现的超简单计算器功能示例
Dec 23 Javascript
解决element-ui里的下拉多选框 el-select 时,默认值不可删除问题
Aug 14 Javascript
VSCode插件安装完成后的配置(常用配置)
Aug 24 Javascript
WebPack工具运行原理及入门教程
Dec 02 Javascript
vue中el-upload上传图片到七牛的示例代码
Oct 19 #Javascript
浅析vue-router原理
Oct 19 #Javascript
vue-cli3.0 脚手架搭建项目的过程详解
Oct 19 #Javascript
vue-quill-editor+plupload富文本编辑器实例详解
Oct 19 #Javascript
vue+VeeValidate 校验范围实例详解(部分校验,全部校验)
Oct 19 #Javascript
浅析JS中什么是自定义react数据验证组件
Oct 19 #Javascript
在小程序Canvas中使用measureText的方法示例
Oct 19 #Javascript
You might like
PHP中的string类型使用说明
2010/07/27 PHP
[原创]php token使用与验证示例【测试可用】
2017/08/30 PHP
LBS blog sql注射漏洞[All version]-官方已有补丁
2007/08/26 Javascript
HTML Dom与Css控制方法
2010/10/25 Javascript
jQuery获取(选中)单选,复选框,下拉框中的值
2014/02/21 Javascript
JS获得浏览器版本和操作系统版本的例子
2014/05/13 Javascript
在JavaScript中操作时间之getMonth()方法的使用
2015/06/10 Javascript
深入浅析JavaScript中prototype和proto的关系
2015/11/15 Javascript
jQuery操作属性和样式详解
2016/04/13 Javascript
总结Node.js中的一些错误类型
2016/08/15 Javascript
jQuery插件HighCharts实现的2D条状图效果示例【附demo源码下载】
2017/03/15 Javascript
JavaScript中闭包的详解
2017/04/01 Javascript
详解angularJs指令的3种绑定策略
2017/04/13 Javascript
vue上传图片到oss的方法示例(图片带有删除功能)
2018/09/27 Javascript
nodejs基础之buffer缓冲区用法分析
2018/12/26 NodeJs
axios+Vue实现上传文件显示进度功能
2019/04/14 Javascript
微信小程序云开发如何实现数据库自动备份实现
2019/08/16 Javascript
Vue 2.0双向绑定原理的实现方法
2019/10/23 Javascript
使用Vue-scroller页面input框不能触发滑动的问题及解决方法
2020/08/08 Javascript
解读Django框架中的低层次缓存API
2015/07/24 Python
python3中os.path模块下常用的用法总结【推荐】
2018/09/16 Python
解决pycharm运行出错,代码正确结果不显示的问题
2018/11/30 Python
详解Python爬取并下载《电影天堂》3千多部电影
2019/04/26 Python
Python从列表推导到zip()函数的5种技巧总结
2019/10/23 Python
Python Gluon参数和模块命名操作教程
2019/12/18 Python
Django与pyecharts结合的实例代码
2020/05/13 Python
小学运动会广播稿200字(十二篇)
2014/01/14 职场文书
爱心捐款倡议书
2014/04/14 职场文书
英语一分钟演讲稿
2014/04/29 职场文书
兵马俑导游词
2015/02/02 职场文书
2015年端午节国旗下演讲稿
2015/03/19 职场文书
房地产销售助理岗位职责
2015/04/14 职场文书
退税申请报告怎么写
2015/05/18 职场文书
联谊会开场白
2015/06/01 职场文书
2015双创工作总结
2015/07/24 职场文书
三严三实学习心得体会(精选N篇)
2016/01/05 职场文书