模擬 VueRouter 的實(shí)現(xiàn)(簡易版)

Study Notes

本博主會(huì)持續(xù)更新各種前端的技術(shù),如果各位道友喜歡,可以關(guān)注、收藏、點(diǎn)贊下本博主的文章。

模擬 VueRouter

前置的知識:

  • 插件
  • slot 插槽
  • 混入
  • render 函數(shù)
  • 運(yùn)行時(shí)和完整版的 Vue

Vue Router 的核心代碼

// 注冊插件
// Vue.use() 內(nèi)部調(diào)用傳入對象的 install 方法
Vue.use(VueRouter);
// 創(chuàng)建路由對象
const router = new VueRouter({
  routes: [{ name: 'home', path: '/', component: homeComponent }],
});
// 創(chuàng)建 Vue 實(shí)例,注冊 router 對象
new Vue({
  router,
  render: (h) => h(App),
}).$mount('#app');

Hash 模式

  • URL 中#后面的內(nèi)容作為路徑地址
  • 監(jiān)聽 hashchange 事件
  • 根據(jù)當(dāng)前路由地址找到對應(yīng)的組件重新渲染

history 模式

  • 通過 history.pushState()方法改變地址欄
  • 監(jiān)聽 popstate 事件
  • 根據(jù)當(dāng)前路由地址找到對應(yīng)的組件重新渲染

實(shí)現(xiàn)思路

這里模擬的是一個(gè)簡單的ruoter的 history 模式,不能嵌套使用

  • 創(chuàng)建 VueRouter 插件,靜態(tài)方法 install
    • 判斷插件是否已經(jīng)被加載
    • 當(dāng) Vue 加載的時(shí)候把傳入的 router 對象掛載到 Vue 實(shí)例上(注意:只執(zhí)行一次)
  • 創(chuàng)建 VueRouter 類
    • 初始化,options、routeMap、app(簡化操作,創(chuàng)建 Vue 實(shí)例作為響應(yīng)式數(shù)據(jù)記錄當(dāng)前路徑)
    • initRouteMap() 遍歷所有路由信息,把組件和路由的映射記錄到 routeMap 對象中
    • 注冊 popstate 事件,當(dāng)路由地址發(fā)生變化,重新記錄當(dāng)前的路徑
    • 創(chuàng)建 router-link 和 router-view 組件
    • 當(dāng)路徑改變的時(shí)候通過當(dāng)前路徑在 routerMap 對象中找到對應(yīng)的組件,渲染 router-view

install

混入 mixin

let _Vue = null;
export default class VueRouter {
  static install(Vue) {
    // 判斷是否已經(jīng)加載過install
    if (!VueRouter.install.installed) {
      // 將狀態(tài)改為已加載
      VueRouter.install.installed = true;
      // 將Vue的構(gòu)造函數(shù)記錄到全局
      _Vue = Vue;
      // 將創(chuàng)建Vue的實(shí)例時(shí)傳入的router對象注入到Vue實(shí)例
      // _Vue.prototype.$router = this.$options.router;
      // 如果我們直接使用Vue的原型鏈將router注入,會(huì)有以下的問題
      // 因?yàn)閕nstall是靜態(tài)方法,會(huì)被VueRouter.install調(diào)用,這時(shí)this將指向VueRouter對象
      // 所以我們這邊使用混入
      _Vue.mixin({
        beforeCreate() {
          // 因?yàn)樵谑褂眠^程中beforeCreate方法會(huì)被不停的調(diào)用,然而我們這邊只需要執(zhí)行一次掛載
          // 判斷this.$options.router是否存在
          if (this.$options.router) {
            _Vue.prototype.$router = this.$options.router;
          }
        },
      });
    }
  }
}

VueRouter 構(gòu)造函數(shù)

Vue.observable(object)

讓一個(gè)對象可響應(yīng)。Vue 內(nèi)部會(huì)用它來處理 data 函數(shù)返回的對象。

