淺談前端路由的概念與vue-router的實(shí)現(xiàn)原理

1.Web路由

1.1 后端路由

??????Web路由的概念簡單來說就是根據(jù)不同URL渲染不同的頁面五垮。在前后端不分離的時(shí)代,路由往往指的是后端路由(服務(wù)端路由)第煮,即當(dāng)服務(wù)端接收到客戶端發(fā)來的 HTTP 請(qǐng)求,就會(huì)根據(jù)所請(qǐng)求的相應(yīng) URL抑党,進(jìn)行文件讀取包警,數(shù)據(jù)庫讀取等操作,使用模板引擎將相應(yīng)結(jié)果與模板結(jié)合后進(jìn)行渲染底靠,將渲染完畢的頁面發(fā)送給客戶端害晦。

優(yōu)缺點(diǎn)

  • 優(yōu)點(diǎn):seo友好,爬蟲爬取到的頁面就是最終的渲染頁面暑中。
  • 缺點(diǎn):每次發(fā)起請(qǐng)求都要刷新頁面壹瘟,用戶體驗(yàn)不好,服務(wù)器壓力大鳄逾。
1.2 前端路由

??????說到前端路由稻轨,必須先提一下Ajax與SPA。Ajax技術(shù)的興起促使了 SPA—單頁面應(yīng)用的出現(xiàn)雕凹,由于Ajax可以做到頁面的局部更新殴俱,因此單頁應(yīng)用頁面的交互和頁面的跳轉(zhuǎn)都是無刷新的,無刷新就意味著無需處理html文件的請(qǐng)求枚抵,因此用戶體驗(yàn)很好线欲。但相應(yīng)的,由于頁面數(shù)據(jù)需要通過Ajax獲取汽摹,因此爬蟲獲取到的html只是模板而不是最終的渲染頁面李丰,因此會(huì)不利于seo。為了實(shí)現(xiàn)單頁應(yīng)用逼泣,所以就有了前端路由趴泌。
??????前端路由的概念簡單來講就是,當(dāng)路由發(fā)生變化圾旨,不請(qǐng)求服務(wù)端踱讨,而是通過js的方式修改dom(組件替換),并發(fā)送Ajax獲取數(shù)據(jù)來達(dá)到頁面跳轉(zhuǎn)的效果砍的。因此實(shí)現(xiàn)前端路由有兩個(gè)關(guān)鍵點(diǎn):

  • 如何改變url不讓瀏覽器向服務(wù)器發(fā)送請(qǐng)求痹筛。
  • 如何監(jiān)聽到url的變化,并執(zhí)行對(duì)應(yīng)的操作

這里就要引出實(shí)現(xiàn)前端路由的兩種路由模式:hash模式和history模式

2.前端路由的實(shí)現(xiàn)模式

2.1 hash模式
概念

??????hash 就是指 url 后的 # 號(hào)以及后面的內(nèi)容

特點(diǎn)

hash模式有以下幾個(gè)特點(diǎn)

  • hash值的變化不會(huì)導(dǎo)致瀏覽器向服務(wù)器發(fā)送請(qǐng)求,不會(huì)引起頁面刷新帚稠。
  • hash值變化會(huì)觸發(fā)hashchange事件谣旁。
  • hash值改變會(huì)在瀏覽器的歷史中留下記錄,使用瀏覽器的后退按鈕滋早,就可以回到上一個(gè)hash值榄审。
  • hash永遠(yuǎn)不會(huì)提交到服務(wù)端,即使刷新頁面也不會(huì)杆麸。

由此可見hash模式的特點(diǎn)完全可以滿足前端路由的實(shí)現(xiàn)需求搁进,所以在 H5 的 history 模式出現(xiàn)之前,基本都是使用 hash 模式來實(shí)現(xiàn)前端路由昔头。

優(yōu)缺點(diǎn)

優(yōu)點(diǎn):

  • 1饼问、兼容性好,支持低版本和IE瀏覽器揭斧。
  • 2莱革、實(shí)現(xiàn)前端路由無需服務(wù)端的支持。

缺點(diǎn):

  • URL帶#讹开,路徑丑
2.2 history模式
概念

