React-Router v6 完全解读指南 - history 篇

15,134 阅读19分钟

前言

react-router v6 稳定版已经发布了一段时间了,相比起原来的 v5 版本,其 api 有着很大的变动,代码包体积也减少了一半多(20k => 8k),源码行数缩减到了 1600 行。由于 v5 版本的观念基本不能在 v6 版本使用了,正好这也是个学习源码的好机会,于是作者详细深入了react-router及其周边生态,大致整理出了两篇文章(其依赖库historyreact-router核心仓库的源码解析)。

本篇为第一篇文章,将会对history这一仓库进行深度剖析。

history

本文基于history v5.2.0版本进行讲解 history是一个用于管理会话历史(包括浏览器历史)的库。

react-router 目前依赖history 5.x 版本,history库也是react-router团队开发的,内部封装了一些系列操作浏览器历史栈的功能,并提供了三种不同性质的history导航创建方法:

  • createBrowserHistory基于浏览器history对象最新 api。
  • createHashHistory:基于浏览器 url 的 hash 参数。
  • createMemoryHistory:基于内存栈,不依赖任何平台。 上面三种方法创建的history对象在react-router中作为三种主要路由的导航器使用:
  • BrowserRouter对应createBrowserHistory,由react-router-dom提供。
  • HashRouter对应createHashHistory,由react-router-dom提供。
  • MemoryRouter对应createMemoryHistory,由react-router提供,主要用于react-native等基于内存的路由系统。 注:
  • 实际上与react-native相对应的包react-router-native使用的是NativeRouter,但其实NativeRouter就是MemoryRouter的简单封装(改了下名字)。
    export interface NativeRouterProps extends MemoryRouterProps {}
    
    /**
     * NativeRouter 就是 react-router 里的 MemoryRouter,使用内存做导航
     * A <Router> that runs on React Native.
     */
    export function NativeRouter(props: NativeRouterProps) {
      return <MemoryRouter {...props} />;
    }
    
  • react-router-dom中其实还有一种路由StaticRouter,不过是用在ssr中的,没有依赖history库,仅仅是对传入的 props 做了校验而已。
    // 具体引入方式
    import { StaticRouter } from 'react-router-dom/server'
    
    react-router-dom v6.1.1 时还新增了HistoryRouter,不过该Router主要是帮助我们手动传入history实例,在这里就暂不说明了,后续在讲到reacr-router-dom时会细说。

路由切换时的 Action

history在每次路由切换时都为其定义了一个特定的action,这些action被分为了三大类:

  • POP
  • PUSH
  • REPLACE

在源码中使用了一个枚举变量来保存,在后续的函数封装中会用到:

export enum Action {
  Pop = 'POP',
  Push = 'PUSH',
  Replace = 'REPLACE'
}

抽象化的 Path 与 Location

对于一次路由跳转的 url,history将其进行了两层抽象:

  • 一层是只基于 url 创建的Path对象,该对象是把 url 解析为 path,query 与 hash 三部分后的结果。

    下面是Path对象的定义:

    // 下面三个分别是对 url 的 path,query 与 hash 部分的类型别名
    export type Pathname = string;
    export type Search = string;
    export type Hash = string;
    
    // 一次跳转 url 对应的对象
    export interface Path {
      pathname: Pathname;
      search: Search;
      hash: Hash;
    }
    

    下面则是history内提供的 url 与Path对象互相转换的方法:

    /**
     *  pathname + search + hash 创建完整 url
     */
    export function createPath({
      pathname = '/',
      search = '',
      hash = ''
    }: Partial<Path>) {
      if (search && search !== '?')
        pathname += search.charAt(0) === '?' ? search : '?' + search;
      if (hash && hash !== '#')
        pathname += hash.charAt(0) === '#' ? hash : '#' + hash;
      return pathname;
    }
    
    /**
     * 解析 url,将其转换为 Path 对象
     */
    export function parsePath(path: string): Partial<Path> {
      let parsedPath: Partial<Path> = {};
    
      if (path) {
        let hashIndex = path.indexOf('#');
        if (hashIndex >= 0) {
          parsedPath.hash = path.substr(hashIndex);
          path = path.substr(0, hashIndex);
        }
    
        let searchIndex = path.indexOf('?');
        if (searchIndex >= 0) {
          parsedPath.search = path.substr(searchIndex);
          path = path.substr(0, searchIndex);
        }
    
        if (path) {
          parsedPath.pathname = path;
        }
      }
    
      return parsedPath;
    }
    
  • 还有一层则是扩展自Path对象,将路由导航这一行为抽象了出来形成的Location对象,该对象除了包含有Path对象的属性外,还拥有与每次导航相关联的上下文信息状态(state)与和这次导航唯一对应的 key 值。

    下面是Location对象的定义:

    // 唯一字符串,与每次跳转的 location 匹配
    export type Key = string;
    
    // 路由跳转抽象化的导航对象
    export interface Location extends Path {
      // 与当前 location 关联的 state 值,可以是任意手动传入的值
      state: unknown;
      // 当前 location 的唯一 key,一般都是自动生成
      key: Key;
    }
    

    history内部创建唯一 key 的方法:

    /**
     * 创建唯一 key
     */
    function createKey() {
      return Math.random().toString(36).substr(2, 8);
    }
    

