原文地址 http://blog.poetries.top/2018/12/20/react-router-anaylse/
一、React Router基礎(chǔ)之history
1.1 History介紹
history是一個(gè)獨(dú)立的第三方j(luò)s庫(kù),可以用來(lái)兼容在不同瀏覽器、不同環(huán)境下對(duì)歷史記錄的管理,擁有統(tǒng)一的API。具體來(lái)說里面的history分為三類
- 老瀏覽器的
history: 主要通過hash來(lái)實(shí)現(xiàn),對(duì)應(yīng)createHashHistory - 高版本瀏覽器: 通過
html5里面的history,對(duì)應(yīng)createBrowserHistory -
node環(huán)境下: 主要存儲(chǔ)在memeory里面,對(duì)應(yīng)createMemoryHistory
上面針對(duì)不同的環(huán)境提供了三個(gè)
API,但是三個(gè)API有一些共性的操作,將其抽象了一個(gè)公共的文件createHistory
// 內(nèi)部的抽象實(shí)現(xiàn)
function createHistory(options={}) {
...
return {
listenBefore, // 內(nèi)部的hook機(jī)制,可以在location發(fā)生變化前執(zhí)行某些行為,AOP的實(shí)現(xiàn)
listen, // location發(fā)生改變時(shí)觸發(fā)回調(diào)
transitionTo, // 執(zhí)行l(wèi)ocation的改變
push, // 改變location
replace,
go,
goBack,
goForward,
createKey, // 創(chuàng)建location的key,用于唯一標(biāo)示該location,是隨機(jī)生成的
createPath,
createHref,
createLocation, // 創(chuàng)建location
}
}
上述這些方式是
history內(nèi)部最基礎(chǔ)的方法,createHashHistory、createBrowserHistory、createMemoryHistory只是覆蓋其中的某些方法而已。其中需要注意的是,此時(shí)的location跟瀏覽器原生的location是不相同的,最大的區(qū)別就在于里面多了key字段,history內(nèi)部通過key來(lái)進(jìn)行location的操作
function createLocation() {
return {
pathname, // url的基本路徑
search, // 查詢字段
hash, // url中的hash值
state, // url對(duì)應(yīng)的state字段
action, // 分為 push、replace、pop三種
key // 生成方法為: Math.random().toString(36).substr(2, length)
}
}
1.2 內(nèi)部解析
三個(gè)
API的大致的技術(shù)實(shí)現(xiàn)如下
-
createBrowserHistory: 利用HTML5里面的history -
createHashHistory: 通過hash來(lái)存儲(chǔ)在不同狀態(tài)下的history信息 -
createMemoryHistory: 在內(nèi)存中進(jìn)行歷史記錄的存儲(chǔ)`
1.2.1 執(zhí)行URL前進(jìn)
-
createBrowserHistory:pushState、replaceState -
createHashHistory:location.hash=*** location.replace() -
createMemoryHistory: 在內(nèi)存中進(jìn)行歷史記錄的存儲(chǔ)
// 偽代碼
// createBrowserHistory(HTML5)中的前進(jìn)實(shí)現(xiàn)
function finishTransition(location) {
...
const historyState = { key };
...
if (location.action === 'PUSH') ) {
window.history.pushState(historyState, null, path);
} else {
window.history.replaceState(historyState, null, path)
}
}
// createHashHistory的內(nèi)部實(shí)現(xiàn)
function finishTransition(location) {
...
if (location.action === 'PUSH') ) {
window.location.hash = path;
} else {
window.location.replace(
window.location.pathname + window.location.search + '#' + path
);
}
}
// createMemoryHistory的內(nèi)部實(shí)現(xiàn)
entries = [];
function finishTransition(location) {
...
switch (location.action) {
case 'PUSH':
entries.push(location);
break;
case 'REPLACE':
entries[current] = location;
break;
}
1.2.2 檢測(cè)URL回退
-
createBrowserHistory:popstate -
createHashHistory:hashchange -
createMemoryHistory:因?yàn)槭窃趦?nèi)存中操作,跟瀏覽器沒有關(guān)系,不涉及UI層面的事情,所以可以直接進(jìn)行歷史信息的回退
// 偽代碼
// createBrowserHistory(HTML5)中的后退檢測(cè)
function startPopStateListener({ transitionTo }) {
function popStateListener(event) {
...
transitionTo( getCurrentLocation(event.state) );
}
addEventListener(window, 'popstate', popStateListener);
...
}
// createHashHistory的后退檢測(cè)
function startPopStateListener({ transitionTo }) {
function hashChangeListener(event) {
...
transitionTo( getCurrentLocation(event.state) );
}
addEventListener(window, 'hashchange', hashChangeListener);
...
}
// createMemoryHistory的內(nèi)部實(shí)現(xiàn)
function go(n) {
if (n) {
...
current += n;
const currentLocation = getCurrentLocation();
// change action to POP
history.transitionTo({ ...currentLocation, action: POP });
}
}
1.2.3 state的存儲(chǔ)
為了維護(hù)
state的狀態(tài),將其存儲(chǔ)在sessionStorage里面:
// createBrowserHistory/createHashHistory中state的存儲(chǔ)
function saveState(key, state) {
...
window.sessionStorage.setItem(createKey(key), JSON.stringify(state));
}
function readState(key) {
...
json = window.sessionStorage.getItem(createKey(key));
return JSON.parse(json);
}
// createMemoryHistory僅僅在內(nèi)存中,所以操作比較簡(jiǎn)單
const storage = createStateStorage(entries); // storage = {entry.key: entry.state}
function saveState(key, state) {
storage[key] = state
}
function readState(key) {
return storage[key]
}
1.3 小結(jié)
路由原理
前端路由實(shí)現(xiàn)起來(lái)其實(shí)很簡(jiǎn)單,本質(zhì)就是監(jiān)聽
URL的變化,然后匹配路由規(guī)則,顯示相應(yīng)的頁(yè)面,并且無(wú)須刷新。目前單頁(yè)面使用的路由就只有兩種實(shí)現(xiàn)方式
-
hash模式 -
history模式
www.test.com/##/就是Hash URL,當(dāng)##后面的哈希值發(fā)生變化時(shí),不會(huì)向服務(wù)器請(qǐng)求數(shù)據(jù),可以通過hashchange事件來(lái)監(jiān)聽到URL的變化,從而進(jìn)行跳轉(zhuǎn)頁(yè)面。
History模式是HTML5新推出的功能,比之Hash URL更加美觀
二、react-router的基本原理
實(shí)現(xiàn)
URL與UI界面的同步。其中在react-router中,URL對(duì)應(yīng)Location對(duì)象,而UI是由react components來(lái)決定的,這樣就轉(zhuǎn)變成location與components之間的同步問題

2.1 優(yōu)點(diǎn)
- 與
React融為一體,專為react量身打造,編碼風(fēng)格與react保持一致,例如路由的配置可以通過component來(lái)實(shí)現(xiàn) - 不需要手工維護(hù)路由
state,使代碼變得簡(jiǎn)單 - 強(qiáng)大的路由管理機(jī)制,體現(xiàn)在如下方面
- 路由配置: 可以通過組件、配置對(duì)象來(lái)進(jìn)行路由的配置
- 路由切換: 可以通過
<Link>Redirect進(jìn)行路由的切換 - 路由加載: 可以同步記載,也可以異步加載,這樣就可以實(shí)現(xiàn)按需加載
- 使用方式: 不僅可以在瀏覽器端的使用,而且可以在服務(wù)器端的使用
2.2 react-router具體實(shí)現(xiàn)
react-router在history庫(kù)的基礎(chǔ)上,實(shí)現(xiàn)了URL與UI的同步,分為兩個(gè)層次來(lái)描述具體的實(shí)現(xiàn)。
組件層面描述實(shí)現(xiàn)過程
在
react-router中最主要的component是Router RouterContext Link,history庫(kù)起到了中間橋梁的作用

以
browserHistory(一種history類型:一個(gè)history知道如何去監(jiān)聽瀏覽器地址欄的變化, 并解析這個(gè)URL轉(zhuǎn)化為location對(duì)象)為例 :
-
browserHistory進(jìn)行路由state管理,主要通過sessionStorage
//保存 路由state(router state)
function saveState(key, state) {
...
window.sessionStorage.setItem(createKey(key), JSON.stringify(state));
}
//讀取路由state
function readState(key) {
...
json = window.sessionStorage.getItem(createKey(key));
return JSON.parse(json);
}
其中
saveState函數(shù)傳進(jìn)來(lái)的state是個(gè)json對(duì)象,如:
{route: '/about'} ///假設(shè)此時(shí)的location為'/about'
進(jìn)行路由匹配,最終渲染對(duì)應(yīng)的組件
const About = React.createClass({/*...*/}) //About 組件
const Inbox = React.createClass({/*...*/}) //Inbox 組件
const Home = React.createClass({/*...*/}) //Home組件
render() {
let Child
switch (this.state.route) {
case '/about': Child = About; break;
case '/inbox': Child = Inbox; break;
default: Child = Home;
}
return (
<div>
<h1>App</h1>
<ul>
<li><a href="#/about">About</a></li>
<li><a href="#/inbox">Inbox</a></li>
</ul>
<Child/>
</div>
)
}
})
React.render(<App />, document.body)
API層面描述實(shí)現(xiàn)過程
為了簡(jiǎn)單說明,只描述使用
browserHistory的實(shí)現(xiàn),hashHistory的實(shí)現(xiàn)過程是類似的,就不在說明

2.3 用戶點(diǎn)擊了Link組件后路由系統(tǒng)中到底發(fā)生了哪些變化

Link組件最終會(huì)渲染為HTML標(biāo)簽<a>,它的to、query、hash屬性會(huì)被組合在一起并渲染為href屬性。雖然Link被渲染為超鏈接,但在內(nèi)部實(shí)現(xiàn)上使用腳本攔截了瀏覽器的默認(rèn)行為,然后調(diào)用了history.pushState方法
- 系統(tǒng)會(huì)將上述
location對(duì)象作為參數(shù)傳入到TransitionTo方法中,然后調(diào)用window.location.hash或者window.history.pushState()修改了應(yīng)用的URL,這取決于你創(chuàng)建history對(duì)象的方式。同時(shí)會(huì)觸發(fā)history.listen中注冊(cè)的事件監(jiān)聽器。 - 接下來(lái)請(qǐng)看路由系統(tǒng)內(nèi)部是如何修改
UI的。在得到了新的location對(duì)象后,系統(tǒng)內(nèi)部的matchRoutes方法會(huì)匹配出Route組件樹中與當(dāng)前location對(duì)象匹配的一個(gè)子集,并且得到了nextState,具體的匹配算法不在這里講解,感興趣的同學(xué)可以點(diǎn)擊查看,state的結(jié)構(gòu)如下
nextState = {
location, // 當(dāng)前的 location 對(duì)象
routes, // 與 location 對(duì)象匹配的 Route 樹的子集,是一個(gè)數(shù)組
params, // 傳入的 param,即 URL 中的參數(shù)
components, // routes 中每個(gè)元素對(duì)應(yīng)的組件,同樣是數(shù)組
};
在
Router組件的componentWillMount生命周期方法中調(diào)用了history.listen(listener)方法。listener會(huì)在上述matchRoutes方法執(zhí)行成功后執(zhí)行listener(nextState),nextState對(duì)象每個(gè)屬性的具體含義已經(jīng)在上述代碼中注釋,接下來(lái)執(zhí)行this.setState(nextState)就可以實(shí)現(xiàn)重新渲染Router組件。舉個(gè)簡(jiǎn)單的例子,當(dāng) URL(準(zhǔn)確的說應(yīng)該是location.pathname) 為/archives/posts時(shí),應(yīng)用的匹配結(jié)果如下圖所示

到這里,系統(tǒng)已經(jīng)完成了當(dāng)用戶點(diǎn)擊一個(gè)由
Link組件渲染出的超鏈接到頁(yè)面刷新的全過程