在網絡工程中,路由能保證信息從源地址傳輸到正確地目的地址,避免在互聯網中迷失方向。而前端應用中的路由,其功能與之類似,也是保證信息的準確性,只不過來源變成URL,目的地變成HTML頁面。
  在傳統的前端應用中,每個HTML頁面都會對應一條URL地址,當訪問某個頁面時,會先請求服務器,然后服務器根據發送過來的URL做出處理,再把響應內容回傳給瀏覽器,最終渲染整個頁面。這是典型的多頁面應用的訪問過程,由服務器控制頁面的路由,而其中最令人詬病的是整頁刷新,不僅存在著資源的浪費(像導航欄、側邊欄等通用部分不需要每次加載),并且讓用戶體驗也變得不再流暢。
  為了彌補多頁面應用的不足,有人提出了另一種網站模型:單頁面應用(Single Page Application,簡稱SPA)。SPA類似于一個桌面應用程序,能根據URL分配控制器(即由JavaScript負責路由),動態加載適當的內容到頁面中,減少與服務器之間的通信次數,不再因為頁面切換而打斷用戶體驗。雖然名稱中包含“單頁”兩字,但瀏覽器中的URL地址還是會發生改變,在視覺上與多頁面保持同步。而實現SPA的關鍵就是路由系統,在React的技術棧中,官方給出了支持的路由庫:React Router,后文將會著重分析該庫。
  當然,SPA也存在著自身的缺陷,例如不利于SEO、增加開發成本等,使用與否還是得看具體項目。
## 一、版本
  在2015年的11月,官方發布了React Router的第一個版本,實現了聲明式的路由。隨后在2016年,主版本號進行了兩次升級,一次是在2月的v2;另一次是在10月的v3。v3能夠兼容v2,刪除了一些會引起警告的棄用代碼,在未來只修復錯誤,所有的新功能都被添加到了2017年3月發布的v4版本中。
  v4不能兼容v3,在內部完全重寫,推崇組件式應用開發,放棄了之前的靜態路由而改成動態路由的設計思路。所謂靜態路由是指事先定義好一堆路由配置,在應用啟動時,再將其加載,從而構建出一張路由表,記錄URL和組件之間的映射關系。雖然v4版本精簡了許多API,降低了學習成本,但是增加了項目升級的難度。
  目前最新的版本已到v5,但官方團隊本來只是想發布v4.4版本。由于人為的操作失誤,導致不得不撤銷v4.4,直接改成v5,因此其API能完全兼容v4.x版本。React Router被拆分成了4個庫(包),如表3所列。
:-: 
:-: 表3 React Router的四個庫
  當運行在瀏覽器環境中時,只需要安裝react-router-dom即可。因為react-router-dom會依賴react-router,所以默認就能使用react-router提供的API。
  v5版本的React Router提供了三大類組件:路由器、路由和導航,將它們組合起來就能實現一套完整的路由系統,如圖11所示。首先根據URL導航到路由器中相應的路由,然后再渲染出指定的組件。
:-: 
圖11 路由系統
## 二、路由器
  Router是React Router提供的基礎路由器組件,一般不會直接使用。在瀏覽器運行環境中,通常引用的是封裝了Router的高級路由器組件:BrowserRouter或HashRouter。以BrowserRouter為例,其部分源碼如下所示。
~~~
class BrowserRouter extends React.Component {
history = createBrowserHistory(this.props);
render() {
return <Router history={this.history} children={this.props.children} />;
}
}
~~~
  在v4.x的版本中,路由器組件可以包裹任意類型的子元素,但數量只能是一個,而在v5.0版本中已經解除了這個限制。下面的BrowserRouter組件包含了兩個子元素,如果將其執行于v4.x中,那么將拋出錯誤。
~~~
<BrowserRouter>
<div>1</div>
<div>2</div>
</BrowserRouter>
~~~
**1)history**
  每個路由器組件都會創建一個history對象,由它來管理會話歷史。history不但會監聽URL的變化,還能將其解析成location對象,觸發路由的匹配和相應組件的渲染。
  history有三種形式,各自對應一種創建函數,應用于不同的路由器組件,具體如表4所示。其中MemoryRouter適用于非瀏覽器環境,例如React Native。
