Skip to content

router/history 模块

这个一个不暴露给开发者的包,主要是作为 react-router 的基座,从而提供三种基础路由方式(MemoryRouter、HistoryRouter、HashRouter)操作浏览器历史栈的核心功能。其将三种方式通过传入不同的处理 location 和 url 的方法,最终提供具有相同操作的实例对象(history),从而磨平了之间的差异。

具体来讲对于我们常用的两种 HistoryRouter、HashRouter 其通过不同的创建方式 createBrowserHistorycreateHashHistory 传入核心的生成 location 和 url 的方法,然后返回具有同一操作方式的实例对象 history,这样就涉及到几个核心的对象

history 对象

ts
/**
 * A history is an interface to the navigation stack. The history serves as the
 * source of truth for the current location, as well as provides a set of
 * methods that may be used to change it.
 *
 * It is similar to the DOM's `window.history` object, but with a smaller, more
 * focused API.
 */
export interface History {
  /**
   * The last action that modified the current location. This will always be
   * Action.Pop when a history instance is first created. This value is mutable.
   */
  readonly action: Action;

  /**
   * The current location. This value is mutable.
   */
  readonly location: Location;

  /**
   * 创建一个不具备location.origin + basename 的相对于react-router 的路由地址
   *   区别与 createHref :  createHref只是创建一个不具备location.origin + basename 的相对于react-router 的路由地址
   *   createURL : 是创建一个相对于浏览器的完整的url地址的 URL对象
   * @param to 路径 或者 location对象
   * @returns
   */
  createHref(to: To): string;

  /**
   * 创建一个完整url地址的 RL对象
   *   区别与 createHref :  createHref只是创建一个不具备location.origin + basename 的相对于react-router 的路由地址
   *   createURL : 是创建一个相对于浏览器的完整的url地址的 URL对象
   * @param to 路径 或者 location对象
   * @returns
   */
  createURL(to: To): URL;

  /**
   * Encode a location the same way window.history would do (no-op for memory
   * history) so we ensure our PUSH/REPLACE navigations for data routers
   * behave the same as POP
   *
   * @param to Unencoded path
   */
  encodeLocation(to: To): Path;

  /**
   * Pushes a new location onto the history stack, increasing its length by one.
   * If there were any entries in the stack after the current one, they are
   * lost.
   *
   * @param to - The new URL
   * @param state - Data to associate with the new location
   */
  push(to: To, state?: any): void;

  /**
   * Replaces the current location in the history stack with a new one.  The
   * location that was replaced will no longer be available.
   *
   * @param to - The new URL
   * @param state - Data to associate with the new location
   */
  replace(to: To, state?: any): void;

  /**
   * Navigates `n` entries backward/forward in the history stack relative to the
   * current index. For example, a "back" navigation would use go(-1).
   *
   * @param delta - The delta in the stack index
   */
  go(delta: number): void;

  /**
   * Sets up a listener that will be called whenever the current location
   * changes.
   *
   * @param listener - A function that will be called when the location changes
   * @returns unlisten - A function that may be used to stop listening
   */
  listen(listener: Listener): () => void;
}

获取 location 实例对象不同

与 HistoryRouter 相比,HashRouter 的获取 location 实例方法的主要区别是对于 pathname、search、hash 的获取上,例如下面的链接地址

createXXXXLocation

https://www.baidu.com/home/homepage?name=1#/account/logout?age=2#hash2

其在两种 Router 中 location 的结果就是不一样的,具体如下

  • HistoryRouter
ts
function createBrowserLocation(
  window: Window,
  globalHistory: Window["history"]
) {
  let { pathname, search, hash } = window.location;
  // ...
}

直接通过 window.location 获取,所以其对应的是 pathname = /home/homepage , search = ?name=1 , hash = #/account/logout?name=2#hash2

  • HashRouter
ts
function createHashLocation(window: Window, globalHistory: Window["history"]) {
  // 获取的 url中第一个 # 之后的值
  let {
    pathname = "/",
    search = "",
    hash = "",
  } = parsePath(window.location.hash.substr(1));
}

其通过 window.location.hash.substr(1) 获取三个值,所以会忽略 window.location 自带的 pathname 、hash、search 的值, 具体其值为 pathname = /account/logout , search = ?age=2 , hash = #/ahash2。 例外其有一个特殊的方法 parsePath() 处理了结果,这个使用的地方特别多,

parsePath

源码
ts
/**
 * Parses a string URL path into its separate pathname, search, and hash components.
 * 与 createPath 相对应, createPath 是将 pathname search hash 转换成真正的url
 * parsePath 则是将真正的url转换成 { pathname search hash }
 *  /home/homepage?name=1#a
 *  =>
 *  {  pathname : "/home/homepage" , search : "name=1" ,  hash : "#a"  }
 */
