如何手写简易的 Vue Router


Posted in Javascript onOctober 10, 2020

前言

还是那样,懂得如何使用一个常用库,还得了解其原理或者怎么模拟实现,今天实现一下 vue-router 。

有一些知识我这篇文章提到了,这里就不详细一步步写,请看我 手写一个简易的 Vuex

基本骨架

  • Vue 里面使用插件的方式是 Vue.use(plugin) ,这里贴出它的用法:

安装 Vue.js 插件。如果插件是一个对象,必须提供 install 方法。如果插件是一个函数,它会被作为 install 方法。install 方法调用时,会将 Vue 作为参数传入。这个方法的第一个参数是 Vue 构造器,第二个参数是一个可选的选项对象。

  • 全局混入

使用 Vue.mixin(mixin)

全局注册一个混入,影响注册之后所有创建的每个 Vue 实例。可以使用混入向组件注入自定义的行为,它将影响每一个之后创建的 Vue 实例。

  • 路由用法

比如简单的:

// 路由数组
const routes = [
 {
  path: '/',
  name: 'Page1',
  component: Page1,
 },
 {
  path: '/page2',
  name: 'Page2',
  component: Page2,
 },
]

const router = new VueRouter({
 mode: 'history', // 模式
 routes,
})

它是传入了moderoutes,我们实现的时候需要在VueRouter构造函数中接收。

在使用路由标题的时候是这样:

<p>
 <!-- 使用 router-link 组件来导航. -->
 <!-- 通过传入 `to` 属性指定链接. -->
 <!-- <router-link> 默认会被渲染成一个 `<a>` 标签 -->
 <router-link to="/page1">Go to Foo</router-link>
 <router-link to="/page2">Go to Bar</router-link>
</p>
<!-- 路由出口 -->
<!-- 路由匹配到的组件将渲染在这里 -->
<router-view></router-view>

故我们需要使用Vue.component( id, [definition] )注册一个全局组件。

了解了大概,我们就可以写出一个基本骨架

let Vue = null

class VueRouter {
 constructor(options) {
  this.mode = options.mode || 'hash'
  this.routes = options.routes || []
 }
}

VueRouter.install = function (_Vue) {
 Vue = _Vue

 Vue.mixin({
  beforeCreate() {
   // 根组件
   if (this.$options && this.$options.router) {
    this._root = this // 把当前vue实例保存到_root上
    this._router = this.$options.router // 把router的实例挂载在_router上
   } else if (this.$parent && this.$parent._root) {
    // 子组件的话就去继承父组件的实例,让所有组件共享一个router实例
    this._root = this.$parent && this.$parent._root
   }
  },
 })

 Vue.component('router-link', {
  props: {
   to: {
    type: [String, Object],
    required: true,
   },
   tag: {
    type: String,
    default: 'a', // router-link 默认渲染成 a 标签
   },
  },
  render(h) {
   let tag = this.tag || 'a'
   return <tag href={this.to}>{this.$slots.default}</tag>
  },
 })

 Vue.component('router-view', {
  render(h) {
   return h('h1', {}, '视图显示的地方') // 暂时置为h1标签,下面会改
  },
 })
}

export default VueRouter

mode

vue-router有两种模式,默认为 hash 模式。

history 模式

通过window.history.pushStateAPI 来添加浏览器历史记录,然后通过监听popState事件,也就是监听历史记录的改变,来加载相应的内容。

  • popstate 事件

当活动历史记录条目更改时,将触发 popstate 事件。如果被激活的历史记录条目是通过对 history.pushState()的调用创建的,或者受到对 history.replaceState()的调用的影响,popstate 事件的 state 属性包含历史条目的状态对象的副本。

  • History.pushState()方法

window.history.pushState(state, title, url)

