摘要:最新一直在看關于和路由這塊的知識,最終發現這些路由框架的模塊功能的實現都是基于瀏覽器原生路由的。在瀏覽器中實現前端路由主要有兩種方式一個是我們常用的,另一個是提供的。該對象的和分別表示的各個部分,它們因此被稱為分解屬性。
最新一直在看關于 Vue 和 React 路由這塊的知識,最終發現這些路由框架的模塊功能的實現都是基于瀏覽器原生路由 API?的。本著追根溯源的初心,于是就想著將瀏覽器原生的路由 API 整體梳理一遍,以便更加順暢的理解 Vue-Router 和 React-Router 的相關實現和原理。
背景瀏覽器的主要功能就是根據輸入的 URL 在窗口加載對應的文檔,與此同時,瀏覽器會記錄一個 tab 窗口載入過的所有文檔,同時會提供 "前進"、"后退" 和 "刷新" 的功能,以便用戶可以在這些已經記錄的文檔之間進行切換瀏覽和重載當前頁面獲取最新的瀏覽信息。
這些功能的實現最早是在服務器端實現的,因為那時候的引用都是前后端不分離的,頁面內容也是動態生成的,所以這些頁面的跳轉、切換、刷新都是在服務端實現的。后來出現了 SPA(Single Page Application 單頁應用),頁面都是通過 JavaScript 動態生成和載入到頁面的,并且可以在無刷新的情況下加載頁面最新的狀態信息,這時候如果要提供上述的功能就需要自己進行處理(因為此時的頁面都是現實在同一個大的框架頁面里面的,根本不存在頁面的跳轉切換),所以催生了各種框架對應的 Router 實現。
在瀏覽器中實現前端路由主要有兩種方式:一個是我們常用的 hash,另一個是 HTML5 提供的 history。其實還有另外一種利用 stack 實現的方式適用于 Node.js 服務器端,這里我們著重說一下瀏覽器提供的 hash 和 history 吧,stack 具體怎么實現等我們說到 x-Router 源碼的時候再詳細說一下。
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 來定位文檔?,F在來驗證一下我們的猜想:
1、首先在新的 tab 窗口打開?react.docschina.org/docs/react-… 頁面,然后在審核元素下找到上圖所展示的 DOM 元素,修改其中的 h3 標簽的 id 屬性值為 reactlazy1,接著在 URL 地址欄追加 #reactlazy hash 值并按下回車鍵,此時頁面并沒有定位到標題為 React.lazy 的文檔片段,最后將 URL 地址欄的 #reactlazy hash 值改成?#reactlazy1 hash 值并按下回車鍵,此時頁面并沒有定位到標題為 React.lazy 的文檔片段,這一系列的表現說明 hash 定位還是和元素的 id 屬性值還是有關聯的;
2、依然是在新的 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 屬性值還是有關聯的;
3、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
1、history 里面新增了兩個 API,history.pushState() 和 history.replaceState()。這兩個 API 都接受同樣的參數:
它們之間的不同之處是:history.pushState() 方法是將新狀態添加到瀏覽器的歷史記錄中,也就是說還可以通過點擊?"后退"?按鈕,退到前一個頁面;history.replaceState() 是用新的狀態代替當前的歷史狀態,也就是說沒有更多的歷史記錄,"后退"?按鈕不能操作了,頁面不能?"后退"?了。
??注意:當執行這兩個 API 時,瀏覽器的 URL 地址欄會變化,但是頁面內容不會刷新!
狀態對象(state
標題(title
地址(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?時就會報錯。
2、除了上面新增的 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 歡迎掃描二維碼關注公眾號,每天都給你推送新鮮的前端技術文章文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/114542.html
摘要:最新一直在看關于和路由這塊的知識,最終發現這些路由框架的模塊功能的實現都是基于瀏覽器原生路由的。在瀏覽器中實現前端路由主要有兩種方式一個是我們常用的,另一個是提供的。該對象的和分別表示的各個部分,它們因此被稱為分解屬性。 最新一直在看關于 Vue 和 React 路由這塊的知識,最終發現這些路由框架的模塊功能的實現都是基于瀏覽器原生路由 API?的。本著追根溯源的初心,于是就想著將瀏覽...
摘要:組件成為前端最基本的物料,融合在組件中的方案日趨成熟。組件成為最基本的前端物料,讓組件化更徹底在的調研報告中,開發者有愿意繼續,有愿意繼續。需要留意的是,有表示對感興趣,因此獲得的最感興趣獎。 簡介: JavaScript 應用范圍廣泛,靜態類型語言 TypeScript 會繼續得到更多開發者的青睞。 組件成為前端最基本的物料,CSS 融合在組件中(CSS in JS)的方案日趨成熟...
摘要:的最后一個大招就是替換一些傳統的服務端語言,例如,,等,在業務層上面使用來開發服務端完全不成問題。更多的的使用細節和技巧建議關注美團博客大搜車論壇下一篇我們開啟如何結合和搭建一個開發環境和項目目錄 往期回顧 前面2期都講得是瀏覽器端的東西比較多,包括Webpack,雖然是Node處理的,但是還是瀏覽器端用的多,對于現在的前端開發來說,不懂一點服務端的東西,簡直沒辦法活,一般的招聘要求都...
摘要:如何在新的技術背景下讓前端數據采集工作更加完善高效,是本文討論的重點。具體來說,我們對前端的數據采集具體主要分為路由切換性能資源錯誤日志上報路由切換等前端技術的快速發展使單頁面應用盛行。 隨著業務的快速發展,我們對生產環境下的問題感知能力越來越關注。作為距離用戶最近的一層,前端的表現是否可靠、穩定、好用,很大程度上決定著用戶對整個產品的體驗和感受。因此,對于前端的監控不容忽視。 搭建一...
閱讀 728·2021-11-24 10:19
閱讀 1105·2021-09-13 10:23
閱讀 3428·2021-09-06 15:15
閱讀 1776·2019-08-30 14:09
閱讀 1680·2019-08-30 11:15
閱讀 1837·2019-08-29 18:44
閱讀 932·2019-08-29 16:34
閱讀 2455·2019-08-29 12:46