一·、前言
在一次业务开发里,我遇到这样一个场景:有一个功能要分批上线,即在某个时间段内,一部分人用老功能,一部分人用新功能。刚接到这个需求的时候,我一时间没反应过来,甚至没想到要用路由来实现,到后面看到了 Mentor 的实现方式,真正有种重剑无锋,大巧不工的感觉,原来 React-Router 还可以这么玩。
于是我痛定思痛,决定跳出原来 CV 配置,改下 path、component就收工的粗糙玩法,对React-Router 源码做些深入了解。
观前提醒😋,本文是读书笔记,所以对源码解析做的注释更详尽,但想要系统学习,建议按照参考文献顺序学习。
二、React-Router 原理
-
两位主角
- 🧑🔬
history:监听路由变化,将最新的location传递给react-router的组件
- 👩🔬
react-router:将location作为上下文传递下去,渲染出匹配上location的组件
-
路由更改-->组件渲染
- 直接修改路径、前进、后退这些动作属于
popState事件,会触发handlePopState函数,该函数体内会通过setState更新location对象并告知Router,然后子组件根据最新的location进行匹配,符合规则的组件就能渲染。
history.push()这个动作属于pushState事件,它会通过window.history.pushState方法改变浏览器当前路由,然后通过setState更新location对象并告知Router,然后子组件根据最新的location进行匹配,符合规则的组件就能渲染。
三、React-Router 项目架构&源码解析
1. 项目架构 🦑🦑🦑
react-router是核心包,包含了大部分逻辑和组件,处理context和路由匹配都在这里。
react-router-dom是浏览器使用的包,像Link这样需要渲染具体的a标签的组件就在这里。
react-router-native是react-native使用的包,里面包含了android和ios具体的项目。
react-router-config:是React-Router的配置处理,我们一般不需要使用
2. 关键组件的源码解析 🔬🔬🔬
2.1 BrowserRouter 🥓
class Router extends React.Component {
// 静态方法,检测当前路由是否匹配
// 这里会返回一个默认的 match 对象,后续的子组件会用到
static computeRootMatch(pathname) {
return { path: '/', url: '/', params: {}, isExact: pathname === '/' };
}
constructor(props) {
super(props);
// 后续要作为上下文传递下去,也是判定子组件是否匹配的标准
this.state = {
location: props.history.location,
};
// _isMounted 表示组件是否加载完成
this._isMounted = false;
// 组件未加载完毕,但是 location 发生的变化,暂存在 _pendingLocation 字段中
this._pendingLocation = null;
// 通过 listen 监听路由变化,并获取最新的 location
// 在组件卸载时调用 unlisten,取消对 history 的监听
this.unlisten = this.props.history.listen((location) => {
if (this._isMounted) {
this.setState({ location });
} else {
this._pendingLocation = location;
}
});
}
componentDidMount() {
this._isMounted = true;
if (this._pendingLocation) {
this.setState({
location: this._pendingLocation,
});
}
}
componentWillUnmount() {
if (unlisten()) {
this.unlisten();
this._isMounted = false;
this._pendingLocation = null;
}
}
render() {
return (
<RouterContext.Provider
value={{
history: this.props.history,
location: this.state.location,
match: computeRootMatch(this.state.location.pathname),
}}
>
<HistoryContext.Provider
value={this.props.history}
children={this.props.children}
></HistoryContext.Provider>
</RouterContext.Provider>
);
}
}
2.2 Switch 🥐
/**
* Switch 组件会遍历子元素(Route 或 Redirect),寻找第一个能匹配上 path 的子元素,并渲 * 染对应的组件
* 一旦找到一个匹配上的子元素,就会停止匹配;否则返回 null
*/
class Switch extends React.Component {
render() {
return (
<RouterContext.Consumer>
{(context) => {
// 当前页面路径存储 Router 里,通过上下文向下传递;
// 在后续遍历子元素时,用子元素的 path 与 location 比较
// 如果有自定义 location,优先采用,但我实在想不到使用场景。。
const location = this.props.location || context.location;
// 这里的 element 用来存储第一个有效的(不一定匹配成功)元素
// match 用来表示是否已匹配上,如果还未匹配上,值为 null
let element, match;
// 原码注释:
// 使用React.Children.forEach来遍历子元素,而不能使用React.Children.toArray().find()
// 因为toArray会给每个子元素添加一个key,这会导致两个有同样component,但是不同URL的<Route>重复渲染
// 个人解读:
// 这里的 React.Children.forEach 与常规写法不同,他是 React 专门用来遍历子元素的方法
// 当子元素为空(this.props.children === null 或 undefined)时,会返回一个 null
React.Children.forEach(this.props.children, (child) => {
if (match === null && React.isValidElement(child)) {
element = child;
// 在 Route 组件里,通过 path 来匹配;在 Redirect 组件里,通过 from 来匹配
const path = child.props.path || child.props.from;
// 如果 path 存在就将 path 与 location 进行匹配,匹配成功后会返回 match 对象;
// 匹配失败则返回 null
// 如果 path 不存在则使用 computeRootMatch 生成的初始 match 对象
match = path
? matchPath(location.pathName, {
...child.props,
path,
})
: context.match;
}
});
// 当子元素片匹配成功后,返回匹配到的子元素的 clone
// 这里为什么要把 match 赋值给 computedMatch,详情见 Route
return match
? React.cloneElement(element, { computedMatch: match })
: null;
}}
</RouterContext.Consumer>
);
}
}
2.3 Route 🥙
/**
* Route 组件是在 path 匹配成功的情况下,进行组件渲染
*/
class Route extends React.Component {
render() {
return (
<RouterContext.Consumer>
{(context) => {
// 当前页面路径存储 Router 里,通过上下文向下传递;
// 在后续遍历子元素时,用子元素的 path 与 location 比较
// 如果有自定义 location,优先采用,但我实在想不到使用场景。。
const location = this.props.location || context.location;
// Switch 组件会在子组件匹配成功时,将 match 赋值给子组件 Route 的 computedRoute,这里有就直接用
// 没有 computedRoute 的话,再看 path 属性,
// 如果 path 存在就将 path 与 location 进行匹配,匹配成功后会返回 match 对象;
// 匹配失败则使用 computeRootMatch 生成的初始 match 对象
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;
// match 和 location 可能都更新过了,这里用 props 来存储最新的上下文信息
const props = { ...this.props, match, location };
// Route 组件有三种渲染方式,分别是children、component、render
// 优先级是children > component > render
// 其中 children 最特殊,他可以是函数,也可以是元素;
// 而当他是函数时,无论 path 是否匹配,他都会渲染;
// (好像平时都用的component,其他两种有啥使用场景,欢迎交流)
let { children, component, render } = props;
// 源码注释:children 默认是个空数组,如果是默认情况,置为 null
if (Array.isArray(children) && children.length === 0) {
children = null;
}
return (
<RouterContext.Provider value={props}>
{props.match
? children
? typeof children === 'function'
? children(props)
: children
: component
? React.cloneElement(component, props)
: render
? render(props)
: null
: typeof children === 'function'
? children(props)
: null}
</RouterContext.Provider>
);
}}
</RouterContext.Consumer>
);
}
}
四、实际应用场景 🦦🦦🦦
场景:分批次上线功能(前言中的例子)
我的实现思路是加一个权限码,有这个权限码的人进行路由跳转时,重定向到新功能页
<Switch>
{hasAuthority ? (
<Redirect path="/old" to={`/new`} />
) : null}
<Route path={`/new`} component={newPage}></Route>
<Route path={`/old`} component={OldPage} />
<Redirect path="*" to={`/home`} />
</Switch>
五、参考文献 🐕🐕🐕
导读:2、3对react-router有全面的介绍,最好同时看,4在源码解读上更胜一筹,其他的不如2、3