前言
- 本篇將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。