返回的對象可以直接用于渲染函數(shù)和計(jì)算屬性內(nèi),并且會(huì)在發(fā)生變更時(shí)觸發(fā)相應(yīng)的更新。也可以作為最小化的跨組件狀態(tài)存儲(chǔ)器,用于簡單的場景

這里我們使用 Vue.observable 創(chuàng)建一個(gè)響應(yīng)式對象

let _Vue = null;
export default class VueRouter {
  constructor(options) {
    this.options = options;
    // 設(shè)置路由模式,默認(rèn)hash
    this.mode = options.mode || 'hash';
    this.routerMap = {};
    let pathname = window.location.pathname;
    let search = window.location.search;
    !window.location.hash &&
      history.pushState({}, '', `${pathname + search}#/`); // 如果hash不存在,則改變地址欄地址
    let hash = window.location.hash;
    // Vue.observable(object)
    // 讓一個(gè)對象可響應(yīng)。Vue 內(nèi)部會(huì)用它來處理 data 函數(shù)返回的對象。
    // 返回的對象可以直接用于渲染函數(shù)和計(jì)算屬性內(nèi),并且會(huì)在發(fā)生變更時(shí)觸發(fā)相應(yīng)的更新。
    // 也可以作為最小化的跨組件狀態(tài)存儲(chǔ)器,用于簡單的場景
    // 這里我們使用Vue.observable創(chuàng)建一個(gè)響應(yīng)式對象
    this.data = _Vue.observable({
      current: this.mode === 'hash' ? hash.replace('#', '') : pathname, // 存儲(chǔ)當(dāng)前路由地址
    });
    this.init();
  }
}

createRouterMap

遍歷所有的路由規(guī)則,把路由規(guī)則解析成鍵值對的形式存儲(chǔ)到routerMap

export default class VueRouter {
  createRouterMap() {
    this.options.routes.forEach((route) => {
      this.routerMap[route.path] = route.component;
    });
  }
}

initComponent

Vue 運(yùn)行版本不支持 template

我們有兩種方案解決

  • 方案一:配置 cli 為完整版 Vue

在項(xiàng)目根目錄下創(chuàng)建vue.config.js,配置runtimeCompiler

是否使用包含運(yùn)行時(shí)編譯器的 Vue 構(gòu)建版本。設(shè)置為 true 后你就可以在 Vue 組件中使用 template 選項(xiàng)了,但是這會(huì)讓你的應(yīng)用額外增加 10kb 左右。

{ "runtimeCompiler": true }
export default class VueRouter {
  initComponent(Vue) {
    const mode = this.mode;

    // 方案二:使用 render 函數(shù)
    Vue.component('router-link', {
      props: {
        to: String,
      },
      // template: `<a :href="to"><slot></slot></a>`,
      render(createElement) {
        return createElement(
          'a',
          {
            attrs: { href: this.to },
            on: {
              click: this.clickHandler, // 添加點(diǎn)擊事件
            },
          },
          [this.$slots.default],
        );
      },
      methods: {
        clickHandler(e) {
          // 阻止默認(rèn)事件
          e.preventDefault();
          // 如果當(dāng)前地址和需要跳轉(zhuǎn)的地址一樣,直接返回
          if (this.to === this.$router.data.current) {
            return;
          }
          // 改變地址欄
          if (mode === 'history') {
            history.pushState({}, '', this.to);
          } else {
            history.pushState(
              {},
              '',
              `${window.location.pathname + window.location.search}#${this.to}`,
            );
          }
          // 將當(dāng)前路由地址改為點(diǎn)擊事件里的href,這里的data是響應(yīng)式對象,它改變時(shí),會(huì)重新渲染頁面
          this.$router.data.current = this.to;
        },
      },
    });
    const self = this;
    Vue.component('router-view', {
      render(createElement) {
        // 獲取當(dāng)前路由地址對應(yīng)的路由組件
        const component = self.routerMap[self.data.current];
        return createElement(component);
      },
    });
  }
}

createElement 參數(shù)

