首先看一下react-router官网示例
const BasicExample = () => (
"/" component={Home} />
"/about" component={About} />
"/topics" component={Topics} />
首先看一下react-router官网示例
const BasicExample = () => (
"/" component={Home} />
"/about" component={About} />
"/topics" component={Topics} />
上面代码的运行效果点击这里
本文的目的是讲清楚react-router如何根据浏览器中的url来渲染不同的组件的,至于url是如何改变的(Link组件)请参见下一篇react-router原理之Link跳转。
react-router提供了专门的路由匹配方法matchPath(位于packages/react-router/modules/matchPath.js),该方法背后依赖的其实是path-to-regexp包。
path-to-regexp输入是路径字符串(也就是Route中定义的path的值),输出包含两部分
针对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方法首先通过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组件维护一个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的话还是会产生问题,比如:
"/about" component={About}/>
"/:user" component={User}/>
复制代码
如果当前访问的url是/about的话,上面的写法会在页面上渲染About、User、NoMatch三个组件,其实我们希望的是只渲染About组件。
针对上面的问题,可以用Switch组件包裹一下
"/about" component={About}/>
"/:user" component={User}/>
复制代码
经过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跳转