??????在 HTML5 之前盅视,瀏覽器就已經(jīng)有了 history 對(duì)象來控制頁面歷史記錄跳轉(zhuǎn),主要有以下方法旦万。

history.forward():前進(jìn)
history.back():后退
history.go(n):加載歷史列表中的某個(gè)具體的頁面

??????在 HTML5 的規(guī)范中闹击,history 新增了以下幾個(gè) API:pushState(追加) 和 replaceState(替換),通過這兩個(gè) API 可以改變 url 地址且不會(huì)發(fā)送請(qǐng)求纸型,同時(shí)還新增popstate 事件拇砰。通過這些API就能用另一種方式來實(shí)現(xiàn)前端路由,其實(shí)現(xiàn)原理跟與hash模式 實(shí)現(xiàn)類似狰腌,只是用了 HTML5 的實(shí)現(xiàn)除破,單頁面應(yīng)用的 url 不會(huì)多出一個(gè)#,會(huì)更加美觀琼腔。

關(guān)于History模式有兩點(diǎn)需要說明:

  • history模式如何監(jiān)聽路由變化
    history模式下瑰枫,瀏覽器的前進(jìn)后退(history.back(), history.forward()等)會(huì)觸發(fā)popstate 事件,但pushState丹莲,replaceState 并不會(huì)觸發(fā)popstate事件光坝。因此要實(shí)現(xiàn)路由變化的偵聽,我們需要重寫這兩個(gè)方法甥材,可以通過事件中心(EventBus)添加事件通知盯另,這里不具體展開,感興趣的小伙伴可以參考這里洲赵。
  • history模式需要后端支持
    由于history模式?jīng)]有 # 號(hào)鸳惯,所以當(dāng)用戶手動(dòng)刷新或直接通過url進(jìn)入應(yīng)用時(shí)商蕴,瀏覽器還是會(huì)給服務(wù)器發(fā)送請(qǐng)求。但服務(wù)端無法識(shí)別這個(gè) url 芝发,因此為了避免出現(xiàn)這種情況绪商,history模式需要服務(wù)端的支持,即服務(wù)端需要把匹配不到的所有路由都重定向到根頁面辅鲸。
優(yōu)缺點(diǎn)

優(yōu)點(diǎn):

  • 路徑好看

缺點(diǎn):

  • 1格郁、兼容性差,不能兼容IE9独悴。
  • 2例书、需要服務(wù)端支持。

3.實(shí)現(xiàn)vue-router

??????介紹完前端路由的概念及其實(shí)現(xiàn)模式刻炒,接下來我們嘗試實(shí)現(xiàn)vue-router插件雾叭,具體包括vue-router類,兩個(gè)全局組件:router-link落蝙,router-view以及install方法。

3.1 實(shí)現(xiàn)router類

??????我們使用Hash模式來實(shí)現(xiàn)暂幼,因此vue-router具體要做的核心點(diǎn)就是要添加hashchange和load事件的事件偵聽筏勒,在回調(diào)中根據(jù)當(dāng)前url從路由表中取出對(duì)應(yīng)的路由組件,提供給router-view渲染旺嬉。因此一個(gè)首要的問題就是:如何根據(jù)url從路由表中取出組件管行?
??????一個(gè)基礎(chǔ)的思路是,我們只需在偵聽到url變化時(shí)邪媳,拿到當(dāng)前的hash值捐顷,然后遍歷路由表找到路徑為當(dāng)前hash值的選項(xiàng)的component即可。不過這樣做的問題也很明顯雨效,就是無法處理嵌套路由迅涮,如果我們?cè)诼酚杀碇信渲昧饲短茁酚桑瑒t單靠hash值是無法匹配到子代路由的徽龟。要解決這個(gè)問題叮姑,我們可以用一個(gè)matched數(shù)組來存儲(chǔ)從父代到子代匹配過程中的各級(jí)組件,這樣各級(jí)router-view組件只需按需渲染即可据悔。
??????說到這里传透,又會(huì)引出另一個(gè)問題,如何能做到在url變化時(shí)router-view也能響應(yīng)式的更新极颓。這里可以利用vue響應(yīng)式數(shù)據(jù)的特點(diǎn)朱盐,我們知道單文件組件中data中的數(shù)據(jù)都是響應(yīng)式的,當(dāng)數(shù)據(jù)更新時(shí)菠隆,所有用到該數(shù)據(jù)的地方都會(huì)響應(yīng)式的更新兵琳。而這里router-view組件顯然會(huì)用到matched數(shù)組狂秘,因此我們只需將matched變?yōu)轫憫?yīng)式數(shù)據(jù)即可。具體來說就是使Vue.util.defineReactive這個(gè)api闰围,它可以定義一個(gè)對(duì)象的響應(yīng)屬性赃绊,用法如下:

Vue.util.defineReactive(obj,key,value,fn)    
  obj: 目標(biāo)對(duì)象,
  key: 目標(biāo)對(duì)象屬性羡榴;
  value: 屬性值

??????我們用它將matched定義為router實(shí)例的一個(gè)響應(yīng)式屬性碧查,這樣即可實(shí)現(xiàn)matched變化時(shí),router-view也會(huì)響應(yīng)式的渲染校仑。這里還要注意忠售,使用該方法要用到vue實(shí)例,如何拿到vue實(shí)例迄沫?我們可以在vue-router的install方法中拿到并保存稻扬,關(guān)于這一點(diǎn)后面會(huì)解釋。接下來我們按照以上思路羊瘩,首先實(shí)現(xiàn)router類泰佳。

// 用于在Install方法中保存vue實(shí)例
let Vue
class myRouter{
    constructor (options){
        this.$options = options
        // 保存當(dāng)前hash值,即匹配路徑
        this.current = window.location.hash.slice(1) || '/' // 給初值
        // 保存匹配過程中的各級(jí)路由信息
        Vue.util.defineReactive(this, 'matched', [])
        // match方法可以遞歸遍歷路由表尘吗,獲得匹配關(guān)系 
        this.match()
        // 添加偵聽事件逝她,事件回調(diào)中用到this,因此要綁定上下文
        window.addEventListener('hashchange', this.onHashChange.bind(this))
        window.addEventListener('load', this.onHashChange.bind(this))
    }
    onHashChange () {
        // 更新匹配路徑
        this.current = window.location.hash.slice(1)
        this.matched = []
        this.match()
      }
    /**
     * @description 遍歷路由表,保存匹配關(guān)系
     */
    match(routes) {
         // 默認(rèn)遍歷總路由表
         routes = routes || this.$options.routes;
         for (let i = 0; i < routes.length; i++) {
               const route = routes[i];
               // 嚴(yán)格匹配根路徑
               if (route.path === "/" && this.current === "/") {
                       this.matched.push(route);
                       break;
              // 當(dāng)前路由包含于url 則推入matched數(shù)組并遞歸遍歷其子路由
             } else if (route.path !== "/" && this.current.includes(route.path)) {
                       this.matched.push(route);
                       if (route.children) {
                            this.match(route.children);
                        }
                       break;
                 }
            }
      } 
}
3.2 實(shí)現(xiàn)兩個(gè)全局組件

vue-router有兩個(gè)全局組件分別是:

  • router-link 路由跳轉(zhuǎn)
  • router-view 路由占位符

我們分別來實(shí)現(xiàn)
router-link
??????router-link用來進(jìn)行路由跳轉(zhuǎn)睬捶,他的實(shí)現(xiàn)比較簡單黔宛,因?yàn)槠浔举|(zhì)其實(shí)就是a標(biāo)簽,因此實(shí)現(xiàn)router-link只需渲染一個(gè)a標(biāo)簽即可擒贸。但要注意的是臀晃,由于此時(shí)是運(yùn)行時(shí)環(huán)境,無法進(jìn)行模板編譯介劫,所以不能使用模板語法徽惋,我們可以使用render函數(shù)。
??????具體實(shí)現(xiàn)思路是蜕猫,使用render渲染一個(gè)a標(biāo)簽寂曹,herf屬性對(duì)應(yīng)router-link的to屬性,標(biāo)簽內(nèi)容就是用戶寫在router-view中的內(nèi)容回右,我們可以通過插槽(this.$slots)來獲取隆圆,并將其添加在實(shí)際的a標(biāo)簽中。

