如何手写简易的 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 相关文章推荐
jquery的live使用注意事项
Feb 18 Javascript
JavaScript中var关键字的使用详解
Aug 14 Javascript
jQuery制作圣诞主题页面 更像是爱情影集
Aug 10 Javascript
javascript中json基础知识详解
Jan 19 Javascript
node.js实现复制文本到剪切板的功能
Jan 23 Javascript
基于JavaScript定位当前的地理位置
Apr 11 Javascript
jQuery序列化form表单数据为JSON对象的实现方法
Sep 20 jQuery
微信小程序实现图片上传
May 23 Javascript
微信小程序 组件的外部样式externalClasses使用详解
Sep 06 Javascript
node实现mock-plugin中间件的方法
Dec 25 Javascript
js实现选项卡效果
Mar 07 Javascript
深入分析jQuery.one() 函数
Jun 03 jQuery
如何手写一个简易的 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实现的在线人员函数库
2008/04/09 PHP
php下intval()和(int)转换使用与区别
2008/07/18 PHP
求PHP数组最大值,最小值的代码
2011/10/31 PHP
深入解析PHP中的(伪)多线程与多进程
2013/07/01 PHP
一个简洁实用的PHP缓存类完整实例
2014/07/26 PHP
PHP列出MySQL中所有数据库的方法
2015/03/12 PHP
Symfony2创建基于域名的路由相关示例
2016/11/14 PHP
用dtree实现树形菜单 dtree使用说明
2011/10/17 Javascript
JavaScript基本编码模式小结
2012/05/23 Javascript
nodejs的require模块(文件模块/核心模块)及路径介绍
2013/01/14 NodeJs
Jquery显示、隐藏元素以及添加删除样式
2013/08/09 Javascript
Boostrap模态窗口的学习小结
2016/03/28 Javascript
老生常谈遮罩层 滚动条的问题
2016/04/29 Javascript
使用 jQuery.ajax 上传带文件的表单遇到的问题
2016/10/31 Javascript
vue init webpack myproject构建项目 ip不能访问的解决方法
2018/03/20 Javascript
vue 插件的方法代码详解
2019/06/06 Javascript
对TypeScript库进行单元测试的方法
2019/07/18 Javascript
javascript设计模式 ? 原型模式原理与应用实例分析
2020/04/10 Javascript
Vuejs通过拖动改变元素宽度实现自适应
2020/09/02 Javascript
[01:01:31]2018DOTA2亚洲邀请赛3月29日小组赛B组 Mineski VS paiN
2018/03/30 DOTA
python从ftp下载数据保存实例
2013/11/20 Python
python里运用私有属性和方法总结
2019/07/08 Python
python 直接赋值和copy的区别详解
2019/08/07 Python
Spring http服务远程调用实现过程解析
2020/06/11 Python
python爬虫利用代理池更换IP的方法步骤
2021/02/21 Python
css3 给页面加个半圆形导航条主要利用旋转和倾斜样式
2014/02/10 HTML / CSS
Yahoo-PHP面试题4
2012/05/05 面试题
幼儿园义卖活动方案
2014/01/17 职场文书
市场营销专业毕业生求职信
2014/07/21 职场文书
2014市府办领导班子“四风问题”对照检查材料思想汇报
2014/09/24 职场文书
会议邀请函
2015/01/30 职场文书
采购员岗位职责
2015/02/03 职场文书
教师求职简历自我评价
2015/03/10 职场文书
Java实现多线程聊天室
2021/06/26 Java/Android
常用的文件对应的MIME类型汇总
2022/04/26 HTML / CSS
Python TypeError: ‘float‘ object is not subscriptable错误解决
2022/12/24 Python