如何手写简易的 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 相关文章推荐
JavaScript高级程序设计(第3版)学习笔记11 内建js对象
Oct 11 Javascript
查看大图功能代码jquery版
Nov 05 Javascript
JavaScript实现找出字符串中第一个不重复的字符
Sep 03 Javascript
JS+DIV+CSS实现仿表单下拉列表效果
Aug 18 Javascript
require.js的用法详解
Oct 20 Javascript
javascript单页面手势滑屏切换原理详解
Mar 21 Javascript
详细谈谈javascript的对象
Jul 31 Javascript
基于JavaScript实现树形下拉框
Aug 10 Javascript
jQuery可见性过滤选择器用法示例
Sep 09 Javascript
vue多页面开发和打包正确处理方法
Apr 20 Javascript
详解JavaScript原生封装ajax请求和Jquery中的ajax请求
Feb 14 jQuery
前端js中的事件循环eventloop机制详解
May 15 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 Class 文章
2007/04/04 PHP
PHP SQLite类
2009/05/07 PHP
ionCube 一款类似zend的PHP加密/解密工具
2010/07/25 PHP
自己写的php中文截取函数mb_strlen和mb_substr
2015/02/09 PHP
Javascript 继承机制的实现
2009/08/12 Javascript
javascript 实用的文字链提示框效果
2010/06/30 Javascript
多个表单中如何获得这个文件上传的网址实现js代码
2013/03/25 Javascript
用JS实现3D球状标签云示例代码
2013/12/01 Javascript
ANGULARJS中使用JQUERY分页控件
2015/09/16 Javascript
jQuery实现鼠标选中文字后弹出提示窗口效果【附demo源码】
2016/09/05 Javascript
原生js实现旋转木马轮播图效果
2017/02/27 Javascript
NodeJS实现微信公众号关注后自动回复功能
2017/05/31 NodeJs
Webpack打包css后z-index被重新计算的解决方法
2017/06/18 Javascript
基于JavaScript实现图片连播和联级菜单实例代码
2017/07/28 Javascript
JS实现网页抢购功能(触发,终止脚本)
2017/11/27 Javascript
JS使用正则表达式获取小括号、中括号及花括号内容的方法示例
2018/06/01 Javascript
详解JS判断页面是在手机端还是在PC端打开的方法
2019/04/26 Javascript
Vue代码整洁之去重方法整理
2019/08/06 Javascript
node.JS的crypto加密模块使用方法详解(MD5,AES,Hmac,Diffie-Hellman加密)
2020/02/06 Javascript
[01:11:35]Liquid vs LGD 2018国际邀请赛小组赛BO2 第一场 8.16
2018/08/17 DOTA
pymongo实现控制mongodb中数字字段做加法的方法
2015/03/26 Python
Python中的各种装饰器详解
2015/04/11 Python
磁盘垃圾文件清理器python代码实现
2020/08/24 Python
详解基于django实现的webssh简单例子
2018/07/17 Python
Python面向对象之类和对象实例详解
2018/12/10 Python
使用python爬取微博数据打造一颗“心”
2019/06/28 Python
Python Process创建进程的2种方法详解
2021/01/25 Python
css3编写浏览器背景渐变背景色的方法
2018/03/05 HTML / CSS
简历中个人求职的自我评价模板
2013/11/29 职场文书
求职信的要素有哪些呢
2013/12/26 职场文书
电工技术比武方案
2014/05/11 职场文书
节约每一滴水演讲稿
2014/09/09 职场文书
教师群众路线剖析材料
2014/09/29 职场文书
2015年庆祝国庆节66周年演讲稿
2015/07/30 职场文书
《富饶的西沙群岛》教学反思
2016/02/16 职场文书
Golang中channel的原理解读(推荐)
2021/10/16 Golang