深入淺出解析 React Router 源碼
最近組裏有同學做了 React Router 源碼相關的分享,我感覺這是個不錯的選題, React Router 源碼簡練好讀,是個切入前端路由原理的好角度。在分享學習的過程中,自己對前端路由也產生了一些思考和見解,所以寫就本文,和大家分享我對前端路由的理解。
本文會先用原生 JavaScript 實現一個基本的前端路由,再介紹 React Router 的源碼實現,通過比較二者的實現方式,分析 React Router 實現的動機和優點。閱讀完本文,讀者們應該能瞭解:
-
前端路由的基本原理
-
React Router 的實現原理
-
React Router 的啓發和借鑑
公衆號
一. 我們應該如何實現一個前端路由
一開始,我們先跳出 React Router,思考如何用原生 JavaScript 實現一個的前端路由,所謂前端路由,我們無非要實現兩個功能:監聽記錄路由變化,匹配路由變化並渲染內容。以這兩點需求作爲基本脈絡,我們就能大致勾勒出前端路由的形狀。
路由示例:
1.Hash 實現
我們都知道,前端路由一般提供兩種匹配模式, hash
模式和 history
模式,二者的主要差別在於對 URL 監聽部分的不同,hash 模式監聽 URL 的 hash 部分,也就是 #
號後面部分的變化,對於 hash 的監聽,瀏覽器提供了 onHashChange
事件幫助我們直接監聽 hash 的變化:
<body>
<a href="#/home">Home</a>
<a href="#/user">User</a>
<a href="#/about">About</a>
<div id="view"></div>
</body>
<script>
// onHashChange事件回調, 匹配路由的改變並渲染對應內容
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;
}
}
// 綁定hash變化事件,監聽路由變化
window.addEventListener('hashchange', onHashChange);
</script>
hash 模式的實現比較簡單,我們通過 hashChange
事件就能直接監聽到路由 hash 的變化,並根據匹配到的 hash 的不同來渲染不同的內容。
2.History 實現
相較於 hash 實現的簡單直接,history 模式的實現需要我們稍微多寫幾行代碼,我們先修改一下 a
標籤的跳轉鏈接,畢竟 history 模式相較於 hash 最直接的區別就是跳轉的路由不帶 #
號,所以我們嘗試直接拿掉 #
號:
<body>
<a href="/home">Home</a>
<a href="/user">User</a>
<a href="/about">About</a>
<div id="view"></div>
</body>
點擊 a
標籤,會看到頁面發生跳轉,並提示找不到跳轉頁面,這也是意料之中的行爲,因爲 a
標籤的默認行爲就是跳轉頁面,我們在跳轉的路徑下沒有對應的網頁文件,就會提示錯誤。那麼對於這種非 hash 的路由變化,我們應該怎麼處理呢?大體上,我們可以通過以下三步來實現 history 模式下的路由:
1. 攔截a
標籤 的點擊事件,阻止它的默認跳轉行爲
2. 使用 H5 的 history API
更新 URL
3. 監聽和匹配路由改變以更新頁面
在開始寫代碼之前,我們有必要先了解一下 H5 的幾個 history API
的基本用法。其實 window.history
這個全局對象在 HTML4 的時代就已經存在,只不過那時我們只能調用 back()
、go()
等幾個方法來操作瀏覽器的前進後退等基礎行爲,而 H5 新引入的 pushState()
和 replaceState()
及 popstate
事件 ,能夠讓我們在不刷新頁面的前提下,修改 URL,並監聽到 URL 的變化,爲 history 路由的實現提供了基礎能力。
// 幾個 H5 history API 的用法
History.pushState(state, title [, url])
// 往歷史堆棧的頂部添加一個狀態,方法接收三個參數:一個狀態對象, 一個標題, 和一個(可選的)URL
// 簡單來說,pushState能更新當前 url,並且不引起頁面刷新
History.replaceState(stateObj, title[, url]);
// 修改當前歷史記錄實體,方法入參同上
// 用法和 pushState類似,區別在於 pushState 是往頁面棧頂新增一個記錄,而 replaceState 則是修改當前記錄
window.onpopstate
// 當活動歷史記錄條目更改時,將觸發popstate事件
// 需要注意的是,pushState 和 replaceState 對 url 的修改都不會觸發onpopstate,它只會在瀏覽器某些行爲下觸發, 比如點擊後退、前進按鈕、a標籤點擊等
詳細的參數介紹和用法讀者們可以進一步查閱 MDN,這裏只介紹和路由實現相關的要點以及基本用法。瞭解了這幾個 API 以後,我們就能按照我們上面的三步來實現我們的 history 路由:
<body>
<a href="/home">Home</a>
<a href="/user">User</a>
<a href="/about">About</a>
<div id="view"></div>
</body>
<script>
// 重寫所有 a 標籤事件
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'))
// 修改當前url(前兩個參數分別是 state 和 title,這裏暫時不需要用到
onPopState()
// 由於pushState不會觸發onpopstate事件, 所以我們需要手動觸發事件
}))
// onpopstate事件回調, 匹配路由的改變並渲染對應內容, 和 hash 模式基本相同
function onPopState() {
const view = document.querySelector('#view')
switch (location.pathname) {
case '/home':
view.innerHTML = 'Home';
break;
case '/user':
view.innerHTML = 'User';
break;
case '/about':
view.innerHTML = 'About';
break;
}
}
// 綁定onpopstate事件, 當頁面路由發生更改時(如前進後退),將觸發popstate事件
window.addEventListener('popstate', onPopState);
</script>
Tips:history 模式的代碼無法通過直接打開 html 文件的形式在本地運行,在切換路由的時候,將會提示:
Uncaught SecurityError: A history state object with URL 'file://xxx.html' cannot be created in a document with origin 'null'.
這是由於 pushState 的 url 必須與當前的 url 同源,而
file://
形式打開的頁面沒有 origin ,導致報錯。如果想正常運行體驗,可以使用http-server
爲文件啓動一個本地服務。
History 模式的實現代碼也比較簡單,我們通過重寫 a
標籤的點擊事件,阻止了默認的頁面跳轉行爲,並通過 history API 無刷新地改變 url,最後渲染對應路由的內容。到這裏,我們基本上了解了hash
和history
兩種前端路由模式的區別和實現原理,總的來說,兩者實現的原理雖然不同,但目標基本一致,都是在不刷新頁面的前提下,監聽和匹配路由的變化,並根據路由匹配渲染頁面內容。既然我們能夠如此簡單地實現前端路由,那麼 React Router 的優勢又體現在哪,它的實現能給我們帶來哪些啓發和借鑑呢。
二. React Router 用法回顧
在分析源碼之前,我們先來回顧一下 React Router 的基本用法,從用法中分析一個前端路由庫的基本設計和需求。只有先把握作爲上游的需求和設計,才能清晰和全面地解析作爲下游的源碼。
React Router 的組件通常分爲三種:
import { BrowserRouter, Switch, Route, Link } from "react-router-dom";
// HashRouter 和 BrowserRouter 二者的使用方法幾乎沒有差別,這裏只演示其一
const App = () => {
return (
<BrowserRouter>
<Link to="/">Home</Link>
<Link to="/about">About</Link>
<Link to="/user">User</Link>
<Switch>
<Route path="/about"><About /></Route>
<Route path="/user"> <User /></Route>
<Route path="/"><Home /></Route>
</Switch>
</BrowserRouter>
);
}
const Home = () => (<h2>Home</h2>);
const About = () => (<h2>About</h2>);
const User = () => (<h2>User</h2>);
export default App;
我們使用 React Router
重新實現了一遍開頭原生路由的功能,二者既有對應,也有差別。<Link>
對應 a
標籤,實現跳轉路由的功能; <Route>
對應 onPopState()
中的渲染邏輯,匹配路由並渲染對應組件;而<BrowserRouter>
對應 addEventListener
對路由變化的監聽。
下面我們就進入 React Router 的源碼,去一探這些組件的實現。
三. React Router 源碼實現
1. 目錄概覽
React Router 的代碼主要存在於 packages
文件夾下,在 v4 版本後,React Router
就分爲了四個包來發布,本文解析的部分主要位於 react-router
和 react-router-dom
文件夾。
├── packages
├── react-router // 核心、公用代碼
├── react-router-config // 路由配置
├── react-router-dom // 瀏覽器環境路由
└── react-router-native // React Native 路由
2.BrowserRouter 和 HashRouter
<BrowserRouter>
和 <HashRouter>
都是路由容器組件,所有的路由組件都必須被包裹在這兩個組件中才能使用:
const App = () => {
return (
<BrowserRouter>
<Route path="/" component={Home}></Route>
</BrowserRouter>
);
}
爲什麼會有這樣的用法,其實我們在看過這兩者的實現後就會理解:
// <BrowserRouter> 源碼
import React from "react";
import { Router } from "react-router";
import { createBrowserHistory as createHistory } from "history";
class BrowserRouter extends React.Component {
history = createHistory(this.props);
render() {
return <Router history={this.history} children={this.props.children} />;
}
}
export default BrowserRouter;
// <HashRouter> 源碼
import React from "react";
import { Router } from "react-router";
import { createHashHistory as createHistory } from "history";
class HashRouter extends React.Component {
history = createHistory(this.props);
render() {
return <Router history={this.history} children={this.props.children} />;
}
}
export default HashRouter;
我們會發現這二者就是一個殼,兩者的代碼量很少,代碼也幾乎一致,都是創建了一個 history
對象,然後將其和子組件一起透傳給了**<Router>
**,二者區別只在於引入的 createHistory()
不同。因此對於這二者的解析,其實是對 **<Router>
** 和 history
庫的解析。
history 庫
history 源碼倉庫: https://github.com/ReactTraining/history
先來看 history 庫,這裏的 history 並非 H5 的 history 對象,而是一個有着 7k+ star 的會話歷史管理庫,是 React Router 的核心依賴。本小節我們來看 history 庫的用法,以及瞭解爲什麼 React Router 要選擇 history 來管理會話歷史。
在看具體用法之前,我們先思考一下我們的 "會話歷史管理" 的需求。所謂會話歷史管理,我們很容易想到維護一個頁面訪問歷史棧,跳轉頁面的時候 push 一個歷史,後退 pop 一個歷史即可。不過我們通過第一節對 hash
和 history
路由的原生實現就能明白,不同路由模式之間,操作會話歷史的 API 不同、監聽會話歷史的方式也不同,而且前端路由並不只有這兩種模式,React Router 還提供了 memory 模式 static 模式,分別用於 RN 開發和 SSR。
所以我們希望在中間加一層抽象,來屏蔽幾種模式之間操作會話歷史的差別,而不是將這些差別和判斷帶進 React Router 的代碼中。
history 使您可以在任何運行 JavaScript 的地方輕鬆管理會話歷史記錄。一個 history 對象可以抽象出各種環境中的差異,並提供一個最小的 API,使您可以管理歷史記錄堆棧,導航和在會話之間保持狀態。
這是 history 文檔的第一句,很好地概括了 history 的作用、優勢和使用範圍,直接來看 API:
import { createBrowserHistory } from 'history';
// 創建history實例
const history = createBrowserHistory();
// 獲取當前 location 對象,類似 window.location
const location = history.location;
// 設置監聽事件回調,回調接收兩個參數 location 和 action
const unlisten = history.listen((location, action) => {
console.log(location.pathname, location.state);
});
// 可以使用 push 、replace、go 等方法來修改會話歷史
history.push('/home', { some: 'state' });
// 如果要停止監聽,調用listen()返回的函數.
unlisten();
API 簡潔好懂,就不再贅述了。出於篇幅的考慮,本小節只介紹 history 庫部分用法,其實現原理放到末尾番外篇,好讓讀者先專注瞭解 React Router 的實現。
Router 的實現
我們已經知道,<BrowserRouter>
和 <HashRouter>
本質上都是 <Router>
,只是二者引入的 createHistory()
方法不同。<Router>
的代碼在 react-router 這個包裏,是一個相對公共的組件,其他包的 <Router>
都引自這裏:
// 這個 RouterContext 並不是原生的 React Context, 由於React16和15的Context互不兼容, 所以React Router使用了一個第三方的 context 以同時兼容 React 16 和 15
// 這個 context 基於 mini-create-react-context 實現, 這個庫也是React context的Polyfil, 所以可以直接認爲二者用法相同
import RouterContext from "./RouterContext";
import React from 'react';
class Router extends React.Component {
// 該方法用於生成根路徑的 match 對象
// 第一次看這個 match 對象可能有點懵逼, 其實後面看到 <Route> 實現的時候就能理解 match 對象的用處, 這個對象是提供給<Route>判斷當前匹配頁面的
static computeRootMatch(pathname) {
return { path: "/", url: "/", params: {}, isExact: pathname === "/" };
}
constructor(props) {
super(props);
// 從傳入的 history 實例中取了 location 對象存到了state裏, 後面會通過setState更改location來觸發重新渲染
// location 對象包含 hash/pathname/search/state 等屬性, 其實就是當前的路由信息
this.state = {
location: props.history.location
};
// isMounted 和 pendingLocation 這兩個私有變量可能讓讀者有點迷惑, 源碼其實也在這進行了一整段註釋說明, 解釋爲什麼在 constructor 而不是 componentDidMount 中去監聽路由變化
// 簡單來說, 由於子組件會比父組件更早完成掛載, 如果在 componentDidMount 進行監聽, 則有可能在監聽事件註冊之前 history.location 已經發生改變, 因此我們需要在 constructor 中就註冊監聽事件, 並把改變的 location 記錄下來, 等到組件掛載完了以後, 再更新到 state 上去
// 其實如果去掉這部分的hack, 這裏只是簡單地設置了路由監聽, 並在路由改變的時候更新 state 中的路由信息
// 判斷組件是否已經掛載, componentDidMount階段會賦值爲true
this._isMounted = false;
// 儲存在構造函數執行階段發生改變的location
this._pendingLocation = null;
// 判斷是否處於服務端渲染 (staticContext 是 staticRouter 傳入<Router>的屬性, 用於服務端渲染)
if (!props.staticContext) {
// 使用 history.listen() 添加路由監聽事件
this.unlisten = props.history.listen(location => {
if (this._isMounted) {
// 如果組件已經掛載, 直接更新 state 的 location
this.setState({ location });
} else {
// 如果組件未掛載, 就先把 location 存起來, 等到 didmount 階段再 setState
this._pendingLocation = location;
}
});
}
}
// 對應構造函數里的處理, 將 _isMounted 置爲 true, 並使用 setState 更新 location
componentDidMount() {
this._isMounted = true;
if (this._pendingLocation) {
this.setState({ location: this._pendingLocation });
}
}
// 組件被卸載時, 同步解綁路由的監聽
componentWillUnmount() {
if (this.unlisten) this.unlisten();
}
render() {
return (
// Provider將value向下傳遞給組件樹上的組件
<RouterContext.Provider
// 透傳子組件
children={this.props.children || null}
value={{
// context 傳遞的值, <Router>下的組件樹就能通過 this.context.xxx 這樣的方式取得這裏的值
// 透傳 history 對象
history: this.props.history,
// 當前路由信息
location: this.state.location,
// 是否爲根路徑
match: Router.computeRootMatch(this.state.location.pathname),
// 服務端渲染用到的 staticContext
staticContext: this.props.staticContext
}}
/>
);
}
}
export default Router
代碼看起來不少,但如果刨除當中各種判斷場景的代碼,其實 <Router>
只做了兩件事,一是給子組件包了一層context
,讓路由信息( history 和 location 對象)能傳遞給其下所有子孫組件;二是綁定了路由監聽事件,使每次路由的改變都觸發setState
。
其實看到這我們就能明白,爲什麼 <Route>
等路由組件要求被包裹在 <BrowerRouter>
等路由器容器組件內才能使用,因爲路由信息都由外層的容器組件通過 context
的方式,傳遞給所有子孫組件,子孫組件在拿到當前路由信息後,才能匹配並渲染出對應內容。此外在路由發生改變的時候,容器組件<Router>
會通過 setState()
的方式,觸發子組件重新渲染。
本章小結
在看完了 <Router>
的實現後,我們來和原生實現做一個比較,我們之前提到,前端路由主要的兩個點是監聽和匹配路由的變化,而<Router>
就是幫我們完成了監聽這一步。在原生實現中,我們分別實現了 hash 模式和 history 模式的監聽,又是綁定事件,又是劫持 a
標籤的點擊,而在 React Router 中,這一步由 history 庫來完成,代碼內調用了history.listen
就完成了對幾種模式路由的監聽。
Route 的實現
我們前面提到,前端路由的核心在於監聽和匹配,上面我們使用 <Router>
實現了監聽,那麼本小節就來分析 <Route>
是如何做匹配的,同樣地我們先回顧 <Route>
的用法:
匹配模式:
// 精確匹配
// 嚴格匹配
// 大小寫敏感
<Route path="/user" exact component={User} />
<Route path="/user" strict component={User} />
<Route path="/user" sensitive component={User} />
路徑 path 寫法:
// 字符串形式
// 命名參數
// 數組形式
<Route path="/user" component={User} />
<Route path="/user/:userId" component={User} />
<Route path={["/users", "/profile"]} component={User} />
渲染方式:
// 通過子組件渲染
// 通過 props.component 渲染
// 通過 props.render 渲染
<Route path='/home'><Home /></Route>
<Route path='/home' component={Home}></Route>
<Route path='/home' render={() => <p>home</p>}></Route>
// 例子: 這裏最終的渲染結果是User, 優先級是子組件 > component > render
<Route path='/home' component={Home} render={() => <p>About</p>}>
<User />
</Route>
<Route>
所做的事情也很簡單,匹配到傳入的 path,渲染對應的組件。此外 <Route>
還提供了幾種不同的匹配模式、path 寫法以及渲染方式,<Route>
的源碼實現,和這些配置項有着緊密的聯繫:
import React from "react";
import RouterContext from "./RouterContext";
import matchPath from "../utils/matchPath.js";
function isEmptyChildren(children) {
return React.Children.count(children) === 0;
}
class Route extends React.Component {
render() {
return (
{/* Consumer 接收了 <Router> 傳下來的 context, 包含了history對象, location(當前路由信息), match(匹配對象)等信息 */}
<RouterContext.Consumer>
{/* 拿到路由信息
拿到 match 對象(來源優先級:Switch → props.path → context)
props.computedMatch 是 <Switch> 傳下來的, 是已經計算好的match, 優先級最高
<Route> 組件上的 path 屬性, 優先級第二
計算 match 對象, 下一小節會詳解這個 matchPath
context 上的 match 對象
把當前的 location 和 match 拼成新的 props,這個 props 會通過 Provider 繼續向下傳
<Route>組件提供的三種渲染方式, 優先級 children > component > render
這裏對children爲空的情況做了一個兼容, 統一賦爲null, 這是因爲 Preact 默認使用空數組來表示沒有children的情況 (Preact是一個3kb的React替代庫, 挺有趣的, 讀者們可以看看)
*/}
{context => {
const location = this.props.location || context.location;
const match = this.props.computedMatch
? this.props.computedMatch
: this.props.path
? matchPath(location.pathname, this.props)
: context.match;
const props = { ...context, location, match };
let { children, component, render } = this.props;
if (Array.isArray(children) && isEmptyChildren(children)) {
children = null;
}
// 把拼好的新的props通過context繼續往下傳
// 第一層判斷: 如果有 match 對象, 就渲染子組件 children 或 Component
// 第二層判斷: 如果有子組件 children, 就渲染 children, 沒有就渲染 component
// 第三層判斷: 如果子組件 children 是函數, 那就先執行函數, 並將路由信息 props 作爲回調參數
return (
<RouterContext.Provider value={props}>
{props.match
? children
? typeof children === "function"
? children(props)
: children
: component
? React.createElement(component, props)
: render
? render(props)
: null
: typeof children === "function"
? children(props)
: null}
</RouterContext.Provider>
);
}}
</RouterContext.Consumer>
);
}
}
export default Route;
Route 的實現相對簡單,代碼分爲兩部分:獲取 match 對象和渲染組件。我們在代碼中會看到多次 match 對象,這個 match 對象其實是由根組件的 computedMatch()
或 matchPath()
生成,包含了當前匹配信息。對於這個 match 對象的生成過程,我們放到下一小節,這裏我們只需要知道,如果當前 Route 匹配了路由,那麼會生成對應 match 對象,如果沒有匹配,match 對象爲 null
。
// match 對象實例
{
isExact: true,
params: {},
path: "/",
url: "/"
}
第二部分是 <Route>
組件的渲染邏輯,這部分代碼還是得從 <Route>
的行爲去理解,Route 提供了三種渲染方式:子組件、props.component
、props.render
,三者之間又存在優先級,因此就形成了我們看到了多層三元表達式渲染的結構。
這部分渲染邏輯不用細看,參照下邊的樹狀圖理解即可,代碼用了四層三元表達式的嵌套,來實現 子組件> component屬性傳入的組件 > children是函數
這樣的優先級渲染。
紅色節點是最終渲染結果:
matchPath
如果讓我們去實現路由匹配,我們會怎麼去做呢?全等比較?正則判斷?反正看起來應該是很簡單的一個實現,但如果我們打開matchPath()
的代碼,卻會發現它用了 60 行代碼、引了一個第三方庫來做這件事情:
import pathToRegexp from "path-to-regexp";
// 這裏建議先直接看 matchPath() 的代碼, 調用到的時候再看 compilePath
const cache = {};
const cacheLimit = 10000;
let cacheCount = 0;
// compilePath 的作用是根據路由路徑path 和匹配參數options等參數拼出正則regexp,和路徑參數keys 是路徑參數
function compilePath(path, options) {
const cacheKey = `${options.end}${options.strict}${options.sensitive}`;
const pathCache = cache[cacheKey] || (cache[cacheKey] = {});
if (pathCache[path]) return pathCache[path];
const keys = [];
// keys是個空數組, pathToRegexp會將在path中解析到的參數追加到keys裏
const regexp = pathToRegexp(path, keys, options);
// 由pathToRegexp拼出正則, pathToRegexp是個將字符串路徑轉換爲正則表達式的工具, 用法比較簡單, 讀者可以自己查查用法
const result = { regexp, keys };
// 返回結果: 正則regexp, 路徑裏解析到的參數keys
console.log('cacheCount', cacheCount);
console.log('cacheLimit', cacheLimit);
console.log('pathCache', pathCache);;
if (cacheCount < cacheLimit) {
pathCache[path] = result;
cacheCount++;
}
return result;
}
function matchPath(pathname, options = {}) {
if (typeof options === "string" || Array.isArray(options)) {
// 正常情況下, options 是 <Route> 的 props, 是個對象; 這裏判斷, 是爲了兼容 `react-router-redux`庫中某個調用傳入的 options 只有 path
options = { path: options };
}
const { path, exact = false, strict = false, sensitive = false } = options;
// 取出路由路徑 path 和匹配參數 exact 等, 並賦初值
const paths = [].concat(path);
// 統一 path 類型 (path可能是數組形式['/', '/user']或字符串形式"/user")
return paths.reduce((matched, path) => {
// 這裏用 reduce, 其實是爲了在遍歷路徑集合 paths 的同時, 只輸出一個結果, 如果用 map之類的 api 做循環, 會得到一個數組
if (!path && path !== "") return null;
// 沒有 path, 返回 null
if (matched) return matched;
// 已經匹配到了, 就返回上次匹配結果
const { regexp, keys } = compilePath(path, {
// 根據路由路徑 path 和匹配參數 exact 等參數拼出正則 regexp, keys 是路徑參數(比如/user:id的id)
end: exact,
strict,
sensitive
});
const match = regexp.exec(pathname);
// 調用正則原型方法exec, 返回一個結果數組或null
if (!match) return null;
// 沒匹配到, 返回 null
const [url, ...values] = match;
// 從結果數組裏
const isExact = pathname === url;
// 是否準確匹配
if (exact && !isExact) return null;
// 要求準確匹配卻沒有全等匹配到, 返回 null
// 這裏給幾個例子, 幫助大家直觀理解這個調用過程
// 傳入的path: /user
// regexp: /^\/user\/?(?=\/|$)/i
// url: /user
// 返回結果: {"path":"/user","url":"/user","isExact":true,"params":{}}
// 例子2
// 傳入的path: /user/:id
// regexp: /^\/user\/(?:([^\/]+?))\/?(?=\/|$)/i
// url: /user/1
// 返回結果: {"path":"/user/:id","url":"/user/1","isExact":true,"params":{"id":"1"}}
return {
path,
// 用於匹配的 path
url: path === "/" && url === "" ? "/" : url,
// url 匹配的部分
isExact,
// 是否準確匹配
params: keys.reduce((memo, key, index) => {
// 把 path-to-regexp 直接返回的路由參數 keys 做一次格式轉換
memo[key.name] = values[index];
return memo;
}, {})
};
}, null);
}
export default matchPath;
小結
本小節我們通過對 <Route>
和 mathPath
源碼的解析,講解 React Router 實現匹配和渲染的過程,匹配路由這部分的工作由 mathPath
通過 path-to-regexp
進行,<Route>
其實相當於一個高階組件,以不同的優先級和匹配模式渲染匹配到的子組件。
尾聲
到這裏,我們基本完成了對 React Router 的主要組件源碼解析,最後回顧一下整體的實現:
-
對於監聽功能的實現,React Router 引入了
history
庫,以屏蔽了不同模式路由在監聽實現上的差異, 並將路由信息以context
的形式,傳遞給被 包裹的組件, 使所有被包裹在其中的路由組件都能感知到路由的變化, 並接收到路由信息 -
在匹配的部分, React Router 引入了
path-to-regexp
來拼接路徑正則以實現不同模式的匹配,路由組件 作爲一個高階組件包裹業務組件, 通過比較當前路由信息和傳入的 path,以不同的優先級來渲染對應組件
整體而言,React Router 的源碼相對簡單清晰,源碼中所體現的前端路由的設計實現,也相信會對讀者們有所啓發借鑑。雖然本文對 React Router 源碼的解析就到此爲止, 但有關前端路由以及 React Router 的探索不會停止,怎樣從源碼到落地,怎樣爲項目做路由選型,怎樣設計一個合理的前端路由系統... 對於前端路由, 我們需要挖掘的東西還有很多, 源碼解析只是在這條道路路上邁出了一小步。在當下這波前端技術的滔滔浪潮中,前端路由,也還會在前端 er 的不斷迭代中, 繼續摸索和前進, 在更廣闊的場景上, 去發揮它的價值。
由於時間緊張, 本文成文比較匆忙,潦草之處,敬請諒解,以下有些坑還沒來得及填, 算是留給讀者們的思考題了~
-
集中式靜態配置路由和分佈式動態組件路由之爭
-
<Switch>
和<Link>
組件源碼解析 -
React Router hooks 源碼解析
-
history 庫源碼解析
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/AJeqXJsKeYjlFz4E8PCpSA