export function parsePath(path: string): Partial<Path> {
  let parsedPath: Partial<Path> = {};

  if (path) {
    // 提取 hash 的内容
    let hashIndex = path.indexOf("#");
    if (hashIndex >= 0) {
      parsedPath.hash = path.substr(hashIndex);
      path = path.substr(0, hashIndex);
    }
    // 提取 search 的内容
    // 注意: search的内容不会转换成对象格式
    let searchIndex = path.indexOf("?");
    if (searchIndex >= 0) {
      parsedPath.search = path.substr(searchIndex);
      path = path.substr(0, searchIndex);
    }

    if (path) {
      parsedPath.pathname = path;
    }
  }

  return parsedPath;
}

生成对应跳转 url 地址不同

createXXXXHref

  • HistoryRouter

提供 history 模式下 生成路由跳转链接的方法

在 history 模式下 window.location.href = pathname + search + hash

ts
function createBrowserHref(window: Window, to: To) {
  return typeof to === "string" ? to : createPath(to);
}
  • HashRouter

在 HashRouter 模式下,window.location.href = http://127.0.0.1:3000/index.html#/pathname + search + hash , 所以其一方面需要通过 pathname + search + hash生成 react-router 关注的路径,另外一方面需要使用 #之前的内容生成 url 前缀,从而最终拼凑成 /home/homepage?name=1# ${pathname} ? ${search} # ${hash}

其核心就是获取基础的 href 内容

ts
function createHashHref(window: Window, to: To) {
  let base = window.document.querySelector("base");
  let href = "";
  // 获取 Url 的基础路径 (如果存在# 那么就移除 #及其后面的内容)
  //  http://127.0.0.1:3000/index.html#/home
  //  => /home
  if (base && base.getAttribute("href")) {
    let url = window.location.href;
    let hashIndex = url.indexOf("#");
    href = hashIndex === -1 ? url : url.slice(0, hashIndex);
  }
  // 生成包含 pathname + search 和 hash 的 hash地址
  return href + "#" + (typeof to === "string" ? to : createPath(to));
}

createPath

相对于 parsePath,此逻辑就比较简单了,就是按照 pathname、search、hash 的顺序转换成真正的 url

ts
/**
 * Creates a string URL path from the given pathname, search, and hash components.
 * 对象类型的 To 会根据 pathname、search、hash 转换成真正的 url
 *  {  pathname : "/home/homepage" , search : "name=1" ,  hash : "#a"  }
 *  =>
 *  /home/homepage?name=1#a
 */
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;
}

action 和 location

ts
// 创建 history 实例对象
let history: History = {
  get action() {
    return action;
  },
  get location() {
    return getLocation(window, globalHistory);
  },
};

action 和 location 属性都是通过 get 方法设置的,使用这种方式定义的值在获取时都会调用对应的 get 方法,这也就意味着这三者的值都是实时获取的,在我们调用 History 对象的方法时会间接更改它们的值。

所以我们可以通过 history.locationhistory.action 去实时获取对应的值

action

主要保存了当前浏览记录是通过何种方式进入的 (POP 的初始化加载, PUSH 的通过 history.push 跳转,REPLACE 的通过 history.replace 跳转)

ts
export enum Action {
  // 初始化的方式
  Pop = "POP",

  // 通过 history.push 的方式进入的
  Push = "PUSH",

  // 通过 history.replace 的方式进入的
  Replace = "REPLACE",
}

对于 action 发现其通过闭包的方式去缓存当前的操作类型,在我们使用 history.push()history.replace() 都会修改当前 action 对象,从而我们在 访问 history.action 的时候可以找到当前 url 是通过那种方式进入的

location

缓存了当前 url 下对应的 location 对象信息,其与 window.location 不同,具体如下

  • pathname

缓存了当前 url 的路由地址

  • search : string

缓存了当前 url 中 ?包含的字符串内容, 需要注意的是 其不是对象类型 而是字符串类型

  • hash

缓存了当前 url 中 # 包含的字符串内容

  • state

  • key

唯一的字符串 Key,每一个路由对象都不同

路由跳转方法

history.push

作用

  1. 将当前导航路由推入到历史栈中,从而形成一个新的历史记录,其 state 的内容为 { usr : 用户state , idx : 下标 , key : 唯一的Key }

  2. 生成新的 location 对象,并修改 history 实例对象中的 action 和 location

  3. 兼容 V5 版本的监听方法

源码

ts
/**
 * 统一的 push 方法
 * @param to  路由地址 /home/homepage
 * @param state  state 对象
 */
