- 測試用例搭建
- Router實現
- Router返回值
- hash值的初始化
- 監聽hash
- 緩存
- Route實現
- 路由的匹配
- 路由的傳參
- pathname、path、url三者的區別
- 路由的渲染
- 動態路由
- Link 組件
- MenuLink 組件
- 登錄驗證與重定向
- Redirect 組件
- Protected 組件
- Login 組件
- Switch組件
[TOC]
## 測試用例搭建
首先是入口文件,
```
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App.js';
ReactDOM.render(
<App/>
,window.root
);
```
我們只讓入口文件干一件事,即渲染真實DOM到掛載元素上。
注意即使只干這么一件事,`react`庫也是必須引入的,否則會報
```
'React' must be in scope when using JSX react/react-in-jsx-scope
```
接下來我們把路由和導航都統一放在`App`組件里,只求一目了然
```
import React from 'react';
import {HashRouter as Router,Route} from './react-router-dom'; //引入我們自己的router庫
export default class App extends React.Component{
render(){
return (
<Router>
<div className='container'>
<ul className='Nav'>
<li><Link to='/home'>首頁</Link></li>
<li><Link to='/user'>用戶</Link></li>
<li><Link to='/profile'>個人設置</Link></li>
</ul>
<div className='View'>
<Route path='/home' component={Home}/>
<Route path='/user'component={User}/>
<Route path='/profile' component={Profile}/>
</div>
</div>
</Router>
)
}
}
```
## Router實現
```
import React from 'react';
import PropTypes from 'prop-types';
export default class Router extends React.Component{
static childContextTypes = {
location:PropTypes.object
,history:PropTypes.object
}
constructor(props){
...
}
getChildContext(){
...
}
componentDidMount(){
...
}
render(){
...
}
}
```
### Router返回值
`Router`組件只是一個路由容器,它并不會生成一個div什么的,它會直接返回它包裹住的子元素們`children`,
```
render(){
return this.props.children; //注意children只是props下的一個屬性
}
```
需要注意的是Router依然遵循`JSX tag`嵌套時的**單一入口規則**,So如果有多個平級的子元素需要用一層div或則其它什么的給包起來。
```
<Router>
<div>
<Route ... />
<Route ... />
...
</div>
</Router>
```
### hash值的初始化
為了效仿原版(沒有hash值時,自動補上`/`)

