首先看一下react-router官網示例
const BasicExample = () => (
<Router>
<Route exact path="/" component={Home} />
<Route path="/about" component={About} />
<Route path="/topics" component={Topics} />
</div>
</Router>
);
複製程式碼
上面程式碼的執行效果點選這裡
本文的目的是講清楚react-router如何根據瀏覽器中的url來渲染不同的元件的,至於url是如何改變的(Link元件)請參見下一篇react-router原理之Link跳轉。
基礎依賴path-to-regexp
react-router提供了專門的路由匹配方法matchPath(位於packages/react-router/modules/matchPath.js),該方法背後依賴的其實是path-to-regexp包。
path-to-regexp輸入是路徑字串(也就是Route中定義的path的值),輸出包含兩部分
- 正規表示式(re)
- 一個陣列(keys)(用於記錄param的key資訊)
針對path中的引數(下例中的:bar)path-to-regexp在生成正則的時候會把它作為一個捕獲組進行定義,同時把引數的名字(bar)記錄到陣列keys中
var pathToRegexp = require('path-to-regexp')
var keys = []
var re = pathToRegexp('/foo/:bar', keys)
console.log(re);
console.log(keys);
// 輸出
/^\/foo\/([^\/]+?)(?:\/)?$/i
[ { name: 'bar',
prefix: '/',
delimiter: '/',
optional: false,
repeat: false,
partial: false,
pattern: '[^\\/]+?' } ]
複製程式碼
matchPath核心
matchPath方法首先通過path-to-regexp的方法來獲取Route上定義的path對應的正則,再將生成的正規表示式與url中的pathname做正則匹配判斷是否匹配。
console.log(re.exec('/foo/randal'));
console.log(re.exec('/foos/randal'));
// 輸出
[ '/foo/randal', 'randal', index: 0, input: '/foo/randal' ]
null
複製程式碼
由於path-to-regexp建立的正則中對param部分建立了捕獲組,同時把param的key記錄在了單獨的陣列keys中,因此通過遍歷正則匹配的結果和keys陣列即可將param的key和value進行關聯,如下所示:
const match = re.exec('/foo/randal');
const [url, ...values] = match;
const params = keys.reduce((memo, key, index) => {
memo[key.name] = values[index];
return memo;
}, {})
console.log(params) // {"bar": "randal"}
複製程式碼
最終matchPath針對未匹配的返回null,匹配成功的則返回一個object
return {
path, // /foo/:bar
url: // /foo/randal
isExact, // false
params: // {"bar": "randal"}
};
複製程式碼
Route渲染
Route元件維護一個state(match),match的值來自於matchPath的執行結果,如下所示
state = {
match: this.computeMatch(this.props, this.context.router)
};
computeMatch({ computedMatch, location, path, strict, exact, sensitive }, router) {
if (computedMatch) return computedMatch; // computedMatch留給Switch使用
const { route } = router;
const pathname = (location || route.location).pathname;
return matchPath(pathname, { path, strict, exact, sensitive }, route.match);
}
複製程式碼
當state.match不為null的時候Route才會建立關聯的component。
Route關聯component有多種形式(render、component、children) children定義形式與render和component的不同在於,children的執行與match無關,即使match為null,children函式也是會執行的,至於為什麼會有children這樣的設計呢,在接下來的一篇關於Link元件的文章中會提到。
render() {
const { match } = this.state;
const { children, component, render } = this.props;
const props = { match, ...};
if (component) return match ? React.createElement(component, props) : null;
if (render) return match ? render(props) : null;
if (typeof children === "function") return children(props);
return null;
}
複製程式碼
至此關於react-router如何根據url渲染不同Route的元件都講解完了,不過有時候只用Route的話還是會產生問題,比如:
<Route path="/about" component={About}/>
<Route path="/:user" component={User}/>
<Route component={NoMatch}/>
複製程式碼
如果當前訪問的url是/about的話,上面的寫法會在頁面上渲染About、User、NoMatch三個元件,其實我們希望的是隻渲染About元件。
Switch 路徑匹配前置
針對上面的問題,可以用Switch元件包裹一下
<Switch>
<Route path="/about" component={About}/>
<Route path="/:user" component={User}/>
<Route component={NoMatch}/>
</Switch>
複製程式碼
經過Switch包裹後, 如果訪問url是/about的話則只會渲染About元件了,如果url是/abouts的話,則只會渲染User元件。
Switch元件的特點是隻會從子children裡挑選一個Route渲染,為了實現只渲染一個的目的,Switch採用的是Route路徑匹配前置,不依賴Route的render方法來渲染元件,而是在Switch中就開始Route的路徑匹配,一旦發現一個匹配的路徑,則將其挑選出來進行渲染。Switch的關鍵程式碼如下
render() {
const { route } = this.context.router;
const { children } = this.props;
const location = this.props.location || route.location;
let match, child;
// 子children相當於只是選項,Switch負責從中挑選與當前url匹配的Route,被選中的子Route才會觸發render方法
React.Children.forEach(children, element => {
if (match == null && React.isValidElement(element)) {
const {
path: pathProp,
exact,
strict,
sensitive,
from
} = element.props;
const path = pathProp || from;
child = element;
match = matchPath(
location.pathname,
{ path, exact, strict, sensitive },
route.match
);
}
});
return match
? React.cloneElement(child, { location, computedMatch: match })
: null;
}
複製程式碼
上面程式碼把matchPath的執行結果match以computedMatch為key傳入到Route中了,這樣就避免了重複匹配,Route的computeMatch方法就可以直接複用了,computeMatch程式碼參見前面的Route渲染章節。