export default {
  props: {
    to: {
      type: String,
      required: true
    }
  },
  render (h) {
    return h('a', { attrs: { href: '#' + this.to } }, this.$slots.default)
  }
}

router-view
??????router-view用來渲染路由組件翔烁,我們前面實(shí)現(xiàn)的router中已經(jīng)添加了對(duì)路由匹配關(guān)系的處理渺氧,他會(huì)根據(jù)當(dāng)前url將各級(jí)匹配關(guān)系存入matched數(shù)組中,router-view如何根據(jù)matched數(shù)組按需渲染呢蹬屹?
??????其實(shí)侣背,對(duì)于一個(gè)嵌套路由來說白华,每一級(jí)路由都有一個(gè)router-view與之對(duì)應(yīng),即router-view也一定是嵌套的贩耐,因此router-view只需知道自身所處的層級(jí)弧腥,具體來說就是matched數(shù)組中的第幾項(xiàng)即可。實(shí)現(xiàn)這一點(diǎn)我們可以給每一個(gè)router-view添加一個(gè)標(biāo)記變量和一個(gè)深度計(jì)數(shù)變量潮太,router-view判斷自己的父節(jié)點(diǎn)有沒有這個(gè)標(biāo)記管搪,有則說明自己是子代路由,則深度加一同時(shí)繼續(xù)向上判斷直到不存在父節(jié)點(diǎn)铡买。這樣最終每個(gè)router-view都會(huì)得到自己所處的層級(jí)更鲁,只需根據(jù)這個(gè)層級(jí)從matched數(shù)組獲取對(duì)應(yīng)的路由組件并渲染即可。下面根據(jù)以上思路來實(shí)現(xiàn)奇钞,注意同樣不能使用模板語法澡为,要使用render函數(shù)。

export default {
  render(h) {
    // 標(biāo)記自己是父級(jí)router-view
    this.$vnode.data.routerView = true; 
    // 統(tǒng)計(jì)深度 
    let depth = 0;
    let parent = this.$parent;
    // 獲取自己的深度
    while (parent) {
      const vnodeData = parent.$vnode && parent.$vnode.data;
         if (vnodeData && vnodeData.routerView) {
           depth++;
         }
      }
      // 不斷向上查找
      parent = parent.$parent;
    }
    let component = null;
    // 獲取當(dāng)前層級(jí)對(duì)應(yīng)的路由
    const route = this.$router.matched[depth];
    // 獲取path對(duì)應(yīng)的component
    if (route) {
      component = route.component;
    }
    return h(component);
  }
};
3.3 實(shí)現(xiàn)install方法

??????vue-router是個(gè)vue插件景埃,我們前面提到過vue插件的實(shí)現(xiàn)原理媒至。它要暴露一個(gè)install方法,用全局混入(Vue.mixin)的方式混入beforeCreate生命周期谷徙,這會(huì)使得所有的組件的beforeCreate鉤子都會(huì)觸發(fā)該行為塘慕。我們?cè)赽eforeCreate中將router實(shí)例掛載到vue原型上,便于在任何地方通過vue原型直接調(diào)用router蒂胞。如何做到這一點(diǎn)呢?
??????我們?cè)谑褂胿ue-router時(shí)會(huì)在main.js中創(chuàng)建Vue根實(shí)例条篷,引入并掛載router選項(xiàng)骗随,也就是說只有Vue根實(shí)例才有router這個(gè)選項(xiàng)。因此我們只需在beforeCreate鉤子中判斷當(dāng)前組件有沒有router選項(xiàng)即可赴叹,有則說明這是vue-router根實(shí)例鸿染,將router其掛載到vue原型即可。
??????前面實(shí)現(xiàn)router類時(shí)說過乞巧,我們要在install方法中保存vue實(shí)例涨椒,為什么可以這樣做呢?vue插件之所以要暴露一個(gè)install方法绽媒,是因?yàn)槲覀兪褂?strong>vue.use()方法注冊(cè)組件時(shí)會(huì)調(diào)用install方法蚕冬,并將vue作為參數(shù)傳入,因此可以在install方法中保存vue實(shí)例是辕。
??????此外囤热,install方法還要注冊(cè)前面實(shí)現(xiàn)的兩個(gè)全局組件。
接下來根據(jù)以上思路具體實(shí)現(xiàn):

