國內存在感最低的前端API——瀏覽器路由

最新一直在看關於 Vue 和 React 路由這塊的知識,最終發現這些路由框架的模塊功能的實現都是基於瀏覽器原生路由 API 的。本着追根溯源的初心,因而就想着將瀏覽器原生的路由 API 總體梳理一遍,以便更加順暢的理解 Vue-Router 和 React-Router 的相關實現和原理。前端

背景

瀏覽器的主要功能就是根據輸入的 URL 在窗口加載對應的文檔,與此同時,瀏覽器會記錄一個 tab 窗口載入過的全部文檔,同時會提供 "前進"、"後退" 和 "刷新" 的功能,以便用戶能夠在這些已經記錄的文檔之間進行切換瀏覽和重載當前頁面獲取最新的瀏覽信息。react

這些功能的實現最先是在服務器端實現的,由於那時候的引用都是先後端不分離的,頁面內容也是動態生成的,因此這些頁面的跳轉、切換、刷新都是在服務端實現的。後來出現了 SPA(Single Page Application 單頁應用),頁面都是經過 JavaScript 動態生成和載入到頁面的,而且能夠在無刷新的狀況下加載頁面最新的狀態信息,這時候若是要提供上述的功能就須要本身進行處理(由於此時的頁面都是現實在同一個大的框架頁面裏面的,根本不存在頁面的跳轉切換),因此催生了各類框架對應的 Router 實現。後端

在瀏覽器中實現前端路由主要有兩種方式:一個是咱們經常使用的 hash,另外一個是 HTML5 提供的 history。其實還有另一種利用 stack 實現的方式適用於 Node.js 服務器端,這裏咱們着重說一下瀏覽器提供的 hash 和 history 吧,stack 具體怎麼實現等咱們說到 x-Router 源碼的時候再詳細說一下。api

Hash

在瀏覽器 URL 地址欄,咱們總會發現像這樣的地址:react.docschina.org/docs/react-… React 官網關於 lazy 的一個地址)。你們確定發現:這串 URL 的最後有以 # 號開始的一串標識,那它究竟是起着什麼樣的做用呢?確定不會無緣無故的出現吧。跨域

hash 特性瀏覽器

你能夠直接在瀏覽器中打開這個連接地址,你是否是發現頁面會自動滾動到(頁面頂部定位到)標題爲 React.lazy 的部分文檔。你再將頁面往上滾動,確定會發現上面還有部分的文檔內容。此時,你修改地址欄的地址爲 react.docschina.org/docs/react-… React.Suspense 部分。前端框架

圖片描述

