注: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 compatconst 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 compatconst 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,因为没有做过相干业务,就没有钻研了。