注:react-router版本为v5.2.0
预备知识
1.前端基础:history、location。
2.react: refs转发、context、useContext(react Hooks)。
3.依赖库:
history(^4.9.0)
path-to-regexp(^1.7.0):主要用到pathToRegexp.compile(path)、pathToRegexp(path, keys, options)两个方法
history库(^4.9.0)
history库是react-router依赖的核心库,它将应用的history做了统一的抽象,包含一系列统一的属性和方法,支持浏览器的BrowserHistory、HashHistory以及服务端的MemoryHistory。
createBrowserHistory的属性和方法
length: globalHistory.length,
action: 'POP',
location: initialLocation,
createHref,
push,
replace,
go,
goBack,
goForward,
block,
listen
createHashHistory的属性和方法
length: globalHistory.length,
action: 'POP',
location: initialLocation,
createHref,
push,
replace,
go,
goBack,
goForward,
block,
listen
createMemoryHistory的属性和方法
length: entries.length,
action: 'POP',
location: entries[index],
index,
entries,
createHref,
push,
replace,
go,
goBack,
goForward,
canGo,
block,
listen
接下来我们讲解一下这三种history的具体实现。
createTransitionManager
createTransitionManager可以创建一个TransitionManager来帮助history管理各种行为,它被三种history都使用了,我们先来介绍它。
这是createTransitionManager的主要功能代码,很容易理解,就是实现了一个发布订阅模式。
let listeners = [];
function appendListener(fn) {
let isActive = true;
function listener(...args) {
if (isActive) fn(...args);
}
listeners.push(listener);
return () => {
isActive = false;
listeners = listeners.filter(item => item !== listener);
};
}
function notifyListeners(...args) {
listeners.forEach(listener => listener(...args));
}
setPrompt()是显示可提示用户进行输入的对话框的意思,这个功能主要是为了一些典型场景,比如:用户点击手机的返回键,让用户确认是否返回上一个url。
let prompt = null;
function setPrompt(nextPrompt) {
warning(prompt == null, 'A history supports only one prompt at a time');
prompt = nextPrompt;
return () => {
if (prompt === nextPrompt) prompt = null;
};
}
confirmTransitionTo在history的行为方法中(push、pop、replace)都会被调用,它的作用是拦截每个行为,让用户或开发者确认能否执行这个行为。
function confirmTransitionTo(
location,
action,
getUserConfirmation,
callback
) {
// TODO: If another transition starts while we're still confirming
// the previous one, we may end up in a weird state. Figure out the
// best way to handle this.
if (prompt != null) {
const result =
typeof prompt === 'function' ? prompt(location, action) : prompt;
if (typeof result === 'string') {
if (typeof getUserConfirmation === 'function') {
getUserConfirmation(result, callback);
} else {
warning(
false,
'A history needs a getUserConfirmation function in order to use a prompt message'
);
callback(true);
}
} else {
// Return false from a transition hook to cancel the transition.
callback(result !== false);
}
} else {
callback(true);
}
}
例如,push方法中confirmTransitionTo是这样使用的,在第四个参数callback中根据返回值是否为true,判断是否真正执行push行为。
function push(path, state) {
const action = 'PUSH';
// ...
transitionManager.confirmTransitionTo(
location,
action,
getUserConfirmation,
ok => {
if (!ok) return;
// ...
}
);
}
history.listen
history.listen在浏览器中主要是利用DOM方法进行事件监听的绑定和取消。
browserHistory中的实现
browserHistory使用的是popstate和hashchange事件。
同时会将监听触发的回调函数添加到前面介绍的transitionManager中,这样监听触发时只需要通过执行transitionManager.notifyListeners()发送通知,执行这些回调函数就可以了。
const PopStateEvent = 'popstate';
const HashChangeEvent = 'hashchange';
let listenerCount = 0;
function checkDOMListeners(delta) {
listenerCount += delta;
if (listenerCount === 1 && delta === 1) {
window.addEventListener(PopStateEvent, handlePopState);
if (needsHashChangeListener)
window.addEventListener(HashChangeEvent, handleHashChange);
} else if (listenerCount === 0) {
window.removeEventListener(PopStateEvent, handlePopState);
if (needsHashChangeListener)
window.removeEventListener(HashChangeEvent, handleHashChange);
}
}
function listen(listener) {
const unlisten = transitionManager.appendListener(listener);
checkDOMListeners(1);
return () => {
checkDOMListeners(-1);
unlisten();
};
}
在很多浏览器中hash change也会触发popstate事件,所以hashchange事件在browserHistory中也是需要监听的。
const needsHashChangeListener = !supportsPopStateOnHashChange();
export function supportsPopStateOnHashChange() {
return window.navigator.userAgent.indexOf('Trident') === -1;
}
hashHistory中的实现
hashHistory中只需要监听hashchange事件就可以了
const HashChangeEvent = 'hashchange';
let listenerCount = 0;
function checkDOMListeners(delta) {
listenerCount += delta;
if (listenerCount === 1 && delta === 1) {
window.addEventListener(HashChangeEvent, handleHashChange);
} else if (listenerCount === 0) {
window.removeEventListener(HashChangeEvent, handleHashChange);
}
}
memoryHistory中的实现
memoryHistory不需要监听事件,它只需要将监听触发的回调函数添加到transitionManager中就可以了。因为它是服务端主动控制的路由,不需要监听被动的路由改变,进而执行一些状态更新。
function listen(listener) {
return transitionManager.appendListener(listener);
}
browserHistory中handlePopState的实现
hashHistory和memoryHistory是没有popState事件的,所以不需要实现它们。
handlePopState主要会执行handlePop方法,handlePop主要会执行setState方法,setState方法主要是合并了history状态,通过transitionManager.notifyListeners通知了添加的listener函数执行。
getDOMLocation生成的就是我们经常见到的location参数。
{
pathname,
state,
hash,
search,
key,
...
}
function handlePopState(event) {
// Ignore extraneous popstate events in WebKit.
if (isExtraneousPopstateEvent(event)) return;
handlePop(getDOMLocation(event.state));
}
function handlePop(location) {
if (forceNextPop) {
forceNextPop = false;
setState();
} else {
const action = 'POP';
transitionManager.confirmTransitionTo(
location,
action,
getUserConfirmation,
ok => {
if (ok) {
setState({ action, location });
} else {
revertPop(location);
}
}
);
}
}
function setState(nextState) {
Object.assign(history, nextState);
history.length = globalHistory.length;
transitionManager.notifyListeners(history.location, history.action);
}
confirmTransitionTo的回调函数范围为false的时候,说明禁止进行这次路由操作。它调用revertPop方法实现,通过计算此次路由操作的delta,调用go(delta)方法将路由恢复到原来的状态,go方法就是原生的history.go方法。
function revertPop(fromLocation) {
const toLocation = history.location;
// TODO: We could probably make this more reliable by
// keeping a list of keys we've seen in sessionStorage.
// Instead, we just default to 0 for keys we don't know.
let toIndex = allKeys.indexOf(toLocation.key);
if (toIndex === -1) toIndex = 0;
let fromIndex = allKeys.indexOf(fromLocation.key);
if (fromIndex === -1) fromIndex = 0;
const delta = toIndex - fromIndex;
if (delta) {
forceNextPop = true;
go(delta);
}
}
const globalHistory = window.history;
function go(n) {
globalHistory.go(n);
}
handleHashChange
browserHistory中的实现
它调用的其实主要也是handlePop方法
function getHistoryState() {
try {
return window.history.state || {};
} catch (e) {
// IE 11 sometimes throws when accessing window.history.state
// See https://github.com/ReactTraining/history/pull/289
return {};
}
}
function handleHashChange() {
handlePop(getDOMLocation(getHistoryState()));
}
hashHistory中的实现
path !== encodedPath这个判断是为了让我们总是有标准的hash路径,后面的操作判断主要是判断一下前后的location是否相同、是否是ignorePath,如果都不是,则会执行handlePop方法。
function handleHashChange() {
const path = getHashPath();
const encodedPath = encodePath(path);
if (path !== encodedPath) {
// Ensure we always have a properly-encoded hash.
replaceHashPath(encodedPath);
} else {
const location = getDOMLocation();
const prevLocation = history.location;
if (!forceNextPop && locationsAreEqual(prevLocation, location)) return; // A hashchange doesn't always == location change.
if (ignorePath === createPath(location)) return; // Ignore this change; we already setState in push/replace.
ignorePath = null;
handlePop(location);
}
}
function replaceHashPath(path) {
const hashIndex = window.location.href.indexOf('#');
window.location.replace(
window.location.href.slice(0, hashIndex >= 0 ? hashIndex : 0) + '#' + path
);
}
handlePop和前面browserHistory介绍的是类似的,有区别的地方是revertPop使用的allPaths作为history的索引,browserHistory使用的allKeys。
function handlePop(location) {
if (forceNextPop) {
forceNextPop = false;
setState();
} else {
const action = 'POP';
transitionManager.confirmTransitionTo(
location,
action,
getUserConfirmation,
ok => {
if (ok) {
setState({ action, location });
} else {
revertPop(location);
}
}
);
}
}
function setState(nextState) {
Object.assign(history, nextState);
history.length = globalHistory.length;
transitionManager.notifyListeners(history.location, history.action);
}
function revertPop(fromLocation) {
const toLocation = history.location;
// TODO: We could probably make this more reliable by
// keeping a list of paths we've seen in sessionStorage.
// Instead, we just default to 0 for paths we don't know.
let toIndex = allPaths.lastIndexOf(createPath(toLocation));
if (toIndex === -1) toIndex = 0;
let fromIndex = allPaths.lastIndexOf(createPath(fromLocation));
if (fromIndex === -1) fromIndex = 0;
const delta = toIndex - fromIndex;
if (delta) {
forceNextPop = true;
go(delta);
}
}
const globalHistory = window.history;
function go(n) {
warning(
canGoWithoutReload,
'Hash history go(n) causes a full page reload in this browser'
);
globalHistory.go(n);
}
allPaths是完整的路径
export function createPath(location) {
const { pathname, search, hash } = location;
let path = pathname || '/';
if (search && search !== '?')
path += search.charAt(0) === '?' ? search : `?${search}`;
if (hash && hash !== '#') path += hash.charAt(0) === '#' ? hash : `#${hash}`;
return path;
}
allKeys是随机的key
function createKey() {
return Math.random()
.toString(36)
.substr(2, keyLength);
}
history.push
browserHistory中的实现
history.push方法很简单,主要调用了history.pushState方法。由于allKeys维护了所有history state中的key,所以在push方法需要做相应的处理。
const globalHistory = window.history;
function push(path, state) {
warning(
!(
typeof path === 'object' &&
path.state !== undefined &&
state !== undefined
),
'You should avoid providing a 2nd state argument to push when the 1st ' +
'argument is a location-like object that already has state; it is ignored'
);
const action = 'PUSH';
const location = createLocation(path, state, createKey(), history.location);
transitionManager.confirmTransitionTo(
location,
action,
getUserConfirmation,
ok => {
if (!ok) return;
const href = createHref(location);
const { key, state } = location;
if (canUseHistory) {
globalHistory.pushState({ key, state }, null, href);
if (forceRefresh) {
window.location.href = href;
} else {
const prevIndex = allKeys.indexOf(history.location.key);
const nextKeys = allKeys.slice(
0,
prevIndex === -1 ? 0 : prevIndex + 1
);
nextKeys.push(location.key);
allKeys = nextKeys;
setState({ action, location });
}
} else {
warning(
state === undefined,
'Browser history cannot push state in browsers that do not support HTML5 history'
);
window.location.href = href;
}
}
);
}
hashHistory中的实现
history.push方法很简单,主要调用了window.location.hash方法。由于allPaths维护了所有的path,所以在push方法需要做相应的处理。
function push(path, state) {
warning(
state === undefined,
'Hash history cannot push state; it is ignored'
);
const action = 'PUSH';
const location = createLocation(
path,
undefined,
undefined,
history.location
);
transitionManager.confirmTransitionTo(
location,
action,
getUserConfirmation,
ok => {
if (!ok) return;
const path = createPath(location);
const encodedPath = encodePath(basename + path);
const hashChanged = getHashPath() !== encodedPath;
if (hashChanged) {
// We cannot tell if a hashchange was caused by a PUSH, so we'd
// rather setState here and ignore the hashchange. The caveat here
// is that other hash histories in the page will consider it a POP.
ignorePath = path;
pushHashPath(encodedPath);
const prevIndex = allPaths.lastIndexOf(createPath(history.location));
const nextPaths = allPaths.slice(
0,
prevIndex === -1 ? 0 : prevIndex + 1
);
nextPaths.push(path);
allPaths = nextPaths;
setState({ action, location });
} else {
warning(
false,
'Hash history cannot PUSH the same path; a new entry will not be added to the history stack'
);
setState();
}
}
);
}
function pushHashPath(path) {
window.location.hash = path;
}
memoryHistory中的实现
由于是在内存中维护history的状态,所以主要是history.entries(所有history location列表)的维护。
function push(path, state) {
warning(
!(
typeof path === 'object' &&
path.state !== undefined &&
state !== undefined
),
'You should avoid providing a 2nd state argument to push when the 1st ' +
'argument is a location-like object that already has state; it is ignored'
);
const action = 'PUSH';
const location = createLocation(path, state, createKey(), history.location);
transitionManager.confirmTransitionTo(
location,
action,
getUserConfirmation,
ok => {
if (!ok) return;
const prevIndex = history.index;
const nextIndex = prevIndex + 1;
const nextEntries = history.entries.slice(0);
if (nextEntries.length > nextIndex) {
nextEntries.splice(
nextIndex,
nextEntries.length - nextIndex,
location
);
} else {
nextEntries.push(location);
}
setState({
action,
location,
index: nextIndex,
entries: nextEntries
});
}
);
}
function setState(nextState) {
Object.assign(history, nextState);
history.length = history.entries.length;
transitionManager.notifyListeners(history.location, history.action);
}
history.replace()
browerHistory中的实现
history.replace方法和push是类似的,主要调用了history.replaceState方法。
function replace(path, state) {
warning(
!(
typeof path === 'object' &&
path.state !== undefined &&
state !== undefined
),
'You should avoid providing a 2nd state argument to replace when the 1st ' +
'argument is a location-like object that already has state; it is ignored'
);
const action = 'REPLACE';
const location = createLocation(path, state, createKey(), history.location);
transitionManager.confirmTransitionTo(
location,
action,
getUserConfirmation,
ok => {
if (!ok) return;
const href = createHref(location);
const { key, state } = location;
if (canUseHistory) {
globalHistory.replaceState({ key, state }, null, href);
if (forceRefresh) {
window.location.replace(href);
} else {
const prevIndex = allKeys.indexOf(history.location.key);
if (prevIndex !== -1) allKeys[prevIndex] = location.key;
setState({ action, location });
}
} else {
warning(
state === undefined,
'Browser history cannot replace state in browsers that do not support HTML5 history'
);
window.location.replace(href);
}
}
);
}
hashHistory中的实现
replace在hashHistory中的实现也很简单,主要调用了window.location.replace方法。
function replace(path, state) {
warning(
state === undefined,
'Hash history cannot replace state; it is ignored'
);
const action = 'REPLACE';
const location = createLocation(
path,
undefined,
undefined,
history.location
);
transitionManager.confirmTransitionTo(
location,
action,
getUserConfirmation,
ok => {
if (!ok) return;
const path = createPath(location);
const encodedPath = encodePath(basename + path);
const hashChanged = getHashPath() !== encodedPath;
if (hashChanged) {
// We cannot tell if a hashchange was caused by a REPLACE, so we'd
// rather setState here and ignore the hashchange. The caveat here
// is that other hash histories in the page will consider it a POP.
ignorePath = path;
replaceHashPath(encodedPath);
}
const prevIndex = allPaths.indexOf(createPath(history.location));
if (prevIndex !== -1) allPaths[prevIndex] = path;
setState({ action, location });
}
);
}
function replaceHashPath(path) {
const hashIndex = window.location.href.indexOf('#');
window.location.replace(
window.location.href.slice(0, hashIndex >= 0 ? hashIndex : 0) + '#' + path
);
}
memoryHistory中的实现
function replace(path, state) {
warning(
!(
typeof path === 'object' &&
path.state !== undefined &&
state !== undefined
),
'You should avoid providing a 2nd state argument to replace when the 1st ' +
'argument is a location-like object that already has state; it is ignored'
);
const action = 'REPLACE';
const location = createLocation(path, state, createKey(), history.location);
transitionManager.confirmTransitionTo(
location,
action,
getUserConfirmation,
ok => {
if (!ok) return;
history.entries[history.index] = location;
setState({ action, location });
}
);
}
history.go()、history.goBack()、history.goForward()
browserHistory、hashHistory中的实现
function go(n) {
globalHistory.go(n);
}
function goBack() {
go(-1);
}
function goForward() {
go(1);
}
memoryHistory中的实现
计算nextIndex(一般为history.index + n),执行POP action即可。
function clamp(n, lowerBound, upperBound) {
return Math.min(Math.max(n, lowerBound), upperBound);
}
function go(n) {
const nextIndex = clamp(history.index + n, 0, history.entries.length - 1);
const action = 'POP';
const location = history.entries[nextIndex];
transitionManager.confirmTransitionTo(
location,
action,
getUserConfirmation,
ok => {
if (ok) {
setState({
action,
location,
index: nextIndex
});
} else {
// Mimic the behavior of DOM histories by
// causing a render after a cancelled POP.
setState();
}
}
);
}
function goBack() {
go(-1);
}
function goForward() {
go(1);
}
history.block()
browserHistory、hashHistory中的实现
block提供了setPrompt的调用接口,因为我们前面介绍过,push、pop、replace action都是在transitionManager.confirmTransitionTo的回调函数中执行的,只有回调函数返回true,才能真正执行这些action。而前面我们看到回调函数的返回结果其实是由用户传递的prompt方法决定的,这样就可以让用户根据自己的逻辑决定是否阻塞路由跳转了。
let isBlocked = false;
function block(prompt = false) {
const unblock = transitionManager.setPrompt(prompt);
if (!isBlocked) {
checkDOMListeners(1);
isBlocked = true;
}
return () => {
if (isBlocked) {
isBlocked = false;
checkDOMListeners(-1);
}
return unblock();
};
}
memoryHistory中的实现
memoryHistory不需要做DOM事件监听的相关处理。
function block(prompt = false) {
return transitionManager.setPrompt(prompt);
}
react-router
我们之所以大篇幅介绍history库,是因为history库才是路由管理的底层逻辑,react-router其实只是使用react框架封装了history库的处理(主要使用context跨组件传递history的状态和方法)。介绍到这,你是不是已经能够大致勾勒出诸如<BrowserRouter>
、<Route>
、<Switch>
、<Link>
、withRouter()
等的简单实现了呢?介绍来让我们看看react-router中具体是怎么实现的。
createNamedContext()
该方法可以创建有displayName的context。
// TODO: Replace with React.createContext once we can assume React 16+
import createContext from "mini-create-react-context";
const createNamedContext = name => {
const context = createContext();
context.displayName = name;
return context;
};
export default createNamedContext;
generatePath()
生成路径,主要调用的是pathToRegexp.compile()方法,generatePath可以根据路径path和参数params生成完整的路径。比如('/a/:id', { id: 1 }) -> '/a/1'
。
import pathToRegexp from "path-to-regexp";
const cache = {};
const cacheLimit = 10000;
let cacheCount = 0;
function compilePath(path) {
if (cache[path]) return cache[path];
const generator = pathToRegexp.compile(path);
if (cacheCount < cacheLimit) {
cache[path] = generator;
cacheCount++;
}
return generator;
}
/**
* Public API for generating a URL pathname from a path and parameters.
*/
function generatePath(path = "/", params = {}) {
return path === "/" ? path : compilePath(path)(params, { pretty: true });
}
export default generatePath;
matchPath()
该方法传入pathname,以及解析pathname的配置,可以得到从pathname中匹配的结果。这是我们使用react-router经常见到的数据,没错,它就是通过matchPath方法解析的。
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;
}, {})
};
import pathToRegexp from "path-to-regexp";
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;
}
/**
* Public API for matching a URL pathname to a path.
*/
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);
}
export default matchPath;
historyContext
创建historyContext。
import createNamedContext from "./createNameContext";
const historyContext = /*#__PURE__*/ createNamedContext("Router-History");
export default historyContext;
routerContext
创建routerContext。这里源码的写法有冗余了。
// TODO: Replace with React.createContext once we can assume React 16+
import createContext from "mini-create-react-context";
const createNamedContext = name => {
const context = createContext();
context.displayName = name;
return context;
};
const context = /*#__PURE__*/ createNamedContext("Router");
export default context;
Lifecycle
创建一个react组件,它是一个空组件,主要是为了在组件生命周期的各个阶段能够调用用户通过props传入的回调函数。
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;
Router
<Router>
是我们很常用的组件,有了前面的知识铺垫,它的实现就非常简单了。
组件内部有一个location的state,如果不是静态路由,通过history.listen方法监听history的变化。这里的history就是我们前面介绍的history库生成的history,它可以采用browserHistory、hashHistory、memoryHistory,history库对这三种history做了一致的接口封装。history如果发生改变,就是调用this.setState({ location })
,组件重新渲染,RouterContext.Provider、HistoryContext.Provider的值更新,它们下面的跨级组件也能感知到,从而获得最新的参数和方法。
import React from "react";
import PropTypes from "prop-types";
import warning from "tiny-warning";
import HistoryContext from "./HistoryContext.js";
import RouterContext from "./RouterContext.js";
/**
* 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._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
value={{
history: this.props.history,
location: this.state.location,
match: Router.computeRootMatch(this.state.location.pathname),
staticContext: this.props.staticContext
}}
>
<HistoryContext.Provider
children={this.props.children || null}
value={this.props.history}
/>
</RouterContext.Provider>
);
}
}
export default Router;
Route
使用RouterContext.Consumer可以感知到上层RouterContext.Provider值的变动,从而自动计算match,根据match的结果渲染匹配的业务组件(使用props传入children, component, render方法之一)。
如果有computedMatch属性说明在<Switch>
组件中已经计算了match,可以直接使用。Switch组件我们后面会介绍。
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.js";
import matchPath from "./matchPath.js";
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;
}
/**
* 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;
Redirect
重定向组件根据传入的push属性可以决定使用history.push还是history.replace进行重定向,根据传入computedMatch, to可以计算出重定向的location。如果在静态组件中,会直接执行重定向。如果不是,采用使用空组件Lifecycle,在组件挂载阶段重定向,在onUpdate中判断重定向是否完成。
import React from "react";
import PropTypes from "prop-types";
import { createLocation, locationsAreEqual } from "history";
import invariant from "tiny-invariant";
import Lifecycle from "./Lifecycle.js";
import RouterContext from "./RouterContext.js";
import generatePath from "./generatePath.js";
/**
* 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;
Switch
被Switch组件包裹的组件只会渲染其中第一个路由匹配成功的组件。
主要通过React.Children.forEach(this.props.children, child => {}),遍历出第一个匹配的路由及组件,并通过React.cloneElement返回这个组件。
import React from "react";
import PropTypes from "prop-types";
import invariant from "tiny-invariant";
import warning from "tiny-warning";
import RouterContext from "./RouterContext.js";
import matchPath from "./matchPath.js";
/**
* 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;
StaticRouter
静态路由组件自己实现了一个简单的history,没有监听history变化的概念,也不需要go、goBack、goForward、listen、block方法。
import React from "react";
import PropTypes from "prop-types";
import { createLocation, createPath } from "history";
import invariant from "tiny-invariant";
import warning from "tiny-warning";
import Router from "./Router.js";
function addLeadingSlash(path) {
return path.charAt(0) === "/" ? path : "/" + path;
}
function addBasename(basename, location) {
if (!basename) return location;
return {
...location,
pathname: addLeadingSlash(basename) + location.pathname
};
}
function stripBasename(basename, location) {
if (!basename) return location;
const base = addLeadingSlash(basename);
if (location.pathname.indexOf(base) !== 0) return location;
return {
...location,
pathname: location.pathname.substr(base.length)
};
}
function createURL(location) {
return typeof location === "string" ? location : createPath(location);
}
function staticHandler(methodName) {
return () => {
invariant(false, "You cannot %s with <StaticRouter>", methodName);
};
}
function noop() {}
/**
* The public top-level API for a "static" <Router>, so-called because it
* can't actually change the current location. Instead, it just records
* location changes in a context object. Useful mainly in testing and
* server-rendering scenarios.
*/
class StaticRouter extends React.Component {
navigateTo(location, action) {
const { basename = "", context = {} } = this.props;
context.action = action;
context.location = addBasename(basename, createLocation(location));
context.url = createURL(context.location);
}
handlePush = location => this.navigateTo(location, "PUSH");
handleReplace = location => this.navigateTo(location, "REPLACE");
handleListen = () => noop;
handleBlock = () => noop;
render() {
const { basename = "", context = {}, location = "/", ...rest } = this.props;
const history = {
createHref: path => addLeadingSlash(basename + createURL(path)),
action: "POP",
location: stripBasename(basename, createLocation(location)),
push: this.handlePush,
replace: this.handleReplace,
go: staticHandler("go"),
goBack: staticHandler("goBack"),
goForward: staticHandler("goForward"),
listen: this.handleListen,
block: this.handleBlock
};
return <Router {...rest} history={history} staticContext={context} />;
}
}
export default StaticRouter;
MemoryRouter
MemoryRouter的history指定使用了createMemoryHistory,内部逻辑就是Router的逻辑。
import React from "react";
import PropTypes from "prop-types";
import { createMemoryHistory as createHistory } from "history";
import warning from "tiny-warning";
import Router from "./Router.js";
/**
* The public API for a <Router> that stores location in memory.
*/
class MemoryRouter extends React.Component {
history = createHistory(this.props);
render() {
return <Router history={this.history} children={this.props.children} />;
}
}
export default MemoryRouter;
Prompt
Prompt组件当Router不是staticRouter且when属性为true时才生效。
调用的是前面介绍的history.block()方法。
import React from "react";
import PropTypes from "prop-types";
import invariant from "tiny-invariant";
import Lifecycle from "./Lifecycle.js";
import RouterContext from "./RouterContext.js";
/**
* The public API for prompting the user before navigating away from a screen.
*/
function Prompt({ message, when = true }) {
return (
<RouterContext.Consumer>
{context => {
invariant(context, "You should not use <Prompt> outside a <Router>");
if (!when || context.staticContext) return null;
const method = context.history.block;
return (
<Lifecycle
onMount={self => {
self.release = method(message);
}}
onUpdate={(self, prevProps) => {
if (prevProps.message !== message) {
self.release();
self.release = method(message);
}
}}
onUnmount={self => {
self.release();
}}
message={message}
/>
);
}}
</RouterContext.Consumer>
);
}
export default Prompt;
withRouter
由于从RouterContext.Consumer的context中可以很方便取到路由参数,所以withRouter就很容易实现了。只需要使用高阶组件的形式,接收被包裹组件作为参数,将context作为参数传入被包裹组件组件,再返回这个组件即可。
component还暴露了wrappedComponentRef属性,可以转发ref。
import React from "react";
import PropTypes from "prop-types";
import hoistStatics from "hoist-non-react-statics";
import invariant from "tiny-invariant";
import RouterContext from "./RouterContext.js";
/**
* 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;
hooks
react-router还使用了useContext hook使用react hook的方式来提供一些路由参数和history。
import React from "react";
import invariant from "tiny-invariant";
import Context from "./RouterContext.js";
import HistoryContext from "./HistoryContext.js";
import matchPath from "./matchPath.js";
const useContext = React.useContext;
export function useHistory() {
return useContext(HistoryContext);
}
export function useLocation() {
return useContext(Context).location;
}
export function useParams() {
const match = useContext(Context).match;
return match ? match.params : {};
}
export function useRouteMatch(path) {
const location = useLocation();
const match = useContext(Context).match;
return path ? matchPath(location.pathname, path) : match;
}
react-router-dom
react-router中还包括react-router-dom库的实现,来提供dom相关的路由操作。
我们在react工程中一般使用的就是react-router-dom库,它的底层是前面介绍的react-router。
BrowserRouter
我们在项目中使用HTML5 history控制路由,可以直接使用react-router-dom中的BrowserRouter。
import React from "react";
import { Router } from "react-router";
import { createBrowserHistory as createHistory } from "history";
import PropTypes from "prop-types";
import warning from "tiny-warning";
/**
* The public API for a <Router> that uses HTML5 history.
*/
class BrowserRouter extends React.Component {
history = createHistory(this.props);
render() {
return <Router history={this.history} children={this.props.children} />;
}
}
export default BrowserRouter;
HashRouter
我们在项目中使用window.location.hash控制路由,可以直接使用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} />;
}
}
export default HashRouter;
Link
<Link>
组件是react-router中常见的路由跳转组件。它使用的是html的a标签,为其绑定了点击事件。用户点击时,既可以执行用户自定义的onClick回调函数,也会执行navigate -> method(location),method可以根据用户传入的replace参数决定是使用history.replace还是history.push,同时点击事件也会阻止事件冒泡以免产生副作用。
Link还暴露了forwardedRef属性,可以转发ref。
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.js";
// 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;
}
/* eslint-disable-next-line jsx-a11y/anchor-has-content */
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>
);
}
);
export default Link;
NavLink
NavLink是基于Link的,它主要功能是可以自定义设置一些activeStyle、className,从而改变Link的样式。
import React from "react";
import { __RouterContext as RouterContext, matchPath } from "react-router";
import PropTypes from "prop-types";
import invariant from "tiny-invariant";
import Link from "./Link.js";
import {
resolveToLocation,
normalizeToLocation
} from "./utils/locationUtils.js";
// React 15 compat
const forwardRefShim = C => C;
let { forwardRef } = React;
if (typeof forwardRef === "undefined") {
forwardRef = forwardRefShim;
}
function joinClassnames(...classnames) {
return classnames.filter(i => i).join(" ");
}
/**
* A <Link> wrapper that knows if it's "active" or not.
*/
const NavLink = forwardRef(
(
{
"aria-current": ariaCurrent = "page",
activeClassName = "active",
activeStyle,
className: classNameProp,
exact,
isActive: isActiveProp,
location: locationProp,
sensitive,
strict,
style: styleProp,
to,
innerRef, // TODO: deprecate
...rest
},
forwardedRef
) => {
return (
<RouterContext.Consumer>
{context => {
invariant(context, "You should not use <NavLink> outside a <Router>");
const currentLocation = locationProp || context.location;
const toLocation = normalizeToLocation(
resolveToLocation(to, currentLocation),
currentLocation
);
const { pathname: path } = toLocation;
// Regex taken from: https://github.com/pillarjs/path-to-regexp/blob/master/index.js#L202
const escapedPath =
path && path.replace(/([.+*?=^!:${}()[\]|/\\])/g, "\\$1");
const match = escapedPath
? matchPath(currentLocation.pathname, {
path: escapedPath,
exact,
sensitive,
strict
})
: null;
const isActive = !!(isActiveProp
? isActiveProp(match, currentLocation)
: match);
const className = isActive
? joinClassnames(classNameProp, activeClassName)
: classNameProp;
const style = isActive ? { ...styleProp, ...activeStyle } : styleProp;
const props = {
"aria-current": (isActive && ariaCurrent) || null,
className,
style,
to: toLocation,
...rest
};
// React 15 compat
if (forwardRefShim !== forwardRef) {
props.ref = forwardedRef || innerRef;
} else {
props.innerRef = innerRef;
}
return <Link {...props} />;
}}
</RouterContext.Consumer>
);
}
);
export default NavLink;
react-router-config
react-router-config是为了方便我们使用类似下面的配置来编写react-router
const routes = [
{
component: Root,
routes: [
{
path: "/",
exact: true,
component: Home
},
{
path: "/child/:id",
component: Child,
routes: [
{
path: "/child/:id/grand-child",
component: GrandChild
}
]
}
]
}
];
它只有两个api,matchRoutes和renderRoutes。
matchRoutes
import { matchPath, Router } from "react-router";
function matchRoutes(routes, pathname, /*not public API*/ branch = []) {
routes.some(route => {
const match = route.path
? matchPath(pathname, route)
: branch.length
? branch[branch.length - 1].match // use parent match
: Router.computeRootMatch(pathname); // use default "root" match
if (match) {
branch.push({ route, match });
if (route.routes) {
matchRoutes(route.routes, pathname, branch);
}
}
return match;
});
return branch;
}
export default matchRoutes;
renderRoutes
renderRoutes在组件中使用,可以根据前面的路由配置渲染相应的组件。
import React from "react";
import { Switch, Route } from "react-router";
function renderRoutes(routes, extraProps = {}, switchProps = {}) {
return routes ? (
<Switch {...switchProps}>
{routes.map((route, i) => (
<Route
key={route.key || i}
path={route.path}
exact={route.exact}
strict={route.strict}
render={props =>
route.render ? (
route.render({ ...props, ...extraProps, route: route })
) : (
<route.component {...props} {...extraProps} route={route} />
)
}
/>
))}
</Switch>
) : null;
}
export default renderRoutes;
使用示例如下:
import { renderRoutes } from "react-router-config";
const routes = [
{
component: Root,
routes: [
{
path: "/",
exact: true,
component: Home
},
{
path: "/child/:id",
component: Child,
routes: [
{
path: "/child/:id/grand-child",
component: GrandChild
}
]
}
]
}
];
const Root = ({ route }) => (
<div>
<h1>Root</h1>
{/* child routes won't render without this */}
{renderRoutes(route.routes)}
</div>
);
const Home = ({ route }) => (
<div>
<h2>Home</h2>
</div>
);
const Child = ({ route }) => (
<div>
<h2>Child</h2>
{/* child routes won't render without this */}
{renderRoutes(route.routes, { someProp: "these extra props are optional" })}
</div>
);
const GrandChild = ({ someProp }) => (
<div>
<h3>Grand Child</h3>
<div>{someProp}</div>
</div>
);
ReactDOM.render(
<BrowserRouter>
{/* kick it all off with the root route */}
{renderRoutes(routes)}
</BrowserRouter>,
document.getElementById("root")
);
react-router-native
react-router里最后一个包是react-router-native,因为没有做过相关业务,就没有研究了。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。