Vue 全家桶之 Vue Router 的简单实现

370 阅读3分钟

日常开发中,如果是Vue生态,Vue Router 的使用,可以说几乎是不可或缺的。以下是 Vue Router插件核心逻辑的拆解及简单实现。

在深入源码内部探究之前,有一个很重要的前置知识是Vue.use

Vue.use到底做了什么:

  1. 目的是安装Vue插件
  2. 会将Vue作为参数传入插件的install方法(如果插件是个函数,那么他就是install方法本身)
  3. Vue.use的核心方法在src/core/global-api/use.js
    • 总计不到20行,核心逻辑如下
    • 获取已安装的插件数组,如果存在则直接返回this
    • 不存在则执行插件的install方法

官方文档中所说的插件

Vue.use( plugin )
参数:

{Object | Function} plugin
用法:

安装 Vue.js 插件。如果插件是一个对象,必须提供 install 方法。如果插件是一个函数,它会被作为 install 方法。install 方法调用时,会将 Vue 作为参数传入。

该方法需要在调用 new Vue() 之前被调用。

当 install 方法被同一个插件多次调用,插件将只会被安装一次。
// Vue.js 的插件应该暴露一个 install 方法。
// 这个方法的第一个参数是 Vue 构造器,第二个参数是一个可选的选项对象:
MyPlugin.install = function (Vue, options) {
  // 1. 添加全局方法或 property
  Vue.myGlobalMethod = function () {
    // 逻辑...
  }

  // 2. 添加全局资源
  Vue.directive('my-directive', {
    bind (el, binding, vnode, oldVnode) {
      // 逻辑...
    }
    ...
  })

  // 3. 注入组件选项
  Vue.mixin({
    created: function () {
      // 逻辑...
    }
    ...
  })

  // 4. 添加实例方法
  Vue.prototype.$myMethod = function (methodOptions) {
    // 逻辑...
  }
}

Vue-Router需求分析

单页面应用程序中,url发生变化时候,不能刷新,显示对应视图内容

  1. SPA页面不能刷新
    • hash (#/about
    • history (api /about
  2. 根据url显示对应的内容
    • router-view
    • 数据响应式:current变量持有url地址,一旦变化,重新动态渲染对应组件

思路

实现一个插件

  • 实现VueRouter类
    • 处理路由选项
    • 监控url变化,hashchange
    • 响应这个变化
  • 实现install方法
    • $router注册
    • 两个全局组件

代码

let Vue;

// 声明插件VueRouter
class VueRouter {
  constructor(options) {
    // 1.保存路由选项
    this.$options = options;

    // 路由变动视图更新的核心
    // 用Vue.set不行的原因,set对参数要求其本身已经是响应式对象
    // Vue.util应该是Vue内部的方法
    // 同时为current设置初始值
    Vue.util.defineReactive(
      this,
      "current",
      window.location.hash.slice(1) || "/"
    );

    // 2.监控hash变化
    window.addEventListener("hashchange", () => {
      // hash: #/about
      this.current = window.location.hash.slice(1);
    });
  }
}

// 文档有说,调用install方法时,会传入Vue构造函数
VueRouter.install = function(_Vue) {
  Vue = _Vue;

  // 最简单的拍脑袋的做法就是,把router的实例放到Vue的原型上
  // Vue.prototype = router
  // 但问题在于install方法在执行的时候,router还未实例化
  // 所以无法在install方法执行时将router实例挂载到Vue原型上
  // 即,要将挂载到原型上的操作延迟执行,延迟到router和vue都实例化完毕之后


  // 1.注册$router,让所有组件实例都可以访问它
  // 混入:Vue.mixin({})
  // 选用Vue.mixin配合beforeCreate实现router注册的逻辑
  // install方法有要求,需要在new Vue之前调用
  // 那么这就意味着,从时机上来说,use的时候,你无法直接拿到VueRouter的实例,你甚至也拿不到自己写的路由表
  // 我们为什么要拿到VueRouter的实例,按照现在官方实现,不管在哪一个组件实例中,你都可以直接使用 this.$router.push 这些方法
  // 那么,这就要求我们必须得把VueRouter 的实例挂载到Vue的构造函数中,这样无论是哪一个组件实例都可以直接用上VueRouter实例中的方法和属性
  Vue.mixin({
    beforeCreate() {
      // 延迟执行:延迟到router实例和vue实例都创建完毕
      if (this.$options.router) {
        // 如果存在说明是根实例,在根实例还是构造函数?放一份,
        // 就可以通过原型拿到了
        Vue.prototype.$router = this.$options.router;
      }
    },
  });

  // 注册两个全局组件,使得我们可以直接在template里使用 router-link router-view
  // <router-link to="/home">home</router-link>
  // - // 注册组件,传入一个选项对象 (自动调用 Vue.extend)
  // - Vue.component('my-component', { /* ... */ }) // 常用
  // router-link功能的本质就是路由跳转,这里实现的hash 模式的
  Vue.component("router-link", {
    props: {
      to: {
        type: String,
        required: true,
      },
    },
    render(h) {
      // <a href="#/home">xxx</a>
      // h是render函数调用时,框架传入的createElement
      // 等同于react中createElement,返回vdom
      // return <a href={'#'+this.to}>{this.$slots.default}</a>
      return h(
        "a",
        {
          attrs: {
            href: "#" + this.to,
          },
        },
        this.$slots.default//存放不具名插槽的数据
      );
    },
  });
  // router-view功能的本质就是渲染路由对应的组件
  Vue.component("router-view", {
    render(h) {
      let component = null;
      // 1.获取当前url的hash部分
      // 2.根据hash部分从路由表中获取对应的组件
      // 下一行的this是啥,是router-view组件的实例
      const route = this.$router.$options.routes.find(
        (route) => route.path === this.$router.current
      );
      if (route) {
        component = route.component;
      }
      return h(component);
    },
  })
};

export default VueRouter;

当然,上面的代码实现很简陋,只对hash模式下的部分功能进行了实现。

代码实现中,我觉得两个全局组件的实现会相对好理解些,其本质无非是组件的渲染和路由的跳转。

而相对陌生的是Vue.util.defineReative这一API的使用,可以说这也是数据响应式概念的体现,数据的变动会让使用到数据的组件得到通知从而重新渲染。