function push(to: To, state?: any) {
  action = Action.Push;
  // 生成当前路由路径的 location 对象
  let location = createLocation(history.location, to, state);
  if (validateLocation) validateLocation(location, to);
  // 生成 history popstate队列的下标
  index = getIndex() + 1;
  // 生成state对象
  // 目的: 将用户传递的state内容 转换成 { usr : 用户state , idx : 下标 , key : 唯一的Key }
  let historyState = getHistoryState(location, index);
  // 生成可能包含 hash 的url
  let url = history.createHref(location);

  // try...catch because iOS limits us to 100 pushState calls :/
  // 调用 pushState 去跳转页面
  try {
    globalHistory.pushState(historyState, "", url);
  } catch (error) {
    // If the exception is because `state` can't be serialized, let that throw
    // outwards just like a replace call would so the dev knows the cause
    // https://html.spec.whatwg.org/multipage/nav-history-apis.html#shared-history-push/replace-state-steps
    // https://html.spec.whatwg.org/multipage/structured-data.html#structuredserializeinternal
    if (error instanceof DOMException && error.name === "DataCloneError") {
      throw error;
    }
    // They are going to lose state here, but there is no real
    // way to warn them about it since the page will refresh...
    window.location.assign(url);
  }

  if (v5Compat && listener) {
    listener({ action, location: history.location, delta: 1 });
  }
}

history.replace

分析

其逻辑跟 push 很像,只是将修改浏览器历史栈的方法改成了 replaceState 方法,还有 action = Action.Replace

源码

ts
function replace(to: To, state?: any) {
  action = Action.Replace;
  let location = createLocation(history.location, to, state);
  if (validateLocation) validateLocation(location, to);

  index = getIndex();
  let historyState = getHistoryState(location, index);
  let url = history.createHref(location);
  globalHistory.replaceState(historyState, "", url);

  if (v5Compat && listener) {
    listener({ action, location: history.location, delta: 0 });
  }
}

history.go

直接调用原生的 go 方法

ts
return globalHistory.go(n);

监听浏览器 Url 切换方法

ReactRouter 跟 VueRouter 在这个方面的区别还是很大的,

在 Vue 中其对于 HashRouter 和 HistoryRouter 两种模式使用了不同的监听方式,如

  • history 模式使用的是 window.addEventListener('popstate', function(e) {}) ;
  • Hash 模式则是使用的 window.addEventListener('hashchange', function(e) {})

在 react-router 中对于这两种模式的监听则没有区分,都是统一使用的 popstate 事件去进行监听。

具体好处没有什么大的区别,对于 Vue 来说 hashchange 的兼容性更好,其也符合 Vue2 版本兼容 ie 低版本的能力。而对于 react-router v6 来说其完全没有 ie 兼容的考虑,完全可以统一使用 popstate 从而做到 hash 和 history api 的统一

浏览器地址处理方法

这两个实际上应该放在一起说明,

  • history.createHref

创建一个不具备 location.origin + basename 的相对于 react-router 的路由地址

  • history.createURL

是创建一个相对于浏览器的完整的 url 地址的 URL 对象

在 history 模式下这两个的差距很小,只是 createURL 比 createHref 多了一个 basename 前缀, 但是在 hash 模式下,createURL 比 createHref 一方面需要多 basename 前缀,另外一方面 还需要生成 hash 的基础 href

例如 : https://www.baidu.com/home/homepage?name=1#/account/logout?age=2#hash2

  • history 模式 (basename = "/admin" )

createHref = /home/homepage?name=1#/account/logout?age=2#hash2

createURL = /admin + /home/homepage?name=1#/account/logout?age=2#hash2

  • hash 模式 (basename = "/admin" )

createHref = /account/logout?age=2#hash2

createURL = /home/homepage?name=1# + /admin + /account/logout?age=2#hash2

history.createHref

对应上述所说的 生成对应跳转 url 地址不同

history.createURL

ts
/**
 * 创建一个完整url地址的 RL对象
 *   区别与 createHref :  createHref只是创建一个不具备location.origin + basename 的相对于react-router 的路由地址
 *   createURL : 是创建一个相对于浏览器的完整的url地址的 URL对象
 * @param to 路径 或者 location对象
 * @returns
 */
function createURL(to: To): URL {
  // window.location.origin is "null" (the literal string value) in Firefox
  // under certain conditions, notably when serving from a local HTML file
  // See https://bugzilla.mozilla.org/show_bug.cgi?id=878297
  let base =
    window.location.origin !== "null"
      ? window.location.origin
      : window.location.href;

  let href = typeof to === "string" ? to : createPath(to);
  invariant(
    base,
    `No window.location.(origin|href) available to create URL for href: ${href}`
  );
  return new URL(href, base);
}