简化版的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 相关文章推荐
给Function做的OOP扩展
May 07 Javascript
JS无限极树形菜单,json格式、数组格式通用示例
Jul 30 Javascript
jqplot通过ajax动态画折线图的方法及思路
Dec 08 Javascript
jquery实现选中单选按钮下拉伸缩效果
Aug 06 Javascript
好好了解一下Cookie(强烈推荐)
Jun 14 Javascript
详细谈谈AngularJS的子级作用域问题
Sep 05 Javascript
angular实现图片懒加载实例代码
Jun 08 Javascript
JavaScript中错误正确处理方式小结你用对了吗
Oct 10 Javascript
详解基于Vue-cli搭建的项目如何和后台交互
Jun 29 Javascript
深入理解 JS 垃圾回收
Jun 03 Javascript
微信小程序从注册账号到上架(图文详解)
Jul 17 Javascript
80行代码写一个Webpack插件并发布到npm
May 24 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一个找二层目录的小东东
2012/08/02 PHP
php实现高效获取图片尺寸的方法
2014/12/12 PHP
php生成网页桌面快捷方式
2017/05/05 PHP
JavaScript Base64编码和解码,实现URL参数传递。
2006/09/18 Javascript
jquery弹出框的用法示例(一)
2013/08/26 Javascript
js中Math之random,round,ceil,floor的用法总结
2013/12/26 Javascript
javascript中的undefined和not defined区别示例介绍
2014/02/26 Javascript
js解决select下拉选不中问题
2014/10/14 Javascript
使用Javascript简单实现图片无缝滚动
2014/12/05 Javascript
jQuery不使用插件及swf实现无刷新文件上传
2014/12/08 Javascript
Javascript中的getUTCHours()方法使用详解
2015/06/10 Javascript
jQuery中cookie插件用法实例分析
2015/12/04 Javascript
使用jQuery操作HTML的table表格的实例解析
2016/03/13 Javascript
jQuery web 组件 后台日历价格、库存设置的代码
2016/10/14 Javascript
详解vue-cli构建项目反向代理配置
2017/09/07 Javascript
vue2.0安装style/css loader的方法
2018/03/14 Javascript
详解vue-cli下ESlint 配置说明
2018/09/03 Javascript
js实现多个倒计时并行 js拼团倒计时
2019/02/25 Javascript
说说如何使用Vuex进行状态管理(小结)
2019/04/14 Javascript
微信小程序 如何保持登录状态
2019/08/16 Javascript
24个解决实际问题的ES6代码片段(小结)
2020/02/02 Javascript
[04:30]显微镜下的DOTA2第五期——拉比克
2013/09/26 DOTA
python在控制台输出进度条的方法
2015/06/20 Python
实例详解Python模块decimal
2019/06/26 Python
美国最大的珠宝首饰网上商城:Jewelry.com
2016/07/22 全球购物
伦敦一家非常流行的时尚精品店:Oxygen Boutique
2017/01/15 全球购物
俄罗斯小米家用电器、电子产品和智能家居商店:Poood.ru
2020/04/03 全球购物
Java的for语句中break, continue和return的区别
2013/12/19 面试题
城市规划毕业生求职信
2013/10/10 职场文书
管理部部长岗位职责
2013/12/05 职场文书
大学军训感言
2014/01/10 职场文书
教师年度考核自我鉴定
2014/01/19 职场文书
年终总结会议主持词
2014/03/17 职场文书
教师求职信
2014/06/17 职场文书
学雷锋志愿者活动方案
2014/08/21 职场文书
sqlserver2017共享功能目录路径不可改的解决方法
2021/04/16 SQL Server