React-router-dom | 原理解析

解析 5.2 版本原始碼

Leo Chiu
手寫筆記
11 min readMay 30, 2020

--

前言

如果要用 React.js 做一個 SPA,第一個想到的第三方套件應該是react-router-dom ,GitHub 有 4 萬顆星星,從 npm trend 上面看到近幾個月皆有超過 300 萬次下載。

雖然很常使用這個套件,可是一直不知道它背後的原理是什麼,不知道如何讓多個 React 元件可以根據 URL 切換。所以,這次就來看看原始碼,一探究竟 react-router-dom 的原理吧!

react-router-dom 基本使用

我們先看看怎麼使用 react-router-dom ,官方的 🔗基本範例 裡面包含四個元件,BrowserRouter、Switch、Route、Link。

  • BrowserRouter 是起手式元件,會包裹在 SPA 元件的外層,它使用 HTML5 History API 讓 UI 與 URL 能夠同步。
  • Switch 是讓第一個符合 URL 的元件會被渲染,反之,如果沒有 Switch 則所有符合 URL 的元件都會被渲染。
  • Route 是定義元件相對應的 path,當 path 符合目前的 URL 時將會被渲染。
  • Link 像是 HTML 的 <a>,能夠在點擊後轉變目前的 Location。

react-router-dom vs react-router

在剛入坑的時候,在尋找要做 SPA 所需要的 router 套件,網路上搜尋 react router,出現的網站標題是「react router」,但是下載的套件名稱卻叫做 「react-router-dom」,當時我是有點疑惑,為什麼要掛羊頭賣狗肉?

後來,看了 GitHub 上的說明,才知道 react-routerreact-router-dom 其實是兩個不同的函式庫。實際上 react-router-dom 的核心就是 react-router ,同樣給 react native 使用的 react-router-native 其核心也是 react-router ,作者把核心功能分離出來,讓核心可以重複地被利用。

https://github.com/ReactTraining/react-router

前面說到 react-router-dom 的核心是 react-router ,其實只是多了以下四個 React 元件:BrowserRouter、 HashRouter、Link、NavLink。

在這篇文章中我們主要看的是在 react-router-dom 中的 BrowserRouter 與 Link,以及在 react-router 中的 Switch、Route,以上四個元件怎麼實作,在了解的過程中還會擴展一些使用到的元件。

history

在說明實作之前,我們先來看看核心的 history 物件,所有的 router 物件皆是基於 history 所構成的。

