如何手写简易的 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 相关文章推荐
juqery 学习之四 筛选过滤
Nov 30 Javascript
js一般方法改写成面向对象方法的无限级折叠菜单示例代码
Jul 04 Javascript
Javascript中typeof 用法小结
May 12 Javascript
浅谈利用JavaScript进行的DDoS攻击原理与防御
Jun 04 Javascript
jquery获取css的color值返回RGB的方法
Dec 18 Javascript
基于JavaScript实现添加到购物车效果附源码下载
Aug 22 Javascript
JavaScript 数据类型详解
Mar 13 Javascript
整理关于Bootstrap列表组的慕课笔记
Mar 29 Javascript
JS实现的四级密码强度检测功能示例
May 11 Javascript
详解webpack-dev-server使用http-proxy解决跨域问题
Jan 13 Javascript
Vue中render方法的使用详解
Jan 26 Javascript
vue中引入第三方字体文件的方法示例
Dec 17 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 UTF8 文件的签名问题
2009/10/30 PHP
与文件上传有关的php配置参数总结
2013/06/14 PHP
PHP实现读取一个1G的文件大小
2013/08/24 PHP
php实现多维数组中每个单元值(数字)翻倍的方法
2015/02/16 PHP
php版微信发红包接口用法示例
2016/09/23 PHP
[原创]PHP正则匹配中英文、数字及下划线的方法【用户名验证】
2017/08/01 PHP
ASP.NET jQuery 实例2 (表单中使用回车在TextBox之间向下移动)
2012/01/13 Javascript
用RadioButten或CheckBox实现div的显示与隐藏
2013/09/21 Javascript
js propertychange和oninput事件
2014/09/28 Javascript
js全选实现和判断是否有复选框选中的方法
2015/02/17 Javascript
javascript实现链接单选效果的方法
2015/05/13 Javascript
Bootstrap每天必学之按钮(一)
2015/11/24 Javascript
jquery 遍历数组 each 方法详解
2016/05/25 Javascript
基于jQuery实现表格的查看修改删除
2016/08/01 Javascript
移动端使用localStorage缓存Js和css文的方法(web开发)
2016/09/20 Javascript
新手学习前端之js模仿淘宝主页网站
2016/10/31 Javascript
网站申请不到支付宝接口、微信接口,免接口收款实现方式几种解决办法
2016/12/14 Javascript
使用 vue 实现灭霸打响指英雄消失的效果附demo
2019/05/06 Javascript
NodeJS http模块用法示例【创建web服务器/客户端】
2019/11/05 NodeJs
vue实现将数据存入vuex中以及从vuex中取出数据
2019/11/08 Javascript
python实现抖音视频批量下载
2018/06/20 Python
通过PHP与Python代码对比的语法差异详解
2019/07/10 Python
python代码实现将列表中重复元素之间的内容全部滤除
2020/05/22 Python
python 如何上传包到pypi
2020/12/24 Python
使用CSS3编写灰阶滤镜来制作黑白照片效果的方法
2016/05/09 HTML / CSS
美国知名玩具品牌:Melissa & Doug
2016/08/16 全球购物
全球领先的各类汽车配件零售商:Advance Auto Parts
2016/08/26 全球购物
美国真皮手袋品牌:GiGi New York
2017/03/10 全球购物
介绍一下write命令
2012/09/24 面试题
介绍一下grep命令的使用
2012/06/28 面试题
平面设计岗位职责
2013/12/14 职场文书
2014年语文教学工作总结
2014/12/17 职场文书
妈妈别哭观后感
2015/06/08 职场文书
金榜题名主持词
2015/07/02 职场文书
在K8s上部署Redis集群的方法步骤
2021/04/27 Redis
MySQL单表千万级数据处理的思路分享
2021/06/05 MySQL