:-: 
:-: 表4 history的三種形式
  history會將瀏覽過的頁面組織成有序的堆棧,無論使用哪種history,其屬性和方法大部分都能保持一致。表5列出了history通用的API。
:-: 
:-: 表5 history的屬性和方法
~~~
{
key: "z4ihbf", //唯一標識
pathname: "/libs/d.html" //路徑和文件名
search: "?page=1", //查詢字符串
hash: "#form", //錨點
state: { //狀態對象
count: 10
}
}
~~~
**2)BrowserRouter**
  此組件會通過HTML5提供的History來保持頁面和URL的同步,其創建的URL格式如下所示。
~~~
http://pwstrick.com/page.html
~~~
  如果使用BrowserRouter組件,那么需要服務器配合部署。以上面的URL為例,當頁面刷新時,瀏覽器會向服務器請求根目錄下的page.html,但根本就沒有這個文件,于是頁面就會報404的錯誤。若要避免這種情況,就需要配置Web服務器軟件(例如Nginx、自建的Node服務器等),具體參數的配置可參考網上的資料。
  BrowserRouter組件包含5個屬性,接下來將一一講解。
  (1)basename屬性用于設置根目錄,URL的首部需要一個斜杠,而尾部則省略,例如“/pwstrick”,如下所示。
~~~
<BrowserRouter basename="/pwstrick" />
<Link to="/article" /> //渲染為<a href="/pwstrick/article">
~~~
  (2)forceRefresh是一個布爾屬性,只有當瀏覽器不支持HTML5的History時,才會設為true,從而可刷新整個頁面。
  (3)keyLength屬性是一個數字,表示location.key的長度。
  (4)children屬性保存著組件的子元素,這是所有的React組件都自帶的屬性。
  (5)getConfirmation屬性是一個確認函數,可攔截Prompt組件,注入自定義邏輯。以下面代碼為例,  當點擊鏈接企圖離開當前頁面時,會執行action()函數,彈出里面的確認框,其提示就是Prompt組件message屬性的值,只有點擊確定后才能進行跳轉(即導航)。
