ReactRouter的實(shí)現(xiàn)方法
ReactRouter的實(shí)現(xiàn)
ReactRouter是React的核心組件,主要是作為React的路由管理器,保持UI與URL同步,其擁有簡(jiǎn)單的API與強(qiáng)大的功能例如代碼緩沖加載、動(dòng)態(tài)路由匹配、以及建立正確的位置過(guò)渡處理等。
描述
React Router是建立在history對(duì)象之上的,簡(jiǎn)而言之一個(gè)history對(duì)象知道如何去監(jiān)聽(tīng)瀏覽器地址欄的變化,并解析這個(gè)URL轉(zhuǎn)化為location對(duì)象,然后router使用它匹配到路由,最后正確地渲染對(duì)應(yīng)的組件,常用的history有三種形式: Browser History、Hash History、Memory History。
Browser History
Browser History是使用React Router的應(yīng)用推薦的history,其使用瀏覽器中的History對(duì)象的pushState、replaceState等API以及popstate事件等來(lái)處理URL,其能夠創(chuàng)建一個(gè)像https://www.example.com/path這樣真實(shí)的URL,同樣在頁(yè)面跳轉(zhuǎn)時(shí)無(wú)須重新加載頁(yè)面,當(dāng)然也不會(huì)對(duì)于服務(wù)端進(jìn)行請(qǐng)求,當(dāng)然對(duì)于history模式仍然是需要后端的配置支持,用以支持非首頁(yè)的請(qǐng)求以及刷新時(shí)后端返回的資源,由于應(yīng)用是個(gè)單頁(yè)客戶端應(yīng)用,如果后臺(tái)沒(méi)有正確的配置,當(dāng)用戶在瀏覽器直接訪問(wèn)URL時(shí)就會(huì)返回404,所以需要在服務(wù)端增加一個(gè)覆蓋所有情況的候選資源,如果URL匹配不到任何靜態(tài)資源時(shí),則應(yīng)該返回同一個(gè)index.html應(yīng)用依賴頁(yè)面,例如在Nginx下的配置。
location / {
try_files $uri $uri/ /index.html;
}
Hash History
Hash符號(hào)即#原本的目的是用來(lái)指示URL中指示網(wǎng)頁(yè)中的位置,例如https://www.example.com/index.html#print即代表example的index.html的print位置,瀏覽器讀取這個(gè)URL后,會(huì)自動(dòng)將print位置滾動(dòng)至可視區(qū)域,通常使用<a>標(biāo)簽的name屬性或者<div>標(biāo)簽的id屬性指定錨點(diǎn)。
通過(guò)window.location.hash屬性能夠讀取錨點(diǎn)位置,可以為Hash的改變添加hashchange監(jiān)聽(tīng)事件,每一次改變Hash,都會(huì)在瀏覽器的訪問(wèn)歷史中增加一個(gè)記錄,此外Hash雖然出現(xiàn)在URL中,但不會(huì)被包括在HTTP請(qǐng)求中,即#及之后的字符不會(huì)被發(fā)送到服務(wù)端進(jìn)行資源或數(shù)據(jù)的請(qǐng)求,其是用來(lái)指導(dǎo)瀏覽器動(dòng)作的,對(duì)服務(wù)器端沒(méi)有效果,因此改變Hash不會(huì)重新加載頁(yè)面。
ReactRouter的作用就是通過(guò)改變URL,在不重新請(qǐng)求頁(yè)面的情況下,更新頁(yè)面視圖,從而動(dòng)態(tài)加載與銷毀組件,簡(jiǎn)單的說(shuō)就是,雖然地址欄的地址改變了,但是并不是一個(gè)全新的頁(yè)面,而是之前的頁(yè)面某些部分進(jìn)行了修改,這也是SPA單頁(yè)應(yīng)用的特點(diǎn),其所有的活動(dòng)局限于一個(gè)Web頁(yè)面中,非懶加載的頁(yè)面僅在該Web頁(yè)面初始化時(shí)加載相應(yīng)的HTML、JavaScript、CSS文件,一旦頁(yè)面加載完成,SPA不會(huì)進(jìn)行頁(yè)面的重新加載或跳轉(zhuǎn),而是利用JavaScript動(dòng)態(tài)的變換HTML,默認(rèn)Hash模式是通過(guò)錨點(diǎn)實(shí)現(xiàn)路由以及控制組件的顯示與隱藏來(lái)實(shí)現(xiàn)類似于頁(yè)面跳轉(zhuǎn)的交互。
Memory History
Memory History不會(huì)在地址欄被操作或讀取,這就可以解釋如何實(shí)現(xiàn)服務(wù)器渲染的,同時(shí)其也非常適合測(cè)試和其他的渲染環(huán)境例如React Native,和另外兩種History的一點(diǎn)不同是我們必須創(chuàng)建它,這種方式便于測(cè)試。
const history = createMemoryHistory(location);
實(shí)現(xiàn)
我們來(lái)實(shí)現(xiàn)一個(gè)非常簡(jiǎn)單的Browser History模式與Hash History模式的實(shí)現(xiàn),因?yàn)?code>H5的pushState方法不能在本地文件協(xié)議file://運(yùn)行,所以運(yùn)行起來(lái)需要搭建一個(gè)http://環(huán)境,使用webpack、Nginx、Apache等都可以,回到Browser History模式路由,能夠?qū)崿F(xiàn)history路由跳轉(zhuǎn)不刷新頁(yè)面得益與H5提供的pushState()、replaceState()等方法以及popstate等事件,這些方法都是也可以改變路由路徑,但不作頁(yè)面跳轉(zhuǎn),當(dāng)然如果在后端不配置好的情況下路由改編后刷新頁(yè)面會(huì)提示404,對(duì)于Hash History模式,我們的實(shí)現(xiàn)思路相似,主要在于沒(méi)有使用pushState等H5的API,以及監(jiān)聽(tīng)事件不同,通過(guò)監(jiān)聽(tīng)其hashchange事件的變化,然后拿到對(duì)應(yīng)的location.hash更新對(duì)應(yīng)的視圖。
<!-- Browser History -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Router</title>
</head>
<body>
<ul>
<li><a href="/home" rel="external nofollow" >home</a></li>
<li><a href="/about" rel="external nofollow" >about</a></li>
<div id="routeView"></div>
</ul>
</body>
<script>
function Router() {
this.routeView = null; // 組件承載的視圖容器
this.routes = Object.create(null); // 定義的路由
}
// 綁定路由匹配后事件
Router.prototype.route = function (path, callback) {
this.routes[path] = () => this.routeView.innerHTML = callback() || "";
};
// 初始化
Router.prototype.init = function(root, rootView) {
this.routeView = rootView; // 指定承載視圖容器
this.refresh(); // 初始化即刷新視圖
root.addEventListener("click", (e) => { // 事件委托到root
if (e.target.nodeName === "A") {
e.preventDefault();
history.pushState(null, "", e.target.getAttribute("href"));
this.refresh(); // 觸發(fā)即刷新視圖
}
})
// 監(jiān)聽(tīng)用戶點(diǎn)擊后退與前進(jìn)
// pushState與replaceState不會(huì)觸發(fā)popstate事件
window.addEventListener("popstate", this.refresh.bind(this), false);
};
// 刷新視圖
Router.prototype.refresh = function () {
let path = location.pathname;
console.log("refresh", path);
if(this.routes[path]) this.routes[path]();
else this.routeView.innerHTML = "";
};
window.Router = new Router();
Router.route("/home", function() {
return "home";
});
Router.route("/about", function () {
return "about";
});
Router.init(document, document.getElementById("routeView"));
</script>
</html>
<!-- Hash History -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Router</title>
</head>
<body>
<ul>
<li><a href="#/home" rel="external nofollow" >home</a></li>
<li><a href="#/about" rel="external nofollow" >about</a></li>
<div id="routeView"></div>
</ul>
</body>
<script>
function Router() {
this.routeView = null; // 組件承載的視圖容器
this.routes = Object.create(null); // 定義的路由
}
// 綁定路由匹配后事件
Router.prototype.route = function (path, callback) {
this.routes[path] = () => this.routeView.innerHTML = callback() || "";
};
// 初始化
Router.prototype.init = function(root, rootView) {
this.routeView = rootView; // 指定承載視圖容器
this.refresh(); // 初始化觸發(fā)
// 監(jiān)聽(tīng)hashchange事件用以刷新
window.addEventListener("hashchange", this.refresh.bind(this), false);
};
// 刷新視圖
Router.prototype.refresh = function () {
let hash = location.hash;
console.log("refresh", hash);
if(this.routes[hash]) this.routes[hash]();
else this.routeView.innerHTML = "";
};
window.Router = new Router();
Router.route("#/home", function() {
return "home";
});
Router.route("#/about", function () {
return "about";
});
Router.init(document, document.getElementById("routeView"));
</script>
</html>
分析
- 我們可以看一下
ReactRouter的實(shí)現(xiàn),commit id為eef79d5,TAG是4.4.0,在這之前我們需要先了解一下history庫(kù),history庫(kù),是ReactRouter依賴的一個(gè)對(duì)window.history加強(qiáng)版的history庫(kù),其中主要用到的有match對(duì)象表示當(dāng)前的URL與path的匹配的結(jié)果,location對(duì)象是history庫(kù)基于window.location的一個(gè)衍生。 ReactRouter將路由拆成了幾個(gè)包:react-router負(fù)責(zé)通用的路由邏輯,react-router-dom負(fù)責(zé)瀏覽器的路由管理,react-router-native負(fù)責(zé)react-native的路由管理。
- 我們以
BrowserRouter組件為例,BrowserRouter在react-router-dom中,它是一個(gè)高階組件,在內(nèi)部創(chuàng)建一個(gè)全局的history對(duì)象,可以監(jiān)聽(tīng)整個(gè)路由的變化,并將history作為props傳遞給react-router的Router組件,Router組件再會(huì)將這個(gè)history的屬性作為context傳遞給子組件。
// packages\react-router-dom\modules\HashRouter.js line 10
class BrowserRouter extends React.Component {
history = createHistory(this.props);
render() {
return <Router history={this.history} children={this.props.children} />;
}
}
接下來(lái)我們到Router組件,Router組件創(chuàng)建了一個(gè)React Context環(huán)境,其借助context向Route傳遞context,這也解釋了為什么Router要在所有Route的外面。在Router的componentWillMount中,添加了history.listen,其能夠監(jiān)聽(tīng)路由的變化并執(zhí)行回調(diào)事件,在這里即會(huì)觸發(fā)setState。當(dāng)setState時(shí)即每次路由變化時(shí) -> 觸發(fā)頂層Router的回調(diào)事件 -> Router進(jìn)行setState -> 向下傳遞 nextContext此時(shí)context中含有最新的location -> 下面的Route獲取新的nextContext判斷是否進(jìn)行渲染。
// line packages\react-router\modules\Router.js line 10
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() {
return (
<RouterContext.Provider
children={this.props.children || null}
value={{
history: this.props.history,
location: this.state.location,
match: Router.computeRootMatch(this.state.location.pathname),
staticContext: this.props.staticContext
}}
/>
);
}
}
我們?cè)谑褂脮r(shí)都是使用Router來(lái)嵌套Route,所以此時(shí)就到Route組件,Route的作用是匹配路由,并傳遞給要渲染的組件props,Route接受上層的Router傳入的context,Router中的history監(jiān)聽(tīng)著整個(gè)頁(yè)面的路由變化,當(dāng)頁(yè)面發(fā)生跳轉(zhuǎn)時(shí),history觸發(fā)監(jiān)聽(tīng)事件,Router向下傳遞nextContext,就會(huì)更新Route的props和context來(lái)判斷當(dāng)前Route的path是否匹配location,如果匹配則渲染,否則不渲染,是否匹配的依據(jù)就是computeMatch這個(gè)函數(shù),在下文會(huì)有分析,這里只需要知道匹配失敗則match為null,如果匹配成功則將match的結(jié)果作為props的一部分,在render中傳遞給傳進(jìn)來(lái)的要渲染的組件。Route接受三種類型的render props,<Route component>、<Route render>、<Route children>,此時(shí)要注意的是如果傳入的component是一個(gè)內(nèi)聯(lián)函數(shù),由于每次的props.component都是新創(chuàng)建的,所以React在diff的時(shí)候會(huì)認(rèn)為進(jìn)來(lái)了一個(gè)全新的組件,所以會(huì)將舊的組件unmount再re-mount。這時(shí)候就要使用render,少了一層包裹的component元素,render展開(kāi)后的元素類型每次都是一樣的,就不會(huì)發(fā)生re-mount了,另外children也不會(huì)發(fā)生re-mount。
// \packages\react-router\modules\Route.js line 17
class Route extends React.Component {
render() {
return (
<RouterContext.Consumer>
{context => {
invariant(context, "You should not use <Route> outside a <Router>");
const location = this.props.location || context.location;
const match = this.props.computedMatch
? this.props.computedMatch // <Switch> already computed the match for us
: this.props.path
? matchPath(location.pathname, this.props)
: context.match;
const props = { ...context, location, match };
let { children, component, render } = this.props;
// Preact uses an empty array as children by
// default, so use null if that's the case.
if (Array.isArray(children) && children.length === 0) {
children = null;
}
if (typeof children === "function") {
children = children(props);
// ...
}
return (
<RouterContext.Provider value={props}>
{children && !isEmptyChildren(children)
? children
: props.match
? component
? React.createElement(component, props)
: render
? render(props)
: null
: null}
</RouterContext.Provider>
);
}}
</RouterContext.Consumer>
);
}
}
我們實(shí)際上我們可能寫(xiě)的最多的就是Link這個(gè)標(biāo)簽了,所以我們?cè)賮?lái)看一下<Link>組件,我們可以看到Link最終還是創(chuàng)建一個(gè)a標(biāo)簽來(lái)包裹住要跳轉(zhuǎn)的元素,在這個(gè)a標(biāo)簽的handleClick點(diǎn)擊事件中會(huì)preventDefault禁止默認(rèn)的跳轉(zhuǎn),所以實(shí)際上這里的href并沒(méi)有實(shí)際的作用,但仍然可以標(biāo)示出要跳轉(zhuǎn)到的頁(yè)面的URL并且有更好的html語(yǔ)義。在handleClick中,對(duì)沒(méi)有被preventDefault、鼠標(biāo)左鍵點(diǎn)擊的、非_blank跳轉(zhuǎn)的、沒(méi)有按住其他功能鍵的單擊進(jìn)行preventDefault,然后push進(jìn)history中,這也是前面講過(guò)的路由的變化與 頁(yè)面的跳轉(zhuǎn)是不互相關(guān)聯(lián)的,ReactRouter在Link中通過(guò)history庫(kù)的push調(diào)用了HTML5 history的pushState,但是這僅僅會(huì)讓路由變化,其他什么都沒(méi)有改變。在Router中的listen,它會(huì)監(jiān)聽(tīng)路由的變化,然后通過(guò)context更新props和nextContext讓下層的Route去重新匹配,完成需要渲染部分的更新。
// packages\react-router-dom\modules\Link.js line 14
class Link extends React.Component {
handleClick(event, history) {
if (this.props.onClick) this.props.onClick(event);
if (
!event.defaultPrevented && // onClick prevented default
event.button === 0 && // ignore everything but left clicks
(!this.props.target || this.props.target === "_self") && // let browser handle "target=_blank" etc.
!isModifiedEvent(event) // ignore clicks with modifier keys
) {
event.preventDefault();
const method = this.props.replace ? history.replace : history.push;
method(this.props.to);
}
}
render() {
const { innerRef, replace, to, ...rest } = this.props; // eslint-disable-line no-unused-vars
return (
<RouterContext.Consumer>
{context => {
invariant(context, "You should not use <Link> outside a <Router>");
const location =
typeof to === "string"
? createLocation(to, null, null, context.location)
: to;
const href = location ? context.history.createHref(location) : "";
return (
<a
{...rest}
onClick={event => this.handleClick(event, context.history)}
href={href}
ref={innerRef}
/>
);
}}
</RouterContext.Consumer>
);
}
}
每日一題
https://github.com/WindrunnerMax/EveryDay
參考
https://zhuanlan.zhihu.com/p/44548552 https://github.com/fi3ework/blog/issues/21 https://juejin.cn/post/6844903661672333326 https://juejin.cn/post/6844904094772002823 https://juejin.cn/post/6844903878568181768 https://segmentfault.com/a/1190000014294604 https://github.com/youngwind/blog/issues/109 http://react-guide.github.io/react-router-cn/docs/guides/basics/Histories.html
到此這篇關(guān)于ReactRouter的實(shí)現(xiàn)方法的文章就介紹到這了,更多相關(guān)ReactRouter的實(shí)現(xiàn)內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
React??memo允許你的組件在?props?沒(méi)有改變的情況下跳過(guò)重新渲染的問(wèn)題記錄
使用?memo?將組件包裝起來(lái),以獲得該組件的一個(gè)?記憶化?版本,只要該組件的?props?沒(méi)有改變,這個(gè)記憶化版本就不會(huì)在其父組件重新渲染時(shí)重新渲染,這篇文章主要介紹了React??memo允許你的組件在?props?沒(méi)有改變的情況下跳過(guò)重新渲染,需要的朋友可以參考下2024-06-06
react-router-domV6版本的路由和嵌套路由寫(xiě)法詳解
本文主要介紹了react-router-domV6版本的路由和嵌套路由寫(xiě)法詳解,文中通過(guò)示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-03-03
react echarts tooltip 區(qū)域新加輸入框編輯保存數(shù)據(jù)功能
這篇文章主要介紹了react echarts tooltip 區(qū)域新加輸入框編輯保存數(shù)據(jù)功能,大概思路是用一個(gè)div包裹echarts, 然后在echarts的同級(jí)新建一個(gè)div用來(lái)用來(lái)模擬真實(shí)tooltip,通過(guò)鼠標(biāo)移入移出事件控制真實(shí)tooltip的顯示與隱藏,需要的朋友可以參考下2023-05-05

