阅读 978

基于vue-router思考🕓实现一个简易版vue-router

前言

单页面的兴起离不开前端路由,记得在小白时期刚接触SPA单页面应用这种概念时,我一度怀疑这种技术靠不靠谱,心中充满着很多不解,比如:把所有东西都写在一个页面上难道没有性能问题?如果某个地方报错了那页面是不是就崩了?

后来随着做了一个又一个SPA项目,逐渐打消了这种顾虑。下面我们就来研究下单页面的灵魂,路由是怎么个实现逻辑吧。

url中#(hash)的含义

vue-router举例,vue-rouer有两种工作模式分别是hashhistory,我们先来了解下#hash

锚点

看到#最容易联想到的就是 锚点 了 , 就像看 掘金 文章一样,可以利用锚点实现点击跳转

这应该是#(hash)最本质的用法了

改变#不触发网页重载

# 还有一个重要特点就是改变#后面的内容后并不会导致页面刷新

也就是这个特性使得使用#实现前端路由成为可能,我们在这里先进行大胆的假设:hash实现原理就是监听#后面内容的变化然后动态渲染出对应的组件

History Api

hash来看,我们可以浅浅的得出一个结论,要想实现前端路由,必须要满足 页面URL变化时,页面不能刷新 这一条件

我们在History Api中发现 pushState 似乎也满足这一条件

下面我们使用代码验证,代码也十分简单,设置一个定时器,一段时间后通过此api改变页面url,看页面是否刷新

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <script>
      setTimeout(() => {
        history.pushState(null, "", "/a");  // 核心代码
      }, 3000);
    </script>
  </body>
</html>
复制代码

注意看下面的url,会从127.0.0.1/aa.html变成127.0.0.1/a,且页面并未刷新。

这也为用来实现前端路由创造了可能。下面我们以vue-router为例,来学习下SPA的前端路由到底是如何实现的。

vue-router工作流程

vue插件

我们在使用vue-router时需要先use一下,可见vue-router本质上来说是 vue插件

import Vue from "vue";
import VueRouter from "vue-router";
import Home from "../views/Home.vue";

Vue.use(VueRouter); // 插件使用

const routes = [
  {
    path: "/",
    name: "Home",
    component: Home,
  },
  {
    path: "/about",
    name: "About",
    component: () =>
      import(/* webpackChunkName: "about" */ "../views/About.vue"),
  },
];

const router = new VueRouter({
  routes,
});

export default router;
复制代码

既然是插件,我们就先来弄清楚 vue 插件的运行机制,先来弄清楚一下几个问题。

  • vue.use都干了什么事?

    我们可以在vue2.0源码中找到答案

    export function initUse(Vue: GlobalAPI) {
    Vue.use = function (plugin: Function | Object) {
      const installedPlugins =
        this._installedPlugins || (this._installedPlugins = []);
      if (installedPlugins.indexOf(plugin) > -1) {
        return this;
      }
      const args = toArray(arguments, 1);
      args.unshift(this); // 第一个参数设置为vue实例
      // 核心代码
      if (typeof plugin.install === "function") {
        plugin.install.apply(plugin, args);
      } else if (typeof plugin === "function") {
        plugin.apply(null, args);
      }
      installedPlugins.push(plugin);
      return this;
    };
    }    
    复制代码

    vue.use() 方法做的最主要的事就是调用插件的install方法,然后把vue实例传给插件,供插件使用。所以我们在开发插件时必须要暴露出一个install方法,供vue调用

  • 为什么vue-rouer需要在main.js中挂载,而有的插件不需要

    我们在使用vue-router通常是这样

    //router/index.js
    import Vue from "vue";
    import VueRouter from "vue-router";
    Vue.use(VueRouter); // 用过use方法调用vue-router
    const routes = [];
    const router = new VueRouter({
      routes,
    });
    export default router;
    复制代码

    然后在main.js中挂载

    import Vue from "vue";
    import App from "./App.vue";
    import router from "./router";
    Vue.config.productionTip = false;
    new Vue({
     router  // 挂载
    }).$mount("#app");
    复制代码

