react-router原理之路徑匹配

Randal發表於2018-05-23

首先看一下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渲染章節。

進入下一篇react-router原理之Link跳轉

相關文章