myRouter.install = function (_Vue){
   // 保存vue實(shí)例
    Vue = _Vue
    Vue.mixin({
        beforeCreate () {
            // 確保根實(shí)例的時(shí)候才執(zhí)行获三,因?yàn)橹挥懈鶎?shí)例才有router這個(gè)選項(xiàng)旁蔼。
            if (this.$options.router) {
              Vue.prototype.$router = this.$options.router
            }
          }
    })
  //注冊(cè)組件
  Vue.component('router-link', Link)
  Vue.component('router-view', View)
}

至此锨苏,基于hash模式的丐版vue-router的已經(jīng)完成。
水平有限棺聊,歡迎指正??伞租。
參考:
https://juejin.cn/post/6844903695365177352#heading-15
https://juejin.cn/post/6854573222231605256#heading-14

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市限佩,隨后出現(xiàn)的幾起案子葵诈,更是在濱河造成了極大的恐慌,老刑警劉巖犀暑,帶你破解...
    沈念sama閱讀 217,185評(píng)論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件驯击,死亡現(xiàn)場離奇詭異,居然都是意外死亡耐亏,警方通過查閱死者的電腦和手機(jī)徊都,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,652評(píng)論 3 393
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來广辰,“玉大人暇矫,你說我怎么就攤上這事≡竦酰” “怎么了李根?”我有些...
    開封第一講書人閱讀 163,524評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長几睛。 經(jīng)常有香客問我房轿,道長,這世上最難降的妖魔是什么所森? 我笑而不...
    開封第一講書人閱讀 58,339評(píng)論 1 293
  • 正文 為了忘掉前任囱持,我火速辦了婚禮,結(jié)果婚禮上焕济,老公的妹妹穿的比我還像新娘纷妆。我一直安慰自己,他們只是感情好晴弃,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,387評(píng)論 6 391
  • 文/花漫 我一把揭開白布掩幢。 她就那樣靜靜地躺著,像睡著了一般上鞠。 火紅的嫁衣襯著肌膚如雪际邻。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,287評(píng)論 1 301
  • 那天芍阎,我揣著相機(jī)與錄音枯怖,去河邊找鬼。 笑死能曾,一個(gè)胖子當(dāng)著我的面吹牛度硝,可吹牛的內(nèi)容都是我干的肿轨。 我是一名探鬼主播,決...
    沈念sama閱讀 40,130評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼蕊程,長吁一口氣:“原來是場噩夢(mèng)啊……” “哼椒袍!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起藻茂,我...
    開封第一講書人閱讀 38,985評(píng)論 0 275
  • 序言:老撾萬榮一對(duì)情侶失蹤驹暑,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后辨赐,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體优俘,經(jīng)...
    沈念sama閱讀 45,420評(píng)論 1 313
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,617評(píng)論 3 334
  • 正文 我和宋清朗相戀三年掀序,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了帆焕。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,779評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡不恭,死狀恐怖叶雹,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情换吧,我是刑警寧澤折晦,帶...
    沈念sama閱讀 35,477評(píng)論 5 345
  • 正文 年R本政府宣布,位于F島的核電站沾瓦,受9級(jí)特大地震影響满着,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜贯莺,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,088評(píng)論 3 328
  • 文/蒙蒙 一漓滔、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧乖篷,春花似錦、人聲如沸透且。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,716評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽秽誊。三九已至鲸沮,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間锅论,已是汗流浹背讼溺。 一陣腳步聲響...
    開封第一講書人閱讀 32,857評(píng)論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留最易,地道東北人怒坯。 一個(gè)月前我還...
    沈念sama閱讀 47,876評(píng)論 2 370
  • 正文 我出身青樓炫狱,卻偏偏與公主長得像,于是被迫代替她去往敵國和親剔猿。 傳聞我的和親對(duì)象是個(gè)殘疾皇子视译,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,700評(píng)論 2 354

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