react-router原理之路径匹配

10,009 阅读4分钟

首先看一下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跳转