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

Study Notes

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

模擬 VueRouter

前置的知識:

  • 插件
  • slot 插槽
  • 混入
  • render 函數(shù)
  • 運(yùn)行時和完整版的 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)思路

這里模擬的是一個簡單的ruoter的 history 模式椭符,不能嵌套使用

  • 創(chuàng)建 VueRouter 插件剪勿,靜態(tài)方法 install
    • 判斷插件是否已經(jīng)被加載
    • 當(dāng) Vue 加載的時候把傳入的 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)路徑改變的時候通過當(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í)例時傳入的router對象注入到Vue實(shí)例
      // _Vue.prototype.$router = this.$options.router;
      // 如果我們直接使用Vue的原型鏈將router注入髓窜,會有以下的問題
      // 因?yàn)閕nstall是靜態(tài)方法扇苞,會被VueRouter.install調(diào)用,這時this將指向VueRouter對象
      // 所以我們這邊使用混入
      _Vue.mixin({
        beforeCreate() {
          // 因?yàn)樵谑褂眠^程中beforeCreate方法會被不停的調(diào)用寄纵,然而我們這邊只需要執(zhí)行一次掛載
          // 判斷this.$options.router是否存在
          if (this.$options.router) {
            _Vue.prototype.$router = this.$options.router;
          }
        },
      });
    }
  }
}

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

Vue.observable(object)

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

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

這里我們使用 Vue.observable 創(chuàng)建一個響應(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)
    // 讓一個對象可響應(yīng)亦歉。Vue 內(nèi)部會用它來處理 data 函數(shù)返回的對象。
    // 返回的對象可以直接用于渲染函數(shù)和計(jì)算屬性內(nèi)畅哑,并且會在發(fā)生變更時觸發(fā)相應(yīng)的更新肴楷。
    // 也可以作為最小化的跨組件狀態(tài)存儲器,用于簡單的場景
    // 這里我們使用Vue.observable創(chuàng)建一個響應(yīng)式對象
    this.data = _Vue.observable({
      current: this.mode === 'hash' ? hash.replace('#', '') : pathname, // 存儲當(dāng)前路由地址
    });
    this.init();
  }
}

createRouterMap

遍歷所有的路由規(guī)則荠呐,把路由規(guī)則解析成鍵值對的形式存儲到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)行時編譯器的 Vue 構(gòu)建版本。設(shè)置為 true 后你就可以在 Vue 組件中使用 template 選項(xiàng)了泥张,但是這會讓你的應(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)式對象瑰剃,它改變時,會重新渲染頁面
          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}
  // 一個 HTML 標(biāo)簽名、組件選項(xiàng)對象粤剧,或者
  // resolve 了上述任何一種的一個 async 函數(shù)歇竟。必填項(xiàng)。
  'div',

  // {Object}
  // 一個與模板中 attribute 對應(yīng)的數(shù)據(jù)對象抵恋』酪椋可選。
  {
    // (詳情見下一節(jié))
  },

  // {String | Array}
  // 子級虛擬節(jié)點(diǎn) (VNodes)弧关,由 `createElement()` 構(gòu)建而成盅安,
  // 也可以使用字符串來生成“文本虛擬節(jié)點(diǎn)”∈滥遥可選别瞭。
  [
    '先寫一些文字',
    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)式對象株憾,它改變時蝙寨,會重新渲染頁面
      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í)例時傳入的router對象注入到Vue實(shí)例
      // _Vue.prototype.$router = this.$options.router;
      // 如果我們直接使用Vue的原型鏈將router注入,會有以下的問題
      // 因?yàn)閕nstall是靜態(tài)方法嗤瞎,會被VueRouter.install調(diào)用墙歪,這時this將指向VueRouter對象
      // 所以我們這邊使用混入
      _Vue.mixin({
        beforeCreate() {
          // 因?yàn)樵谑褂眠^程中beforeCreate方法會被不停的調(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)
    // 讓一個對象可響應(yīng)。Vue 內(nèi)部會用它來處理 data 函數(shù)返回的對象掉瞳。
    // 返回的對象可以直接用于渲染函數(shù)和計(jì)算屬性內(nèi)毕源,并且會在發(fā)生變更時觸發(fā)相應(yīng)的更新髓帽。
    // 也可以作為最小化的跨組件狀態(tài)存儲器,用于簡單的場景
    // 這里我們使用Vue.observable創(chuàng)建一個響應(yīng)式對象
    this.data = _Vue.observable({
      current: this.mode === 'hash' ? hash.replace('#', '') : pathname, // 存儲當(dāng)前路由地址
    });
    this.init();
  }

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

  createRouterMap() {
    // 遍歷所有的路由規(guī)則脑豹,把路由規(guī)則解析成鍵值對的形式存儲到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)行時編譯器的 Vue 構(gòu)建版本郑藏。
    // 設(shè)置為 true 后你就可以在 Vue 組件中使用 template 選項(xiàng)了.
    // 但是這會讓你的應(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)式對象,它改變時俱饿,會重新渲染頁面
          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)式對象,它改變時拍埠,會重新渲染頁面
      this.data.current =
        this.mode === 'hash'
          ? window.location.hash.replace('#', '')
          : window.location.pathname;
    });
  }
}
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末失驶,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子枣购,更是在濱河造成了極大的恐慌嬉探,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,376評論 6 491
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件棉圈,死亡現(xiàn)場離奇詭異涩堤,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)分瘾,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,126評論 2 385
  • 文/潘曉璐 我一進(jìn)店門胎围,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人德召,你說我怎么就攤上這事白魂。” “怎么了上岗?”我有些...
    開封第一講書人閱讀 156,966評論 0 347
  • 文/不壞的土叔 我叫張陵福荸,是天一觀的道長。 經(jīng)常有香客問我液茎,道長逞姿,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,432評論 1 283
  • 正文 為了忘掉前任捆等,我火速辦了婚禮滞造,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘栋烤。我一直安慰自己谒养,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,519評論 6 385
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著买窟,像睡著了一般丰泊。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上始绍,一...
    開封第一講書人閱讀 49,792評論 1 290
  • 那天瞳购,我揣著相機(jī)與錄音,去河邊找鬼亏推。 笑死学赛,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的吞杭。 我是一名探鬼主播盏浇,決...
    沈念sama閱讀 38,933評論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼芽狗!你這毒婦竟也來了绢掰?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,701評論 0 266
  • 序言:老撾萬榮一對情侶失蹤童擎,失蹤者是張志新(化名)和其女友劉穎滴劲,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體柔昼,經(jīng)...
    沈念sama閱讀 44,143評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡哑芹,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,488評論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了捕透。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,626評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡碴萧,死狀恐怖乙嘀,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情破喻,我是刑警寧澤,帶...
    沈念sama閱讀 34,292評論 4 329
  • 正文 年R本政府宣布,位于F島的核電站磺芭,受9級特大地震影響骆撇,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜羽德,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,896評論 3 313
  • 文/蒙蒙 一几莽、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧宅静,春花似錦章蚣、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,742評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽矾策。三九已至,卻和暖如春峭沦,著一層夾襖步出監(jiān)牢的瞬間贾虽,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,977評論 1 265
  • 我被黑心中介騙來泰國打工吼鱼, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留蓬豁,地道東北人。 一個月前我還...
    沈念sama閱讀 46,324評論 2 360
  • 正文 我出身青樓蛉抓,卻偏偏與公主長得像庆尘,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子巷送,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,494評論 2 348

推薦閱讀更多精彩內(nèi)容