前言
本文是基于react-router的v5版本(v5.3.3),不适用最新的v6版本
参考文章:手写React-Router源码,深入理解其原理
概述
首先,简单概括一下Router
、Route
、Redirect
、Switch
的作用:
Router:创建一个
context
上下文对象,并注入history
、location
、match
等全局变量。BrowerRouter
、HashRouter
只是调用了history
不同的方法
Route:创建一个组件,当前路由与其path匹配时,返回对应的组件,否则返回null。
Redirect:创建一个组件,只要组件被挂载或更新时,就会执行重定向。注意,这个组件内部是不进行路由匹配的
Switch:
Switch
的作用是循环遍历子节点children
数组,依次和当前路由进行匹配,只要匹配到就不再进行匹配,返回匹配到的路由。
特别说明
1、Route
内部进行的路由匹配是独立的,也就是如果有多个Route
同时和当前路由匹配,会把所有匹配到的路由都渲染,Switch
的作用就是控制Route
只匹配一次。
2、Redirect
本身是不进行路由匹配的,所以需要依赖Switch的路由匹配逻辑。也就是说,使用Redirect
时必须使用Switch
作为父节点。
3、Switch
进行路由匹配时,遍历的子节点只是一级子节点,并不会去遍历孙节点,且遍历子节点的顺序是Route
和Redirect
在jsx
中从上到下的顺序。所以,Route
、Redirect
只能作为Switch
的一级子节点,如果有嵌套路由,每级路由都需要加上Switch
源码解析
了解了基本原理,我们结合源码解析一下
Router组件
代码语言:jsx复制class Router extends React.Component {
// 创建match对象
static computeRootMatch(pathname) {
return { path: "/", url: "/", params: {}, isExact: pathname === "/" };
}
constructor(props) {
super(props);
// ... 定义属性
}
componentDidMount() {
//... 做一些初始化
}
componentWillUnmount() {
//... 组件销毁重置属性值
}
render() {
return (
// 1、返回context上下文
<RouterContext.Provider
// 2、注入history、location、match等全局变量
value={{
history: this.props.history,
location: this.state.location,
match: Router.computeRootMatch(this.state.location.pathname),
staticContext: this.props.staticContext
}}
>
<HistoryContext.Provider
children={this.props.children || null}
value={this.props.history}
/>
</RouterContext.Provider>
);
}
}
可以看出,Router
最重要的作用就是注入了注入history
、location
、match
等全局变量,以便在所有组件中都可以使用
Route组件
代码语言:javascript复制class Route extends React.Component {
render() {
return (
<RouterContext.Consumer>
{context => {
// 1、<Route>必须在<Router>内部
invariant(context, "You should not use <Route> outside a <Router>");
const location = this.props.location || context.location;
// 2、通过matchPath方法将path值和当前路由进行匹配,如果<Switch>中已经匹配过,直接使用匹配结果
const match = this.props.computedMatch
? this.props.computedMatch // <Switch> already computed the match for us
: this.props.path
? matchPath(location.pathname, this.props)
: context.match;
const props = { ...context, location, match };
let { children, component, render } = this.props;
// ...
return (
<RouterContext.Provider value={props}>
{
// 3、如果匹配当前路由,就渲染children或component或render(),否则返回null
props.match
? children
? typeof children === "function"
? __DEV__
? evalChildrenDev(children, props, this.props.path)
: children(props)
: children
: component
? React.createElement(component, props)
: render
? render(props)
: null
: typeof children === "function"
? __DEV__
? evalChildrenDev(children, props, this.props.path)
: children(props)
: null}
</RouterContext.Provider>
);
}}
</RouterContext.Consumer>
);
}
}
注释2处的matchPath
是路由匹配的关键方法,Switch
也是使用该方法进行的匹配。我们来看一下它的实现
matchPath:路由匹配方法
代码语言:javascript复制// 1、可以看到,路由匹配使用的是path-to-regexp
import pathToRegexp from "path-to-regexp";
// ... 定义缓存变量
function compilePath(path, options) {
// ... 读取缓存
const keys = [];
// 2、使用pathToRegexp获取匹配正则表达式
const regexp = pathToRegexp(path, keys, options);
const result = { regexp, keys };
// ... 存入缓存
return result;
}
function matchPath(pathname, options = {}) {
// 将路径值统一放到options.path中
if (typeof options === "string" || Array.isArray(options)) {
options = { path: options };
}
const { path, exact = false, strict = false, sensitive = false } = options;
const paths = [].concat(path);
return paths.reduce((matched, path) => {
if (!path && path !== "") return null;
// 3、只要匹配到就直接返回匹配结果
if (matched) return matched;
// 4、获取路由匹配正则表达式
const { regexp, keys } = compilePath(path, {
end: exact,
strict,
sensitive
});
// 5、使用正则进行路由匹配
const match = regexp.exec(pathname);
// 6、没有匹配结果返回null
if (!match) return null;
const [url, ...values] = match;
const isExact = pathname === url;
// 7、如果设置了exact,进行全匹配
if (exact && !isExact) return null;
// 8、匹配结果
return {
path, // the path used to match
url: path === "/" && url === "" ? "/" : url, // the matched portion of the URL
isExact, // whether or not we matched exactly
params: keys.reduce((memo, key, index) => {
memo[key.name] = values[index];
return memo;
}, {})
};
}, null);
}
可以看到,路径匹配实际使用的是path-to-regexp
下面我们也可以看到,Redirect
中并没有路由匹配的逻辑。
Redirect组件
代码语言:javascript复制function Redirect({ computedMatch, to, push = false }) {
return (
<RouterContext.Consumer>
{context => {
// 1、<Redirect>必须在<Router>中
invariant(context, "You should not use <Redirect> outside a <Router>");
const { history, staticContext } = context;
// 2、重定向跳转的方式
const method = push ? history.push : history.replace;
// 3、使用to属性创建路由对象
const location = createLocation(
computedMatch
? typeof to === "string"
? generatePath(to, computedMatch.params)
: {
...to,
pathname: generatePath(to.pathname, computedMatch.params)
}
: to
);
// ... staticContext非空时的处理,可先忽略
return (
// 4、Lifecycle组件就是一个简单的class组件,组件挂载时回调onMount,更新时回调onUpdate
<Lifecycle
onMount={() => {
// 5、Redirect组件只要挂载时就会执行路由跳转方法
method(location);
}}
onUpdate={(self, prevProps) => {
// 6、需要更新的to路由信息
const prevLocation = createLocation(prevProps.to);
// 7、如果新的to路由和旧的to路由不相等,则进行重定向,避免死循环
if (
!locationsAreEqual(prevLocation, {
...location,
key: prevLocation.key
})
) {
// 8、执行路由跳转方法
method(location);
}
}}
to={to}
/>
);
}}
</RouterContext.Consumer>
);
}
通过注释5可见,Redirect
组件只要挂载就会进行重定向,所以需要通过Switch
进来路由匹配控制
Switch组件
代码语言:javascript复制class Switch extends React.Component {
render() {
return (
<RouterContext.Consumer>
{context => {
// 1、<Switch>必须在<Router>中
invariant(context, "You should not use <Switch> outside a <Router>");
const location = this.props.location || context.location;
let element, match;
// 2、遍历Switch的子节点children
React.Children.forEach(this.props.children, child => {
// 3、如果还没有匹配到结果
if (match == null && React.isValidElement(child)) {
element = child;
// 4、获取Route的path、或Redirect的from属性值
const path = child.props.path || child.props.from;
// 5、使用matchPath将子节点的路径和当前路径进行匹配
match = path
? matchPath(location.pathname, { ...child.props, path })
: context.match;
}
});
// 6、如果匹配到,返回对应的子节点;如果没有匹配到,返回null
return match
? React.cloneElement(element, { location, computedMatch: match })
: null;
}}
</RouterContext.Consumer>
);
}
}
从注释3处可以看到,Switch
通过macth
变量控制只要匹配到立即返回匹配到的路由。所以,需要注意Route
和Redirect
组件的顺序,特别是通过*
做404重定向时,必须将其他所有Route
和Redirect
组件放到*
路由之前
<Switch>
// ... 其他Route和Redirect组件必须放前面
<Redirect from="*" to="/" />
</Switch>