乐趣区

关于前端:reactrouter源码完全解读

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

退出移动版