接下來你需要熟悉的是如何在 createElement 函數(shù)中使用模板中的那些功能。這里是 createElement 接受的參數(shù):

// @returns {VNode}
createElement(
  // {String | Object | Function}
  // 一個(gè) HTML 標(biāo)簽名、組件選項(xiàng)對象,或者
  // resolve 了上述任何一種的一個(gè) async 函數(shù)。必填項(xiàng)。
  'div',

  // {Object}
  // 一個(gè)與模板中 attribute 對應(yīng)的數(shù)據(jù)對象??蛇x。
  {
    // (詳情見下一節(jié))
  },

  // {String | Array}
  // 子級虛擬節(jié)點(diǎn) (VNodes),由 `createElement()` 構(gòu)建而成,
  // 也可以使用字符串來生成“文本虛擬節(jié)點(diǎn)”??蛇x。
  [
    '先寫一些文字',
    createElement('h1', '一則頭條'),
    createElement(MyComponent, {
      props: {
        someProp: 'foobar',
      },
    }),
  ],
);

initEvent

export default class VueRouter {
  initEvent() {
    // 監(jiān)聽點(diǎn)擊后退鍵
    window.addEventListener('popstate', () => {
      // 將當(dāng)前路由地址改為地址欄中的pathname,這里的data是響應(yīng)式對象,它改變時(shí),會(huì)重新渲染頁面
      this.data.current =
        this.mode === 'hash'
          ? window.location.hash.replace('#', '')
          : window.location.pathname;
    });
  }
}

完整示例

let _Vue = null;
export default class VueRouter {
  static install(Vue) {
    // 判斷是否已經(jīng)加載過install
    if (!VueRouter.install.installed) {
      // 將狀態(tài)改為已加載
      VueRouter.install.installed = true;
      // 將Vue的構(gòu)造函數(shù)記錄到全局
      _Vue = Vue;
      // 將創(chuàng)建Vue的實(shí)例時(shí)傳入的router對象注入到Vue實(shí)例
      // _Vue.prototype.$router = this.$options.router;
      // 如果我們直接使用Vue的原型鏈將router注入,會(huì)有以下的問題
      // 因?yàn)閕nstall是靜態(tài)方法,會(huì)被VueRouter.install調(diào)用,這時(shí)this將指向VueRouter對象
      // 所以我們這邊使用混入
      _Vue.mixin({
        beforeCreate() {
          // 因?yàn)樵谑褂眠^程中beforeCreate方法會(huì)被不停的調(diào)用,然而我們這邊只需要執(zhí)行一次掛載
          // 判斷this.$options.router是否存在
          if (this.$options.router) {
            _Vue.prototype.$router = this.$options.router;
          }
        },
      });
    }
  }

  constructor(options) {
    this.options = options;
    // 設(shè)置路由模式,默認(rèn)hash
    this.mode = options.mode || 'hash';
    this.routerMap = {};
    let pathname = window.location.pathname;
    let search = window.location.search;
    !window.location.hash &&
      history.pushState({}, '', `${pathname + search}#/`); // 如果hash不存在,則改變地址欄地址
    let hash = window.location.hash;
    // Vue.observable(object)
    // 讓一個(gè)對象可響應(yīng)。Vue 內(nèi)部會(huì)用它來處理 data 函數(shù)返回的對象。
    // 返回的對象可以直接用于渲染函數(shù)和計(jì)算屬性內(nèi),并且會(huì)在發(fā)生變更時(shí)觸發(fā)相應(yīng)的更新。
    // 也可以作為最小化的跨組件狀態(tài)存儲(chǔ)器,用于簡單的場景
    // 這里我們使用Vue.observable創(chuàng)建一個(gè)響應(yīng)式對象
    this.data = _Vue.observable({
      current: this.mode === 'hash' ? hash.replace('#', '') : pathname, // 存儲(chǔ)當(dāng)前路由地址
    });
    this.init();
  }

  init() {
    this.createRouterMap();
    this.initComponent(_Vue);
    this.initEvent();
  }