该方法用于在历史中添加一条记录,接收三个参数,依次为:

  • state:一个与添加的记录相关联的状态对象,主要用于popstate事件。该事件触发时,该对象会传入回调函数。也就是说,浏览器会将这个对象序列化以后保留在本地,重新载入这个页面的时候,可以拿到这个对象。如果不需要这个对象,此处可以填null。
  • title:新页面的标题。但是,现在所有浏览器都忽视这个参数,所以这里可以填空字符串。
  • url:新的网址,必须与当前页面处在同一个域。浏览器的地址栏将显示这个网址。

hash 模式

使用 URL 的 hash 来模拟一个完整的 URL。,通过监听hashchange事件,然后根据hash值(可通过 window.location.hash 属性读取)去加载对应的内容的。

继续增加代码,

let Vue = null

class HistoryRoute {
 constructor() {
  this.current = null // 当前路径
 }
}

class VueRouter {
 constructor(options) {
  this.mode = options.mode || 'hash'
  this.routes = options.routes || []
  this.routesMap = this.createMap(this.routes)
  this.history = new HistoryRoute() // 当前路由
  this.initRoute() // 初始化路由函数
 }

 createMap(routes) {
  return routes.reduce((pre, current) => {
   pre[current.path] = current.component
   return pre
  }, {})
 }

 initRoute() {
  if (this.mode === 'hash') {
   // 先判断用户打开时有没有hash值,没有的话跳转到 #/
   location.hash ? '' : (location.hash = '/')
   window.addEventListener('load', () => {
    this.history.current = location.hash.slice(1)
   })
   window.addEventListener('hashchange', () => {
    this.history.current = location.hash.slice(1)
   })
  } else {
   // history模式
   location.pathname ? '' : (location.pathname = '/')
   window.addEventListener('load', () => {
    this.history.current = location.pathname
   })
   window.addEventListener('popstate', () => {
    this.history.current = location.pathname
   })
  }
 }
}

VueRouter.install = function (_Vue) {
 Vue = _Vue

 Vue.mixin({
  beforeCreate() {
   if (this.$options && this.$options.router) {
    this._root = this
    this._router = this.$options.router
    Vue.util.defineReactive(this, '_route', this._router.history) // 监听history路径变化
   } else if (this.$parent && this.$parent._root) {
    this._root = this.$parent && this.$parent._root
   }
   // 当访问this.$router时即返回router实例
   Object.defineProperty(this, '$router', {
    get() {
     return this._root._router
    },
   })
   // 当访问this.$route时即返回当前页面路由信息
   Object.defineProperty(this, '$route', {
    get() {
     return this._root._router.history.current
    },
   })
  },
 })
}

export default VueRouter

router-link 和 router-view 组件

VueRouter.install = function (_Vue) {
 Vue = _Vue

 Vue.component('router-link', {
  props: {
   to: {
    type: [String, Object],
    required: true,
   },
   tag: {
    type: String,
    default: 'a',
   },
  },
  methods: {
   handleClick(event) {
    // 阻止a标签默认跳转
    event && event.preventDefault && event.preventDefault()
    let mode = this._self._root._router.mode
    let path = this.to
    this._self._root._router.history.current = path
    if (mode === 'hash') {
     window.history.pushState(null, '', '#/' + path.slice(1))
    } else {
     window.history.pushState(null, '', path.slice(1))
    }
   },
  },
  render(h) {
   let mode = this._self._root._router.mode
   let tag = this.tag || 'a'
   let to = mode === 'hash' ? '#' + this.to : this.to
   console.log('render', this.to)
   return (
    <tag on-click={this.handleClick} href={to}>
     {this.$slots.default}
    </tag>
   )
   // return h(tag, { attrs: { href: to }, on: { click: this.handleClick } }, this.$slots.default)
  },
 })

 Vue.component('router-view', {
  render(h) {
   let current = this._self._root._router.history.current // current已经是动态响应
   let routesMap = this._self._root._router.routesMap
   return h(routesMap[current]) // 动态渲染对应组件
  },
 })
}

至此,一个简易的vue-router就实现完了,案例完整代码附上:

let Vue = null

class HistoryRoute {
 constructor() {
  this.current = null
 }
}