~~~
const action = (message, callback) => {
const allowTransition = window.confirm(message);
callback(allowTransition);
}
<BrowserRouter getUserConfirmation={action}>
<div>
<Prompt message="確認要離開嗎?" />
<Link to="page.html">首頁</Link>
</div>
</BrowserRouter>
~~~
**3)HashRouter**
  此組件會通過window.location.hash來保持頁面和URL的同步,其創建的URL格式比較特殊,需要包含井號(#),如下所示。
~~~
http://pwstrick.com/#/page.html
~~~
  在使用HashRouter時,不需要配置服務器。因為服務器會忽略錨點(即#/page.html),只會處理錨點之前的部分,所以刷新上面的URL也不會報404的錯誤。
  HashRouter組件包含4個屬性,其中3個與BrowserRouter組件相同,分別是basename、children和getUserConfirmation。獨有的hashType屬性用來設置hash類型,有三個關鍵字可供選擇,如下所列。
  (1)slash:默認值,井號后面跟一個斜杠,例如“#/page”。
  (2)noslash:井號后面沒有斜杠,例如“#page”。
  (3)hashbang:采用Google風格,井號后面跟感嘆號和斜杠,例如“#!/page”。
## 三、路由
  Route是一個配置路由信息的組件,其職責是當頁面的URL能匹配Route組件的path屬性時,就渲染出對應的組件,而渲染方式有三種。接下來會講解Route組件的屬性、渲染方式以及其它的相關概念。
**1)路徑**
  與路徑相關的屬性有3個,分別是path、exact和strict,接下來會一一講解。
  (1)path是一個記錄路由匹配路徑的屬性,當路由器是BrowserRouter時,path會匹配location中的pathname屬性;而當路由器是HashRouter時,path會匹配location中的hash屬性。
  path屬性的值既可以是普通字符串,也可以是能被path-to-regexp解析的正則表達式。下面是一個示例,如果沒有特殊說明,默認使用的路由器是BrowserRouter。
~~~
<Route path="/main" component={Main} />
<Route path="/list/:page+" component={List} />
~~~
  第一個Route組件能匹配“/main”或以“/main”為前綴的pathname屬性,下面兩條URL能正確匹配。
~~~
http://www.pwstrick.com/main
http://www.pwstrick.com/main/article
~~~
  第二個Route組件能匹配以“/list”為前綴的pathname屬性,下面兩條URL只能匹配第二條。
~~~
http://www.pwstrick.com/list
http://www.pwstrick.com/list/1
~~~
  React Router內部依賴了path-to-regexp庫,此庫定義了一套正則語法,例如命名參數、修飾符(\*、+或?)等,具體規則可參考[官方文檔](https://github.com/pillarjs/path-to-regexp),本文不做展開。
  在“/list/:page+”中,帶冒號前綴的“:page”是命名參數,類似于一個函數的形參,可以傳遞任何值;正則末尾的加號要求至少匹配一個命名參數,沒有命名參數就匹配失敗。
  注意,如果省略path屬性,那么路由將總是匹配成功。
  (2)exact是一個布爾屬性,當設為true時,路徑要與pathname屬性完全匹配,如表6所示。
:-: 
:-: 表6 exact屬性匹配說明
  (3)strict也是一個布爾屬性,當設為true時,路徑末尾如果有斜杠,那么pathname屬性匹配到的部分也得包含斜杠。在表7的第三行中,雖然pathname屬性的末尾沒有斜杠,但是依然能正確匹配。
:-: 
:-: 表7 strict屬性匹配說明
  如果將strict和exact同時設為true,那么就可強制pathname屬性的末尾不能包含斜杠。例如pathname屬性的值為“/main/”,路徑為“/main”,此時匹配會失敗。
**2)渲染方式**
  Route組件提供了3個用來渲染組件的屬性:component、render和children,每個屬性對應一種渲染方式,每種方式傳遞的props都會包含3個路由屬性:match、location和history。
  (1)component屬性的值是一個組件(如下代碼所示),當路由匹配成功時,會創建一個新的React元素(調用了React.createElement()方法)。
~~~
<Route path="/name" component={Name} />
~~~
  如果組件以內聯函數的方式傳給component屬性,那么會產生不必要的重新掛載。對于內聯渲染,可以用render屬性替換。
  (2)render屬性的值是一個返回React元素的內聯函數,當路由匹配成功時,會調用這個函數,此時可以傳遞額外的參數進來,如下代碼所示。由于React元素不會被反復創建,因此不會出現重新掛載的情況。
~~~
<Route path="/name" render={(props) => {
return <Name {...props} age="30">Strick</Name>
}}/>
~~~
  (3)children屬性的值也是一個返回React元素的內聯函數,它的一大特點是無論路由是否匹配成功,這個函數都會被調用,該屬性的工作方式與render屬性基本一致。注意,當匹配不成功時,props的match屬性的值為null。
  不要將3個渲染屬性應用于同一個Route組件,因為三種渲染方式有先后順序,component的優先級最高,其次是render,最后是children。
  三個路由屬性除了match之外,另外兩個location和history已在前文做過講解,接下來將重點分析match屬性。
  Route會將路由匹配后的信息記錄到match對象中,然后將此對象作為props的match屬性傳遞給被渲染的組件。match對象包含4個屬性,在表8中,不僅描述了各個屬性的作用,還在第三列記錄了點擊read鏈接后,各個屬性被賦的值。
~~~
<Link to="/list/article/1">read</Link>
<Route path="/list/:type" component={Name} />
~~~
:-: 
:-: 表8 match對象的屬性
**3)Switch**
  如果將一堆Route組件放在一起(如下代碼所示),那么會對每個Route組件依次進行路由匹配,例如當前pathname的屬性值是“/age”,那么被渲染的組件是Age1和Age3。
~~~
<Route path='/' component={Age1} />
<Route path='/article' component={Age2} />
<Route path='/:list' component={Age3} />
~~~
  而如果將這三個Route用Switch組件包裹(如下代碼所示),那么只會對第一個路徑匹配的組件進行渲染。
~~~
<Switch>
<Route path='/' component={Age1} />
<Route path='/article' component={Age2} />
<Route path='/:list' component={Age3} />
</Switch>
~~~
  Switch的子元素既可以是Route,也可以是Redirect。其中Route元素匹配的是path屬性,而Redirect元素匹配的是from屬性。
