Read react-router
XXHolic opened this issue · 0 comments
XXHolic commented
目录
引子
看了 react-router 后,整理一下个人的理解。
源码版本 5.1.2 。
简介
react-router 用于 React 路由管理,新老版本都有在维护发布。
react-router 库是用独立包的方式进行管理,各个包侧重点是:
- react-router : 提供路由的核心功能,是另外三个包的依赖。
- react-router-dom : 适用于 Web 端应用的路由。
- react-router-native : 适用于原生应用的路由。
- react-router-config : 路由配置辅助工具。
其中包之间有相互引用的情况,想着从 Web 官方示例入手介绍。
源码
示例
import React from "react";
import {
BrowserRouter as Router,
Switch,
Route,
Link
} from "react-router-dom";
export default function App() {
return (
<Router>
<div>
<nav>
<ul>
<li>
<Link to="/">Home</Link>
</li>
<li>
<Link to="/about">About</Link>
</li>
<li>
<Link to="/users">Users</Link>
</li>
</ul>
</nav>
{/* A <Switch> looks through its children <Route>s and
renders the first one that matches the current URL. */}
<Switch>
<Route path="/about">
<About />
</Route>
<Route path="/users">
<Users />
</Route>
<Route path="/">
<Home />
</Route>
</Switch>
</div>
</Router>
);
}
function Home() {
return <h2>Home</h2>;
}
function About() {
return <h2>About</h2>;
}
function Users() {
return <h2>Users</h2>;
}
在上面的示例中使用了 react-router-dom 的 BrowserRouter
、Switch
、Route
、Link
。
看了代码后,发现在 react-router-dom 中定义的组件只有:BrowserRouter
、HashRouter
、Link
、NavLink
。其它的是直接从 react-router 中导入。下面依次看下源码。
BrowserRouter
BrowserRouter 相关主要源码
// BrowserRouter.js 主要代码
import React from "react";
import { Router } from "react-router";
import { createBrowserHistory as createHistory } from "history";
class BrowserRouter extends React.Component {
history = createHistory(this.props);
render() {
return <Router history={this.history} children={this.props.children} />;
}
}
export default BrowserRouter;
// react-router 的 Router 组件主要代码
import React from "react";
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 is a bit of a hack. We have to start listening for location
// changes here in the constructor in case there are any <Redirect>s
// on the initial render. If there are, they will replace/push when
// they mount and since cDM fires in children before parents, we may
// get a new location before the <Router> is mounted.
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
}}
/>
);
}
}
export default Router;
从代码中可以发现:
- react-router-dom 的
BrowserRouter
组件创建了统一的路由对象history
,history
具体实现见 Read history 。 - react-router 的
Router
组件用于共享数据,RouterContext
具体实现见 Read mini-create-react-context 。 HashRouter
跟BrowserRouter
主要区别就是创建的history
有差异,具体见 Read history 。
Switch
Switch 相关主要源码
// Switch.js 主要代码
import React from "react";
import RouterContext from "./RouterContext";
import matchPath from "./matchPath"; // 路径匹配
/**
* 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;
// We use React.Children.forEach instead of React.Children.toArray().find()
// here because toArray adds keys to all child elements and we do not want
// to trigger an unmount/remount for two <Route>s that render the same
// component at different URLs.
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>
);
}
}
export default Switch;
从代码中可以发现:
Switch
是一个“消费”组件,可以获取共享的数据,Switch
对所有后代进行遍历,根据当前访问的pathname
与后代的path
属性进行匹配,返回第一个匹配到的后代。- 最后返回的后代进行了克隆。
Route
Route 相关主要源码
import React from "react";
import RouterContext from "./RouterContext";
import matchPath from "./matchPath";
/**
* The public API for matching a single path and rendering.
*/
class Route extends React.Component {
render() {
return (
<RouterContext.Consumer>
{context => {
invariant(context, "You should not use <Route> outside a <Router>");
const location = this.props.location || context.location;
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;
// Preact uses an empty array as children by
// default, so use null if that's the case.
if (Array.isArray(children) && children.length === 0) {
children = null;
}
return (
<RouterContext.Provider value={props}>
{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>
);
}
}
export default Route;
从代码中可以发现:
Route
是一个消费组件,可以获取共享的数据。- 跟
Switch
同样具有匹配路由的能力。 - 返回了一个新的共享数据的组件。
Link
Link 相关主要源码
import React from "react";
import { __RouterContext as RouterContext } from "react-router";
import { resolveToLocation, normalizeToLocation } from "./utils/locationUtils";
// React 15 compat
const forwardRefShim = C => C;
let { forwardRef } = React;
if (typeof forwardRef === "undefined") {
forwardRef = forwardRefShim;
}
function isModifiedEvent(event) {
return !!(event.metaKey || event.altKey || event.ctrlKey || event.shiftKey);
}
const LinkAnchor = forwardRef(
(
{
innerRef, // TODO: deprecate
navigate,
onClick,
...rest
},
forwardedRef
) => {
const { target } = rest;
let props = {
...rest,
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();
}
}
};
// React 15 compat
if (forwardRefShim !== forwardRef) {
props.ref = forwardedRef || innerRef;
} else {
props.ref = innerRef;
}
return <a {...props} />;
}
);
/**
* The public API for rendering a history-aware <a>.
*/
const Link = forwardRef(
(
{
component = LinkAnchor,
replace,
to,
innerRef, // TODO: deprecate
...rest
},
forwardedRef
) => {
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) : "";
const props = {
...rest,
href,
navigate() {
const location = resolveToLocation(to, context.location);
const method = replace ? history.replace : history.push;
method(location);
}
};
// React 15 compat
if (forwardRefShim !== forwardRef) {
props.ref = forwardedRef || innerRef;
} else {
props.innerRef = innerRef;
}
return React.createElement(component, props);
}}
</RouterContext.Consumer>
);
}
);
export default Link;
从代码中可以发现:
Link
是一个消费组件,可以获取共享的数据。Link
是基于 history 进行路径处理。Link
基于原生的a
标签,对部分特性进行了重新封装,返回一个新的元素。NavLink
基于Link
添加了激活的状态。
Redirect
Redirect 相关主要源码
import React from "react";
import { createLocation, locationsAreEqual } from "history";
import Lifecycle from "./Lifecycle";
import RouterContext from "./RouterContext";
import generatePath from "./generatePath";
/**
* The public API for navigating programmatically with a component.
*/
function Redirect({ computedMatch, to, push = false }) {
return (
<RouterContext.Consumer>
{context => {
invariant(context, "You should not use <Redirect> outside a <Router>");
const { history, staticContext } = context;
const method = push ? history.push : history.replace;
const location = createLocation(
computedMatch
? typeof to === "string"
? generatePath(to, computedMatch.params)
: {
...to,
pathname: generatePath(to.pathname, computedMatch.params)
}
: to
);
// When rendering in a static context,
// set the new location immediately.
if (staticContext) {
method(location);
return null;
}
return (
<Lifecycle
onMount={() => {
method(location);
}}
onUpdate={(self, prevProps) => {
const prevLocation = createLocation(prevProps.to);
if (
!locationsAreEqual(prevLocation, {
...location,
key: prevLocation.key
})
) {
method(location);
}
}}
to={to}
/>
);
}}
</RouterContext.Consumer>
);
}
export default Redirect;
import React from "react";
class Lifecycle extends React.Component {
componentDidMount() {
if (this.props.onMount) this.props.onMount.call(this, this);
}
componentDidUpdate(prevProps) {
if (this.props.onUpdate) this.props.onUpdate.call(this, this, prevProps);
}
componentWillUnmount() {
if (this.props.onUnmount) this.props.onUnmount.call(this, this);
}
render() {
return null;
}
}
export default Lifecycle;
从代码中可以发现:
Redirect
是一个“消费”组件,可以获取共享的数据。Redirect
基于history
进行路径处理。- 可以直接触发跳转。
withRouter
withRouter 相关主要源码
import React from "react";
import RouterContext from "./RouterContext";
import hoistStatics from "hoist-non-react-statics"; // 拷贝静态方法
/**
* A public higher-order component to access the imperative API
*/
function withRouter(Component) {
const displayName = `withRouter(${Component.displayName || Component.name})`;
const C = props => {
const { wrappedComponentRef, ...remainingProps } = props;
return (
<RouterContext.Consumer>
{context => {
invariant(
context,
`You should not use <${displayName} /> outside a <Router>`
);
return (
<Component
{...remainingProps}
{...context}
ref={wrappedComponentRef}
/>
);
}}
</RouterContext.Consumer>
);
};
C.displayName = displayName;
C.WrappedComponent = Component;
return hoistStatics(C, Component);
}
export default withRouter;
从代码中可以发现:
withRouter
是一个消费组件,可以获取共享的数据。- 通过这种方式可以获取
history
等数据。