在早些年,hash 做爲 URL 的一部分主要用來定位文檔中的文檔片斷。在上面的例子中,咱們經過在 URL 後面添加 #reactlazy 和 #reactsuspense 定位到了文檔對應標題爲 React.lazy 和 React.Suspense 的部分。那他們究竟是怎麼作到的呢?
經過審覈元素咱們發現:在 React.lazy 和 React.Suspense 對應的標題部分分別都有一個 h3 標籤,並且標籤的 id 屬性對應就是咱們在 URL 地址欄輸入的 hash 值部分(只是少了 # 號)。服務器

圖片描述

hash 定位文檔片斷
可能有同窗會有疑惑:爲何 hash 是經過元素上面的 id 屬性來定位文檔的?前面咱們提到過,URL 中的 hash 部分是用來定位文檔中的文檔片斷的。你們想一想:所須要定位的文檔片斷確定是惟一的,否則定位確定是不許確了,那這個定位文檔就有點雞肋了,在文檔中標識惟一的屬性只有是 id 了,若是是我,我也會經過 hash 匹配元素的 id 來定位文檔。如今來驗證一下咱們的猜測:微信

一、首先在新的 tab 窗口打開 react.docschina.org/docs/react-… 頁面,而後在審覈元素下找到上圖所展現的 DOM 元素,修改其中的 h3 標籤的 id 屬性值爲 reactlazy1,接着在 URL 地址欄追加 #reactlazy hash 值並按下回車鍵,此時頁面並無定位到標題爲 React.lazy 的文檔片斷,最後將 URL 地址欄的 #reactlazy hash 值改爲 #reactlazy1 hash 值並按下回車鍵,此時頁面並無定位到標題爲 React.lazy 的文檔片斷,這一系列的表現說明 hash 定位仍是和元素的 id 屬性值仍是有關聯的;框架

圖片描述

二、依然是在新的 tab 窗口打開 react.docschina.org/docs/react-… 頁面,而後將頁面手動滾動到標題爲 React.lazy 的文檔片斷,將鼠標放在標題上會出現一個錨點的圖標,點擊圖標發現頁面定位到了標題爲 React.lazy 的文檔片斷而且 URL 地址欄變成了 react.docschina.org/docs/react-… #reactlazy hash 值。此時再回頭看看咱們前面給出的截圖發現 id 屬性值爲 reactlazy 的 h3 標籤中有一個 href 屬性值爲 #reactlazy 的 a 標籤,其實咱們在頁面上看到的錨點圖標就是這個 a 標籤的展現。當咱們點擊錨點圖標就是點擊了 a 連接,而後將 url 定位到了 id 屬性值爲 reactlazy 的 h3 標籤,仍是很好的說明了 hash 定位仍是和元素的 id 屬性值仍是有關聯的;

圖片描述

三、MDN 官方定義以下:
圖片描述

MDN 官方文檔上有明確的定義,可是咱們仍是經過兩個方面來證實了咱們的推論,乍一看好像說了不少沒有用的東西,其實這樣反覆的推敲更有利於咱們深入的理解相關的知識點以及爲何是這樣,而不是那樣!

hash 路由

hash 的存在除了能夠經過設置文檔中元素的 ID 來定位文檔片斷以外,還能夠設置爲任意的字符串來表示路由。在 Vue、React 等現代前端框架中,爲了實現功能完備的 SPA 應用都配備了對應的路由系統。在這些路由系統都會提供 hash 路由模式。

在 hash 模式下,hash 會支持任意的字符串來表示對應的 URL。這些路由系統針對 hash 模式的實現基本都是大同小異:在設置 location.hash 屬性值後,應用就會想盡一切辦法檢測狀態值變化,以便可以讀取出存儲在片斷標識符中的狀態並相應地更新本身的狀態。支持 HTML5 的瀏覽器一旦發現片斷標識符發生了變化,就會在 Window 對象上觸發 hashchange 事件,這時就會觸發對象的函數處理邏輯 —— 對 location.hash 的值進行解析,而後使用該值包含的狀態信息從新渲染應用。

這裏只是提到了一個基礎的思路,路由系統的具體實現,後續會娓娓道來!

hash 事件

// 在 window 下監聽 hashchange 事件
window.onhashchange = function() {
  // 當事件觸發時輸出當前的 hash 值
  console.log(window.location.hash)
}

在不支持 HTML5 的瀏覽器中,咱們能夠經過 100ms 輪詢監聽 url 變化來模擬:

(function(window){
  // 若是瀏覽器不支持原生實現的事件,則開始模擬,不然退出。
  if ( "onhashchange" in window.document.body ) return;
  
  var location = window.location,
      oldUrl = location.href,
      oldHash = location.hash;

  // 每隔 100ms 檢查 hash 是否發生變化
  setInterval(function() {
    var newUrl = location.href,
        newHash = location.hash;

    // hash 發生變化且全局註冊有 onhashchange 方法(這個名字是爲了和模擬的事件名保持統一);
    if (newHash !== oldHash && typeof window.onhashchange === "function"  ) {
      // 執行方法
      window.onhashchange({
        type: "hashchange",
        oldURL: oldUrl,
        newURL: newUrl
      });

      oldUrl = newUrl;
      oldHash = newHash;
    }
  }, 100);
})(window)

⚠️注意:設置 location.hash 屬性會更新顯示在地址欄中的 URL,同時會在瀏覽器的歷史記錄中添加一條記錄。

History

爲了標準化管理瀏覽器歷史管理,HTML5 定義了相對複雜的 API —— history。

history api

一、history 裏面新增了兩個 API,history.pushState() 和 history.replaceState()。這兩個 API 都接受一樣的參數:

它們之間的不一樣之處是:history.pushState() 方法是將新狀態添加到瀏覽器的歷史記錄中,也就是說還能夠經過點擊 "後退" 按鈕,退到前一個頁面;history.replaceState() 是用新的狀態代替當前的歷史狀態,也就是說沒有更多的歷史記錄,"後退" 按鈕不能操做了,頁面不能 "後退" 了。

⚠️注意:當執行這兩個 API 時,瀏覽器的 URL 地址欄會變化,可是頁面內容不會刷新!

  • 狀態對象(state<Object | Null>):** 一個 JavaScript 對象,該對象包含用於恢復當前文檔所需的全部信息。能夠是任何可以經過 JSON.stringify() 方法轉換成相應字符串形式的對象,也能夠是其餘相似 Date、RegExp 這樣特定的本地類型。
  • 標題(title<String | Null>):**瀏覽器可使用它標識瀏覽歷史記錄中保存的狀態,能夠傳一個空字符串,也能夠傳入一個簡短的標題,標明將要進入的狀態。
  • 地址(URL):**用來表示當前狀態的位置。新的 URL 不必定是絕對路徑;若是是相對路徑,它將以當前 URL 爲基準(相似 #reactlazy 這樣的 hash);傳入的 URL 與當前 URL 應該是同源的,不然 pushState() 會拋出異常。該參數是可選的;不指定的話則爲文檔當前 URL。

爲此,咱們能夠利用語雀網站作一系列的實驗:

window.history.pushState(null, null, "https://www.yuque.com/dashboard/?name=littleLane");
// result: https://www.yuque.com/dashboard/?name=littleLane

window.history.pushState(null, null, "https://www.yuque.com/dashboard/name/littleLane");
//result: https://www.yuque.com/dashboard/name/littleLane

window.history.pushState(null, null, "?name=littleLane");
//result: https://www.yuque.com/dashboard?name=littleLane

window.history.pushState(null, null, "name=littleLane");
//result: https://www.yuque.com/dashboard/name=littleLane

window.history.pushState(null, null, "/name/littleLane");
//result: https://www.yuque.com/dashboard/name/littleLane

在控制檯中執行上面一系列語句時,瀏覽器的 URL 變化成了咱們備註的 result 的結果,可是頁面並無發生重渲染,還有當咱們每次執行 pushState 時,瀏覽器歷史都會添加一條記錄,你們能夠經過 "後退" 按鈕進行查看。你們執行完上面的測試語句後,還能夠將 pushState 替換成 replaceState 再次進行一輪測試,此時新的瀏覽記錄都會代替當前的歷史記錄,仍是能夠經過 "後退" 按鈕進行查看。

⚠️注意:這裏的 url 不支持跨域,當咱們把 www.yuque.com 換成 www.baidu.com 時就會報錯。

圖片描述

二、除了上面新增的 API,history 對象上還有表示瀏覽歷史列表數量的 length 屬性,還定義了 back()、forward() 和 go() 進行瀏覽記錄切換的方法。
History 對象的 back() 和 forward() 方法與瀏覽器的 "後退" 和 "前進" 按鈕功能同樣:它們可使瀏覽器在瀏覽歷史中後退或前進跳轉一格。而 go() 方法會接受一個整數,能夠在瀏覽歷史列表中向前(接受正整數參數)或向後(接受負整數參數)跳過任意多個頁。好比 history.go(-1) 就會向後跳轉一頁,history.go(0) 就是刷新當前頁,history.go(1) 就會向前跳轉一頁。

history 事件 - popstate

當用戶經過 "前進" 和 "後退" 按鈕瀏覽保存的歷史狀態時,瀏覽器會在 Window 對象上觸發一個 popstate 事件。與該事件相關的事件對象有一個 state 屬性,該屬性包含傳遞給 pushState() 方法的狀態對象的副本。

// 在 window 下監聽 onpopstate 事件
window.onpopstate = function(state) {
  // 當 onpopstate 事件 (用戶經過 "前進" 和 "後退" 按鈕切換瀏覽記錄) 觸發時輸出當前狀態
  console.log(state)
}

Location

Window 對象的 location 屬性和 Document 對象的 location 屬性引用的都是 Location 對象,它用來表示該窗口中當前顯示的文檔的 URL,並定義了方法來使窗口載入新的文檔。

window.location === document.location  // 老是返回 true

解析 URL

Location 對象的 href 屬性是一個字符串,表示當前 URL 的完整文本。Location 對象的 toString() 方法返回 href 屬性的值,所以在會隱式調用 toString() 的狀況下,可使用 location 代替 location.href。

該對象的 protocol、host、hostname、port、pathname 和 search 分別表示 URL 的各個部分,它們所以被稱爲 URL 分解屬性。通常咱們用的比較多的就是提取 URL 裏面的參數了:

// 獲取地址欄參數
const getUrlParame = (paramName) => {
    const urlParams = {};
    let params = window.location.search.substring(1);
    if (!params) {
        return;
    }
    params = params.split('&');
    for (let i = 0; i < params.length; i += 1) {
        let item = params[i];
        item = item.split('=');
        urlParams[item[0]] = decodeURIComponent(item[1]);
    }
    if (paramName) {
        return urlParams[paramName];
    }
    return urlParams;
};

載入新文檔

Location 對象的 assign() 方法可使窗口載入並顯示指定的 url 中的文檔。replace() 方法也有相似的功能,可是它會在新文檔載入以前將當前文檔從瀏覽歷史中刪除,就是說 "後退" 按鈕並不會將瀏覽器帶到原始的文檔。
Location 對象還定義可 reload() 方法用來從新載入當前文檔。

總結

上述的內容咱們主要了解了在瀏覽器中支持的兩種路由模式 —— hash 和 history,而後對它們各自的特性、api 和對應的事件作了詳細的講解,後面又說到了瀏覽器路由中相當重要的對象 —— Location,這一系列的內容爲咱們後續理解 Vue-Router、React-Router 等路由系統的實現和閱讀源碼打下了堅實的基礎。

做者:littleLane


本文首發微信公衆號:qianduanshenru

圖片描述

歡迎掃描二維碼關注公衆號,天天都給你推送新鮮的前端技術文章

相關文章
相關標籤/搜索