今天開始,我們開始揭開react-router-dom神祕的頭蓋骨,哦不,面紗。 在此之前,我們需要了解一些預備知識:React的context和react-router-dom的基本使用。需要複習的同學請移步:
下面是我跟小S同學一起閱讀原始碼的過程。 大家可以參照這個思路,進行其他開源專案原始碼的學習。
我: 小S,今天我們來一起學習React-router-dom的原始碼吧
好呀!
我: 首先,react-router的官網上,有基本的使用方法。 這裡 (中文點選這裡) 列出了常用的元件,以及它們的用法
- Router (BrowserRouter, HashRouter)
- Route
- Switch
- Link
好的, 繼續
我: 先從這些元件的原始碼入手,那肯定第一個就是BrowserRouter,或者HashRouter
那應該怎麼入手呢?
我:
首先,從github上,得到與文件版本對應的程式碼。
我:
接著看路徑結構。是這樣的:
接下來我一般就是找教程先簡單過一遍,程式碼下下來然後把node__modules複製出來debugger 然後看不懂了就放棄
我: 不,你進入細節之前,要先搞清楚程式碼的結構
恩啊, 不然怎麼找程式碼
我: 你看到這個路徑之後,第一步,應該看一看,這些資料夾都是幹啥的,哪個是你需要的
script是build, website是doc, packges是功能
這個都差不多
我: 對。開啟各個資料夾,會發現,packages裡面的東西,是我們想要的原始碼。
我:
我們肯定先從原始碼看起,因為這次讀原始碼首先要學習的是實現原理,並不是如何構建
我:
那我們們就從react-router-dom開始唄
我:
開啟react-router-dom,奔著modules去
直接從github上下載master的分支麼
我: 嗯
為啥看modules
不應該先看package.json和rollup麼
我: 核心程式碼,肯定是在modules裡了。我要先看看整個的結構,有個大致的印象
恩恩
我:
開啟modules就看到了我們剛剛文件中提及的幾個元件了
我:
我們先從BrowserRouter.js入手
嗯哼
我:
那我要開啟這個檔案,開始看程式碼了
我:
我先不關注package.json這些配置檔案
殘暴
我:
因為我這次是要看原理,不是看整個原始碼如何build
我:
配置檔案也是輔助而已
嗯啊。
可是有時候還是很重要的
我: 那就用到了再說
是不是至少看一下都用了什麼和幾個入口
我: 用到了什麼也不需要在package.json中看,因為我關注的那幾個元件,用到啥會import的。所以看原始碼,最重要的是focus on。你要有關注點,因為有的原始碼,是非常龐大的。一不小心就掉進了細節的海洋出不來了。
有道理
比如react
我:
對,你不可能一次就讀懂他裡面的東西,所以你要看很多次
我:
每次的關注點可以不同
恩啊
確實如此
我:
都揉到一起,會覺得非常亂,最後就放棄了
我:
而且,我們學習原始碼,也不一定要把原始碼中的每個特性都在同一個專案中都用到,還是要分開學,分開用
有道理
我就總忍不住亂看
我:
那就先看BrowserRouter.js了。
我:
開啟檔案,看了一下,挺開心,程式碼沒幾行
import React from "react";
import { Router } from "react-router";
import { createBrowserHistory as createHistory } from "history";
import PropTypes from "prop-types";
import warning from "tiny-warning";
/**
* The public API for a <Router> that uses HTML5 history.
*/
class BrowserRouter extends React.Component {
history = createHistory(this.props);
render() {
return <Router history={this.history} children={this.props.children} />;
}
}
if (__DEV__) {
//此處省略若干行程式碼
}
export default BrowserRouter;
複製程式碼
然後一臉懵逼記不住, 看不懂
我:
哈哈,程式碼這麼少,那肯定是有依賴元件了
我:
先看看依賴了哪些元件
我:
我最感興趣的是history和react-router。如下:
import React from "react";
import { Router } from "react-router";
import { createBrowserHistory as createHistory } from "history";
import PropTypes from "prop-types";
import warning from "tiny-warning";
複製程式碼
history是庫啊
等等
我有點沒跟上
我:
等待了30秒......
為啥我感興趣這倆呢
你的興趣點對
我以前看過原始碼相關教程,瞭解一點history
我: 嗯。官網說了啊。
Routers
At the core of every React Router application should be a router component. For web projects, react-router-dom provides and routers. Both of these will create a specialized history object for you.
我:
在實現路由的時候,肯定是用到history的
我:
所以,這個可能會作為讀原始碼的預備知識。(如果夥伴們有需求,請在評論中說明,我們可以再加一篇關於history的文章)
我:
但是我先不管他,看看影響react-router的閱讀不
我:
另外,之前說過,這個檔案原始碼行數很少,肯定依賴了其他的元件。看起來,這個react-router擔當了重要職責。
我:
所以現在有兩個Todos: history 和 react-router
嗯
我:
那一會需要關注的就是react-router這個包了
我:
我暫時先不管剛才的兩個todos,我把這個元件(BrowserRouter)先看看,反正程式碼又不多
class BrowserRouter extends React.Component {
history = createHistory(this.props);
render() {
return <Router history={this.history} children={this.props.children} />;
}
}
if (__DEV__) {
//此處省略若干行程式碼
}
複製程式碼
我:
我要把if(__DEV__)的分支略過,因為我現在要看的是最最核心的東西
我:
切記過早的進入__DEV__,那個是方便開發用的,通常與核心的概念關係不大
我:
那就只剩倆東西了
//......
class BrowserRouter extends React.Component {
history = createHistory(this.props);
render() {
return <Router history={this.history} children={this.props.children} />;
}
}
//......
複製程式碼
我: 所以現在BrowserRouter的任務,就是建立一個history物件,傳給react-router的<Router>元件
這個時候
我: 嗯,你說
你會選擇看react-router還是history
我: 哈哈,這個時候,我其實想看一眼HashRouter
我也是
我: 因為import的那句話
import { createBrowserHistory as createHistory } from "history";
複製程式碼
所以我有理由懷疑,HashRouter的程式碼類似,只是從history包中匯入了不同的函式
HashRouter.js
import React from "react";
import { Router } from "react-router";
import { createHashHistory as createHistory } from "history";
import PropTypes from "prop-types";
import warning from "tiny-warning";
複製程式碼
還真是
我:
那我就把關注點,放在react-router上了
我:
因為
- history我猜出他是幹啥了,跟瀏覽器路徑有關
- router裡面如果用到history了,我等到在讀碼時,遇到了阻礙,再去看history,這樣行不行
恩啊
我: 回到這個路徑
我: 去看react-router
為什麼是他
我:
因為它匯入包時,沒加相對路徑啊
我:
說明這是一個已經發布的node包,匯入時需要在node_modules路徑下找
import { Router } from "react-router";
複製程式碼
我: 我就往上翻一翻唄,當然,估計在配置檔案中,應該會有相關配置
恩恩
我:
進這個路徑,檔案真tmd多,mmp的
我是這個習慣,先看index是不是隻做了import
我: 但是其實我們在使用recat-router-dom的時候,網上會有一些與react-router的比較的討論,
沒太注意
稀裡糊塗
我: 所以,react-router是一個已經發布的node包。但是,我並不確定他的程式碼在哪,如果找不到,我可能會從github上其他的位置找,或者從npm的官網找連結了
恩啊
我:
進index.js吧
"use strict";
if (process.env.NODE_ENV === "production") {
module.exports = require("./cjs/react-router.min.js");
} else {
module.exports = require("./cjs/react-router.js");
}
複製程式碼
我:
程式碼不多,分成production和else倆分支
我:
我會選擇else分支
我:
但是發現一個問題啊,我艹
我:
當前路徑下,沒有cjs資料夾
我:
因為BrowserRouter匯入的是一個包
我:
所以這個包,得是build之後的
這個時候就要看packge的script了
我:
嗯,可以的
我:
不過我感覺略微跑偏了
我:
我要回到router本身上
好好
繼續
怎麼回到router本身
我:
/react-router下,有一個router.js檔案
我:
開啟看,只有那兩行程式碼,不是我要的東西啊
我:
它匯出的,還是index.js編譯之後的
看modules
我:
對,看modules
我:
開啟modules下的Router.js
要是我的話, 這個時候就跑偏了
直接去看rollup了
然後最後找到router
router.js
我:
我也可能會跑偏
我:
我之前就跑到history上去了
我:
但是後來想想,這樣不太好
我:
從看原始碼角度說,直接找到modules下的Router.js很容易
我:
因為其他檔案,一看就不是原始碼實現
嗯啊
我:
現在開啟它,一看,挺像啊,那先看看有多少行
我:
百十來行,有信心了,哈哈
import React from "react";
import PropTypes from "prop-types";
import warning from "tiny-warning";
import RouterContext from "./RouterContext";
import warnAboutGettingProperty from "./utils/warnAboutGettingProperty";
function getContext(props, state) {
return {
history: props.history,
location: state.location,
match: Router.computeRootMatch(state.location.pathname),
staticContext: props.staticContext
};
}
/**
* The public API for putting history on context.
*/
class Router extends React.Component {
static computeRootMatch(pathname) {
return { path: "/", url: "/", params: {}, isExact: pathname === "/" };
}
constructor(props) {
super(props);
this.state = {
location: props.history.location
};
// This is a bit of a hack. We have to start listening for location
// changes here in the constructor in case there are any <Redirect>s
// on the initial render. If there are, they will replace/push when
// they mount and since cDM fires in children before parents, we may
// get a new location before the <Router> is mounted.
this._isMounted = false;
this._pendingLocation = null;
if (!props.staticContext) {
this.unlisten = props.history.listen(location => {
if (this._isMounted) {
this.setState({ location });
} else {
this._pendingLocation = location;
}
});
}
}
componentDidMount() {
this._isMounted = true;
if (this._pendingLocation) {
this.setState({ location: this._pendingLocation });
}
}
componentWillUnmount() {
if (this.unlisten) this.unlisten();
}
render() {
const context = getContext(this.props, this.state);
return (
<RouterContext.Provider
children={this.props.children || null}
value={context}
/>
);
}
}
// TODO: Remove this in v5
if (!React.createContext) {
Router.childContextTypes = {
router: PropTypes.object.isRequired
};
Router.prototype.getChildContext = function() {
const context = getContext(this.props, this.state);
if (__DEV__) {
const contextWithoutWarnings = { ...context };
Object.keys(context).forEach(key => {
warnAboutGettingProperty(
context,
key,
`You should not be using this.context.router.${key} directly. It is private API ` +
"for internal use only and is subject to change at any time. Instead, use " +
"a <Route> or withRouter() to access the current location, match, etc."
);
});
context._withoutWarnings = contextWithoutWarnings;
}
return {
router: context
};
};
}
if (__DEV__) {
Router.propTypes = {
children: PropTypes.node,
history: PropTypes.object.isRequired,
staticContext: PropTypes.object
};
Router.prototype.componentDidUpdate = function(prevProps) {
warning(
prevProps.history === this.props.history,
"You cannot change <Router history>"
);
};
}
export default Router;
複製程式碼
然後這麼少的程式碼
第一反應看一下引入
我:
對
我:
但是你看,一共五個
import React from "react";
import PropTypes from "prop-types";
import warning from "tiny-warning";
import RouterContext from "./RouterContext";
import warnAboutGettingProperty from "./utils/warnAboutGettingProperty";
複製程式碼
前三個忽略,一看就沒用
我:
是的
我:
我現在其實有點關注第五個了
我會看render
我:
先不著急
我:
因為如果第五個的名字叫做warnXXXX
我:
是警告的意思
恩恩
搜一下
我: 警告通常都是開發版本的東西,如果能排除,那就剩第四個依賴了
可能沒用
再一看,是在__DEV__裡面的
我:
對,當前檔案搜尋了一下,在__DEV__分支下,不看了,哈哈
我:
那就剩一個context.js了唄
過分
我: 我覺得我現在想掃一眼這個檔案,如果內容不多,我就先搞他,如果多的話,那就先放那
恩恩
我:
那我去看一看吧,哈哈
我:
進RouterContext.js這個檔案了
// TODO: Replace with React.createContext once we can assume React 16+
import createContext from "create-react-context";
const context = createContext();
context.Provider.displayName = "Router.Provider";
context.Consumer.displayName = "Router.Consumer";
export default context;
複製程式碼
我: 我次奧了
狗
我:
十行不到,我把他搞定,我就可以專注Router.js那個檔案了。那個檔案裡面的內容,就是全部Router的核心了
我:
這裡是標準context用法,店長推薦的,參見這個
我:
返回Router.js了哈
然後呢
看createContext麼
我:
createContex就是最新的context用法,參見這個
我:
所以,需要有準備知識,哈哈
我:
簡單點說,就是一個提供者(Provider),一個是消費者(Consumer)
我:
我這次看的是react-router
我:
別跑偏了
我:
回到router.js去了
我:
這個時候,可以稍微進入細節一些了
我:
從第一個函式定義開始
function getContext(props, state) {
return {
history: props.history,
location: state.location,
match: Router.computeRootMatch(state.location.pathname),
staticContext: props.staticContext
};
}
複製程式碼
我: 從名字看,是獲取context的,每次呼叫返回一個新建立的物件,多餘的不知道,先放著,往後看
嗯
我:
我先大概掃一眼元件都有哪些方法。另外發現,除了元件,還有其他程式碼
我:
除了元件內容,元件下面有一個判斷,看起來應該是處理老版本react的相容問題的。那我就先不看了
// TODO: Remove this in v5
if (!React.createContext) {
Router.childContextTypes = {
router: PropTypes.object.isRequired
};
Router.prototype.getChildContext = function() {
const context = getContext(this.props, this.state);
if (__DEV__) {
const contextWithoutWarnings = { ...context };
Object.keys(context).forEach(key => {
warnAboutGettingProperty(
context,
key,
`You should not be using this.context.router.${key} directly. It is private API ` +
"for internal use only and is subject to change at any time. Instead, use " +
"a <Route> or withRouter() to access the current location, match, etc."
);
});
context._withoutWarnings = contextWithoutWarnings;
}
return {
router: context
};
};
}
複製程式碼
我:
所以,重點就是在這個元件裡面了。元件裡面就是一些生命週期函式
我:
constructor、componentDidMount
我:
這倆,是初始化的地方
嗯嗯
我:
一個一個看
我:
重點是那個判斷
if (!props.staticContext) {
this.unlisten = props.history.listen(location => {
if (this._isMounted) {
this.setState({ location });
} else {
this._pendingLocation = location;
}
});
}
複製程式碼
我:
if (!props.staticContext) {}的作用,是保證Router裡面再巢狀Router時,使用的是相同的history
我:
裡面是一個監聽,監聽history中的location的改變,也就是說,當通過這個history改變路徑時,會統一監聽,統一處理
嗯嗯
我: 那裡面就呼叫了setState了唄,接著render就執行了
嗯
我: render非常簡單,就是把context的value值,修改了一下
嗯啊
我: 我們知道,只要context的value一變化,對應的consumer的函式,就會被呼叫,是吧
嗯嗯
我:
那現在Router就結束了
我:
接下來,我們好奇的是,哪些元件使用了Consumer
找route
我:
對。根據React-router的使用,估計就是每個<Route>,都會監聽這個context,然後進行路徑匹配,決定是否要渲染自己的component屬性所指定的內容
我:
接下來,我們就可以繼續看這個元件了。先吃飯去吧,<Route>解讀,且聽下回分解。
嗯,好的。拜拜。