react-router 中使用的 history 是經過封裝的物件,它提供多種產生 history 物件的方法:

  • createBrowserHistory 是一個在瀏覽器中使用 HTML5 history API 處理 URL,處理像是這樣的 URL: example.com/some/path。這個方法通常必須依賴伺服器端讓 URL 都映射至 index.html,否則會出現 404。
  • createHashHistory 是使用 hash tag (#) 處理 URL 的方法,處理像是這樣的 URL: example.com/#/some/path。由於 # 後的 URL 不會出現在 HTTP 請求中,所以在請求時都是用 example.com 請求 index.html,因此伺服器端不用修改也沒有問題。
  • createMemoryHistory 適用在沒有 DOM 的環境,例如 React Native 或是前端的測試。

Location

Location 物件是 history 中最重要的物件,用於描述目前頁面 URL 的各種屬性像是用於 GET 請求的 search 、目前頁面的位置 pathname 、URL 上的 hash 等。

state 則是可以在該 Location 中儲存一些額外的資料。 (我現在還想不到能存什麼 😅)

key 則是一個唯一用於識別該 Location 的字串。

Listen

History 是使用 observer pattern 讓在外部的程式碼能夠監聽 Location 的轉變。每個 history 物件都有 listen 函式,它包含一個 callback function 參數,當 Location 改變時,將會執行這個 callback function。

這是 react-router 的關鍵之一,路由元件就靠它了。

BrowserRouter

👉 11~14 行,BrowserRouter 的實作非常簡單,先是在元件的開頭使用 history 函式庫中的 createBrowserHistory 建立使用 HTML5 History API 處理 location 的物件。最後將 historychildren 傳遞到 <Router> 中。

Router

👉 19~21行設定了元件內部的狀態 locationprops.history.locationprops.history 是 BrowserRouter 中所建立的 history 物件,而 location 的內容如下,包含 URL 的一些訊息。

👉 從 32 ~ 38 行可以看到使用 history.listen 監聽了 Location 的變化,當 Location 改變時將會改變在 Router 元件中的狀態。react-router 都是靠儲存在 Router 元件中的 location 狀態路由各個元件

31行的 staticContext 是 StaticRouter 元件中的物件,在這篇文章中不討論。

Route

👉 20~25 行則是使用 matchPath 判斷目前的 URL 是否符合定義在 <Route> 上的 path,有興趣 matchPath 的實作細節可以去看 🔗matchPath.js

這段程式碼可以三個部分理解,其實就是三元運算子的三個選項 😅 :

  • 第一個 computedMatch 是有使用 Switch 時才會有值,Switch 會使用 matchPath 函式判斷子元件 (children) 相對應的 path 是否符合 URL,然後回傳 match 物件。
  • 第二個是 matchPath(location.pathname, this.props) 則是判斷在 <Route> 定義個 path 是否符合目前的 URL,然後回傳 match 物件。
  • 第三個是 context.match ,這是從 Router 傳遞下來的物件,其值為 "/" 。也就是說,如果沒有在 <Route> 上設定 path,預設將會是 "/"

👉 在 38 行用 Context Provider 提供 context,你應該會好奇會什麼還要再包一層 Context。這是因為有時我們會想在元件中取得一些 router 的訊息,像是 useParamsuseRouteMatch 這些 Hooks API 就是透過 Context 取得 router 中的訊息。後面提到的 <Link> 也會用到 context 中的資料。

👉 最後 39~55 行之間,可以簡單理解為如果目前的 URL 符合定義在 <Route> 上的 path,則渲染 <Route> 裡面的內容,包括可能會有 class component、functional component 或是一般 HTML 的內容等。

Switch

👉 27~37 行,Switch 中如同在程式語言中的 switch case,會用一個 forEach 迭代所有的子元件,直到找到在 <Route> 上定義的 path 符合目前的 URL 。

這邊特別注意的是,如果子元件上沒有定義 path,則會直接拿 "/" 當作預設值。也就是說,沒有定義 path 的元件很容易就會被渲染。

為什麼這樣說,舉一個例子,<About /> 元件外的 <Route> 沒有定義 path,所以經過 <Switch> 時會使用預設的 "/" ,直接當作是符合的元件。所以,無論 URL 怎麼改變, <About /> 都會是唯一被渲染的元件。

Link

因為 Link 的程式碼太長,所以就只擷取重點部分的程式碼。

Link 的程式碼有兩個部分,其中一個為 Link 預設的元件 LinkAnchor,另一個則為 Link 的邏輯。

先講 LinkAnchor 的程式碼

👉 13~33 行,LinkAnchor 封裝了 onClick 的事件,當使用者點選了 Link 所產生的 <a> 時,會先執行 onClick 事件,然後才進行跳轉頁面。

再來看看 Link 的程式碼

👉 19~22 行是傳入 Route 中的 to 參數,以及目前的 location 物件。然後正規化的 to 的格式,例如傳入的 to 開頭沒有 "/" ,經過正規化後會加上 "/" 變成正常的 URL。最後,回傳一個 location 物件。

但其是我不太明白的是 resolveToLocation 原始碼判斷了 to 是否為一個 function,當 to 是一個 function 時,會呼叫 to(context.location) 。我不清楚為什麼 to 可能會傳入一個函式?😅

🔗 resolveToLocation 原始碼連結

👉 24~34 行是建立 LinkAnchor 元件所需要的 props 物件,其中在 24 行的history.createHref 是根據傳入的 location 物件轉成一般的 URL,例如 /users 。在 28 行的 navigate() 則是根據 <Link> 是否把 replace 做為 props 傳入,然後選擇使用 history.pushhistory.repalce

history.pushhistory.repalce 的差別:

  • history.push:原始位置在 /users ,在 push('/about') 後,頁面轉移至了 /about 。而如果使用者點選上一頁,頁面會回到 /users
  • history.replace :原始的位置在 /users ,在 replace('/about'),頁面童要轉移至 /about 。但是使用者點選上一頁,就不是回到 '/users ,而是回到在進入 /users 前的頁面。

Recap

我們看完了 react-router 主要的幾個元件,了解了程式碼運行的流程,重新釐清一下整個運作原理。

react-router 的簡易運作原理
  1. 首先, <Router> 元件用 history.location 初始化 location 狀態。
  2. <Route> 元件會從 Context 中拿到 location,然後渲染符合 location 的元件。
  3. 當使用者點擊 UI 上的 <Link> 時,會呼叫 history.push() 把定義在 <Route> 上的 to 放進 history 中,這時瀏覽器上的 URL 就會跟著一起更動,但是不會跳轉頁面。
  4. 接著,位置的改動會觸發 history.listen(),進而改變原本儲存在 <Router> 中的 location 狀態,然後重複以上第 2 步驟。

以上就是 react-router 的簡易運作原理,中間省略一些對於 location 物件的處理,或是渲染物件的流程等等,有興趣再自行深入吧 😄。

結論

這篇文章講解了 react-router 跟 react-router-dom 5.2 版的程式碼,解析了 BrowserRouter、Router、Route、Link、Switch 以上五個元件,最後總結了整個 react-router 的運作流程。

看了原始碼我當下是在想為什麼不用 functional component,而是用較為原始的 class component,沒用 Hooks API 還有點不太習慣。不知道 react-router v6 會不會改寫成基於 functional component,讓整體程式碼更乾淨一點。

Route 渲染元件的那一段程式碼不知道在搞什麼鬼 😆

--

--

Leo Chiu
手寫筆記