深入理解 History 对象

在之前我们提到了history内部的三种导航方法会创建出三个有着不同性质的History对象,但其实这些History对象向外暴露的 api 大体都是一致的,所以我们可以先从它们共同的 api 定义入手。

一个最基础的history对象包含:

  • 两个属性:当前路由对应的跳转行为(action)与导航对象(location
  • 一个工具方法:createHref用于用户将history内部定义的Path对象转换为原始的 url。
    history.createHref({
        pathname: '/home',
        search: 'the=query',
        hash:'hash'
    }) // 输出: /home?the=query#hash
    
  • 五个路由跳转方法:pushreplacegobackforward用于在路由栈中进行路由跳转。
    // 将一个新的历史导航推入历史栈,并且移动当前指针到该历史导航
    history.push('/home');
    // 将当前的路由使用新传入的历史导航替换
    history.replace('/home');
    
    // 此方法可以传入一个 Path 对象,同时也可以接收第二个参数 state,可用于保存在内存中的历史导航上下文信息
    history.push({
      pathname: '/home',
      search: '?the=query'
    }, {
      some: state
    });
    
    // replace 方法同上
    history.replace({
      pathname: '/home',
      search: '?the=query'
    }, {
      some: state
    });
    
    // 返回上一个历史导航
    history.go(-1);
    history.back();
    
    // 去往下一个历史导航
    history.go(1);
    history.forward();
    
  • 两个路由监听方法:监听路由跳转的钩子(类似后置守卫)listen与阻止路由跳转的钩子(如果想正常跳转必须要取消监听,可以封装成类似前置钩子的功能)block
    // 开始监听路由跳转
    let unlisten = history.listen(({ action, location }) => {
      // The current location changed.
    });
    
    // 取消监听
    unlisten();
    
    // 开始阻止路由跳转
    let unblock = history.block(({ action, location, retry }) => {
      // retry 方法可以让我们重新进入被阻止跳转的路由
      // 取消监听,如果想要 retry 生效,必须要在先取消掉所有 block 监听,否则 retry 后依然会被拦截然后进入 block 监听中
      unblock();
      retry();
    });
    

    关于block监听的阻止路由跳转

    这里的阻止跳转并不是阻止新的路由的推入,而是监听到新路由跳转后马上返回到前一个路由,也就是强制执行了history.go(index - nextIndex)index是原来的路由在路由栈的索引,nextIndex是跳转路由在路由栈的索引。

整个History对象的接口在源码中的定义如下:

// listen 回调的参数,包含有更新的行为 Action 和 Location 对象
export interface Update {
  action: Action; // 上面提到的 Action
  location: Location; // 上面提到的 Location
}

// 监听函数 listener 的定义
export interface Listener {
  (update: Update): void;
}


// block 回调的参数,除了包含有 listen 回调参数的所有值外还有一个 retry 方法
// 如果阻止了页面跳转(blocker 监听),可以使用 retry 重新进入页面
export interface Transition extends Update {
  /**
   * 重新进入被 block 的页面
   */
  retry(): void;
}

/**
 * 页面跳转失败后拿到的 Transition 对象
 */
export interface Blocker {
  (tx: Transition): void;
}

// 跳转链接,可以是完整的 url,也可以是 Path 对象
export type To = string | Partial<Path>;

export interface History {
  // 最后一次浏览器跳转的行为,可变
  readonly action: Action;

  // 挂载有当前的 location 可变
  readonly location: Location;

  // 工具方法,把 to 对象转化为 url 字符串,其实内部就是对之前提到的 createPath 函数的封装
  createHref(to: To): string;

  // 推入一个新的路由到路由栈中
  push(to: To, state?: any): void;
  // 替换当前路由
  replace(to: To, state?: any): void;
  // 将当前路由指向路由栈中第 delta 个位置的路由
  go(delta: number): void;
  // 将当前路由指向当前路由的前一个路由
  back(): void;
  // 将当前路由指向当前路由的后一个路由
  forward(): void;

  // 页面跳转后触发,相当于后置钩子
  listen(listener: Listener): () => void;

  // 也是监听器,但是会阻止页面跳转,相当于前置钩子,注意只能拦截当前 history 对象的钩子,也就是说如果 history 对象不同,是不能够拦截到的
  block(blocker: Blocker): () => void;
}

History 对象的创建

在之前我们提到了三种创建History对象的方法:createBrowserHistorycreateHashHistorycreateMemoryHistory。其中:

  • createBrowserHistory用于给用户提供的创建基于浏览器 history API 的History对象,适用于绝大多数现代浏览器(除了少部分不支持 HTML5 新加入的 history API 的浏览器,也就是浏览器的history对象需要具有pushStatereplaceStatestate等属性和方法),同时在生产环境需要服务端的重定向配置才能正常使用。
  • createHashHistory用于给用户提供基于浏览器 url hash 值的History对象,一般来说使用这种方式可以兼容几乎所有的浏览器,但是考虑到目前浏览器的发展,在5.x版本内部其实同createBrowserHistory,也是使用最新的 history API 实现路由跳转的(如果你确实需要兼容旧版本浏览器,应该选择使用4.x版本),同时由于浏览器不会将 url 的 hash 值发送到服务端,前端发送的路由 url 都是一致的,就不用服务端做额外配置了。
  • createMemoryHistory用于给用户提供基于内存系统的History对象,适用于所有可以运行 JavaScript 的环境(包括 Node),内部的路由系统完全由用户支配。

而在history中,这三种方法内部创建History对象的过程基本都是一致的,仅在内部依赖了不同的 API 来实现,所以这里就同时分析这三个方法内部的创建流程了。

我们可以先来看下这些方法创建的History对象的类型:

export interface BrowserHistory extends History {}
export interface HashHistory extends History {}
export interface MemoryHistory extends History {
  readonly index: number;
}

BrowserHistoryHashHistory的类型其实就是我们之前提到的History对象的类型,而MemoryHistory还额外有一个index属性,因为是基于内存的路由系统,所以我们可以清楚知道当前路由在历史栈中的位置,这个属性就是告诉用户目前的内存历史栈索引的(其余两个路由对象其实内部也有index,但是这个index与浏览器中的历史栈索引并不能一一对应,所以没有暴露给用户,只是在内部用作区分)。

下面我们来分别看看三个创建方法本身:

  • createBrowserHistory:
    // 可以传入指定的 window 对象作为参数,默认为当前 window 对象
    export type BrowserHistoryOptions = { window?: Window };
    
    export function createBrowserHistory(
      options: BrowserHistoryOptions = {}
    ): BrowserHistory {
      let { window = document.defaultView! } = options;
      // 拿到浏览器的 history 对象,后续会基于此对象封装方法
      let globalHistory = window.history;
      // 初始化 action 与 location
      let action = Action.Pop;
      let [index, location] = getIndexAndLocation(); // 获取当前路由的 index 和 location
    
      // 省略其余代码
      
      let history: BrowserHistory = {
        get action() {
          return action;
        },
        get location() {
          return location;
        },
        createHref,
        push,
        replace,
        go,
        back() {
          go(-1);
        },
        forward() {
          go(1);
        },
        listen(listener) {
           // 省略其余代码
        },
        block(blocker) {
           // 省略其余代码
        }
      };
    
      return history;
    }
    
  • createHashHistory:
    // 这里同 BrowserRouter
    export type HashHistoryOptions = { window?: Window };
    
    export function createHashHistory(
      options: HashHistoryOptions = {}
    ): HashHistory {
      let { window = document.defaultView! } = options;
      // 浏览器本身就有 history 对象,只是 HTML5 新加入了几个有关 state 的 api
      let globalHistory = window.history;
      let action = Action.Pop;
      let [index, location] = getIndexAndLocation();
    
      // 省略其余代码
      
      let history: HashHistory = {
        get action() {
          return action;
        },
        get location() {
          return location;
        },
        createHref,
        push,
        replace,
        go,
        back() {
          go(-1);
        },
        forward() {
          go(1);
        },
        listen(listener) {
           // 省略其余代码
        },
        block(blocker) {
          // 省略其余代码
        }
      };
    
      return history;
    }
    
  • createMemoryHistory:
    // 这里与 BrowserRouter 和 HashRouter 相比有略微不同,因为没有浏览器的参与,所以我们需要模拟历史栈
    // 用户提供的描述历史栈的对象
    export type InitialEntry = string | Partial<Location>;// 上面提到的 Location 
    
    // 因为不是真实的路由,所以不需要 window 对象,取而代之的是
    export type MemoryHistoryOptions = {
      // 初始化的历史栈
      initialEntries?: InitialEntry[];
      // 初始化的 index
      initialIndex?: number;
    };
    
    
    // 判断上下限值
    function clamp(n: number, lowerBound: number, upperBound: number) {
      return Math.min(Math.max(n, lowerBound), upperBound);
    }
    
    export function createMemoryHistory(
      options: MemoryHistoryOptions = {}
    ): MemoryHistory {
      let { initialEntries = ['/'], initialIndex } = options;
      // 将用户传入的 initialEntries 转换为包含 Location 对象数组,会在之后用到
      let entries: Location[] = initialEntries.map((entry) => {
        // readOnly 就是调用 Object.freeze 冻结对象,这里做了个开发模式的封装,遇到都可以直接跳过
        let location = readOnly<Location>({
          pathname: '/',
          search: '',
          hash: '',
          state: null,
          key: createKey(),
          ...(typeof entry === 'string' ? parsePath(entry) : entry)
        });
        return location;
      });
      // 这里的 location 与 index 的获取方式不同了,是直接从初始化的 entries 中取的
      let action = Action.Pop;
      let location = entries[index];
      // clamp 函数用于取上下限值,如果 没有传 initialIndex 默认索引为最后一个 location
      // 这这里调用是为了规范初始化的 initialIndex 的值
      let index = clamp(
        initialIndex == null ? entries.length - 1 : initialIndex,
        0,
        entries.length - 1
      );
    
      // 省略其余代码
      let history: MemoryHistory = {
        get index() {
          return index;
        },
        get action() {
          return action;
        },
        get location() {
          return location;
        },
        createHref,
        push,
        replace,
        go,
        back() {
          go(-1);
        },
        forward() {
          go(1);
        },
        listen(listener) {
           // 省略其余代码
        },
        block(blocker) {
           // 省略其余代码
        }
      };
    
      return history;
    }
    

ok,我们现在应该已经能够看出,这些方法内部就是通过对History对象方法的封装,最后返回一个符合我们之前定义标准的History对象,所以现在只需要对其中的每个属性或方法做单独解析就行了。

action、location 与 index(MemoryRouter 独有)

回到我们上面的代码中,可以发现上面三个属性都是通过get方法设置的,使用这种方式定义的值在获取时都会调用对应的get方法,这也就意味着这三者的值都是实时获取的,在我们调用History对象的方法时会间接更改它们的值。

除了createMemoryHistory,其余两个方法的indexlocation都是通过getIndexAndLocation()获取的。下面是getIndexAndLocation()方法的内部逻辑:

  • createBrowserHistory:
    export function createBrowserHistory(
      options: BrowserHistoryOptions = {}
    ): BrowserHistory {
      let { window = document.defaultView! } = options;
      let globalHistory = window.history;
    
      /**
       * 拿到当前的 state 的 idx 和 location 对象
       */
      function getIndexAndLocation(): [number, Location] {
        let { pathname, search, hash } = window.location;
        // 获取当前浏览器的 state
        let state = globalHistory.state || {};
        // 可以看到下面很多属性都是保存到了 history api 的 state 中
        return [
          state.idx,
          readOnly<Location>({
            pathname,
            search,
            hash,
            state: state.usr || null,
            key: state.key || 'default'
          })
        ];
      }
      let action = Action.Pop;
      let [index, location] = getIndexAndLocation();
      
      // 初始化 index
      if (index == null) {
        index = 0;
        // 调用的是 history api 提供的 replaceState 方法传入 index,这里只是初始化浏览器中保存的 state,没有改变 url
        globalHistory.replaceState({ ...globalHistory.state, idx: index }, '');
      }
    
      //...
    
      let history: BrowserHistory = {
        get action() {
          return action;
        },
        get location() {
          return location;
        }
        // ...
      };
    
      return history;
    }
    
  • createHashHistory:
    export function createHashHistory(
      options: HashHistoryOptions = {}
    ): HashHistory {
      let { window = document.defaultView! } = options;
      let globalHistory = window.history;
    
     function getIndexAndLocation(): [number, Location] {
        // 注意这里和 browserHistory 不同了,拿的是 hash,其余逻辑是一样的
        // parsePath 方法前面有讲到过,解析 url 为 Path 对象
        let {
          pathname = '/',
          search = '',
          hash = ''
        } = parsePath(window.location.hash.substr(1));
        let state = globalHistory.state || {};
        return [
          state.idx,
          readOnly<Location>({
            pathname,
            search,
            hash,
            state: state.usr || null,
            key: state.key || 'default'
          })
        ];
      }
    
      let action = Action.Pop;
      let [index, location] = getIndexAndLocation();
    
     if (index == null) {
        index = 0;
        globalHistory.replaceState({ ...globalHistory.state, idx: index }, '');
      }
      //...
    
      let history: HashHistory = {
        get action() {
          return action;
        },
        get location() {
          return location;
        }
        // ...
      };
    
      return history;
    }
    

createHref

createHref方法的功能很简单,它主要用于将history内部定义的To对象(type To = string | Partial<Path>)转回为 url 字符串:

  • createBrowserHistory:
    export function createBrowserHistory(
      options: BrowserHistoryOptions = {}
    ): BrowserHistory {
       // ...
       // BrowserHistory 只需要简单判断一下类型就可以了
      function createHref(to: To) {
        return typeof to === 'string' ? to : createPath(to);
      }
      // ...
      let history: BrowserHistory = {
          // ...
          createHref
          // ...
      }
      return history
    }
    
  • createHashHistory:
    export function createHashHistory(
      options: HashHistoryOptions = {}
    ): HashHistory {
       // ...
       /**
       * 查看是否有 base 标签,如果有则取 base 的 url(不是从 base 标签获取,是从 window.location.href 获取)
       */
      function getBaseHref() {
        let base = document.querySelector('base');
        let href = '';
    
        if (base && base.getAttribute('href')) {
          let url = window.location.href;
          let hashIndex = url.indexOf('#');
          // 拿到去除了 # 的 url
          href = hashIndex === -1 ? url : url.slice(0, hashIndex);
        }
    
        return href;
      }
      // HashHistory 需要额外拿到当前页面的 base url
      function createHref(to: To) {
        return getBaseHref() + '#' + (typeof to === 'string' ? to : createPath(to));
      }
    
      // ...
    
      let history: HashHistory = {
          // ...
          createHref
          // ...
      }
      return history
    }
    
  • createMemoryHistory:
    export function createMemoryHistory(
      options: MemoryHistoryOptions = {}
    ): MemoryHistory {
       // ...
       // 同 BrowserHistory
      function createHref(to: To) {
        return typeof to === 'string' ? to : createPath(to);
      }
      // ...
    
      let history: MemoryHistory = {
          // ...
          createHref
          // ...
      }
      return history
    }
    

listen

listen方法是一个监听方法,本质上就是实现了一个发布订阅模式。发布订阅模型在源码中实现如下:

/**
 * 事件对象
 */
type Events<F> = {
  length: number;
  push: (fn: F) => () => void;
  call: (arg: any) => void;
};

/**
 * 内置的发布订阅事件模型
 */
function createEvents<F extends Function>(): Events<F> {
  let handlers: F[] = [];

  return {
    get length() {
      return handlers.length;
    },
    // push 时返回对应的 clear 语句
    push(fn: F) {
      handlers.push(fn);
      return function () {
        handlers = handlers.filter((handler) => handler !== fn);
      };
    },
    call(arg) {
      handlers.forEach((fn) => fn && fn(arg));
    }
  };
}

创建完成后,history会拿到的对应模型对象,在路由改变时依次调用传入的handlers

下面是listen方法的定义:

export interface Update {
  action: Action;
  location: Location;
}
// Listener 类型在之前有提到过,可以往回看看
export interface Listener {
  (update: Update): void;
}

export function createBrowserHistory(
  options: BrowserHistoryOptions = {}
): BrowserHistory {
  // ...
  let listeners = createEvents<Listener>();
  // ...
  let history: BrowserHistory = {
    // ...
    listen(listener) {
      return listeners.push(listener);
    },
    // ...
  };

  return history;
}

export function createHashHistory(
  options: HashHistoryOptions = {}
): HashHistory {
  // ...
  let listeners = createEvents<Listener>();
  // ...
  let history: HashHistory = {
    // ...
    listen(listener) {
      return listeners.push(listener);
    },
    // ...
  };

  return history;
}

export function createMemoryHistory(
  options: MemoryHistoryOptions = {}
): MemoryHistory {
  // ...
  let listeners = createEvents<Listener>();
  // ...
  let history: MemoryHistory = {
    // ...
    listen(listener) {
      return listeners.push(listener);
    },
    // ...
  };

  return history;
}

block

block方法的实现则和listen方法基本相同,只是BrowserHistoryHashHistory内部额外对于浏览器的beforeunload事件做了监听。

  • createBrowserHistory 与 createHashHistory :
    export interface Update {
      action: Action;
      location: Location;
    }
    export interface Transition extends Update {
      retry(): void;
    }
    
    // Blocker 我们之前也提到过
    export interface Blocker {
      (tx: Transition): void;
    }
    
    const BeforeUnloadEventType = 'beforeunload';
    
    export function createBrowserHistory(
      options: BrowserHistoryOptions = {}
    ): BrowserHistory {
      // ...
      let blockers = createEvents<Blocker>();
      // ...
      let history: BrowserHistory = {
        // ...
        block(blocker) {
          let unblock = blockers.push(blocker);
    
          // 当我们需要监听跳转失败时才加入,并且只需要一个事件来阻止页面关闭
          if (blockers.length === 1) {
            window.addEventListener(BeforeUnloadEventType, promptBeforeUnload);
          }
    
          return function () {
            unblock();
            // 当没有 blocker 监听时应该删除 beforeunload 事件的监听
            if (!blockers.length) {
              window.removeEventListener(BeforeUnloadEventType, promptBeforeUnload);
            }
          };
        }
        // ...
      };
    
      return history;
    }
    
    export function createHashHistory(
      options: HashHistoryOptions = {}
    ): HashHistory {
      // ...
      let blockers = createEvents<Blocker>();
      // ...
      let history: HashHistory = {
        // ...
        block(blocker) {
          let unblock = blockers.push(blocker);
    
          if (blockers.length === 1) {
            window.addEventListener(BeforeUnloadEventType, promptBeforeUnload);
          }
    
          return function () {
            unblock();
            if (!blockers.length) {
              window.removeEventListener(BeforeUnloadEventType, promptBeforeUnload);
            }
          };
        }
        // ...
      };
      return history;
    }
    
  • createMemoryHistory:
    // MemoryHistory 这里同 listen 方法一样
    export function createMemoryHistory(
      options: MemoryHistoryOptions = {}
    ): MemoryHistory {
      // ...
      let blockers = createEvents<Blocker>();
      // ...
      let history: MemoryHistory = {
        // ...
        // 这里就没有监听浏览器的 beforeunload 事件了
        block(blocker) {
          return blockers.push(blocker);
        }
        // ...
      };
    
      return history;
    }
    

push 和 replace

pushreplace两个方法内部要做的事更多一点,除了需要封装将新的导航推入到历史栈的功能外,还需要同时改变当前的actionlocation,并且判断并调用相应的监听方法。

  • createBrowserHistorycreateHashHistory中这两个方法的结构定义是相同的:
    function push(to: To, state?: any) {
        let nextAction = Action.Push;
        let nextLocation = getNextLocation(to, state);
    
        /**
         * 重新执行 push 操作
         */
        function retry() {
          push(to, state);
        }
    
        // 当没有 block 监听时 allowTx 返回 true,否则都是返回 false,不会推送新的导航
        if (allowTx(nextAction, nextLocation, retry)) {
          let [historyState, url] = getHistoryStateAndUrl(nextLocation, index + 1);
    
          // try...catch 是因为 ios 限制最多调用 100 次 pushState 方法,否则会报错
          try {
            globalHistory.pushState(historyState, '', url);
          } catch (error) {
            // push 失败后就没有 state 了,直接使用 href 跳转
            window.location.assign(url);
          }
    
          applyTx(nextAction);
        }
    }
    
    function replace(to: To, state?: any) {
        let nextAction = Action.Replace;
        let nextLocation = getNextLocation(to, state);
        function retry() {
          replace(to, state);
        }
    
        // 同 push 函数,否则不会替换新的导航
        if (allowTx(nextAction, nextLocation, retry)) {
          let [historyState, url] = getHistoryStateAndUrl(nextLocation, index);
    
          globalHistory.replaceState(historyState, '', url);
    
          applyTx(nextAction);
        }
    }
    
  • createMemoryHistory中则是改变已定义的entries数组:
      function push(to: To, state?: any) {
        let nextAction = Action.Push;
        let nextLocation = getNextLocation(to, state);
        function retry() {
          push(to, state);
        }
    
        if (allowTx(nextAction, nextLocation, retry)) {
          // 修改 index 与 entries 历史栈数组
          index += 1;
          // 添加一个新的 location,删除原来 index 往后的栈堆
          entries.splice(index, entries.length, nextLocation);
          applyTx(nextAction, nextLocation);
        }
      }
    
      function replace(to: To, state?: any) {
        let nextAction = Action.Replace;
        let nextLocation = getNextLocation(to, state);
        function retry() {
          replace(to, state);
        }
    
        if (allowTx(nextAction, nextLocation, retry)) {
          // 覆盖掉原来的 location
          entries[index] = nextLocation;
          applyTx(nextAction, nextLocation);
        }
      }
    

在上面的代码封装中,我们先定义对应方法的Action,将用户传入的参数格式化为Location对象。

其中格式化参数的getNextLocation()方法的定义如下:

 function getNextLocation(to: To, state: any = null): Location {
     // 简单的格式化
    return readOnly<Location>({
      // 这前面三个实际上是默认值
      pathname: location.pathname,
      hash: '',
      search: '',
      // 下面胡返回带有 pathname、hash、search 的对象
      ...(typeof to === 'string' ? parsePath(to) : to),
      state,
      key: createKey()
    });
  }

然后定义了一个递归的retry方法用于在block回调中传入,最后使用allowTx方法判断是否满足路由跳转条件,如果满足则调用路由跳转相关代码(包括改变actionlocation属性,以及调用回调监听等)。

其中allowTx()方法的定义如下:

/**
* 调用所有 blockers,没有 blocker 的监听时才会返回 true,否则都是返回 false
*/
function allowTx(action: Action, location: Location, retry: () => void) {
    return (
      !blockers.length || (blockers.call({ action, location, retry }), false)
    );
}

getHistoryStateAndUrl()方法(这个只有createBrowserHistorycreateHashHistory才有):

type HistoryState = {
  usr: any;
  key?: string;
  idx: number;
};

 function getHistoryStateAndUrl(
    nextLocation: Location,
    index: number
  ): [HistoryState, string] {
    return [
      {
        usr: nextLocation.state,
        key: nextLocation.key,
        idx: index
      },
      createHref(nextLocation)
    ];
  }

applyTx()方法:

// createBrowserHistory 与 createHashHistory
function applyTx(nextAction: Action) {
    action = nextAction;
    // 在这里修改了 index 与 location,getIndexAndLocation 方法我们在前面将 location 属性的时候有提到过
    [index, location] = getIndexAndLocation();
    listeners.call({ action, location });
}

// createMemoryHistory
function applyTx(nextAction: Action, nextLocation: Location) {
    // 没有在这里改变 index ,和其余 router 不同,将 index 改变操作具体到了 push 和 go 等函数中
    action = nextAction;
    location = nextLocation;
    listeners.call({ action, location });
}

浏览器 popstate 事件的监听

在浏览器环境下,除了手动调用history.pushhistory.replace外,用户还可以通过浏览器的前进和后退按钮改变导航历史,这样的行为在history中则对应着ActionPOP,同时浏览器也提供了对应的事件popstate,所以我们还需要额外处理一下这样的情形。

这里只有createBrowserHistorycreateHashHistory才会有该事件的监听:

const HashChangeEventType = 'hashchange';
const PopStateEventType = 'popstate';

export function createBrowserHistory(
  options: BrowserHistoryOptions = {}
): BrowserHistory {
   //...

  let blockedPopTx: Transition | null = null;
  /**
   * 事件监听回调函数
   * 如果设置了 blocker 的监听器,该函数会执行两次,第一次是跳回到原来的页面,第二次是执行 blockers 的所有回调
   * 这个函数用于监听浏览器的前进后退,因为我们封装的 push 函数已经被我们拦截了
   */
  function handlePop() {
    if (blockedPopTx) {
      blockers.call(blockedPopTx);
      blockedPopTx = null;
    } else {
      let nextAction = Action.Pop;
      let [nextIndex, nextLocation] = getIndexAndLocation();

      // 如果有前置钩子
      if (blockers.length) {
        if (nextIndex != null) {
          // 计算跳转层数
          let delta = index - nextIndex;
          if (delta) {
            // Revert the POP
            blockedPopTx = {
              action: nextAction,
              location: nextLocation,
              // 恢复页面栈,也就是 nextIndex 的页面栈
              retry() {
                go(delta * -1);
              }
            };
            // 跳转回去(index 原本的页面栈)
            go(delta);
          }
        } else {
          // asset
          // nextIndex 如果为 null 会进入该分支打警告信息,这里就先不管它
        }
      } else {
        // 改变当前 action,调用所有的 listener
        applyTx(nextAction);
      }
    }
  }

  // 可以看到在创建 History 对象的时候就进行监听了
  window.addEventListener(PopStateEventType, handlePop);
  //...
}


export function createHashHistory(
  options: HashHistoryOptions = {}
): HashHistory {
   //...

  // 下面和 createBrowserHistory 一样
  let blockedPopTx: Transition | null = null;
  function handlePop() {
    //...
  }
  
    
  // 下面额外监听了 hashchange 事件
  window.addEventListener(PopStateEventType, handlePop);
  // 低版本兼容,监听 hashchange 事件
  // https://developer.mozilla.org/de/docs/Web/API/Window/popstate_event
  window.addEventListener(HashChangeEventType, () => {
    let [, nextLocation] = getIndexAndLocation();

    // 如果支持 popstate 事件这里就会相等,因为会先执行 popstate 的回调
    if (createPath(nextLocation) !== createPath(location)) {
      handlePop();
    }
  });
  //...
}

go、back 和 forward

比起pushreplace,这三个方法的封装就简单很多了,在BrowserHistoryHashHistory中它们都只是 history API 的包装而已,而在MemoryHistory中因为没有路由的 pop 监听事件与来自代码外的历史栈改变,所以直接将监听的回调移动到了这里面。

  • createBrowserHistory 与 createHashHistory :
    export function createBrowserHistory(
      options: BrowserHistoryOptions = {}
    ): BrowserHistory {
      // ...
      function go(delta: number) {
        globalHistory.go(delta);
      }
      // ...
      let history: BrowserHistory = {
        go,
        back() {
          go(-1);
        },
        forward() {
          go(1);
        },
        // ...
      };
    
      return history;
    }
    
    export function createHashHistory(
      options: HashHistoryOptions = {}
    ): HashHistory {
      // ...
      function go(delta: number) {
        globalHistory.go(delta);
      }
      // ...
      let history: HashHistory = {
        go,
        back() {
          go(-1);
        },
        forward() {
          go(1);
        },
        // ...
      };
    
      return history;
    }
    
  • createMemoryHistory:
export function createMemoryHistory(
  options: MemoryHistoryOptions = {}
): HashHistory {
  // ...
  function go(delta: number) {
    // 跳转到原来的 location
    let nextIndex = clamp(index + delta, 0, entries.length - 1);
    let nextAction = Action.Pop;
    let nextLocation = entries[nextIndex];
    function retry() {
      go(delta);
    }

    if (allowTx(nextAction, nextLocation, retry)) {
      index = nextIndex;
      applyTx(nextAction, nextLocation);
    }
  }
  // ...
  let history: MemoryHistory = {
    go,
    back() {
      go(-1);
    },
    forward() {
      go(1);
    },
    // ...
  };

  return history;
}

总结

history整体是对浏览器 api 的二次封装,但是并没有太过深入的封装,仅仅是对每次页面跳转时做了抽象处理,并且加入了额外的监听与特殊的阻止跳转功能。

下一篇文章我们将会进入主题,分析react-router v6中的路由封装思路。

本篇文章相关的源码分析仓库可以在 github 查看。