class VueRouter {
 constructor(options) {
  this.mode = options.mode || 'hash'
  this.routes = options.routes || []
  this.routesMap = this.createMap(this.routes)
  this.history = new HistoryRoute() // 当前路由
  // 初始化路由函数
  this.initRoute()
 }

 createMap(routes) {
  return routes.reduce((pre, current) => {
   pre[current.path] = current.component
   return pre
  }, {})
 }

 initRoute() {
  if (this.mode === 'hash') {
   // 先判断用户打开时有没有hash值,没有的话跳转到 #/
   location.hash ? '' : (location.hash = '/')
   window.addEventListener('load', () => {
    this.history.current = location.hash.slice(1)
   })
   window.addEventListener('hashchange', () => {
    this.history.current = location.hash.slice(1)
   })
  } else {
   // history模式
   location.pathname ? '' : (location.pathname = '/')
   window.addEventListener('load', () => {
    this.history.current = location.pathname
   })
   window.addEventListener('popstate', () => {
    this.history.current = location.pathname
   })
  }
 }
}

VueRouter.install = function(_Vue) {
 Vue = _Vue

 Vue.mixin({
  beforeCreate() {
   // 根组件
   if (this.$options && this.$options.router) {
    this._root = this // 把当前vue实例保存到_root上
    this._router = this.$options.router // 把router的实例挂载在_router上
    Vue.util.defineReactive(this, '_route', this._router.history) // 监听history路径变化
   } else if (this.$parent && this.$parent._root) {
    // 子组件的话就去继承父组件的实例,让所有组件共享一个router实例
    this._root = this.$parent && this.$parent._root
   }
   // 当访问this.$router时即返回router实例
   Object.defineProperty(this, '$router', {
    get() {
     return this._root._router
    },
   })
   // 当访问this.$route时即返回当前页面路由信息
   Object.defineProperty(this, '$route', {
    get() {
     return this._root._router.history.current
    },
   })
  },
 })

 Vue.component('router-link', {
  props: {
   to: {
    type: [String, Object],
    required: true,
   },
   tag: {
    type: String,
    default: 'a',
   },
  },
  methods: {
   handleClick(event) {
    // 阻止a标签默认跳转
    event && event.preventDefault && event.preventDefault() // 阻止a标签默认跳转
    let mode = this._self._root._router.mode
    let path = this.to
    this._self._root._router.history.current = path
    if (mode === 'hash') {
     window.history.pushState(null, '', '#/' + path.slice(1))
    } else {
     window.history.pushState(null, '', path.slice(0))
    }
   },
  },
  render(h) {
   let mode = this._self._root._router.mode
   let tag = this.tag || 'a'
   let to = mode === 'hash' ? '#' + this.to : this.to
   return (
    <tag on-click={this.handleClick} href={to}>
     {this.$slots.default}
    </tag>
   )
   // return h(tag, { attrs: { href: to }, on: { click: this.handleClick } }, this.$slots.default)
  },
 })

 Vue.component('router-view', {
  render(h) {
   let current = this._self._root._router.history.current // current已经是动态
   let routesMap = this._self._root._router.routesMap
   return h(routesMap[current]) // 动态渲染对应组件
  },
 })
}

export default VueRouter

ps: 个人技术博文 Github 仓库,觉得不错的话欢迎 star,给我一点鼓励继续写作吧~

以上就是如何手写简易的 Vue Router的详细内容,更多关于手写简易的 Vue Router的资料请关注三水点靠木其它相关文章!