**4)嵌套路由**
  從v4版本開始,嵌套路由不再通過多個Route組件相互嵌套實現,而是在被渲染的組件中聲明另外的Route組件,以這種方式實現嵌套路由。下面用一個例子來演示嵌套路由,首先用Switch組件包裹兩個Route組件,第一個只有當處在根目錄時才會渲染Main組件,第二個路徑匹配成功渲染的是Children組件。
~~~
<Switch>
<Route exact path='/' component={Main} />
<Route path='/list/:article' component={Children} />
</Switch>
~~~
  然后定義Children組件,它也包含一個Route組件,從而形成了嵌套路由。注意,其路徑讀取了match對象的path屬性,通過沿用父路由中要匹配的路徑,可減少許多重復代碼。
~~~
let Children = (props) => {
return <Route path={`${props.match.path}/:id`} component={Article} />;
};
let Article = (props) => {
return <h5>文章內容</h5>;
};
~~~
  當pathname的屬性值是“/list/article/1”時,就能成功渲染出Article組件。
## 四、導航
  當需要在頁面之間進行切換時,就該輪到Link、NavLink和Redirect三個導航組件登場了。其中Link和NavLink組件最終會被解析成HTML中的元素。
**1)Link**
  當點擊Link組件時會渲染匹配路由中的組件,并且能在更新URL時,不重載頁面。它有兩個屬性:to和replace,其中to屬性用于定義導航地址,其值的類型既可以是字符串,也可以是location對象(包含pathname、search等屬性),如下所示。
~~~
<Link to="/main">字符串</Link>
<Link to={{pathname: "/main", search: "?type=1"}}>對象</Link>
~~~
  replace是一個布爾屬性,默認值為false,當設為true時,能用新地址替換掉會話歷史里的原地址。
**2)NavLink**
  它是一個封裝了的Link組件,其功能包括定義路徑匹配成功后的樣式、限制匹配規則、優化無障礙閱讀等,接下來將依次講解多出的屬性。
  首先是activeClassName和activeStyle,兩個屬性都會在路徑匹配成功時,賦予元素樣式(如下代碼所示)。其中前者定義的是CSS類,默認值為“active”;后者定義的是內聯樣式,書寫規則可參照React元素的style屬性。
~~~
<style>
.btn {
color: blue;
}
</style>
<NavLink to="/list" activeClassName="btn">CSS類</NavLink>
<NavLink to="/list" activeStyle={{color: "blue"}}>內聯樣式</NavLink>
~~~
  然后是exact和strict,兩個布爾屬性的功能可分別參考Route元素的exact和strict,它們的用法相同。如果將exact和strict設為true(如下代碼所示),那么匹配規則會改變,其中前者要路徑完全匹配,后者得符合strict的路徑匹配規則。只有當匹配成功時,才能將activeClassName或activeStyle屬性的值賦予元素。
~~~
<NavLink to="/list" exact>完全</NavLink>
<NavLink to="/list" strict>斜杠</NavLink>
~~~
  接著是函數類型的isActive屬性,此函數能接收2個對象參數:match和location,返回一個布爾值。在函數體中可添加路徑匹配時的額外邏輯,當返回值是true時,才能賦予元素定義的匹配樣式。注意,無論匹配是否成功,isActive屬性中的函數都會被回調一次,因此如果要使用match參數,那么需要做空值判斷(如下代碼所示),以免出錯。
~~~
let fn = (match, location) => {
if (!match) {
return false
}
return match.url.indexOf("article") >= 0;
};
<NavLink to="/list" isActive={fn}>函數</NavLink>
~~~
  最后是兩個特殊功能的屬性:location和aria-current,前者是一個用于比對的location對象;后者是一個為存在視覺障礙的用戶服務的ARIA屬性,用于標記屏幕閱讀器可識別的導航類型,例如頁面、日期、位置等。可供選擇的關鍵字包括page、step、location、date、time和true,默認值為page。
**3)Redirect**
  此組件用于導航到一個新地址,類似于服務端的重定向(HTTP的狀態碼為3XX),其屬性如表9所示。
:-: 
:-: 表9 Redirect元素的屬性
  Redirect可與Switch搭配使用,如下代碼所示,當URL與“/main”匹配時,重定向到“/page”,并渲染Page組件。
