前言
- 本篇将react路由组件过一遍。初学时候被路由坑死了,不了解原理。现在看这些源码都不成问题,同时实现出简易的react路由一套,马上记录下。
react-router-dom的HashRouter
- 源码:
import React from "react";
import { Router } from "react-router";
import { createHashHistory as createHistory } from "history";
import PropTypes from "prop-types";
import warning from "tiny-warning";
/**
* The public API for a <Router> that uses window.location.hash.
*/
class HashRouter extends React.Component {
history = createHistory(this.props);
render() {
return <Router history={this.history} children={this.props.children} />;
}
}
if (__DEV__) {
HashRouter.propTypes = {
basename: PropTypes.string,
children: PropTypes.node,
getUserConfirmation: PropTypes.func,
hashType: PropTypes.oneOf(["hashbang", "noslash", "slash"])
};
HashRouter.prototype.componentDidMount = function () {
warning(
!this.props.history,
"<HashRouter> ignores the history prop. To use a custom history, " +
"use `import { Router }` instead of `import { HashRouter as Router }`."
);
};
}
export default HashRouter;
-
可以看见就是借用history和Router组件进行了个属性控制。
-
BrowserHistory同理,不放了。
react-router-dom的Link
- 源码:
import React from "react";
import { __RouterContext as RouterContext } from "react-router";
import PropTypes from "prop-types";
import invariant from "tiny-invariant";
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} />;
}
);
if (__DEV__) {
LinkAnchor.displayName = "LinkAnchor";
}
/**
* 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>
);
}
);
- 这个有点长,可以看见拿的是react-router中的__RouterContext作为上下文,上下文中有个history对象,这个就是前面Router组件里传入的history对象了,然后本质都是通过history对象上提供的各种路由跳转方法完成的。
- 本身是个函数组件,拿forwardref套了下,传递ref,兼容老版本react的属性上innerRef的写法。
- 最后的渲染就是渲染传入component,这个component是LinkAnchor,可以看见是个a标签,给他装了属性。
- 我将其简化一下,做成自己的link标签,也是可以跳转的:
const LinkAnchor = forwardRef(
(
{
innerRef, // TODO: deprecate
navigate,
onClick,
...rest
},ref
) => {
let props = {
...rest
};
return <a {...props} />;
}
);
const Mylink = forwardRef(
(
{
component=LinkAnchor,
replace,
to,
innerRef, // TODO: deprecate
...rest
},
forwardedRef
) => {
return (
<RouterContext.Consumer>
{context => {
const { history } = context;
const location = createLocation(to, null, null, context.location)
const href = location ? history.createHref(location) : "";
const props = {
...rest,
href
};
return React.createElement(component, props);
}}
</RouterContext.Consumer>
);
}
);
- 这就是个精简版的。
- 所以,其实这个link跟我们写a标签有多大区别?最大区别就是是browser还是hash路由都可以写同一个路由,如果你要用hash路由,用a标签得写个#号。而history路由的话,a标签会刷新页面,而link其实是借用history功能主动触发。
- 然后这个路由改变,其实是浏览器行为,浏览器把路由改变后,会影响context,从而进行条件渲染。
react-router的Router
- 源码:
import React from "react";
import PropTypes from "prop-types";
import warning from "tiny-warning";
import RouterContext from "./RouterContext";
/**
* The public API for putting history on context.
*/
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
}}
/>
);
}
}
if (__DEV__) {
Router.propTypes = {
children: PropTypes.node,
history: PropTypes.object.isRequired,
staticContext: PropTypes.object
};
Router.prototype.componentDidUpdate = function(prevProps) {
warning(
prevProps.history === this.props.history,
"You cannot change <Router history>"
);
};
}
export default Router;
- 我们知道react-router-dom实际就是给router传了个history和children,那么Router到底是咋回事?
- 这玩意其实就是必须要传入个history,这个history就是hash或者browser,然后放入这个组件的state中,
- 我们可以精简下写成这样:
class MyRouter 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: MyRouter.computeRootMatch(this.state.location.pathname),
staticContext: this.props.staticContext
}}
/>
);
}
}
- 这样就很清楚了,主要就是调history的listen监听浏览器路由改变,从而改变组件内state,组件卸载就卸载监听。
- 组件可能会加载慢,那么就等组件加载完改变组件内state。
- 最后通过context传递。
条件渲染的Route
- react-router-dom源码有这样句:
export {
MemoryRouter,
Prompt,
Redirect,
Route,
Router,
StaticRouter,
Switch,
generatePath,
matchPath,
withRouter,
useHistory,
useLocation,
useParams,
useRouteMatch
} from "react-router";
export { default as BrowserRouter } from "./BrowserRouter.js";
export { default as HashRouter } from "./HashRouter.js";
export { default as Link } from "./Link.js";
export { default as NavLink } from "./NavLink.js";
- 所以react-router-dom的route就是react-router的route。
- 看一下route源码:
import React from "react";
import { isValidElementType } from "react-is";
import PropTypes from "prop-types";
import invariant from "tiny-invariant";
import warning from "tiny-warning";
import RouterContext from "./RouterContext";
import matchPath from "./matchPath";
function isEmptyChildren(children) {
return React.Children.count(children) === 0;
}
function evalChildrenDev(children, props, path) {
const value = children(props);
warning(
value !== undefined,
"You returned `undefined` from the `children` function of " +
`<Route${path ? ` path="${path}"` : ""}>, but you ` +
"should have returned a React element or `null`"
);
return value || null;
}
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>
);
}
}
if (__DEV__) {
Route.propTypes = {
children: PropTypes.oneOfType([PropTypes.func, PropTypes.node]),
component: (props, propName) => {
if (props[propName] && !isValidElementType(props[propName])) {
return new Error(
`Invalid prop 'component' supplied to 'Route': the prop is not a valid React component`
);
}
},
exact: PropTypes.bool,
location: PropTypes.object,
path: PropTypes.oneOfType([
PropTypes.string,
PropTypes.arrayOf(PropTypes.string)
]),
render: PropTypes.func,
sensitive: PropTypes.bool,
strict: PropTypes.bool
};
Route.prototype.componentDidMount = function() {
warning(
!(
this.props.children &&
!isEmptyChildren(this.props.children) &&
this.props.component
),
"You should not use <Route component> and <Route children> in the same route; <Route component> will be ignored"
);
warning(
!(
this.props.children &&
!isEmptyChildren(this.props.children) &&
this.props.render
),
"You should not use <Route render> and <Route children> in the same route; <Route render> will be ignored"
);
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"
);
};
Route.prototype.componentDidUpdate = function(prevProps) {
warning(
!(this.props.location && !prevProps.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(
!(!this.props.location && prevProps.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.'
);
};
}
export default Route;
- 这个比较长,把一些警告无用的给删了后会发现它的构造很神奇。它既是消费者也是生产者,所以这就是多级路由必须写全才可以匹配的原因!多级路由下,它将从它开始,往下传递的context给改掉了。
- 精简后:
class MyRoute extends React.Component {
render() {
return (
<RouterContext.Consumer>
{context => {
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;
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.createElement(component, props)
: render
? render(props)
: null
: typeof children === "function"
? children(props)
: null
}
</RouterContext.Provider>
);
}}
</RouterContext.Consumer>
);
}
}
- 我们要在路由组件里传path和component,有时传exact,这个是在这个组件的props里。
- 在处理匹配时,会交给matchPath来进行计算。这个matchPatch是别人做的一个匹配url的正则搞得:
const cache = {};
const cacheLimit = 10000;
let cacheCount = 0;
function compilePath(path, options) {
const cacheKey = `${options.end}${options.strict}${options.sensitive}`;
const pathCache = cache[cacheKey] || (cache[cacheKey] = {});
if (pathCache[path]) return pathCache[path];
const keys = [];
const regexp = pathToRegexp(path, keys, options);
const result = { regexp, keys };
if (cacheCount < cacheLimit) {
pathCache[path] = result;
cacheCount++;
}
return result;
}
function matchPath(pathname, options = {}) {
if (typeof options === "string" || Array.isArray(options)) {
options = { path: options };
}
const { path, exact = false, strict = false, sensitive = false } = options;
const paths = [].concat(path);
return paths.reduce((matched, path) => {
if (!path && path !== "") return null;
if (matched) return matched;
const { regexp, keys } = compilePath(path, {
end: exact,
strict,
sensitive
});
const match = regexp.exec(pathname);
if (!match) return null;
const [url, ...values] = match;
const isExact = pathname === url;
if (exact && !isExact) return null;
return {
path, // the path used to match
url: path === "/" && url === "" ? "/" : url, // the matched portion of the URL
isExact, // whether or not we matched exactly
params: keys.reduce((memo, key, index) => {
memo[key.name] = values[index];
return memo;
}, {})
};
}, null);
}
- 可以看见为了防止重复运算,还做了个缓存对象。
- 其中,我们写的exact就会传给这个配置项end。然后搞出个匹配url的正则函数,通过正则匹配。
- 缓存那个计数写的有点神奇,下面那个判断是针对动态路由的,有点意思。同时防止缓存过多,一个路由上限缓存1w个。
- 然后它的判断是这样:正则不匹配就提前return 返回个null,匹配的话就返回个match对象。
- 最后渲染时候,不匹配渲染个null,匹配了才渲染。
- 渲染那个三目运算写的有点恶心,这里面有优先级问题,可以看见,children是最先匹配的,所以如果Route里面传children,那么不会渲染component render之类的。
测试用例
- 我简化的路由代码可以拿去试一下,跟原版可以同时使用:
import React, { Component, forwardRef } from 'react';
import { HashRouter as Router, Link, Route } from 'react-router-dom';
import reactDom from 'react-dom';
import { __RouterContext as RouterContext } from "react-router";
import { createLocation,createHashHistory } from "history";
import pathToRegexp from "path-to-regexp";
const Home = () => (
<div>
<h2>Home</h2>
</div>
)
const About = () => (
<div>
<h2>About</h2>
</div>
)
const Product = (props) => (
<div>
<h2>Product</h2>
<Mylink to={`${props.match.path}/j44`}>j4444</Mylink>
<MyRoute path={`${props.match.path}/j44`} component={About}></MyRoute>
</div>
)
const LinkAnchor = forwardRef(
(
{
innerRef, // TODO: deprecate
navigate,
onClick,
...rest
},ref
) => {
let props = {
...rest
};
return <a {...props} />;
}
);
const Mylink = forwardRef(
(
{
component=LinkAnchor,
replace,
to,
innerRef, // TODO: deprecate
...rest
},
forwardedRef
) => {
return (
<RouterContext.Consumer>
{context => {
const { history } = context;
const location = createLocation(to, null, null, context.location)
const href = location ? history.createHref(location) : "";
const props = {
...rest,
href
};
return React.createElement(component, props);
}}
</RouterContext.Consumer>
);
}
);
class MyRouter 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: MyRouter.computeRootMatch(this.state.location.pathname),
staticContext: this.props.staticContext
}}
/>
);
}
}
const cache = {};
const cacheLimit = 10000;
let cacheCount = 0;
function compilePath(path, options) {
const cacheKey = `${options.end}${options.strict}${options.sensitive}`;
const pathCache = cache[cacheKey] || (cache[cacheKey] = {});
if (pathCache[path]) return pathCache[path];
const keys = [];
const regexp = pathToRegexp(path, keys, options);
const result = { regexp, keys };
if (cacheCount < cacheLimit) {
pathCache[path] = result;
cacheCount++;
}
return result;
}
function matchPath(pathname, options = {}) {
if (typeof options === "string" || Array.isArray(options)) {
options = { path: options };
}
const { path, exact = false, strict = false, sensitive = false } = options;
const paths = [].concat(path);
return paths.reduce((matched, path) => {
if (!path && path !== "") return null;
if (matched) return matched;
const { regexp, keys } = compilePath(path, {
end: exact,
strict,
sensitive
});
const match = regexp.exec(pathname);
if (!match) return null;
const [url, ...values] = match;
const isExact = pathname === url;
if (exact && !isExact) return null;
return {
path, // the path used to match
url: path === "/" && url === "" ? "/" : url, // the matched portion of the URL
isExact, // whether or not we matched exactly
params: keys.reduce((memo, key, index) => {
memo[key.name] = values[index];
return memo;
}, {})
};
}, null);
}
class MyRoute extends React.Component {
render() {
return (
<RouterContext.Consumer>
{context => {
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;
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.createElement(component, props)
: render
? render(props)
: null
: typeof children === "function"
? children(props)
: null}
</RouterContext.Provider>
);
}}
</RouterContext.Consumer>
);
}
}
class App extends Component {
render() {
return (
<MyRouter history={ createHashHistory (this.props)}>
<div className="App">
<Link to="/">Home</Link>
<Link to="/About">About</Link>
<Link to="/Product">Product</Link>
<Mylink to='/j'>333</Mylink>
<a href='#/about'>dddddd</a>
<hr/>
<Route path="/j" exact component={About}></Route>
<Route path="/" exact component={Home}></Route>
<Route path="/product" component={Product}></Route>
<MyRoute path='/product' component={Product}></MyRoute>
<MyRoute path='/about' component={About}></MyRoute>
</div>
</MyRouter>
);
}
}
reactDom.render(
<App></App>
,document.getElementById('root')
)
总结
- react路由就是通过context传递history监听到的路由变化从而产生的对象。link之类就是通过history来生成跳转。组件条件渲染其实就是拿到history的当前url,与自己本身的url做正则匹配,匹配成功渲染,同时改变自己组件下的context。