React-router-dom | 為了瞭解原理,那就來實作一個 react-router-dom 吧!

Leo Chiu
手寫筆記
Published in
10 min readJun 5, 2020

前言

上個星期看了 react-router-dom 的原始碼,寫了一篇原始碼解析的文章。大致上理解了 BrowserRouter、Switch、Route、Link 這幾個基本的元件後,這周想實現一個 react-router-dom 簡單版本。

如果還沒看過,可以先看一下,看過以後會更容易理解怎麼實作 😃。

之前都已經實作了 react-reduxredux-sagapromise 這些函式庫了,這次不跟上怎麼行,所以就讓我來細說怎麼實作吧! 😆

實作前的事前準備

其實要實作的程式碼很少,比起之前史詩級的各種函式庫, react-router-dom 實作起來真的很開心 👀。

首先,你需要的是顆善良的心,哦!不是,是你要了解 react-router-dom 的原理,所以直接拿上次作的流程圖出來看一下。

react-router-dom 原理

在這裡我假設你已經理解 history 這個函式庫的功能以及使用方法,如果還不知道是什麼可以看我的 🔗文章或是 🔗官方文件

react-router-dom 運作流程圖
  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 步驟。

這邊 <Router> 你可以當作就是 BrowserRouter。

react-router-dom 元件

現在你知道 react-router-dom 的簡易運行原理了,所以接下來我們重新釐清一下要寫哪些元件:

  • BrowserRouter: 起手式元件,讓包在其中的 Child Component 擁有路由的能力,並使用 HTML5 History API 管理 Location。
  • Route:是定義元件相對應的 path,當 path 符合目前的 URL 時將會被渲染。
  • Link:像是 HTML 的 <a>,能夠在點擊後轉變目前的 Location。
  • Switch:讓第一個符合 URL 的元件會被渲染,反之,如果沒有 Switch 則所有符合 URL 的元件都會被渲染。
  • hooks API — useParams:可以取得在 URL 上的參數,例如我們定義 path/user/:id ,當 URL 為 /user/123 時,能夠取得 URL 上的 id 為 123。

react-router-dom 輔助程式

我們會另外撰寫兩個輔助的程式:

  • Context:使用 React Context API 管理資料,讓在 BrowserRouter 中的
  • matchPath:使用 Regular Expression 判斷目前的 URL 是否符合在 Route 上定義的 path

第三方函式庫

以下兩個第三方函式庫皆是在 react-router 的實作中有用到的函式庫,我們使用函式庫減輕程式碼實作的量,讓我們可以專注實作 react-router 的邏輯。

🔧 history

History API 我們直接拿 react-router 官方的 🔗 history 函式庫來用。

🔧 path-to-regexp

這個套件個功能就如它的名字一樣,可以幫助我們將定義在 Route 上的 path 轉換成 Regular Expression,方便我們可以利用產生的 regexp 萃取出 URL 上的資訊。

使用方法很簡單, pathToRegexp 可以傳入兩個參數,第一個參數為要轉換成 regexp 的 path ,第二個為輸出的參數, path 中的 parms 資料會儲存在 keys 中,例如 :bar

Clean Code 的作者 uncle Bob 表示他不喜歡把輸入的參數當作輸出的參數。

從零開始實作 react-router-dom

本篇文章的實作方式都是使用 functional component 以及 hooks API,因為看起來比較簡潔乾淨 😃。

起手式,先來處理兩個輔助函式

Context

首先是基本建立 Context 的方法,不熟悉 Context API 請洽官方文件 🔗Context🔗Hooks API

matchPath

這是用來匹配 URL 與定義在 Route 元件上定義的 path ,會用到前面提到的 path-to-regexp 這個函式庫,將 path 轉換成 regexp,然後萃取出 URL 上的訊息。

請注意這個函式的兩個參數,很容易搞混:

  • pathname:由 history.location.pathname 取得的 URL 訊息。
  • path:定義在 Route 元件上的 path

regexp.exc(pathname) 回傳的是一個陣列,舉例來說, path 定義為 /user/:id ,匹配 pathname 後會回傳 ["/user/123", "123"] ,陣列中第一個值為符合的字串,而後面皆是字串中符合的 params。