~~~
<Switch>
<Redirect from="/main" to="/page" />
<Route path="/page" component={Page} />
</Switch>
~~~
## 五、集成Redux
   [第11篇](https://www.cnblogs.com/strick/p/10775503.html)中對Redux做過詳細講解,本節將通過一個示例分三步來描述React Router集成Redux的過程,第一步是創建Redux的三個組成部分:Action、Reducer和Store,如下所示。
~~~
function caculate(previousState = {digit: 0}, action) { //Reducer
let state = Object.assign({}, previousState);
switch (action.type) {
case "ADD":
state.digit += 1;
break;
case "MINUS":
state.digit -= 1;
}
return state;
}
function add() { //Action創建函數
return {type: "ADD"};
}
let store = createStore(caculate); //Store
~~~
**1)withRouter**
  在說明第二步之前,需要先了解一下React Router提供的一個高階組件:withRouter。它能將history、location和match三個路由對象傳遞給被包裝的組件,其中match對象來自于離它最近的父級Route組件的match屬性。
  正常情況下,只有Route要渲染的組件(例如下面的List)會自帶這三個對象,但如果List組件還有一個子組件,那么這個子組件就無法自動獲取到這三個對象了,除非顯式地傳遞。
~~~
<Route path="/" component={List} />
~~~
  在使用withRouter后,就能避免逐級傳遞。并且當把withRouter應用于react-redux庫中的connect()函數后(如下代碼所示),就能讓函數返回的容器組件監聽到路由的變化。
~~~
withRouter(connect(...)(MyComponent))
~~~
**2)路由**
  第二步就是創建路由,并自定義三個組件:Btn、List和Article。在Btn組件中聲明了Link和Route兩個組件,其中路由匹配成功后會渲染List組件;在List組件中聲明了WithArticle組件,而WithArticle就是通過withRouter包裝后的Article組件。
~~~
class Btn extends React.Component {
render() {
return (
<div>
<Link to="/list">列表</Link>
<Route path="/list" component={List} />
<button onClick={this.props.add}>提交</button>
</div>
);
}
}
let List = (props) => {
return <WithArticle content="內容"/>;
};
let Article = (props) => {
const { match, location, history } = props;
return <h5>{props.content}</h5>;
};
let WithArticle = withRouter(Article); //withRouter包裝后的Article組件
~~~
**3)渲染**
  第三步就是用react-redux庫中的Provider組件包裹BrowserRouter組件(即連接路由器),并注入Store,最后將眾組件渲染到頁面中。