首先我們在Router的構造函數中先創建一個location的狀態對象,并這對象中的`pathname`屬性賦一個初始值,這個pathname就是我們以后的hash值了(**除去#部分**)
```
constructor(props){
super(props);
this.state = {
location:{
pathname:window.location.hash.slice(1)||'/'
}
}
}
```
接著我們需要在Router組件掛載完畢時對`location.hash`進行賦值
```
window.location.hash = this.state.location.pathname;
```
這樣就完成了`/`的自動補全功能。
### 監聽hash
`Router`最重要的功能之一就是監聽hash值,一旦hash值發生改變,Router應該讓路由`Route`重新渲染。
那么,怎么才能讓路由組件重新匹配和渲染呢?
嗯,只需要調用`this.setState`即可,React中的setState方法只要被調用,就會重新渲染調用這個方法的組件,理所當然,也包括其子組件。
>[danger] **注意:** 雖然setState只要調用就會重新渲染,但有一種情況例外,則是setState()什么也不傳的時。而只要有傳參,哪怕是一個{},也會重啟渲染。
我們選在在組件渲染完畢時開啟監聽
```
componentDidMount(){
...
window.addEventListener('hashchange',()=>{
this.setState({location:{pathname:window.location.hash.slice(1)||'/'}});
});
}
```
>關于setState:
setState雖然有自動合并state的功能,但若這個state里還嵌套了一層,它是不會自動合并的,比如你有一個location的state,它長這樣`{location:{pathname:xxx,other:yyy}}`,然后你像這樣更新了一下state `{location:{pathname:xxx}}`,那么location中的other將不再保留,因為setState并不支持第二層嵌套的自動合并。
### 緩存
Router在監聽hash的時會實時的把hash值同步緩存在state上,這樣我們就不用在每一次的路由匹配中都重頭獲取這個hash值而只需要從Router中拿即可。
那么我們怎么在`route`中從一個父組件(router)中拿取東東呢?
React中提供了一個叫context的東東,在一個組件類中添加這個東東,就相當于開辟了一塊作用域,讓子孫組件能夠輕易的通過這個作用域拿到父組件共享出的屬性和方法,我稱之為React中的**任意門**。
這個門有**兩側**,一側在開通這個`context`的根組件(相對于其子孫組件的稱謂)這邊
```
// 申明父組件要在context作用域里放哪些東東
...
static childContextTypes = {
location:PropTypes.object
,history:PropTypes.object
};
// 定義要放這些東東的具體細節
getChildContext(){
return {
location:this.state.location
,history:{
push(path){
window.location.hash = path;
}
}
}
}
...
```
一側在要從根組件拿取東東的子孫組件這邊
>[danger] **注意:** 這里的的靜態屬性不再帶child字樣
```
...
// 和根組件相比 去除了child字樣
// 要用哪些東東就需要申明哪些東東
static contextTypes = {
location:propTypes.object
,history:propTypes.object
}
// 在聲明完要從根組件中拿取哪些東東后,可以在任意地方獲取到這些東東
fn(){
...
console.log(this.context.location);
}
...
```
## Route實現
從上一節中我們已經知道,Router組件最后返回的其實是它的children們,So,也就是一條條`Route`.
```
<Router>
<Route path='/a' component={myComponent1} />
<Route path='/b' component={myComponent2} />
<Route path='/c' component={myComponent3} />
</Router>
```
其中每一條`<Route .. />`都代表一次Route類的實例化,并且返回這個類中`render`函數所返回的東東。
我們通過將準備要渲染的組件作為屬性傳遞給`<Route ../>`組件,以求Route組件能幫我們控制住我們真正想要渲染的那些組件的渲染。(路由`Route`的角色就類似于編輯,需要對要渲染的內容進行審稿)
### 路由的匹配
實際中,我們**只有當url中的pathname和我們在Route中設置的path相匹配時**才會讓Route組件渲染我們傳遞給它的那些個真正想要渲染在頁面上的可視組件。
像這樣
```
...
// 接收根組件(Router)Context作用域中的 location 和 history
static contextTypes = {
location:propTypes.object
,history:propTypes.object
}
...
// class Route の render方法中
...
let {component:Component,path} = this.props;
let {location} = this.context;
let pathname = location.pathname;
if(path==pathname||pathname.startsWith(path)){
return <Component />
}else{
return null;
}
...
```
### 路由的傳參
當路由真正被匹配上時,會傳遞三個參數給真正要渲染的可視組件
```
// class Route の render方法中
....
...
static contextTypes = {
location:PropTypes.object
,history:PropTypes.object
}
...
let props = {
location
,history:this.context.history
,match:{}
};
...
if(path==pathname||pathname.startsWith(path)){
return <Component {...props}/>
...
```
如上所示,這三個參數屬性分別是:
- location:主要存放著當前實時pathname
- history:主要存放著各種跳轉路由的方法
- match:存放著url 和 給route指定的path 以及動態路由參數params對象
#### pathname、path、url三者的區別
`pathname`在hashrotuer中是指#后面那一串,是url的子集。
而`path`是我們給Route組件手動指定的匹配路徑,和pathname進行匹配的,但不一定等于pathname,有startsWith匹配。除此之外path還可能是一個`/user/:id`這樣的動態路由。
最后`url`,在react中它并不是我們的url地址,而是pathname經過path轉換成的正則匹配后的結果,它不一定等于path(因為還有動態路由)。

### 路由的渲染
React中路由的渲染有三種方式
1. component
```
<Route path=.. compoent={Component1}/>
```
這種就是最常見的,會根據路徑是否匹配決定是否渲染傳遞過來的組件。
2. render (多用于權限驗證)
```
<Route path=.. render={(props)=>{...}}>
```
采用render方式渲染時,組件是否渲染不僅要看路徑是否匹配,還要由render屬性所接受的函數來共同決定。
注意,此時render函數會接受一個參數`props`,即當前`Route`組件的props對象。
3. children (多用于菜單)
```
<Route path=.. children={(props)=>{...}}>
```
貌似和render沒區別,實則區別挺大!因為這貨不論路由的路徑是否匹配都會調用children這個回調函數。
So,分清楚了三種渲染方式的區別后,我們來大概寫下如何實現
```
// Routeのrender函數中
...
if(result){ //表示路由匹配得上
if(this.props.render){
return this.props.render(this.props);
}else{
return <Component {...this.props}/>
}
}else{
if(this.props.children){ //如果children存在,就算路徑沒有匹配也會調用
return this.props.children(this.props);
}else{
return null;
}
}
...
```
### 動態路由
要實現動態路由,需要我們將給`Route`設置的`/xxx/:xxx`們替換成正則用以匹配路徑,為了代碼的清晰我門使用`path-to-regexp`模塊對所有路由(包括非動態路由)都進行正則替換。
> path-to-regexp 模塊的實現在我的這篇文章中講過 [Express源碼級實現の路由全解析(下闋)](https://juejin.im/post/5aa88207f265da23870e84ba#heading-7)
而這一步需要在路由初始化的時候就完成
```
constructor(props){
super(props);
let {path} = props; //user/detail/:id
this.keys = [];
this.regexp = pathToRegexp(path,this.keys,{end:false}); //false表示只要開頭匹配即可
this.keys = this.keys.map(key=>key.name); //即是傳遞給渲染組件的match對象中的params對象
}
```
這樣路由規則就不會在每次render重繪時都進行一次計算
接下來我們需要在每次render中對路徑重新進行匹配
```
// render()中
...
let result = location.pathname.match(this.regexp);
...
```
如果匹配上了,有結果,還要準備一個params對象傳放進match對象中傳遞給渲染組件
```
if(result){
let [url,...values] = result;
props.match = {
url //匹配上的路徑(<=pathname)
,path //route上的path
,params:this.keys.reduce((memo,key,idx)=>{
memo[key] = values[idx];
return memo;
},{})
};
}
```
最后再判斷是根據三種渲染方式中的哪一種來渲染
```
if (result) {
...
if (render) {
return render(props);
} else if (Component) {
return <Component {...props}/>
} else if (children) {
return children(props);
}
return null;
} else {
if (children) {
return children(props);
} else {
return null;
}
}
```
## Link 組件
Link組件能讓我們通過點擊連接來達到切換顯示路由組件的效果
```
export default class xxx extends React.Component{
static contextTypes = {
history:PropTypes.object
};
render(){
return (
<a onClick={()=>this.context.history.push(this.props.to)}>
{this.props.children}
</a>
)
}
}
```
## MenuLink 組件
```
export default ({to,children})=>{
return <Route path={to} children={props=>(
<li className={props.match?"active":""}>
<Link to={to}>{children}</Link>
</li>
)}/>
}
```
```
<ul className='Nav'>
<MenuLink to='/home'>首頁</MenuLink>
<MenuLink to='/user'>用戶</MenuLink>
<MenuLink to='/profile'>詳情</MenuLink>
</ul>
```
這組件的作用即是讓匹配得上當前路由的link高亮
## 登錄驗證與重定向

在介紹三個相關組件之前需要對Router中存儲的push方法做出調整,以便保存Redirect跳轉前的路徑
```
...
push(path){
if(typeof path === 'object'){
let {pathname,state} = path;
that.setState({location:{...that.state.location,state}},()=>{
window.location.hash = pathname;
})
}else{
window.location.hash = path; //會自動添加'#'
}
}
...
```
### Redirect 組件
```
export default class xxx extends React.Component {
static contextTypes = {
history:PropTypes.object
}
componentWillMount() {
this.context.history.push(this.props.to);
}
render() {
return null;
}
}
```
### Protected 組件
```
export default function({component:Component,...rest}){
return <Route {...rest} render={props=>(
localStorage.getItem('login')?<Component {...props}/>:<Redirect to={{pathname:'/login',state:{from:props.location.pathname}}}/>
)}/>;
}
```
### Login 組件
```
import React from 'react';
export default class xxx extends React.Component{
handleClick=()=>{
localStorage.setItem('login',true);
this.props.history.push(this.props.location.state.from);
}
render(){
return (
<div>
<button onClick={this.handleClick} className="btn btn-primary">登錄</button>
</div>
)
}
}
```
## Switch組件
```
<Router>
<Route path='/a' component={myComponent1} />
<Route path='/b' component={myComponent2} />
<Route path='/c' component={myComponent3} />
</Router>
```
通常情況下我們這樣寫Route有一點不好的是,不管第一個路由匹配沒匹配上,Router都會接著往下匹配,這樣就增加運算量。
So,`Switch`組件就是為了解決這個問題
```
<Router>
<Switch>
<Route path='/a' component={myComponent1} />
<Route path='/b' component={myComponent2} />
<Route path='/c' component={myComponent3} />
</Switch>
</Router>
```
```
export default class xxx extends React.Component{
static contextTypes = {
location:PropTypes.object
}
render(){
console.log('Router render'); //只會打印一次
let {pathname} = this.context.location;
let children = this.props.children;
for(let i=0;i<children.length;++i){
let child = children[i]; //一個route
let {path} = child.props;
if(pathToRegexp(path,[],{end:false}).test(pathname)){
return child;
}
}
return null;
}
}
```
這樣只有一個Route會被**初始化以及渲染**。
但,有一個bug,我們上面寫`Route`時,是將path轉正則的部分放在constructor里的,這意味著只有在這個Route初始化的時候才會將path轉換為正則,這樣很好,只用計算一次,但和`Switch`搭配使用時就不好了,因為React的復用機制,即使路由路徑已經不一樣了,它仍然把上次的Route拿過來進行渲染,So此時的正則還是上一次的,也就不會被匹配上,嗯,bug。
解決方案:
- 第一種,給Route增加key
- 第二種,將正則替換的部分放在render中
## 獲取demo代碼
>倉庫地址:[點我~點我!](https://github.com/fancierpj0/i-react-router)
---
推薦:
- [react-router了解一下](https://juejin.im/post/5ac8c10551882555731c6247)
=== ToBeContinue ===
- 空白目錄
- 01.JSX,了解一下?
- JSX與虛擬DOM
- React
- 02.React文檔精讀(上)`
- React路由
- 關于BrowserRouter
- 關于Route
- 應用
- 權限認證
- case1
- context
- 新context
- 03.React路由
- 04.Diff
- 05.styled-components
- redux設計思想與API
- redux實現1
- 06.redux2
- 06.redux3
- 關于狀態初始化
- saga
- 新版
- 使用saga進行業務邏輯開發
- react-router-redux
- React性能優化
- immutable使用
- 未整理
- FAQ
- 常用中間件
- pureComponent
- 項目相關總結
- antd分尸
- 按需加載
- ReactWithoutJSX
- 我的組件庫
- C領域
- 用戶接口
- htmlType
- style
- show
- conjure
- grid
- inject
- stop
- 內部接口
- 衍生組件
- Button
- 報錯集錦
- ReactAPI
- 類上的那些屬性
- prop-types
- React.createElement
- React.cloneElement
- React.Children和props.children
- react元素和react組件關于作為children方面的那些問題
- react組件與虛擬dom
- ref