  createRouterMap() {
    // 遍歷所有的路由規(guī)則,把路由規(guī)則解析成鍵值對的形式存儲(chǔ)到routerMap中
    this.options.routes.forEach((route) => {
      this.routerMap[route.path] = route.component;
    });
  }

  initComponent(Vue) {
    // Vue運(yùn)行版本不支持template
    // 我們有兩種方案解決

    // 方案一:配置cli為完整版Vue
    // 在項(xiàng)目根目錄下創(chuàng)建vue.config.js
    // 配置runtimeCompiler
    // 是否使用包含運(yùn)行時(shí)編譯器的 Vue 構(gòu)建版本。
    // 設(shè)置為 true 后你就可以在 Vue 組件中使用 template 選項(xiàng)了.
    // 但是這會(huì)讓你的應(yīng)用額外增加 10kb 左右。
    // {runtimeCompiler: true}

    const mode = this.mode;

    // 方案二:使用 render 函數(shù)
    Vue.component('router-link', {
      props: {
        to: String,
      },
      // template: `<a :href="to"><slot></slot></a>`,
      render(createElement) {
        return createElement(
          'a',
          {
            attrs: { href: this.to },
            on: {
              click: this.clickHandler, // 添加點(diǎn)擊事件
            },
          },
          [this.$slots.default],
        );
      },
      methods: {
        clickHandler(e) {
          // 阻止默認(rèn)事件
          e.preventDefault();
          // 如果當(dāng)前地址和需要跳轉(zhuǎn)的地址一樣,直接返回
          if (this.to === this.$router.data.current) {
            return;
          }
          // 改變地址欄
          if (mode === 'history') {
            history.pushState({}, '', this.to);
          } else {
            history.pushState(
              {},
              '',
              `${window.location.pathname + window.location.search}#${this.to}`,
            );
          }
          // 將當(dāng)前路由地址改為點(diǎn)擊事件里的href,這里的data是響應(yīng)式對象,它改變時(shí),會(huì)重新渲染頁面
          this.$router.data.current = this.to;
        },
      },
    });
    const self = this;
    Vue.component('router-view', {
      render(createElement) {
        // 獲取當(dāng)前路由地址對應(yīng)的路由組件
        const component = self.routerMap[self.data.current];
        return createElement(component);
      },
    });
  }

  initEvent() {
    // 監(jiān)聽點(diǎn)擊后退鍵
    window.addEventListener('popstate', () => {
      // 將當(dāng)前路由地址改為地址欄中的pathname,這里的data是響應(yīng)式對象,它改變時(shí),會(huì)重新渲染頁面
      this.data.current =
        this.mode === 'hash'
          ? window.location.hash.replace('#', '')
          : window.location.pathname;
    });
  }
}
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 前置知識 插件 混入 Vue.observable() 插槽 render函數(shù) 運(yùn)行時(shí)和完整版的vue 首先使用v...
    旺財(cái)麻麻閱讀 395評論 0 2
  • ?????? 主要是思路,自己定義組件的時(shí)候可以借鑒 ?????? ??Vue-router的 類圖 VueRouter 類名 ...
    leslie1943閱讀 275評論 0 0
  • vue筆記 一.vue實(shí)例 vue的生命周期 beforeCreate(創(chuàng)建前), created(創(chuàng)建后), b...
    秋殤1002閱讀 1,126評論 0 1
  • # vue 面試題 性能優(yōu)化: 1.passive 是性能優(yōu)化的一種方案,如果有 passive 那么意味著 ev...
    徒步旅行_72c5閱讀 534評論 0 1
  • 久違的晴天,家長會(huì)。 家長大會(huì)開好到教室時(shí),離放學(xué)已經(jīng)沒多少時(shí)間了。班主任說已經(jīng)安排了三個(gè)家長分享經(jīng)驗(yàn)。 放學(xué)鈴聲...
    飄雪兒5閱讀 7,813評論 16 22

友情鏈接更多精彩內(nèi)容