我们来手写一个mini版vue-router叭(超详细,超基础!)

552 阅读4分钟

本文将讲解,如何手动实现一个mini版的vue-router,(本文中我们称为KRouter

我们的KRouter要实现的功能:

  • 正常的路由跳转

需求分析

我们需要考虑以下俩点:

  • spa页面应用不能刷新,所以我们想到了俩种模式。(本文使用hash,后续在写vuex的时候会用History)

    • hash模式 (eg:#/about、event:onchange
    • History api模式 (eg:/about、event:pushState

    因为是单页面应用,所以我们只能通过hashhistory这俩种方式实现我们的需求,并且这俩种方式都有对应的事件。

  • 根据url显示对应的内容

    • router-view
    • 数据响应式:current变量持有url地址,一旦变化,动态重新执行渲染

    router-view其实我们可以理解为组件容器,定义current变量代表当前的url,根据current从路由表里面取出对应的component,也就实现了router-view展示的功能。还需要考虑的一点是,如果current改变,那我们对应的router-view中展示的component也要相应改变。

代码实现

我们的写代码的目标:

  • 实现KRouter类
  • 实现install方法

1、环境搭建

  • 创建项目

    vue create myapp
    npm run serve
    
  • 新建src/KRouter/index.js

    image.png

  • 修改src/router/index.js,引用我们自己的KRouter;

    import VueRouter from "vue-router";
    // 修改为
    import VueRouter from "../KRouter/index";
    

此时我们浏览器看一下我们的项目运行报错了。这一点不奇怪,因为我们KRouter/index.js文件是空的,无任何导出。接下来我们看看如何去实现!

image.png

2、实现KRouter的外壳儿

  • src/KRouter/index.js文件中

    class KRouter {}
    
    KRouter.install = function () {};
    
    export default KRouter;
    
  • 此时看一下浏览器,是不是里程碑式的效果。此时的报错想要解决还不简单吗?

    image.png

  • 修改src/KRouter/index.js文件

    因为我们编写的是一个插件,在Vue.use(Plugin)时,会给install方法传入Vue的构造函数,(这也是编写插件的格式,不会的同学请自行百度),所以我们只要利用Vue.component这个全局api来注册router-linkrouter-view这俩个全局组件就可以。

    class KRouter {}
    
    KRouter.install = function (_Vue) {
      _Vue.component("router-link", {
        render(h) {
          return h("a", "超链接");
        },
      });
      _Vue.component("router-view", {
        render(h) {
          return h("div", "组件容器");
        },
      });
    };
    export default KRouter;
    
    
  • 此时再看浏览器,是不是很完美,很简单,一点错误都没有。

    image.png

3、实现KRouter类

之前的第二步的时候先简单的实现了install方法,只是为了不报错,并不完善。接下来我们顺着思维先来实现KRouter类。

首先,我们先思考平时我们使用vue-router的方式,一般都是this.$router.xxx,然后再配置一个路由表,实例化之后挂载到Vue实例上。如下图:

image.png

从图中我们可以看出,会将routes传到构造函数里面,其次我们需要一个变量(我们定义为current)来存储我们当前的url。我们顺着这个思维一步一步实现。所以代码变成下面这个样子:

class KRouter {
  constructor(options) {
    this.$options = options;
    this.current = "/";
  }
}

KRouter.install = function (_Vue) {
  _Vue.component("router-link", {
    render(h) {
      return h("a", "超链接");
    },
  });
  _Vue.component("router-view", {
    render(h) {
      return h("div", "组件容器");
    },
  });
};
export default KRouter;

4、将KRouter挂载到Vue的原型上

Vue.mixin这个全局api,如果有不理解的同学可以去看一下,我们通过this.$options这个常用实例api可以拿到给组件实例传入的参数,可以自己输出看一下。

官网传送门:Vue.mixinthis.$options

class KRouter {
  ...
}

KRouter.install = function (_Vue) {
  _Vue.mixin({
    beforeCreate() {
      if (this.$options.router) {
        _Vue.prototype.$router = this.$options.router;
      }
    },
  });
 ...
};
export default KRouter;

现在this.$options.router其实就是根组件,因为我们只在根组件里面传入了router,如下图:

image.png

什么,你还不相信?

那我们改代码测试一下

class KRouter {
 ...
}

KRouter.install = function (_Vue) {
  _Vue.mixin({
    beforeCreate() {
      console.log(this.$options.router);
    },
  });
 ...
};
export default KRouter;

看一下浏览器的输出,是不是只有一个组件有this.$options.router,没错那就是我们的根组件。学废了没

image.png

5、完善router-view组件

class KRouter {
  ...
}

KRouter.install = function (_Vue) {
 ...
  _Vue.component("router-view", {
    render(h) {
      const current = this.$router.current;
      const component = this.$router.$options.routes.find(
        (v) => v.path == current
      ).component;
      return h(component);
    },
  });
};
export default KRouter;

在上一步_Vue.mixin的时候,已经在Vue.prototype上挂载了KRouter实例,也就是$router

所以我们直接用this.$router就可以拿到当前的url,也就是我们的变量current,根据这个变量从routes里面查找,找到符合path === current这个条件的某一项,拿到component

最后通过h函数进行渲染,这也就是router的核心原理。

6、最重要的一步,响应式处理。

现在我们的KRouter已经可以渲染组件了,但是既然是路由,不能跳转,那是不是太low了点。所以接下来我们要实现的就是路由跳转

修改router-link组件

正常使用router-link组件如下:

<router-link to="/home">首页</router-link>

所以我们修改我们router-link的代码

KRouter.install = function (_Vue) {
  _Vue.component("router-link", {
    props: {
      to: {
        type: String,
        required: true,
      },
    },

    render(h) {
      return h("a", { attrs: { href: `#${this.to}` } }, this.$slots.default);
    },
  });

};
export default KRouter;

KRouter中监听hashchange事件

class KRouter {
  constructor(options) {
    this.$options = options;
    this.current = "/";
    window.addEventListener("hashchange", () => {
      this.current = window.location.hash.slice(1);
    });
  }
}
KRouter.install = function(){
    ...
}

export default KRouter;

响应式处理current

此时你发现,该做的都做了,当你点击路由的时候router-view还是不能渲染出对应的组件。没错,那是因为你current不是响应式,接下来我们要做的的这个处理就是让current变成响应式。

Vue中给我们提供了一个方法defineReactive。调用方式:Vue.util.defineReactive(obj,key,value)

class KRouter {
  constructor(options) {
    this.$options = options;

    Vue.util.defineReactive(this, "current", "/");//给实例添加一个current属性,默认值是"/"
    window.addEventListener("hashchange", () => {
      this.current = window.location.hash.slice(1);
    });
  }
}
export default KRouter;

有人会问,诶,你的Vue从哪里来的,可以直接import Vue from "vue"吗?

答案是:当然可以

但是你想一下,你开发一个插件你还要npm install vue 吗?显然会造成资源浪费。

所以调整一下思路,然后再贴一份完整代码,如下:

let Vue;
class KRouter {
  constructor(options) {
    this.$options = options;

    Vue.util.defineReactive(this, "current", "/");
    window.addEventListener("hashchange", () => {
      this.current = window.location.hash.slice(1);
    });
  }
}

KRouter.install = function (_Vue) {
  Vue = _Vue;
  _Vue.mixin({
    beforeCreate() {
      if (this.$options.router) {
        _Vue.prototype.$router = this.$options.router;
      }
    },
  });
  _Vue.component("router-link", {
    props: {
      to: {
        type: String,
        required: true,
      },
    },

    render(h) {
      return h("a", { attrs: { href: `#${this.to}` } }, this.$slots.default);
    },
  });
  _Vue.component("router-view", {
    render(h) {
      const current = this.$router.current;
      const component = this.$router.$options.routes.find(
        (v) => v.path == current
      ).component;
      return h(component);
    },
  });
};
export default KRouter;

大家可以发散思维,想想this.$router.push这种类似的api,要怎么实现?是不是已经有想法了呢!

总结

来掘金写文章,本意是想做一下笔记,防止后面自己会忘。做程序员的都知道,过一段时间不触碰,就会生疏。索性就写的详细一点,跟大家互相交流。愿我们在程序的道路上,互相扶持,不忘初心。

排版有点low,大家不要介意,哈哈哈。