然後透過 match.slice(1) 把 params 儲存到 values 中,但是這時候的 values 只是一個陣列, useParams 需要的是 key 與 value 的對應,所以最後用 keysvalues 產生一個物件表示 URL 中符合的 params。

接下來就是主程式了

BrowserRouter

BrowserRouter 是 react-router 的根節點,它讓包裹在裡面的元件擁有路由的能力。以下說明這個元件的做的事情:

  • 使用 createBrowserHistory 初始化 history。
  • 定義 location 狀態儲存 history.location,也就是目前網頁在的位置。
  • 定義 history.listen ,讓儲存目前網頁位置的 history.location 改變時,可以把改變的值儲存到 location 中。
  • computeRoomMatchreact-router 的函式,這邊沒有做更動。實際上這次的實作沒有包含 Route 上 exact 的參數,所以這個函式僅僅只用在 Switch 元件取得 default 的 location 。
  • 最後用 Context API 傳遞 historylocation 等在子元件用得到的狀態。

Route

Route 的目的很簡單,匹配目前的 URL 與在 Route 元件上定義的 path ,然後渲染符合的元件。

所以首先透過 Context 取的目前的 location ,然後看看 props.computedMatch 有沒有值,如果有值代表使用了 Switch 元件匹配符合的 path 。反之,如果沒有值,則代表沒有 Switch 元件,因此,使用 matchPath 匹配目前的 URL,也就是 location.pathnamepath 是否一樣。

最終,如果 match 有值,則代表定義在 Route 上的 path 是符合目前的 URL ,因此渲染在 Route 中的子元件。

Link

Link 的目標為提供使用者 <a href="..."> ,讓使用者點擊後可以在各個元件中路由,但是不會重新載入頁面。

我們一樣整理一下這個元件要做的兩件事:

  • 定義 handleOnClick ,讓使用者在點擊時,使用 event.preventDefault() 攔截跳轉頁面的行為,然後把目的地 to 儲存到 history 中。
  • 回傳 <a href="..."> 並在標籤上註冊點擊事件的 callback function,最後渲染子元件。

Switch

Switch 的目標是讓第一個符合 URL 的元件會被渲染,反之,如果沒有 Switch 則所有符合 URL 的元件都會被渲染。

所以,很簡單的思考模式即是迭代所有的子元件,然後判斷目前的 URL 是否匹配子元件上定義的 path ,當符合時則把該元件作為唯一會被渲染的元件。

這裡會用到 React 的高階 API — React.Children.forEach 迭代子元件,你可能會想問為什麼不用 props.children.forEach 就好呢?我上網查了一下資料,唯一能夠解釋的是 React.Children.forEach 可以處理 nullundefined 的值。

最後,渲染匹配的元件使用的一樣是 React 高階API — React.cloneElement。我們直接看官方文件如何解釋這個 API。

React.cloneElement :複製並回傳一個基於 element 的新 React element。這個回傳的 element 的 prop 將會是原本 element 的 prop 與新的 prop 進行 shallow 合併之後的結果。新的 children 將會取代原先的 children。 原先 element 的 keyref 將會被保留。

結論

如果想看實作程式碼,可以到 👉 Codesandbox 上看看。

這篇文章我們用 Functional Component 與 Hooks API 實作了一個簡易的 react-router-dom ,包括 BrowserRouter、Route、Link、Switch 以上四個元件,其程式邏輯皆是來自於 react-router 官方在 GitHub 上的程式碼。

這次的實作比之前 react-reduxredux-sagapromise 簡單許多,目前實作的難度比較大概是 promise == redux-saga > react-redux > react-router-dom ,不過由於都是最低限度的實作,為的是了解套件的原理,難免有些不周全,所以難度比較僅限參考。

分享就到這邊,如果喜歡我的文章可以幫我拍個幾下手,在閱讀文章時如果有遇到什麼問題,或是有什麼建議,都歡迎留言告訴我,謝謝。 😃

--

--

Leo Chiu
手寫筆記

每天進步一點點,在終點遇見更好的自己。 Instragram 小帳:@leo.web.dev