Javascript 相关文章推荐
ASP.NET中基于JQUERY的高性能的TreeView补充
Feb 23 Javascript
js中单引号与双引号冲突问题解决方法
Oct 04 Javascript
Jquery简单实现GridView行高亮的方法
Jun 15 Javascript
JavaScript中字符串与Unicode编码互相转换的实现方法
Dec 18 Javascript
基于Bootstrap的Metronic框架实现条码和二维码的生成及打印处理操作
Aug 29 Javascript
Windows环境下npm install 报错: operation not permitted, rename的解决方法
Sep 26 Javascript
Html中 IFrame的用法及注意点
Dec 22 Javascript
详解AngularJs ui-router 路由的简单介绍
Apr 26 Javascript
Django+Vue.js搭建前后端分离项目的示例
Aug 07 Javascript
jQuery实现的鼠标滚轮控制图片缩放功能实例
Oct 14 jQuery
vue中子组件传递数据给父组件的讲解
Jan 27 Javascript
解决ele ui 表格表头太长问题的实现
Nov 13 Javascript
如何手写一个简易的 Vuex
Oct 10 #Javascript
echarts实现晶体球面投影的实例教程
Oct 10 #Javascript
详解Vue中Axios封装API接口的思路及方法
Oct 10 #Javascript
在Vue中使用Echarts实例图的方法实例
Oct 10 #Javascript
基于Vue.js+Nuxt开发自定义弹出层组件
Oct 09 #Javascript
IDEA配置jQuery, $符号不再显示黄色波浪线的问题
Oct 09 #jQuery
vue中解决chrome浏览器自动播放音频和MP3语音打包到线上的实现方法
Oct 09 #Javascript
You might like
定义php常量的详解
2013/06/09 PHP
取得单条网站评论以数组形式进行输出
2014/07/28 PHP
PHP代码实现爬虫记录――超管用
2015/07/31 PHP
分享3个php获取日历的函数
2015/09/25 PHP
linux下为php添加iconv模块的方法
2016/02/28 PHP
java解析json方法总结
2019/05/16 PHP
php中array_fill函数的实例用法
2021/03/02 PHP
js 方法实现返回多个数据的代码
2009/04/30 Javascript
检测input每次的输入是否合法遇到汉字输入就有问题
2012/05/23 Javascript
js(jQuery)获取时间的方法及常用时间类搜集
2013/10/23 Javascript
js 点击页面其他地方关闭弹出层(示例代码)
2013/12/24 Javascript
js实现刷新iframe的方法汇总
2015/04/27 Javascript
实例讲解jquery与json的结合
2016/01/07 Javascript
微信小程序开发实战教程之手势解锁
2016/11/18 Javascript
分享一个精简的vue.js 图片lazyload插件实例
2017/03/13 Javascript
jQuery实现表格的增、删、改操作示例
2019/01/27 jQuery
微信小程序实现单选选项卡切换效果
2020/06/19 Javascript
微信小程序点击生成朋友圈分享图(遇到的坑)
2020/06/17 Javascript
[02:22]完美世界DOTA2联赛PWL S3 集锦第一期
2020/12/15 DOTA
十个Python程序员易犯的错误
2015/12/15 Python
python3实现磁盘空间监控
2018/06/21 Python
基于wxPython的GUI实现输入对话框(1)
2019/02/27 Python
Windows下PyCharm2018.3.2 安装教程(图文详解)
2019/10/24 Python
python代码xml转txt实例
2020/03/10 Python
Python爬虫爬取、解析数据操作示例
2020/03/27 Python
HTML5 embed标签定义和用法详解
2014/05/09 HTML / CSS
猫咪家具:CatsPlay
2018/11/03 全球购物
Mountain Warehouse德国官网:英国户外零售商
2019/08/11 全球购物
Athleta官网:购买女士瑜伽服、技术运动服和休闲运动服
2020/11/12 全球购物
怎样比较两个类型为String的字符串
2016/08/17 面试题
秋季运动会广播稿大全
2014/02/17 职场文书
公司酒会主持词
2015/07/02 职场文书
Python实现8种常用抽样方法
2021/06/27 Python
SQLServer 错误: 15404,无法获取有关 Windows NT 组/用户 WIN-8IVSNAQS8T7\Administrator 的信息
2021/06/30 SQL Server
mysql的数据压缩性能对比详情
2021/11/07 MySQL
MySQL 分区表中分区键为什么必须是主键的一部分
2022/03/17 MySQL