出于对react-router一些参数的好奇,对react-router的源码简单的看了一下,这里记录下一些理解和备忘,用于以后复查,以及希望对正在看这篇文章的你,也有所帮助。
目录结构
在react-router下分了五个包:
- react-router
- react-router-dom
- react-router-native
- react-router-redux
- react-router-config
其中,react-router是核心包,react-router-dom是对react-router的简单封装,其余的三个没有了解。
对于浏览器端的页面,直接引用react-router-dom就可以。(这里和react相反,需要引入react和react-dom)。
五个包的源码,都在对应的modules目录下。
react-router-dom/index
export BrowserRouter from "./BrowserRouter";
export HashRouter from "./HashRouter";
export Link from "./Link";
export MemoryRouter from "./MemoryRouter";
export NavLink from "./NavLink";
export Prompt from "./Prompt";
export Redirect from "./Redirect";
export Route from "./Route";
export Router from "./Router";
export StaticRouter from "./StaticRouter";
export Switch from "./Switch";
export generatePath from "./generatePath";
export matchPath from "./matchPath";
export withRouter from "./withRouter";
以上为react-router-dom
暴露的所有对象。
其中只有BrowserRouter、HashRouter、Link、NavLink进行了简单的再封装。
其他都是直接引用的react-router中的模块。
react-router-dom/XxxRouter
对于BrowserRouter,HashRouter,MemoryRouter主要靠传入不同的history实例实现不同的功能。
这里以HashRouter为例:
import Router from "./Router";
import { createHashHistory as createHistory } from "history";
class HashRouter extends React.Component {
static propTypes = {
basename: PropTypes.string,
getUserConfirmation: PropTypes.func,
hashType: PropTypes.oneOf(["hashbang", "noslash", "slash"]),
children: PropTypes.node
};
history = createHashHistory(this.props);
componentWillMount() {
//warning(...);
}
render() {
return <Router history={this.history} children={this.props.children} />;
}
}
BrowserRouter
,HashRouter
,MemoryRouter
分别对应history
的createBrowserHistory
,createHashHistory
,createMemoryHistory
。
并且router的所有props都是直接传给history
的create方法的。
关于history的详细介绍,可以查看该链接。
react-router/Router
看下Router源码:
class Router extends React.Component {
static propTypes = {
history: PropTypes.object.isRequired,
children: PropTypes.node
};
static contextTypes = { router: PropTypes.object };
static childContextTypes = { router: PropTypes.object.isRequired };
getChildContext() {
return {
router: {
...this.context.router,
history: this.props.history,
route: {
location: this.props.history.location,
match: this.state.match
}
}
};
}
state = { match: this.computeMatch(this.props.history.location.pathname) };
computeMatch(pathname) {
return {
path: "/",
url: "/",
params: {},
isExact: pathname === "/"
};
}
componentWillMount() {
const {children, history} = this.props;
invariant(children == null || React.Children.count(children) === 1,
"A <Router> may have only one child element");
this.unlisten = history.listen(() => {
this.setState({
match: this.computeMatch(history.location.pathname)
});
});
}
componentWillReceiveProps(nextProps) {
warning(this.props.history === nextProps.history,
"You cannot change <Router history>");
}
componentWillUnmount() { this.unlisten(); }
render() {
const {children} = this.props;
return children ? React.Children.only(children) : null;
}
}
首先,我们能看到router的几个特性:
- 具有两个props参数:
history
和chidlren
- router下只能有一个子元素(通过React.Children.only进行限制)
- props.history初始化后,不能再次更改
render
先看render():
render() {
const {children} = this.props;
return children ? React.Children.only(children) : null;
}
render()很简单,直接输出children。
context
Router组件与子组件通信,主要是靠context,而且是旧版的context API.
旧版context语法中,设置在父模块的属性有: getChildContext
,childContextTypes
,设置在子模块的属性有: contextTypes
。
而Router
同时存在这三个属性,在于对嵌套路由的支持。
这里主要是对子组件暴露一个context.router
对象,格式为: { history, route:{ location, match } }
。包含当前history实例(history
),以及当前路由匹配信息(route
)。
在context.router
中使用了扩展运算符展开了父Router(this.context.router
),但是我觉得后面的history
和route
应该覆盖了展开的父Router的属性,所以我还不清楚这里展开父Router有什么意义。
context.router.route.location
为当前history实例的location。context.router.route.match
指向this.state.match
。
router逻辑总结
到这里,应该大致能猜一下react-router的代码逻辑了。
对于url主要有以下5中操作:
- 路径格式: 通过HTML5 history API 或者 hash
- 路径导航: push(),replace(),back(), …
- 路径分析: 上层的路由path和绝对url的转化( 嵌套路由,html5 API/hash 区别处理 ,等)
- 事件监听: 可通过事件形式,监听路由变化等。
- 路由守卫: 在react中叫做block。
这些操作,全部都封装在history中。然后将对应的history实例传入给Router组件。
Router只是通过history实例,监听路由变化,每次变化后都执行setState
更新state.match
。
由于context中的match
指向的是state.match
,从而造成context发生变化,触发子组件的componentWillReceiveProps回调。
react-router/Route
Route
源码:
class Route extends React.Component {
static propTypes = {
computedMatch: PropTypes.object, // private, from <Switch>
path: PropTypes.string,
exact: PropTypes.bool,
strict: PropTypes.bool,
sensitive: PropTypes.bool,
component: PropTypes.func,
render: PropTypes.func,
children: PropTypes.oneOfType([PropTypes.func, PropTypes.node]),
location: PropTypes.object
};
static contextTypes = {
router: PropTypes.shape({
history: PropTypes.object.isRequired,
route: PropTypes.object.isRequired,
staticContext: PropTypes.object
})
};
static childContextTypes = {
router: PropTypes.object.isRequired
};
getChildContext() {
return {
router: {
...this.context.router,
route: {
location: this.props.location || this.context.router.route.location,
match: this.state.match
}
}
};
}
state = {
match: this.computeMatch(this.props, this.context.router)
};
computeMatch(
{ computedMatch, location, path, strict, exact, sensitive },
router
) {
if (computedMatch) return computedMatch; // <Switch> already computed the match for us
invariant(
router,
"You should not use <Route> or withRouter() outside a <Router>"
);
const { route } = router;
const pathname = (location || route.location).pathname;
return matchPath(pathname, { path, strict, exact, sensitive }, route.match);
}
componentWillMount() {
warning(
!(this.props.component && this.props.render),
"You should not use <Route component> and <Route render> in the same route; <Route render> will be ignored"
);
warning(
!(
this.props.component &&
this.props.children &&
!isEmptyChildren(this.props.children)
),
"You should not use <Route component> and <Route children> in the same route; <Route children> will be ignored"
);
warning(
!(
this.props.render &&
this.props.children &&
!isEmptyChildren(this.props.children)
),
"You should not use <Route render> and <Route children> in the same route; <Route children> will be ignored"
);
}
componentWillReceiveProps(nextProps, nextContext) {
warning(
!(nextProps.location && !this.props.location),
'<Route> elements should not change from uncontrolled to controlled (or vice versa). You initially used no "location" prop and then provided one on a subsequent render.'
);
warning(
!(!nextProps.location && this.props.location),
'<Route> elements should not change from controlled to uncontrolled (or vice versa). You provided a "location" prop initially but omitted it on a subsequent render.'
);
this.setState({
match: this.computeMatch(nextProps, nextContext.router)
});
}
render() {
const { match } = this.state;
const { children, component, render } = this.props;
const { history, route, staticContext } = this.context.router;
const location = this.props.location || route.location;
const props = { match, location, history, staticContext };
if (component) return match ? React.createElement(component, props) : null;
if (render) return match ? render(props) : null;
if (typeof children === "function") return children(props);
if (children && !isEmptyChildren(children))
return React.Children.only(children);
return null;
}
}
props
首先可以看到参数格式,对外暴露总共7个参数,以及props.children。
其中,路径匹配相关的props
参数:
- location
- path
- exact
- strict
- sensitive
渲染相关:
- component
- render
- children
context
可以看到Route
也实现了context的接口。向子元素暴露context.router
对象。格式为:{ history, route:{ location,match } }
。
其中,
history
指向this.context.router.history
,即从Router组件中一层层向下传递的history实例。route.location
为this.props.location || this.context.router.route.location
,其实假设如果完全没有使用过props.location
参数,则该route.location
实际为当前history实例上的location。route.match
指向this.state.match
state.match
当前的路由信息保存在state.match
中,并且在componentWillReceiveProps
回调中通过setState()
进行更新。state.match
的格式为: { path, url, isExact, params }
,如果未匹配当前路径,则为null
。
state.match更新逻辑
- 如果未指定
props.path
,则返回父组件的context.match
。 - 根据
location.pathname
,进行匹配,如果匹配失败,则返回null。(详细匹配规则参照该文档path-to-regexp) - 匹配成功,返回
{ path, url, isExact, params }
。
componentWillReceiveProps(nextProps, nextContext)
在该回调中执行setState()
,每当父组件context变化时,会触发该回调,并传入新的context对象作为nextContext
参数
props限制
props.location
在初始化后,不允许再更改,其他props属性可以随意更改,每次更改都会更新state.match
。
render()伪代码:
var childProps= { match, location, history };
if (props.component) return match ? React.createElement(props.component, childProps) : null;
else if (props.render) return match ? props.render(props) : null;
else if (typeof props.children === "function") return props.children(props);
else if (children) return React.Children.only(children);
else return null;
注意:
component
和render
都判断了是否匹配,如果是通过props.chidlren
渲染,则不验证是否匹配。- 如果未指定
props.path
,则匹配结果跟随父组件。
匹配规则总结
到这里,react-router最核心的匹配规则已经很清楚了。
- 最外层包裹一个Router组件,代表一个路由实例。
- Router组件管理着一个history实例,通过history实例,每当路径变化,就触发更新,重新计算
state.match
。 - 每当Router的
state.match
变化,会触发所有子组件更新,并下发history实例和history.location对象。 - Route为Router的子组件,每次更新时,会根据history.location.pathname,检测自身匹配状态,然后进行渲染。
实现功能总结
在回头看下index.js中的暴露对象,可以总结下react-router所有实现的功能:
- 路由类型: StaticRouter, BrowserRouter, HashRouter, MemoryRouter, Router
- 路由导航: Link, NavLink
- 路由守卫: Prompt
- 路由匹配: Route, Redirect, Switch
- 帮助方法-获取路由相关对象: withRouter
- 帮助方法-path匹配规则: matchPath
- 帮助方法-生成path: generatePath
仅我个人而言,react-router已经满足绝大部分需求了,如果再有路由相关的需求,则可以对照该列表比较,如果不满足就需要额外开发,目前我只遇到了关于权限控制的需求,需要额外开发。