前言
如果要用 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-router
跟 react-router-dom
其實是兩個不同的函式庫。實際上 react-router-dom
的核心就是 react-router
,同樣給 react native 使用的 react-router-native
其核心也是 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 的物件。最後將 history
跟 children
傳遞到 <Router>
中。
Router
👉 19~21行設定了元件內部的狀態 location
為 props.history.location
, props.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 的訊息,像是 useParams
、useRouteMatch
這些 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
可能會傳入一個函式?😅
👉 24~34 行是建立 LinkAnchor 元件所需要的 props 物件,其中在 24 行的history.createHref
是根據傳入的 location 物件轉成一般的 URL,例如 /users
。在 28 行的 navigate()
則是根據 <Link>
是否把 replace
做為 props 傳入,然後選擇使用 history.push
或 history.repalce
。
history.push
與 history.repalce
的差別:
history.push
:原始位置在/users
,在push('/about')
後,頁面轉移至了/about
。而如果使用者點選上一頁,頁面會回到/users
。history.replace
:原始的位置在/users
,在replace('/about')
,頁面童要轉移至/about
。但是使用者點選上一頁,就不是回到'/users
,而是回到在進入/users
前的頁面。
Recap
我們看完了 react-router
主要的幾個元件,了解了程式碼運行的流程,重新釐清一下整個運作原理。
- 首先,
<Router>
元件用history.location
初始化location
狀態。 <Route>
元件會從 Context 中拿到location
,然後渲染符合location
的元件。- 當使用者點擊 UI 上的
<Link>
時,會呼叫history.push()
把定義在<Route>
上的to
放進 history 中,這時瀏覽器上的 URL 就會跟著一起更動,但是不會跳轉頁面。 - 接著,位置的改動會觸發
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 渲染元件的那一段程式碼不知道在搞什麼鬼 😆。
分享就到這邊,如果喜歡我的文章可以幫我拍個幾下手,在閱讀文章時如果有遇到什麼問題,或是有什麼建議,都歡迎留言告訴我,謝謝。 😃