React Router 是完整的 React 路由解决方案
React Router 保持 UI 与 URL 同步。它拥有简单的 API 与强大的功能例如代码缓冲加载、动态路由匹配、以及建立正确的位置过渡处理。今天我们就来大概的看一下这个强大的组件是如何实现的。
react-router为了能实现跨平台的路由解决方案,将react-router分成了三个模块:react-router
,react-router-native
,react-router-dom
三个包,react-router
是另外两个模块的基础。这里我们主要分析react-router-dom的基本实现。
react-router源码地址: https://github.com/ReactTraining/react-router
RouterContext
react-router使用context实现跨组件间数据传递,所以react-router定义了一个routerContext作为数据源,
// packages/react-router/modules/RouterContext.js
import createContext from "mini-create-react-context";
const createNamedContext = name => {
const context = createContext();
context.displayName = name;
return context;
};
const context = createNamedContext("Router");
export default context;
代码非常好理解,定义createNamedContext
函数,然后调用它来创建一个context。
Router
我们在使用react-router-dom的使用,经常会用到BroswerRouter和HashRouter两个组件,这两个组件都使用了Router组件,所以所Router组件是基础,我们先来看看Router组件定义
// packages/react-router/modules/Router.js
import RouterContext from "./RouterContext";
class Router extends React.Component {
static computeRootMatch(pathname) {
return { path: "/", url: "/", params: {}, isExact: pathname === "/" };
}
constructor(props) {
super(props);
this.state = {
location: props.history.location
};
this._isMounted = false;
this._pendingLocation = null;
if (!props.staticContext) {
this.unlisten = 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 (this.unlisten) this.unlisten();
}
render() {
return (
<RouterContext.Provider
children={this.props.children || null}
value={{
history: this.props.history,
location: this.state.location,
match: Router.computeRootMatch(this.state.location.pathname),
staticContext: this.props.staticContext
}}
/>
);
}
}
Router组件引入了刚刚提到的RouterContext,将其作为Provider为子组件提提供数据,主要的数据有两个history
和location
,这两个对象主要封装了路由跳转的相关功能和路由的相关信息
BrowserRouter和HashRouter
看完了Router的基本实现后,我们可以来看看BrowserRouter和HashRouter的实现了,
- BrowserRouter
// BrowserRouter packages/react-router-dom/modules/BrowserRouter.js
import { Router } from "react-router";
import { createBrowserHistory as createHistory } from "history";
import PropTypes from "prop-types";
import warning from "tiny-warning";
class BrowserRouter extends React.Component {
history = createHistory(this.props);
render() {
return <Router history={this.history} children={this.props.children} />;
}
}
BrowserRouter.propTypes = {
basename: PropTypes.string,
children: PropTypes.node,
forceRefresh: PropTypes.bool,
getUserConfirmation: PropTypes.func,
keyLength: PropTypes.number
}
// HashRouter packages/react-router-dom/modules/HashRouter.js
class HashRouter extends React.Component {
history = createHistory(this.props);
render() {
return <Router history={this.history} children={this.props.children} />;
}
}
HashRouter.propTypes = {
basename: PropTypes.string,
children: PropTypes.node,
getUserConfirmation: PropTypes.func,
hashType: PropTypes.oneOf(["hashbang", "noslash", "slash"])
}
两个Router的实现非常简单,而且非常相似:创建一个history对象,然后渲染Router组件,但这里history对象是怎么创建出来的呢? 感兴趣的朋友可以看看这篇文章:https://blog.youkuaiyun.com/tangzhl/article/details/79696055。
在这里我大致说一下他们的作用
createBrowserHistory
是通过使用HTML5 history的API来实现路由的跳转(pushState和replaceState
)并通过监听popstate
事件路由变换,进而更新location对象(Router中提到,location对象封装了路由信息),location对象的改变出发了组件的重新渲染,从而根据路由来渲染不同的组件
createHashHistory
通过浏览器的hash来改变路由,然后监听hashChange
事件监听hash的改版,然后出发location对象的改变,进而更新试图
Route组件
我们常用route组件来进行路径和组件的匹配,所以我们来看看Route组件的实现
我们先看看Route的不同使用方式
<Route path="xxx" component={Xxx} />
<Route path="xxx" render={() =>(</Xxx>)} />
<Route path="xxx"><Xxx /></Route>
在来看源码,方便理解,都打上了注释,为了代码更新其,也删除了一些判断
// /packages/react-router/modules/Switch.js
class Route extends React.Component {
render() {
return (
<RouterContext.Consumer>
{context => {
// 获取location对象 Router组件上面的
const location = this.props.location || context.location;
// 判断path和location是否对应
const match = this.props.computedMatch
? this.props.computedMatch // <Switch> already computed the match for us
: this.props.path
? matchPath(location.pathname, this.props) // 在这里判断path和locations是否对象
: context.match;
const props = { ...context, location, match };
// 获取Route上的对应的渲染组件,
let { children, component, render } = this.props;
// 判断是children长度是否为0
if (Array.isArray(children) && children.length === 0) {
children = null;
}
// 获取children
if (typeof children === "function") {
children = children(props);
}
return (
<RouterContext.Provider value={props}>
{children && !isEmptyChildren(children)
? children
: props.match // 判断path与locaiton是否匹配,如果匹配则渲染component,否则返回null
? component
? React.createElement(component, props)
: render
? render(props)
: null
: null}
</RouterContext.Provider>
);
}}
</RouterContext.Consumer>
);
}
}
Route工作核心步骤只有两三步,获取RouterContext的信息(location对象
),获取path
和component
属性,判断path和当前的location是否匹配,如果匹配,则渲染component
,否则返回null
,不渲染任何内容
Switch
// /packages/react-router/modules/Route.js
class Switch extends React.Component {
render() {
return (
<RouterContext.Consumer>
{context => {
const location = this.props.location || context.location;
let element, match;
React.Children.forEach(this.props.children, child => {
if (match == null && React.isValidElement(child)) {
element = child;
const path = child.props.path || child.props.from;
// 判断Route的path和当前location是否匹配,如果匹配则渲染,否则不渲染
match = path
? matchPath(location.pathname, { ...child.props, path })
: context.match;
}
});
return match
? React.cloneElement(element, { location, computedMatch: match })
: null;
}}
</RouterContext.Consumer>
);
}
}
Switch
组件实现很简单,遍历所有子元素(Route
),判断Route
的path
和location是否匹配,如果匹配,则渲染,否则不渲染。从代码可以看出,貌似Switch
会渲染最后一个匹配的Route
Link组件
// /packages/react-router-dom/modules/Link.js
function LinkAnchor({ innerRef, navigate, onClick, ...rest }) {
const { target } = rest;
return (
<a
{...rest}
ref={innerRef} // TODO: Use forwardRef instead
onClick={event => {
try {
if (onClick) onClick(event);
} catch (ex) {
event.preventDefault();
throw ex;
}
if (
!event.defaultPrevented && // onClick prevented default
event.button === 0 && // ignore everything but left clicks
(!target || target === "_self") && // let browser handle "target=_blank" etc.
!isModifiedEvent(event) // ignore clicks with modifier keys
) {
event.preventDefault();
navigate();
}
}}
/>
);
}
function Link({ component = LinkAnchor, replace, to, ...rest }) {
return (
<RouterContext.Consumer>
{context => {
invariant(context, "You should not use <Link> outside a <Router>");
const { history } = context;
const location = normalizeToLocation(
resolveToLocation(to, context.location),
context.location
);
const href = location ? history.createHref(location) : "";
return React.createElement(component, {
...rest,
href,
navigate() {
const location = resolveToLocation(to, context.location);
const method = replace ? history.replace : history.push;
method(location);
}
});
}}
</RouterContext.Consumer>
);
}
从代码意义看出,Link
组件本质就是a
标签,它修改了a
标签的默认行为,当点击Link时,会导航到对应的路由,导致locaiton
对象的改变,出发组件的更新
withRouter
withRouter是要高阶函数,对传入的组件进行加强,功能就是获取routerContext上面的信息,然后作为props传给需要加强的组件
// /packages/react-router/modules/withRouter.js
function withRouter(Component) {
const C = props => {
const { wrappedComponentRef, ...remainingProps } = props;
return (
<RouterContext.Consumer>
{context => {
return (
<Component
{...remainingProps}
{...context}
ref={wrappedComponentRef}
/>
);
}}
</RouterContext.Consumer>
);
};
}
return hoistStatics(C, Component)
总结
React-Router博大精深,同志仍需努力