本文分兩部分,一說前端路由的基本原理,二說 React Router 的實現原理
前端路由的基本原理
不說屁話,從時間線上講,Web 應用原本是後端渲染,後來隨著技術的發展,有了單頁面應用,慢慢從後端渲染髮展成前端渲染
在部落格前端路由hash、history的實現 一問中我已經介紹過這兩種模式
hash 模式
- 透過監聽 hashchange 事件,匹配 hash 值並渲染頁面模組
history 模式
- 利用
history.pushState
+popState
實現 history.pushState
能實現不重新整理頁面,但往歷史棧中新增一個記錄popState
則會在歷史記錄條目被更改時,觸發pushState
只會改變歷史棧,修改它沒有什麼API可以監聽,所以要與popState
配合
簡單來說,前端路由的基本原理無非要實現兩個功能:
- 監聽記錄路由變化
- 匹配路由變化並渲染內容
hash模式
hash 模式的實現比較簡單,我們透過 hashChange
事件就能直接監聽到路由 hash 的變化,並根據匹配到的 hash 的不同來渲染不同的內容。
<body> <a href="#/home">Home</a> <a href="#/user">User</a> <a href="#/about">About</a> <div id="view"></div> </body> <script> function onHashChange() { const view = document.getElementById("view"); switch (location.hash) { case "#/home": view.innerHTML = "Home"; break; case "#/user": view.innerHTML = "User"; break; case "#/about": view.innerHTML = "About"; break; default: view.innerHTML = "Home"; break; } } window.addEventListener("hashchange", onHashChange); </script>
history模式
- 攔截 a 標籤的點選事件,阻止它的預設跳轉行為
- 使用 H5 的 history API 更新 URL
- 監聽和匹配路由改變以更新頁面
利用 pushState
往歷史棧中新增記錄且不重新整理頁面的特性 + 監聽 popstate
瀏覽器操作導致的 URL 變化
-
window.history.pushState(state, title, path)
-
window.addEventListener("popstate", onPopState)
<body> <a href="/home">Home</a> <a href="/user">User</a> <a href="/about">About</a> <div id="view"></div> </body> <script> const elements = document.querySelectorAll("a[href]"); elements.forEach(el => el.addEventListener("click", e => { e.preventDefault(); const test = el.getAttribute("href"); history.pushState(null, null, el.getAttribute("href")); onPopState(); }) ); function onPopState() { const view = document.getElementById("view"); switch (location.pathname) { case "/home": view.innerHTML = "Home"; break; case "/user": view.innerHTML = "User"; break; case "/about": view.innerHTML = "About"; break; default: view.innerHTML = "Home"; break; } } window.addEventListener("popstate", onPopState); </script>
hash模式 VS History模式
模式 | 優點 | 缺點 |
---|---|---|
Hash | 瀏覽器相容性更好,不需要後端路由支援 | 有 # 號 |
History | 需要現代瀏覽器,需要後端路由支援 | 無 # 號 |
history 模式下當頁面重新整理時,因為找不到相對應的路由(因為只有一個頁面,路由由前端控制),所以會報404錯誤,需要在 Nginx(或者伺服器)中配置,如果找不到ur,則返回 首頁
nginxlocation / { try_files $uri $uri/ /index.html; }
React Router 的實現原理
先用最簡單的話來概括一下 React Router 到底做了什麼?
本質上, React Router 就是在頁面 URL 發生變化的時候,透過我們寫的 path 去匹配,然後渲染對應的元件。
那麼,我們想一下如何分步驟實現:
- 如何監聽 url 的變化 ?
- 如何匹配 path ?
- 渲染對應的元件
換句話說,也是一個元件,透過渲染不同的元件來控制路由切換
整體設計
我們用一張圖來理解一下整個 react-router 是怎麼實現的:
接下來我們看看每一個步驟是怎麼實現的。
如何監聽 url 的變化 ?
正常情況下,當 URL 發生變化時,瀏覽器會像服務端傳送請求,但使用以下 3 種辦法不會向服務端傳送請求:
- 基於 ajax(實現起來很麻煩)
- 基於 hash
- 基於 history
react-router 使用了 history 這個核心庫。它本質是遮蔽不同模式下載監聽實現上的差異,使用釋出訂閱模式
來實現,這裡不做探究
總結
從後端路由到前端路由,最大的改變是體驗,體驗更良好了
前端路由模式有兩種:hash 模式 和 history 模式,兩者分別利用瀏覽器自由特性實現單頁面導航
- hash 模式:window.location 或 a 標籤改變錨點值,window.hashchange() 監聽錨點變化
- history 模式:history.pushState()、history.replaceState() 定義目標路由,window.popstate() 監聽瀏覽器操作導致的 URL 變化
React Router 匹配路由由 mathPath
透過 path-to-regexp
進行,<Route>
相當於一個高階元件,以不同的優先順序和匹配模式渲染匹配到的子元件
React Router 的主要元件原始碼,整體的實現:
- 對於監聽功能的實現,React Router 引入了
history
庫,以遮蔽了不同模式路由在監聽實現上的差異, 並將路由資訊以context
的形式,傳遞給被<Router>
包裹的元件, 使所有被包裹在其中的路由元件都能感知到路由的變化, 並接收到路由資訊 - 在匹配的部分, React Router 引入了
path-to-regexp
來拼接路徑正則以實現不同模式的匹配,路由元件·<Route>
作為一個高階元件包裹業務元件, 透過比較當前路由資訊和傳入的 path,以不同的優先順序來渲染對應元件
參考資料
-
深入淺出解析 React Router 原始碼
-
單頁面應用路由實現原理:以 React-Router 為例
-
剖析單頁面應用路由實現原理
-
SPA 路由三部曲之核心原理
-
SPA 路由三部曲之實戰演練
-
圖解 React-router 帶你深入理解路由本質