vue-router的实现原理_浅析vuerouter源码并尝试实现一个简单的前端路由

c9498ffd7203ce941c0c3dd88e071e3e.pngc8e18ec0c95b751a2ed12d5a6a07d7e3.png

前言

作为vue生态圈中的重要一员:vue-router已经成为了前端工程师要掌握的基本技能之一。本文抛开了vue-router的日常使用,从源码入手,一起学习下源码并自己尝试实现一个自己的vue-router。阅读过程中,如有不足之处,恳请斧正。

本文共2000余字,阅读本篇大概需要15分钟。如有不足之处,恳请斧正
17d6aecb1ed206c63129411114ab599d.png

此处推荐一篇之前实现一个自己的vuex的文章,可与本篇搭配观看

从0到1手写一个vuex

源码浅析

首先来看下源码目录结构:
// 路径:node_modules/vue-router├── dist ---------------------------------- 构建后文件的输出目录├── package.json -------------------------- 不解释├── src ----------------------------------- 包含主要源码│   ├── components --------------------------- 路由组件│   │   ├── link.js ---------------- router-link组件│   │   ├── view.js -- router-view组件│   ├── history -------------------------- 路由模式│   │   ├── abstract.js ------------------------ abstract路由模式│   │   ├── base.js ----------------------- history路由模式│   │   ├── errors.js ------------------ 错误类处理│   │   ├── hash.js ---------------------- hash路由模式│   │   ├── html5.js -------------------------- HTML5History模式封装│   ├── util ---------------------------- 工具类功能封装│   ├── create-matcher.js ------------------------- 路由映射表│   ├── create-route-map.js ------------------------------- 创建路由映射状态树│   ├── index.js ----------------------------  主入口文件|   ├── install.js ---------------------------- 路由装载文件

入口开始分析

vue-router实例

从入口文件index.js中我们可以看到暴露出了一个VueRouter类,这个就是我们在 vue 项目中引入 vue-router 的时候所用到的new Router() 其中具体内部代码如下(为了方便阅读,省略部分代码)

export default class VueRouter {  constructor (options: RouterOptions = {}) {    this.app = null    this.apps = []    this.options = options    this.beforeHooks = []    this.resolveHooks = []    this.afterHooks = []    this.matcher = createMatcher(options.routes || [], this)    let mode = options.mode || 'hash'    this.fallback = mode === 'history' && !supportsPushState && options.fallback !== false    if (this.fallback) {      mode = 'hash'    }    if (!inBrowser) {      mode = 'abstract'    }    this.mode = mode    switch (mode) {      case 'history':        this.history = new HTML5History(this, options.base)        break      case 'hash':        this.history = new HashHistory(this, options.base, this.fallback)        break      case 'abstract':        this.history = new AbstractHistory(this, options.base)        break      default:        if (process.env.NODE_ENV !== 'production') {          assert(false, `invalid mode: ${mode}`)        }    }  }  init () {}  function registerHook (){}  go(){}  push(){}  VueRouter.install = install  VueRouter.version = '__VERSION__'      if (inBrowser && window.Vue) {      window.Vue.use(VueRouter)  }

从入口文件中我们可以看出里面包含了以下几个主要几个步骤:

  1. 初始化路由模式

  2. 根据传入的routes参数生成路由状态表

  3. 获取当前路由对象

  4. 初始化路由函数

  5. 注册Hooks等事件

  6. 添加install装载函数

install注册函数

上述暴露出的router类中挂载了一个install方法,这里我们对其做一个简要的分析(这也是我们下面实现一个自己路由的思维引导)。在我们引入vue-router并且实例化它的时候,vue-router内部帮助我们将router实例装载入vue的实例中,这样我们才可以在组件中可以直接使用router-link、router-view等组件。以及直接访问this.$router、this.$route等全局变量,这里主要归功于install.js帮助实现这一个过程,主要分以下几个步骤:

  1. 首先引入vue-router后需要利用beforeCreate生命周期进行装载它,用来初始化_routerRoot,_router,_route等数据,
  2. 同时设置全局访问变量$router$router
  3. 完成router-linkrouter-view 两个组件的注册

在源码install.js中可以体现

import View from './components/view'import Link from './components/link'export let _Vueexport function install (Vue) {  if (install.installed && _Vue === Vue) return  install.installed = true  _Vue = Vue  // 混入vue实例中  Vue.mixin({    beforeCreate () {      if (isDef(this.$options.router)) {        this._routerRoot = this        this._router = this.$options.router        this._router.init(this)        Vue.util.defineReactive(this, '_route', this._router.history.current)      } else {        this._routerRoot = (this.$parent && this.$parent._routerRoot) || this      }      registerInstance(this, this)    },    destroyed () {      registerInstance(this)    }  })  // 设置全局访问变量$router  Object.defineProperty(Vue.prototype, '$router', {    get () { return this._routerRoot._router }  })  Object.defineProperty(Vue.prototype, '$route', {    get () { return this._routerRoot._route }  }) // 注册组件  Vue.component('RouterView', View)  Vue.component('RouterLink', Link)}

上述我们对vue-router的源码做了一个粗略的脉络梳理,下面我们将实现一个简化版的vue-router。在此之前我们需要简单的了解一些知识点

前置知识

我们都知道vue-router提供一个mode参数配置,我们可以设置history 或者是hash 两种参数背后的实现原理也各不相同

hash的实现原理

http://localhost:8080/#login

#符号本身以及它后面的字符称之为hash,可通过window.location.hash属性读取。H5新增了一个hashchange来帮助我们监听浏览器链接的hash值变化。

<html lang="en"><head>  <meta charset="UTF-8">  <meta name="viewport" content="width=device-width, initial-scale=1.0">  <meta http-equiv="X-UA-Compatible" content="ie=edge">  <title>老 yuantitle>head><body>  <h1 id="app">h1>  <a href="#/jin">掘金a>  <a href="#/sn">黄小虫a>  <script>      window.addEventListener('hashchange',()=>{          app.innerHTML = location.hash.slice(2)      })script>body>html>

history的实现原理

http://localhost:8080/login同样H5也新增了pushStatepopstate来帮助我们无感知刷新浏览器url

<body>  <h1 id="app">h1>  <a onclick="to('jin')">掘金a>  <a onclick="to('sn')">黄小虫a><script >    function to(pathname) {        history.pushState({},null,pathname)        app.innerHTML = pathname    }    window.addEventListener('popstate',()=>{        to(location.pathname)    })script>body>

在我们理清楚无感知刷新url的原理后,我们要基于这些原理封装出一个vue-router

开始实现自己的vue-router


实现install装载方法

首先我们需要初始化一下我们项目结构,新建 simple-vue-router.js,根据上面分析,这里我们需要暴露出一个 router类,其中需要包含一个 install方法
let Vue // 用于保存vue实例class VueRouter(){ // router类    }function install(_vue){ // 装载函数    }export default {    VueRouter,    install}

其中的install需要实现以下几点功能

  1. 初始化_routerRoot_router_route等数据,

  2. 设置全局访问变量$router$router

  3. 完成router-linkrouter-view两个组件的注册

代码如下:

let Vue // 用于保存vue实例class VueRouter {  // router类}VueRouter.install = function(_vue) {  // 装载函数  //每个组件都有 this.$router / this.$route 所以要mixin一下  Vue = _vue  // 在每个组件中都可以获取到 this.$router与this.$route,这里进行混入vue实例中  Vue.mixin({    beforeCreate() {      // 如果是根组件则      if (this.$options && this.$options.router) {        this._root = this //把当前vue实例保存到_root上        this._router = this.$options.router // 把router的实例挂载在_router上      } else {        // 如果是子组件则去继承父组件的实例(这样所有组件共享一个router实例)        this._root = this.$parent._root      }      // 定义router实例 当访问this.$router时即返回router实例      Object.defineProperty(this, '$router', {        get() {          return this._root._router        }      })      // 定义route 当访问this.$route时即返回当前页面路由信息      Object.defineProperty(this, '$route', {        get() {          return {}        }      })    }  })  // 全局注册 router的两个组件  Vue.component('router-link', {    render(h) {}  })  Vue.component('router-view', {    render(h) {}  })}export default VueRouter

实现router

上述实现了 install方法帮助我们将 router挂载在 vue实例中,接下来我们需要完善一下 router类中的功能。按照上文源码中的分析,我们需要实现以下几点功能:
  1. 生成根据传入的rotues参数生成,路由状态表。即如若传入参数为
  routes:[    {      path: '/',      name: 'index',      component: index    },    {      path: '/login',      name: 'login',      component: login    },    {      path: '/learn',      name: 'learn',      component: learn    },  ]

将其用path为key,component为value的规律格式化为

{ '/':index, '/login':login, '/learn':learn}
  1. 定义当前路由变量,通过劫持进行实时渲染对应组件
  2. 定义一个函数,具体实现不同模式应对应使用的处理方法

具体代码如下

let Vue // 用于保存vue实例class VueRouter {  // router类  constructor(options) {    // 默认为hash模式    this.mode = options.mode || 'hash'    this.routes = options.routes || []    // 路由映射表    this.routeMaps = this.generateMap(this.routes)    // 当前路由    this.currentHistory = new historyRoute()    // 初始化路由函数    this.initRoute()  }  generateMap(routes) {    return routes.reduce((prev, current) => {      prev[current.path] = current.component      return prev    }, {})  }  initRoute() {    // 这里仅处理hash模式与history模式    if (this.mode === 'hash') {      // 先判断用户打开时url中有没有hash,没有重定向到#/      location.hash ? '' : (location.hash = '/')      // 监控浏览器load事件,改变当前存储的路由变量      window.addEventListener('load', () => {        this.currentHistory.current = location.hash.slice(1)      })      window.addEventListener('hashchange', () => {        this.currentHistory.current = location.hash.slice(1)      })    } else {      location.pathname ? '' : (location.pathname = '/')      window.addEventListener('load', () => {        this.currentHistory.current = location.pathname      })      window.addEventListener('popstate', () => {        this.currentHistory.current = location.pathname      })    }  }}class historyRoute {  constructor() {    this.current = null  }}VueRouter.install = function(_vue) {// 省略部分代码}export default VueRouter

完善代码,实现实时刷新页面视图

在构建完 r outer类之后,我们发现还存在一个问题,那就是当前路由状态 currentHistory.current还是静态的,当我们改变当前路由的时候页面并不会显示对应模板。这里我们可以利用 vue自身的双向绑定机制实现具体代码如下
let Vue // 用于保存vue实例class VueRouter {  // router类  constructor(options) {    // 默认为hash模式    this.mode = options.mode || 'hash'    this.routes = options.routes || []    // 路由映射表    this.routeMaps = this.generateMap(this.routes)    // 当前路由    this.currentHistory = new historyRoute()    // 初始化路由函数    this.initRoute()  }  generateMap(routes) {    return routes.reduce((prev, current) => {      prev[current.path] = current.component      return prev    }, {})  }  initRoute() {    // 这里仅处理hash模式与history模式    if (this.mode === 'hash') {      // 先判断用户打开时url中有没有hash,没有重定向到#/      location.hash ? '' : (location.hash = '/')      // 监控浏览器load事件,改变当前存储的路由变量      window.addEventListener('load', () => {        this.currentHistory.current = location.hash.slice(1)      })      window.addEventListener('hashchange', () => {        this.currentHistory.current = location.hash.slice(1)      })    } else {      location.pathname ? '' : (location.pathname = '/')      window.addEventListener('load', () => {        this.currentHistory.current = location.pathname      })      window.addEventListener('popstate', () => {        this.currentHistory.current = location.pathname      })    }  }}class historyRoute {  constructor() {    this.current = null  }}VueRouter.install = function(_vue) {  // 装载函数  //每个组件都有 this.$router / this.$route 所以要mixin一下  Vue = _vue  // 在每个组件中都可以获取到 this.$router与this.$route,这里进行混入vue实例中  Vue.mixin({    beforeCreate() {      // 如果是根组件则      if (this.$options && this.$options.router) {        this._root = this //把当前vue实例保存到_root上        this._router = this.$options.router // 把router的实例挂载在_router上        //利用vue工具库对当前路由进行劫持        Vue.util.defineReactive(this,'route',this._router.currentHistory)      } else {        // 如果是子组件则去继承父组件的实例(这样所有组件共享一个router实例)        this._root = this.$parent._root      }      // 定义router实例 当访问this.$router时即返回router实例      Object.defineProperty(this, '$router', {        get() {          return this._root._router        }      })      // 定义route 当访问this.$route时即返回当前页面路由信息      Object.defineProperty(this, '$route', {        get() {          return {            // 当前路由            current: this._root._router.history.current          }        }      })    }  })  // 全局注册 router的两个组件  Vue.component('router-link', {    props: {      to: String,      tag: String    },    methods: {      handleClick(event) {        // 阻止a标签默认跳转        event && event.preventDefault && event.preventDefault()        let mode = this._self._root._router.mode        let path = this.to        this._self._root._router.currentHistory.current = path        if (mode === 'hash') {          window.history.pushState({}, '', '#/' + path.slice(1))        } else {          window.history.pushState({}, '', path.slice(1))        }      }    },    render(h) {      let mode = this._self._root._router.mode      let tag = this.tag || 'a'      return (        this.handleClick} href={mode ===           {this.$slots.default}              )    }  })  Vue.component('router-view', {    render(h) {      // 这里的current通过上面的劫持已经是动态了      let current = this._self._root._router.currentHistory.current      let routeMap = this._self._root._router.routeMaps      return h(routeMap[current]) // 动态渲染对应组件    }  })}export default VueRouter
到此为止,一个简单的路由管理器已经完成。实际上相对 vue-router来说还是缺少了很多诸如导航守卫、动态路由等功能。千里之行始于足下,本篇文章旨在通过简要分析 vue-router的原理以及实践一个简单的路由器帮助大家走进 vue-router原理的大门,后面的就要靠大家自己坚持继续深入学习了。

(转载)

作者:黄小虫

原文链接:https://juejin.im/post/5e435815f265da57340233ee

一套从0到1开发的前后端一体项目+Nginx部署node.js实战,请注意查收

b09ded1200af009b7f37b88e8c2f0352.pngEND

6b638c2cdbde7645cda484f2c4cd0d61.png

3061656a2c02b174619c812d6e0b6eb2.gif   点击
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值