React-router源码解析
截图的源码加了关键性注释,简版实现github.com/upxin/react…
路由的概述
1.当用户刷新页面时,浏览器会默认根据当前 URL 对资源进行重新定位(发送请求)。这个动作对 SPA 是不必要的,因为 SPA 作为单页面,无论如何也只会有一个资源与之对应。此时若走正常的请求-刷新流程,反而会使用户的前进后退操作无法被记录。单页面应用对服务端来说,就是一个 URL、一套资源,做到用“不同的 URL”来映射不同的视图内容感知 URL 的变化。一旦我们感知到了,我们就根据这些变化、用 JS 去给它生成不同的内容。 2. 拦截用户的刷新操作,避免服务端盲目响应、返回不符合预期的资源内容,把刷新这个动作完全放到前端逻辑里消化掉; 3.实现前端路由的方式hash 与 history我们(以hash为例,两者在源码实现上相差不大)
看一下这张流程图,捋清楚思路
react router的管理与架构
react router也是monorepo的方式管理的,monorepo管理的好处就是能让同一仓库同时管理多个独立项目,各个项目之间又能互相引用,下图是react-router主要代码的目录,然后router同级别的目录router-dom引用了router
就以hash为例,然后我们主要就看两个文件就行了 其中主要思想就体现了 一个是Router代表(Provider)传递路由信息,一个是Switch代表(Consumer)匹配合法的组件
/* ***通常我们这样使用的
<Router>
<Switch>
<Route></Route>
<Route></Route>
</Switch>
</Router> */
import React from "react";
import { Router } from "react-router";
// 这里可以看出 除了用来history库的不同api来监听地址变化,这两种模式在实现上其实没有啥区别了
//import { createBrowserHistory as createHistory } from "history";
import { createHashHistory as createHistory } from "history";
class HashRouter extends React.Component {
history = createHistory(this.props);
render() {
// 传入history,这是主要的监听api
return <Router history={this.history} children={this.props.children} />;
}
}
export default HashRouter;
上面用到了Router,去看看router写了什么
class Router extends React.Component {
constructor(props) {
//初始化监听url变化,保存了改变后的location赋值给_pendingLocation
this.unlisten = props.history.listen(location => {
if (this._isMounted) {
this.setState({ location });
} else {
this._pendingLocation = location;
}
});
}
componentDidMount() {
this._isMounted = true;
if (this._pendingLocation) {
// 重置了location
this.setState({ location: this._pendingLocation });
}
}
render() {
return (
<RouterContext.Provider
// 这里其实就是react creatContext的Provider
value={{
history: this.props.history,
// 传入了location
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>
);
}
}
简单这里我们讲一下Switch,它其实就是匹配一个组件就返回
import React from "react";
import RouterContext from "./RouterContext.js";
import matchPath from "./matchPath.js";
/**
* The public API for rendering the first <Route> that matches.
*/
class Switch extends React.Component {
render() {
return (
<RouterContext.Consumer>
{context => {
invariant(context, "You should not use <Switch> outside a <Router>");
const location = this.props.location || context.location;
let element, match;
// 遍历children,返回一个唯一的route组件,matchPath方法就是匹配路径的逻辑,如果match匹配到了 那么设置match的值不为null,这样就会返回当前对应的elment,大家可以去详细看看matchPath方法,用到了一个插件path-to-regexp,方便我们将(搜集到route传入的path生成一个正则,去匹配location的pathname),是强匹配还是模糊匹配等,都可以通过参数传入用path-to-regexp生成一个正则,所以这里一定是用 child的path生成的正则,去匹配pathname,可以去试一下非精确匹配的时候 如果你是home
React.Children.forEach(this.props.children, child => {
if (match == null && React.isValidElement(child)) {
element = child;
const path = child.props.path || child.props.from;
match = path
? matchPath(location.pathname, { ...child.props, path })
: context.match;
}
});
return match
? React.cloneElement(element, { location, computedMatch: match })
: null;
}}
</RouterContext.Consumer>
);
}
}
##举个例子,看下图
这里我们去'/search/robot' 直接重定向到子路由 '/search/robot/identify', 但是'/search/robot'没有加exact,就是因为此时我们搜集的location pathname是 '/search/robot/identify',那么'/search/robot'的Robot组件也会被渲染,因为不是精确匹配。如果是精确的匹配Robot组件不会渲染,子路由组件也就不存在了,此时路由地址pathname为'/search/robot/identify',路由组件生产的正则大致为 /^/search/robot(?:/#?)?(?=[/#?]|[]|$)/i,言下之意就是/search/robot后面跟任何内容都会渲染Robot组件 ,Robot组件渲染了,它的子组件才能渲染。
最后,router源码有个Provider和Consumer的pollify版本
class Provider extends Component<ProviderProps<T>> {
// 发布订阅中心
emitter = createEventEmitter(this.props.value);
static childContextTypes = {
[contextProp]: PropTypes.object.isRequired
};
// 该方法传递一个context给子组件
getChildContext() {
return {
[contextProp]: this.emitter
};
}
}
class Consumer extends Component<ConsumerProps<T>, ConsumerState<T>> {
getValue(): T {
if (this.context[contextProp]) {
// getChildContext 返回值就是 this.context 这是react的原生api
return this.context[contextProp].get();
} else {
return defaultValue;
}
}
onUpdate = (newValue: any, changedBits: number) => {
const observedBits: number = this.observedBits | 0;
if ((observedBits & changedBits) !== 0) {
this.setState({ value: this.getValue() });
}
};
render() {
return onlyChild(this.props.children)(this.state.value);
}
}