简化版的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 相关文章推荐
javascript Array.prototype.slice使用说明
Oct 11 Javascript
javascript实现在网页中运行本地程序的方法
Feb 03 Javascript
JavaScript function函数种类详解
Feb 22 Javascript
老生常谈 关于JavaScript的类的继承
Jun 24 Javascript
vue v-model实现自定义样式多选与单选功能
Jul 05 Javascript
浅谈在Vue.js中如何实现时间转换指令
Jan 06 Javascript
JavaScript显式数据类型转换详解
Mar 18 Javascript
Vue 中可以定义组件模版的几种方式
Aug 06 Javascript
详解vue页面首次加载缓慢原因及解决方案
Nov 06 Javascript
详解Vue的ref特性的使用
Jan 24 Javascript
JavaScript基于用户照片姓名生成海报
May 29 Javascript
Vue 组件复用多次自定义参数操作
Jul 27 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
批量获取memcache值并按key的顺序返回的实现代码
2011/06/14 PHP
利用Ffmpeg获得flv视频缩略图和视频时间的代码
2011/09/15 PHP
如何用php获取程序执行的时间
2013/06/09 PHP
win7计划任务定时执行PHP脚本设置图解
2014/05/09 PHP
php操作redis缓存方法分享
2015/06/03 PHP
php实现编辑和保存文件的方法
2015/07/20 PHP
ThinkPHP 模板substr的截取字符串函数详解
2017/01/09 PHP
php+redis实现商城秒杀功能
2020/11/19 PHP
JavaScript类属性的访问方式详解
2014/02/11 Javascript
借助javascript代码判断网页是静态还是伪静态
2014/05/05 Javascript
JavaScript实现数组随机排序的方法
2015/06/26 Javascript
Angularjs实现分页和分页算法的示例代码
2016/12/23 Javascript
详解angular用$sce服务来过滤HTML标签
2017/04/11 Javascript
jQuery实现的简单在线计算器功能
2017/05/11 jQuery
Vue项目数据动态过滤实践及实现思路
2018/09/11 Javascript
vue-cli 使用axios的操作方法及整合axios的多种方法
2018/09/12 Javascript
JS求1到任意数之间的所有质数的方法详解
2019/05/20 Javascript
深入浅析golang zap 日志库使用(含文件切割、分级别存储和全局使用等)
2020/02/19 Javascript
vue实现全屏滚动效果(非fullpage.js)
2020/03/07 Javascript
JS实现电脑虚拟键盘的操作
2020/06/24 Javascript
JavaScript仿京东轮播图效果
2021/02/25 Javascript
[54:54]Newbee vs Serenity 2018国际邀请赛小组赛BO2 第一场 8.17
2018/08/18 DOTA
Python实现统计单词出现的个数
2015/05/28 Python
Django中处理出错页面的方法
2015/07/15 Python
python3结合openpyxl库实现excel操作的实例代码
2018/09/11 Python
使用python进行广告点击率的预测的实现
2019/07/04 Python
Pottery Barn阿联酋:购买家具、家居装饰及更多
2019/12/08 全球购物
大学生学习生活的自我评价
2013/11/01 职场文书
简单英文演讲稿
2014/01/01 职场文书
CAD制图人员的自荐信
2014/02/07 职场文书
我们的节日清明节活动方案
2014/03/05 职场文书
班组拓展活动方案
2014/08/14 职场文书
2015秋季田径运动会广播稿
2015/08/19 职场文书
js Proxy的原理详解
2021/05/25 Javascript
spring cloud gateway中如何读取请求参数
2021/07/15 Java/Android
python工具dtreeviz决策树可视化和模型可解释性
2022/03/03 Python