在初次使用vue-router时,就有一种疑惑,老子都通过use方法调用了,为什么在这里还要挂载?这显然多此一举

但我在vue官网看到关于vue-router插件介绍时,我感觉这件事并没有这么简单

再来看看 vue-router 源码时,发现 通过全局混入来添加一些组件选项 的意思就是通过混入的方式拿到vue-rouer的配置项

// vue-router  src/install.js
  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)
    }
  })
复制代码

那问题来了, 为什么用混入的方式去拿vue-router配置呢?其实也不难理解,我们在使用vue.use(vueRouter)的时候,这个时候vue还没有new出来,也就是说vue.use(vueRouter)要比new vue()先执行... 文字描述太难了,还是画图吧

以上逻辑都顺理成章,且从源码中得知vue-router也是这样做的,但是有时候我还是认为在new vue中挂载router 是多余的,我们明明也可以通过参数传过去,例如:

import Vue from "vue";
import App from "./App.vue";
import router from "./router";
import VueRouter from "vue-router";

Vue.use(VurRouter,router) // 在main.js中调用use,把router当做第二个参数传过去
复制代码

那在vue-router中也可以拿到vue-rotuer配置项,如下所示:

class VueRouter{}
VueRouter.install=(vue,option)=>{}
复制代码

vue-rotuer为什么没有这样做呢?欢迎大家在留言讨论

vue-router 运行机制

vue-router核心功能就是当URL改变时自动渲染对应组件

一图胜过千言万语,我们来总结下vue-rouer主工作流程图

这里可能会牵扯到一些细节,比如:

  1. <router-view><router-link>如何实现;
  2. 组件是如何被渲染的;
  3. 嵌套路由如何渲染

<router-view><router-link>其实就是在vue-rotuer注册的Vue全局组件,其中<router-link>比较简单,其实就是一个a便签,下面我们着重研究下<router-view>及嵌套路由如何渲染

需要注意的是组件渲染是 重外到内,先渲染父组件,如果发现父组件中存在<router-view>在去渲染对应的子组件,直到所有组件渲染完成。 渲染是通过render函数的h方法进行渲染,以下是vue-rouer关于router-view部分核心代码

简易版vue-rotuer实现

vue-rotuer源码虽然不长,但想要完全读懂也并不简单,我把核心代码抽离出来,实现了简易版的vue-router,效果如下所示:

主要实现功能有:

  1. 实现根据路由渲染对应组件,并实现嵌套渲染
  2. 实现this.$rouer.push()方法,其他方法可自由扩展

具体细节实现如下:

router-link实现

function isActive(location) {
  return window.location.hash.slice(1) === location;
}
export default {
  functional: true,
  render(h, { props, slots }) {
    const active = isActive(props.to) ? "my-vue-router-active" : "";
    return h(
      "a",
      {
        attrs: {
          href: `#${props.to}`,
          class: [`${active}`],
        },
      },
      slots().default[0].text
    );
  },
};
复制代码

router-view实现

export default {
  functional: true,
  render(h, { parent, data }) {
    let route = parent.$route;
    let matched = route.matched;
    data.routerView = true;
    let deep = 0;
    while (parent) {
      if (parent.$vnode && parent.$vnode.data.routerView) {
        deep++;
      }
      parent = parent.$parent;
    }
    let record = matched[deep];
    if (!record) {
      return h();
    }
    let component = record.component;
    return h(component, data);
  },
};
复制代码

源码地址: simple-vue-router

个人项目:基于webpack自动生成路由打包多页面应用 lyh-pages,欢迎大家 star 哦,万分感谢!

最后

如有帮助,欢迎点赞关注哦!😘

文章分类
前端
文章标签