聊聊BrowserRouter的内部实现

聊聊BrowserRouter的内部实现

2 年前 · 来自专栏 React技术栈

最近开始翻源码,我们都知道react-router有两种模式,一种是BrowserRouter、一种是HashRouter。所以今天主要聊聊 BrowserRouter 的内部实现。

入口

BrowserRouter 将会监听 URL 的变化,当 URL 变更时,它将使浏览器显示相应的页面。

  • BrowserRouter本身是一个类组件,还是一个高阶组件,在内部创建一个全局的 history 对象(可以监听整个路由的变化),并将 history 作为 props 传递给 react-router 的 Router 组件(Router 组件再会将这个 history 的属性作为 context 传递给子组件)
class BrowserRouter extends React.Component {
  history = createHistory(this.props);
  render() {
    return <Router history={this.history} children={this.props.children} />;
export default BrowserRouter;
<BrowserRouter basename="/calendar">
    <Link to="/today"/> // renders <a href="/calendar/today">
    <Link to="/tomorrow"/> // renders <a href="/calendar/tomorrow">
</BrowserRouter>
<BrowserRouter
  getUserConfirmation={(message, callback) => {
    // this is the default behavior
    const allowTransition = window.confirm(message);
    callback(allowTransition);
<BrowserRouter forceRefresh={true} />
<BrowserRouter keyLength={12} />


Router与BrowserRouter


结合上述示例我们可以看出Router所接受的props主要就是history、children。

createHistory 创建history对象

那么接下来我们主要关注的是history是如何创建的。

import { createBrowserHistory as createHistory } from "history";

createHistory方法来自history库的 createBrowserHistory 方法。方法。

这里顺便讲述下history、react-router、react-router-dom三者之间关系。

- react-router: 是底层核心库,里面封装了Router,Route,Switch等核心组件,实现了从路由的改变到组件的更新的核心功能
- react-router-dom: 在react-router的核心基础上,添加了用于跳转的Link、NavLink组件,和histoy模式下的BrowserRouter和hash模式下的HashRouter组件等。所谓BrowserRouter和HashRouter,也只不过用了history库中createBrowserHistory和createHashHistory方法
- history:是整个路由原理的核心,里面集成了各类history等底层路由实现的原理方法


这里有挺多细节可以慢慢看, 看源码建议先看入参、反参、知道大致流程,再深挖细节。

function createBrowserHistory(props = {}) {
  // props来自BrowserRouter组件 this.props
  const history = {
    // window.history属性长度
    length: globalHistory.length,
    // history 当前行为(包含PUSH-进入、POP-弹出、REPLACE-替换)
    action: 'POP',
    // location对象(与地址有关)
    location: initialLocation,
    // 当前地址(包含pathname)
    createHref,
    // 跳转的方法
    push,
    replace,
    goBack,
    goForward,
    // 截取
    block,
    // 监听
    listen
  // 返回一个history对象
  return history;
export default createBrowserHistory;

createBrowserHistory方法的目的就是:根据HTML5 history API来创建一个history对象。

判断当前环境、是否可以操作DOM

我们都知道react-router根据不同平台拆分为了不同包,react-router-dom、react-router-native。

我们这里指的是浏览器端环境,所有createBrowserHistory方法内需要先判断当前是否是浏览器环境,是否可以操作DOM。

function createBrowserHistory(props = {}) {
  invariant(canUseDOM, 'Browser history needs a DOM');
export const canUseDOM = !!(
  typeof window !== 'undefined' &&
  window.document &&
  window.document.createElement

判断是否支持HTML5 history API

function createBrowserHistory(props = {}) {
  invariant(canUseDOM, 'Browser history needs a DOM');
  // 挂载history对象
  const globalHistory = window.history;
  // 判断是否支持H5 history
  const canUseHistory = supportsHistory();
 * Returns true if the HTML5 history API is supported. Taken from Modernizr.
 * https://github.com/Modernizr/Modernizr/blob/master/LICENSE
 * https://github.com/Modernizr/Modernizr/blob/master/feature-detects/history.js
 * changed to avoid false negatives for Windows Phones: https://github.com/reactjs/react-router/issues/586
export function supportsHistory() {
  const ua = window.navigator.userAgent;
  // 通过userAgent排除一部分终端
  if (
    (ua.indexOf('Android 2.') !== -1 || ua.indexOf('Android 4.0') !== -1) &&
    ua.indexOf('Mobile Safari') !== -1 &&
    ua.indexOf('Chrome') === -1 &&
    ua.indexOf('Windows Phone') === -1
    return false;
 // 根据API判断
  return window.history && 'pushState' in window.history;

另外这里也建议大家养成良好习惯,边界判断逻辑要封装好、并且置于主函数作用域顶部执行。

接下来处理basename

function createBrowserHistory(props = {}) {
  invariant(canUseDOM, 'Browser history needs a DOM');
  // 挂载history对象
  const globalHistory = window.history;
  // 判断是否支持H5 history
  const canUseHistory = supportsHistory();
  const needsHashChangeListener = !supportsPopStateOnHashChange();
  // 从props取出对应值、并且提供一些默认值
  const {
    forceRefresh = false,
    getUserConfirmation = getConfirmation,
    keyLength = 6
  } = props;
  // 对basename处理 斜杠/的位置
  const basename = props.basename
    ? stripTrailingSlash(addLeadingSlash(props.basename))
    : '';
 * Returns true if browser fires popstate on hash change.
 * IE10 and IE11 do not.
export function supportsPopStateOnHashChange() {
  return window.navigator.userAgent.indexOf('Trident') === -1;
// 处理basename使其最终形成的path是带有`/${basename}`
export function addLeadingSlash(path) {
  return path.charAt(0) === '/' ? path : '/' + path;
// 去除path尾部多余的"/"
export function stripTrailingSlash(path) {
  return path.charAt(path.length - 1) === '/' ? path.slice(0, -1) : path;

createTransitionManager

创建一个路由切换的管理器。返回一个集成对象,对象中包含了关于history地址或者对象改变时候的监听函数。

function createTransitionManager() {
  // 把用户设置的提示信息存储在prompt变量
  let prompt = null;
  // 用于设置url跳转时弹出的文字提示
  function setPrompt(nextPrompt) {
    // 提示prompt只能存在一个
    warning(prompt == null, 'A history supports only one prompt at a time');
    prompt = nextPrompt;
    // 解除
    return () => {
      if (prompt === nextPrompt) prompt = null;
  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) {
     // prompt 可以是一个函数,如果是一个函数返回执行的结果
      const result =
        typeof prompt === 'function' ? prompt(location, action) : prompt;
      if (typeof result === 'string') {
        if (typeof getUserConfirmation === 'function') {
          // 默认 调用window.confirm来显示提示信息
          // callback接收用户 选择了true或者false
          getUserConfirmation(result, callback);
        } else {
         // 提示开发者 getUserConfirmatio应该是一个function来展示阻止路由跳转的提示
          warning(
            false,
            'A history needs a getUserConfirmation function in order to use a prompt message'
         // 相当于用户选择true 不进行拦截
          callback(true);
      } else {
        // Return false from a transition hook to cancel the transition.
        callback(result !== false);
    } else {
     // 当不存在prompt时,直接执行回调函数,进行路由的切换和rerender
      callback(true);
  // 被subscribe的列表,即在Router组件添加的setState方法,每次push replace 或者 go等操作都会触发
  let listeners = [];
  // 将回调函数添加到listeners,一个发布订阅模式
  function appendListener(fn) {
    let isActive = true;
    function listener(...args) {
      // 为什么要在这里判断isActive
      // 其实是为了避免一种情况,比如注册了多个listeners: a,b,c 但是在a函数中注销了b函数
      // 理论上来说b函数应该不能在执行了,但是注销方法里使用的是数组的filter,每次返回的是一个新的listeners引用
      // 故每次解绑如果不添加isActive这个开关,那么当前循环还是会执行b的事件。加上isActive后,原始的liteners中
      // 的闭包b函数的isActive会变为false,从而阻止事件的执行,当循环结束后,原始的listeners也会被gc回收
      if (isActive) fn(...args);
    listeners.push(listener);
    // 返回的是一个函数
    return () => {
      isActive = false;
      // listeners是未加入之前的队列
      listeners = listeners.filter(item => item !== listener);
  // 通知被订阅的事件开始执行
  function notifyListeners(...args) {
    listeners.forEach(listener => listener(...args));
  return {
    setPrompt,
    confirmTransitionTo,
    appendListener,
    notifyListeners
export default createTransitionManager;

任何对地址栏的更新都会经过confirmTransitionTo 这个方法进行验证。

// getUserConfirmation默认是getConfirmation
export function getConfirmation(message, callback) {
  callback(window.confirm(message)); // eslint-disable-line no-alert


上述的边界判断、基础状态处理完之后,我们挨个看看history对象具体的实现逻辑。

const history = {
    // 返回一个整数,该整数表示会话历史中元素的数目,包括当前加载的页。
    // 例如,在一个新的选项卡加载的一个页面中,这个属性返回1。只读属性
    length: globalHistory.length, 
    action: 'POP',
    location: initialLocation,
    createHref,
    push,
    replace,
    goBack,
    goForward,
    block,
    listen

location

看看history.location对象是如何创建的。

const initialLocation = getDOMLocation(getHistoryState());
// 获取原生的histoty.state 只读属性
// 返回在 history 栈顶的 任意 值的拷贝。 
// 通过这种方式可以查看 state 值,不必等待 popstate事件发生后再查看。
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 {};

关于window.history.state的值默认是null,只有调用了pushState、replaceState才有值。

  function getDOMLocation(historyState) {
    // 取出key state
    const { key, state } = historyState || {};
    // 从location获取pathname、search、hash
    const { pathname, search, hash } = window.location;
    // 拼接除了origin外完整path,
    // 类似/search?type=content&q=BrowserRouter
    let path = pathname + search + hash;
    warning(
      !basename || hasBasename(path, basename),
      'You are attempting to use a basename on a page whose URL path does not begin ' +
        'with the basename. Expected path "' +
        path +
        '" to begin with "' +
        basename +
    // 如果basename有传,拼接path带上basename
    if (basename) path = stripBasename(path, basename);
    // 到了这一步 path 已经是除了origin外完整path
    return createLocation(path, state, key);
// 判断path是否在开头包含了前缀prefix
export function hasBasename(path, prefix) {
  return new RegExp('^' + prefix + '(\\/|\\?|#|$)', 'i').test(path);
// 处理path已经包括了basename作为前缀的场景
export function stripBasename(path, prefix) {
  return hasBasename(path, prefix) ? path.substr(prefix.length) : path;

createLocation

创建真正的location对象。

export function createLocation(path, state, key, currentLocation) {
  let location;
  // 根据path类型分别处理为一致的接口
  if (typeof path === 'string') {
    // Two-arg form: push(path, state)
    // 解析处理hash search pathname
    location = parsePath(path);
    location.state = state;
  } else {
    // path是一个对象时候
    // One-arg form: push(location)
    location = { ...path };
    if (location.pathname === undefined) location.pathname = '';
    if (location.search) {
      if (location.search.charAt(0) !== '?')
        location.search = '?' + location.search;
    } else {
      location.search = '';
    if (location.hash) {
      if (location.hash.charAt(0) !== '#') location.hash = '#' + location.hash;
    } else {
      location.hash = '';
    if (state !== undefined && location.state === undefined)
      location.state = state;
  // 解码pathname
  try {
    location.pathname = decodeURI(location.pathname);
  } catch (e) {
    if (e instanceof URIError) {
      throw new URIError(
        'Pathname "' +
          location.pathname +
          '" could not be decoded. ' +
          'This is likely caused by an invalid percent-encoding.'
    } else {
      throw e;
  // 挂载key
  if (key) location.key = key;
  // currentLocation指的是window.history.location
  if (currentLocation) {
    // Resolve incomplete/relative pathname relative to current location.
    if (!location.pathname) {
      location.pathname = currentLocation.pathname;
    } else if (location.pathname.charAt(0) !== '/') {
      location.pathname = resolvePathname(
        location.pathname,
        currentLocation.pathname
  } else {
    // When there is no prior location and pathname is empty, set it to /
    if (!location.pathname) {
      location.pathname = '/';
  return location;

根据path字符串,生成对象{pathname, search, hash}。

export function parsePath(path) {
  let pathname = path || '/';
  let search = '';
  let hash = '';
  // 先拆分hash
  const hashIndex = pathname.indexOf('#');
  if (hashIndex !== -1) {
    hash = pathname.substr(hashIndex);
    // 剔除了hash部分
    pathname = pathname.substr(0, hashIndex);
 // 再拆分search
  const searchIndex = pathname.indexOf('?');
  if (searchIndex !== -1) {
    search = pathname.substr(searchIndex);
    // 再次剔除search部分
    pathname = pathname.substr(0, searchIndex);
 // 返回拆分后的对象
  return {
    pathname,
    search: search === '?' ? '' : search,
    hash: hash === '#' ? '' : hash

生成location过程结束,我们对比下history.location与window.location区别。

history.location
window.location

createHref

createHref 函数的作用是返回当前路径名。

 function createHref(location) {
    return basename + createPath(location);
// 根据window.location拼接path
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;

push

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';
    // 创建一个location对象
    const location = createLocation(path, state, createKey(), history.location);
    // 进行路由切换
    transitionManager.confirmTransitionTo(
      location,
      action,
      getUserConfirmation,
      ok => {
        // ok 为 true表示不拦截
        if (!ok) return;
        const href = createHref(location);
        const { key, state } = location;
        if (canUseHistory) {
          // 调用HTML5 history pushState history.pushState(state, title[, url])
          // https://developer.mozilla.org/zh-CN/docs/Web/API/History/pushState
          // pushState可以改变url但是不会刷新页面、也不会发起请求
          globalHistory.pushState({ key, state }, null, href);
          if (forceRefresh) {
            // 强制刷新页面
            window.location.href = href;
          } else {
            const prevIndex = allKeys.indexOf(history.location.key);
            const nextKeys = allKeys.slice(
              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;
// 随机生成路由key
function createKey() {
    return Math.random()
      .toString(36)
      .substr(2, keyLength);

setState({ action, location })作用是根据当前地址信息(location)更新history,以及执行所有的listener。

function setState(nextState) {
    Object.assign(history, nextState);
    history.length = globalHistory.length;
    transitionManager.notifyListeners(history.location, history.action);

以push为例,path可以是一个对象、或者字符串。

我们一般可以这样用:

push('/pointPartyGiftList?age=2')
push({
        pathname: '/anniversaryAddress',
        state: {
          orderNo,
          type: 'MONTHLY_INTEGRAL',
          otherParams: {
            type: '2'

每次调用push或者replace都会随机生成一个key,代表这个路径的唯一hash值,并将用户传递的state和key作为state,注意这部分state会被保存到 浏览器 中是一个长效的缓存,将拼接好的path作为传递给history的第三个参数,调用history.pushState(state, null, path),这样地址栏的地址就得到了更新。

地址栏地址得到更新后,页面在不使用foreceRefrsh的情况下是不会自动更新的。

replace

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);

go、goBack、goForward

 function go(n) {
    globalHistory.go(n);
  function goBack() {
    go(-1);
  function goForward() {
    go(1);

block、listen

/**
 * Returns true if a given popstate event is an extraneous WebKit event.
 * Accounts for the fact that Chrome on iOS fires real popstate events
 * containing undefined state when pressing the back button.
export function isExtraneousPopstateEvent(event) {
  event.state === undefined && navigator.userAgent.indexOf('CriOS') === -1;
// popstate事件的handler
function handlePopState(event) {
    // 忽略WebKit中无关的popstate事件
    if (isExtraneousPopstateEvent(event)) return;
    // getDOMLocation(event.state): 根据event.state构建location
    handlePop(getDOMLocation(event.state));
function handleHashChange() {
    handlePop(getDOMLocation(getHistoryState()));
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);
// 记录是否有添加listener
let listenerCount = 0;
// 监听历史条目记录的改变
function checkDOMListeners(delta) {
    listenerCount += delta;
    if (listenerCount === 1 && delta === 1) {
      // 监听popstate事件
      window.addEventListener(PopStateEvent, handlePopState);
      if (needsHashChangeListener)
        // 监听hash change事件
        window.addEventListener(HashChangeEvent, handleHashChange);
    } else if (listenerCount === 0) {
      // 移除popstate事件
      window.removeEventListener(PopStateEvent, handlePopState);
      if (needsHashChangeListener)
        // 移除hash change事件
        window.removeEventListener(HashChangeEvent, handleHashChange);
 let isBlocked = false;
// prompt可以是字符串或者返回字符串的函数
  function block(prompt = false) {
    const unblock = transitionManager.setPrompt(prompt);
    if (!isBlocked) {
      checkDOMListeners(1);
      isBlocked = true;
    return () => {
      if (isBlocked) {
        isBlocked = false;
        checkDOMListeners(-1);
      return unblock();
function listen(listener) {
    const unlisten = transitionManager.appendListener(listener);
    // 开启监听
    checkDOMListeners(1);
    return () => {
     // 卸载监听
      checkDOMListeners(-1);
      unlisten();

checkDOMListeners的功能主要是:监听 / 移除 "https://www.jianshu.com/p/ddb7fcdf5962">popstate事件 (监听用户在浏览器点击后退、前进,点击a标签锚点,或者在js中调用histroy.back(),history.go(),history.forward()等,但监听不到pushState、replaceState方法)。

needsHashChangeListener :为了兼容 hash 改变不触发 popstate 事件的浏览器,所以需要额外增加了监听 hashchange 事件。

history.listen主要做两件事情:

  • 绑定我们要设置的监听函数listener
  • 当历史记录条目发生改变,触发执行对应的监听函数listener
// 监听当前location的更改。
const unlisten = history.listen((location, action) => {
  console.log(action, location.pathname, location.state);
// 若要停止监听,请调用listen()返回的函数.
unlisten();

history.block的功能主要是当历史记录条目发生改变时候,触发提示信息。

history.block(function (location, action) {