圖解 React-router 帶你深入理解路由本質
點擊上方“前端簡報”,選擇“設為星標”
第一時間關注技術干貨!
閱讀源碼小 tips:從整體到細節(jié),剛開始不要太拘泥于一行代碼的實現(xiàn),先從整體去理解,理解好各自的關系,再去讀源碼。
首先,我們先不糾結于源碼細節(jié)。先用最簡單的話來概括一下 React-router 到底做了什么?
本質上, React-Router 就是在頁面 URL 發(fā)生變化的時候,通過我們寫的 path 去匹配,然后渲染對應的組件。
那么,從這句話,我們想一下如何分步驟實現(xiàn):
如何監(jiān)聽 url 的變化 ? 如何匹配 path,按什么規(guī)則 ? 渲染對應的組件
了解好需要實現(xiàn)的關鍵步驟,我們來將倉庫源碼下載下來。
接下來我們看一下 GitHub, 它使用 lerna 管理同時管理多個包.也就是 Multirepo 概念。
react-router 使用 lerna 來同時管理多個包. ( lerna 的好處特別多,對于依賴關系大,同類型的包推薦使用 lerna 來統(tǒng)一管理。)


核心庫是 react-router. react-router-dom 是在瀏覽器中使用的,react-router-native是在 rn 中使用的。
如果不理解,直接看一下源碼就懂了。其實 react-router-dom 只是多了下面四個組件 BrowserRouter、 Link、NavLink、HashRouter, 其他其實都是直接引用 react-router 的。

了解完多包的組織關系之后,我們回到前面如何實現(xiàn) react-router 的 3個關鍵步驟,如下:
如何監(jiān)聽 url 的變化 ? 如何匹配 path ? 渲染對應的組件
我們不自己來實現(xiàn),直接看源碼,站在巨人的肩膀上來學習??。接下來我們來看一下 react-router-dom 官方文檔 的基本使用。
export default function App() {
return (
<BrowserRouter>
<div>
<Link to="/">Home</Link>
<Link to="/about">About</Link>
<Link to="/topics">Topics</Link>
<Switch>
<Route path="/about">
<About />
</Route>
<Route path="/">
<Home />
</Route>
</Switch>
</div>
</BrowserRouter>
);
}
從代碼中,我們可以觀察到下面幾點:
最外層包裹了 <BrowserRouter>,它有什么意義?在 <Route />匹配的外層,包裹了<Switch>,作用是如果匹配了一個,則不會再繼續(xù)渲染另外一個。如何實現(xiàn)?Route 中有 path 匹配路徑,包裹的則是渲染的組件。
整體設計
我們用一張圖來理解一下整個 react-router 是怎么實現(xiàn)的:
接下來我們看看每一個步驟是怎么實現(xiàn)的。

一、監(jiān)聽 URL 的變化
正常情況下,當 URL 發(fā)生變化時,瀏覽器會像服務端發(fā)送請求,但使用以下2種辦法不會向服務端發(fā)送請求:
基于 hash 基于 history
react-router 使用了 history 這個核心庫。
1. 選擇方式: history 或 hash
HashRouter 先是從 history 中引用 createBrowserHistory ,然后將 history 和 children 傳入到 Router 。BrowseHistory同理。

BrowseHistory 必須依賴服務器讓 url 都映射到 index.html ,否則會 404 。
2. 監(jiān)聽 URL 的變化,拿到對應的 history,location,match 等通過 Provider 注入到子組件中。

二、Route 中匹配渲染組件

這代碼可以分兩部分理解:
是否匹配 渲染組件
1. 是否匹配
computedMatch 是使用 Switch 包裹的子組件才有的值,Switch的作用是從上到下開始渲染,只要匹配到一個,其他的就不匹配。所以這里會先判斷 computedMatch 。

匹配解析 path ,這里使用了第三方庫 path-to-regexp
// Make sure you consistently `decode` segments.
const fn = match("/user/:id", { decode: decodeURIComponent });
fn("/user/123"); //=> { path: '/user/123', index: 0, params: { id: '123' } }
fn("/invalid"); //=> false
fn("/user/caf%C3%A9"); //=> { path: '/user/caf%C3%A9', index: 0, params: { id: 'café' } }2. 組件渲染方式
從文檔來看,它支持三種方式的渲染,如下:
// children 方式
<Route exact path="/">
<HomePage />
</Route>
// func 方式
<Route
path="/blog/:slug"
render={({ match }) => {
// Do whatever you want with the match...
return <div />;
}}
/>
// component 方式
<Route path="/user/:username" component={User} />
源碼部分如下:

吐槽一下,作者怎么就不能好好用 if else 來寫,非要寫這么多變態(tài)的 ?:,請不要學習,除非你的項目只有你一個前端??。
一下子看不懂也沒關系,我們來看下面的流程圖。

從上面的代碼我們可以看出:
Router 渲染的優(yōu)先級:children > component > render,三種方式互斥,只能使用一種。 不匹配的情況下,只要 children 是函數(shù),也會渲染 component 是使用 createComponent 來創(chuàng)建的, 這會導致不再更新現(xiàn)有組件,而是直接卸載再去掛載一個新的組件。如果是使用匿名函數(shù)來傳入 component ,每次 render 的時候,這個 props 都不同,會導致重新渲染掛載組件,導致性能特別差。因此,當使用匿名函數(shù)的渲染時,請使用 render 或 children 。
// 不要這么使用
<Route path="/user/:username" component={() => <User/> } />
結論
對于依賴關系大,同類型的包使用 lerna 來統(tǒng)一管理。盡量抽象出共用不可變的地方,比如 react-router 中的方法。
React-router 使用了Compound components(復合組件模式),在這種模式中,組件將被一起使用,它們可以方便的共享一種隱式的狀態(tài),比如 Switch , 可以在這里通過 React.children 來控制包裹組件的渲染優(yōu)先級,而無須使用者去控制。再比如我們經(jīng)常使用的
<select />和<option>, 可以通過 React.children 和 React.cloneElement 來劫持修改子組件,讓組件使用者通過更少的 api 來觸發(fā)更強大的功能。
如果覺得這篇文章還不錯,來個【轉發(fā)、收藏、在看】三連吧,讓更多的人也看到~
?? 順手點個在看唄 ↓
