基於vue-router思考🕓實現一個簡易版vue-router

前言

單頁面的興起離不開前端路由,記得在小白時期剛接觸SPA單頁面應用這種概念時,我一度懷疑這種技術靠不靠譜,心中充滿着不少不解,好比:把全部東西都寫在一個頁面上難道沒有性能問題?若是某個地方報錯了那頁面是否是就崩了?javascript

後來隨着作了一個又一個SPA項目,逐漸打消了這種顧慮。下面咱們就來研究下單頁面的靈魂,路由是怎麼個實現邏輯吧。html

url中#(hash)的含義

vue-router舉例,vue-rouer有兩種工做模式分別是hashhistory,咱們先來了解下#hash前端

錨點

看到#最容易聯想到的就是 錨點 了 , 就像看 掘金 文章同樣,能夠利用錨點實現點擊跳轉vue

這應該是#(hash)最本質的用法了java

改變#不觸發網頁重載

# 還有一個重要特色就是改變#後面的內容後並不會致使頁面刷新node

也就是這個特性使得使用#實現前端路由成爲可能,咱們在這裏先進行大膽的假設:hash實現原理就是監聽#後面內容的變化而後動態渲染出對應的組件webpack

History Api

hash來看,咱們能夠淺淺的得出一個結論,要想實現前端路由,必需要知足 頁面URL變化時,頁面不能刷新 這一條件git

咱們在History Api中發現 pushState 彷佛也知足這一條件github

下面咱們使用代碼驗證,代碼也十分簡單,設置一個定時器,一段時間後經過此api改變頁面url,看頁面是否刷新web

<!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 哦,萬分感謝!

最後

若有幫助,歡迎點贊關注哦!😘

相關文章
相關標籤/搜索