~~~
let Smart = connect(state => state, { add })(Btn); //容器組件
let Router = <Provider store={store}>
<BrowserRouter>
<Smart />
</BrowserRouter>
</Provider>;
ReactDOM.render(Router, document.getElementById("container"));
~~~
*****
> 原文出處:
[博客園-React躬行記](https://www.cnblogs.com/strick/category/1455720.html)
[知乎專欄-React躬行記](https://zhuanlan.zhihu.com/pwreact)
已建立一個微信前端交流群,如要進群,請先加微信號freedom20180706或掃描下面的二維碼,請求中需注明“看云加群”,在通過請求后就會把你拉進來。還搜集整理了一套[面試資料](https://github.com/pwstrick/daily),歡迎瀏覽。

推薦一款前端監控腳本:[shin-monitor](https://github.com/pwstrick/shin-monitor),不僅能監控前端的錯誤、通信、打印等行為,還能計算各類性能參數,包括 FMP、LCP、FP 等。
- ES6
- 1、let和const
- 2、擴展運算符和剩余參數
- 3、解構
- 4、模板字面量
- 5、對象字面量的擴展
- 6、Symbol
- 7、代碼模塊化
- 8、數字
- 9、字符串
- 10、正則表達式
- 11、對象
- 12、數組
- 13、類型化數組
- 14、函數
- 15、箭頭函數和尾調用優化
- 16、Set
- 17、Map
- 18、迭代器
- 19、生成器
- 20、類
- 21、類的繼承
- 22、Promise
- 23、Promise的靜態方法和應用
- 24、代理和反射
- HTML
- 1、SVG
- 2、WebRTC基礎實踐
- 3、WebRTC視頻通話
- 4、Web音視頻基礎
- CSS進階
- 1、CSS基礎拾遺
- 2、偽類和偽元素
- 3、CSS屬性拾遺
- 4、浮動形狀
- 5、漸變
- 6、濾鏡
- 7、合成
- 8、裁剪和遮罩
- 9、網格布局
- 10、CSS方法論
- 11、管理后臺響應式改造
- React
- 1、函數式編程
- 2、JSX
- 3、組件
- 4、生命周期
- 5、React和DOM
- 6、事件
- 7、表單
- 8、樣式
- 9、組件通信
- 10、高階組件
- 11、Redux基礎
- 12、Redux中間件
- 13、React Router
- 14、測試框架
- 15、React Hooks
- 16、React源碼分析
- 利器
- 1、npm
- 2、Babel
- 3、webpack基礎
- 4、webpack進階
- 5、Git
- 6、Fiddler
- 7、自制腳手架
- 8、VSCode插件研發
- 9、WebView中的頁面調試方法
- Vue.js
- 1、數據綁定
- 2、指令
- 3、樣式和表單
- 4、組件
- 5、組件通信
- 6、內容分發
- 7、渲染函數和JSX
- 8、Vue Router
- 9、Vuex
- TypeScript
- 1、數據類型
- 2、接口
- 3、類
- 4、泛型
- 5、類型兼容性
- 6、高級類型
- 7、命名空間
- 8、裝飾器
- Node.js
- 1、Buffer、流和EventEmitter
- 2、文件系統和網絡
- 3、命令行工具
- 4、自建前端監控系統
- 5、定時任務的調試
- 6、自制短鏈系統
- 7、定時任務的進化史
- 8、通用接口
- 9、微前端實踐
- 10、接口日志查詢
- 11、E2E測試
- 12、BFF
- 13、MySQL歸檔
- 14、壓力測試
- 15、活動規則引擎
- 16、活動配置化
- 17、UmiJS版本升級
- 18、半吊子的可視化搭建系統
- 19、KOA源碼分析(上)
- 20、KOA源碼分析(下)
- 21、花10分鐘入門Node.js
- 22、Node環境升級日志
- 23、Worker threads
- 24、低代碼
- 25、Web自動化測試
- 26、接口攔截和頁面回放實驗
- 27、接口管理
- 28、Cypress自動化測試實踐
- 29、基于Electron的開播助手
- Node.js精進
- 1、模塊化
- 2、異步編程
- 3、流
- 4、事件觸發器
- 5、HTTP
- 6、文件
- 7、日志
- 8、錯誤處理
- 9、性能監控(上)
- 10、性能監控(下)
- 11、Socket.IO
- 12、ElasticSearch
- 監控系統
- 1、SDK
- 2、存儲和分析
- 3、性能監控
- 4、內存泄漏
- 5、小程序
- 6、較長的白屏時間
- 7、頁面奔潰
- 8、shin-monitor源碼分析
- 前端性能精進
- 1、優化方法論之測量
- 2、優化方法論之分析
- 3、瀏覽器之圖像
- 4、瀏覽器之呈現
- 5、瀏覽器之JavaScript
- 6、網絡
- 7、構建
- 前端體驗優化
- 1、概述
- 2、基建
- 3、后端
- 4、數據
- 5、后臺
- Web優化
- 1、CSS優化
- 2、JavaScript優化
- 3、圖像和網絡
- 4、用戶體驗和工具
- 5、網站優化
- 6、優化閉環實踐
- 數據結構與算法
- 1、鏈表
- 2、棧、隊列、散列表和位運算
- 3、二叉樹
- 4、二分查找
- 5、回溯算法
- 6、貪心算法
- 7、分治算法
- 8、動態規劃
- 程序員之路
- 大學
- 2011年
- 2012年
- 2013年
- 2014年
- 項目反思
- 前端基礎學習分享
- 2015年
- 再一次項目反思
- 然并卵
- PC網站CSS分享
- 2016年
- 制造自己的榫卯
- PrimusUI
- 2017年
- 工匠精神
- 2018年
- 2019年
- 前端學習之路分享
- 2020年
- 2021年
- 2022年
- 2023年
- 2024年
- 日志
- 2020