React Router 所用 history 库源码解析

273 阅读8分钟

HTML5 history API简介

方法/属性说明语法示例
pushState()将新的历史记录条目添加到浏览器的历史记录栈,更新 URL,但不刷新页面。history.pushState(state, title, url);
replaceState()替换当前历史记录条目,更新 URL,但不刷新页面。history.replaceState(state, title, url);
back()相当于点击浏览器的“后退”按钮,回到历史记录栈中的前一个页面。history.back();
forward()相当于点击浏览器的“前进”按钮,跳到历史记录栈中的下一个页面。history.forward();
go()跳到历史记录栈中的指定位置(正数为前进,负数为后退)。history.go(n);
length返回历史记录栈中的记录数。history.length
state返回当前历史记录条目的 state 对象(仅在调用 pushState() 或 replaceState() 后有效)。history.state

history 库的核心功能

v5 用于 React Router v 6。

v4 在 React Router 版本 4 和 5 中使用。

在API的区别上 v5将v4的goBack替换为back,forward同理。

// history v4 的 history 对象
  const history = {
    length: globalHistory.length,
    action: 'POP',
    location: initialLocation,
    createHref,
    push,
    replace,
    go,
    goBack,
    goForward,
    block,
    listen
  };

// history v5 的 history 对象
  let history: HashHistory = {
    get action() {
      return action;
    },
    get location() {
      return location;
    },
    createHref,
    push,
    replace,
    go,
    back() {
      go(-1);
    },
    forward() {
      go(1);
    },
    listen(listener) {
      return listeners.push(listener);
    },
    block(blocker) {
      let unblock = blockers.push(blocker);

      if (blockers.length === 1) {
        window.addEventListener(BeforeUnloadEventType, promptBeforeUnload);
      }

      return function () {
        unblock();

        // Remove the beforeunload listener so the document may
        // still be salvageable in the pagehide event.
        // See https://html.spec.whatwg.org/#unloading-documents
        if (!blockers.length) {
          window.removeEventListener(BeforeUnloadEventType, promptBeforeUnload);
        }
      };
    },
  };
功能描述语法示例
createBrowserHistory()创建一个可以使用浏览器的 history.pushState() 和 history.replaceState() API 的历史记录对象,支持路径管理和 URL 操作。import { createBrowserHistory } from 'history';const history = createBrowserHistory();
createHashHistory()创建一个使用哈希路由的历史记录对象,适用于不支持 HTML5 history API 的环境。import { createHashHistory } from 'history';const history = createHashHistory();
createMemoryHistory()创建一个内存中的历史记录对象,适用于服务端渲染或非浏览器环境,历史记录不会影响浏览器的地址栏。import { createMemoryHistory } from 'history';const history = createMemoryHistory();
push()在历史记录栈中添加一个新的条目,同时更新地址栏 URL。通常用于导航到新页面。history.push('/new-page');
replace()替换当前历史记录条目,更新地址栏 URL,但不添加新的历史记录条目。history.replace('/new-page');
go()跳转到历史记录栈中的某个位置,正数前进,负数后退。history.go(-1); // 后退1步
back()与 history.go(-1) 相同,回到历史记录栈中的上一个条目。history.back();
forward()与 history.go(1) 相同,前进到历史记录栈中的下一个条目。history.forward();
listen()监听历史记录的变化,当 URL 改变时触发回调函数。const unlisten = history.listen(location => { console.log(location); });
location当前的历史记录条目的位置对象,包含当前的 URL 路径、查询参数、hash 等信息。console.log(history.location.pathname);
createHref()将一个给定的 location对象转换为一个可以用于浏览器地址栏的 路径字符串。const href = history.createHref(location);

history v5 源码分析

createHashHistory 和 createBrowserHistory的区别

createHashHistory 和 createBrowserHistory的区别就是前者直接通过 pushState 和 replaceState 来更新历史记录,同时修改浏览器的 URL。后者通过修改 window.location.hash 来更新 URL,但也使用 window.history 对象的 pushState 和 replaceState 以类似的方式进行管理历史状态。在事件监听上,createHashHistory除了popstate之外,它还需要额外监听 hashchange 事件。因为在一些旧浏览器(如 IE 11 和旧版 Edge)中,popstate 事件在哈希变化时不会触发,所以需要使用 hashchange 事件来处理哈希的变化。

history.location

在对象实例就返回出由getIndexAndLocation的useState

  function getIndexAndLocation(): [number, Location] {
    let { pathname, search, hash } = window.location;
    let state = globalHistory.state || {};
    return [
      state.idx,
      readOnly<Location>({
        pathname,
        search,
        hash,
        state: state.usr || null,
        key: state.key || 'default'
      })
    ];
  }

history.listen

在 createBrowserHistory 中,listen 方法就是使用了观察者模式。它允许开发者注册一系列回调函数(观察者),这些回调会在浏览器历史状态(即 URL 和历史记录)发生变化时被触发,从而实现页面的动态更新。

function applyTx(nextAction: Action) {
  action = nextAction;
  [index, location] = getIndexAndLocation();
  listeners.call({ action, location });
}

function createEvents<F extends Function>(): Events<F> {
  let handlers: F[] = [];

  return {
    get length() {
      return handlers.length;
    },
    push(fn: F) {
      handlers.push(fn);
      return function () {
        handlers = handlers.filter((handler) => handler !== fn);
      };
    },
    call(arg) {
      handlers.forEach((fn) => fn && fn(arg));
    },
  };
}

history.push() & history.replace()

调用push和replace时在未被block阻塞时会执行applyTx,和原生的pushState的区别就是封装了url和状态的匹配,通过一个统一的函数 getHistoryStateAndUrl 来同时生成历史记录的状态和 URL,从而避免了开发者手动去维护这两者的同步,也对ios限制100次pushState做了边界处理。

  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)) {
      let [historyState, url] = getHistoryStateAndUrl(nextLocation, index + 1);
      // try...catch 对ios限制100次pushState做了边界处理
      try {
        globalHistory.pushState(historyState, "", url);
      } catch (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);
      }

      applyTx(nextAction);
    }
  }

  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)) {
      let [historyState, url] = getHistoryStateAndUrl(nextLocation, index);

      globalHistory.replaceState(historyState, "", url);

      applyTx(nextAction);
    }
  }

listen,block去监听go、back方法则是通过监听popstate事件去触发handlePop,在handlePop里若没有注册阻塞函数,则执行applyTx,执行监听回调函数的同时更新index和location。

为什么go、back通过监听popstate事件去执行监听回调,因为原生的back()、forward(),会被popstate监听到,而调用 history.pushState() 或者 history.replaceState() 不会触发 popstate 事件。

popstate 事件只会在浏览器某些行为下触发,比如点击后退按钮(或者在 JavaScript 中调用 history.back() 方法)。即,在同一文档的两个历史记录条目之间导航会触发该事件。

history相比于原生API的优势

  1. push、replace相比于pushState、replaceState调用更加简洁,无需再传递unused参数。且push、replace会执行listen监听时传入的回调函数。
  2. 原生history对象需要手动解析和管理 location(路径、查询字符串、哈希等),history提供了一个封装好的 location 对象,它包括了路径、查询字符串、哈希值等信息,并且这些信息可以在历史记录中进行自动跟踪。
  3. createBrowserHistory 封装了 history.pushState 和 history.replaceState,并且它提供了 自动处理页面状态 和 历史状态 的功能。它会通过一个统一的函数 getHistoryStateAndUrl 来同时生成历史记录的状态和 URL,从而避免了开发者手动去维护这两者的同步。原生的API 只关心 URL 和 state,它们本身并不关心页面的其他状态(如页面渲染的内容)。如果在 URL 改变时更新页面的内容,你需要自己手动处理页面状态和历史记录之间的一致性。
  4. createBrowserHistory 提供了 listen 和 block 方法,允许开发者更精确地控制历史记录的行为,特别是在需要拦截用户的导航行为时。
  • block:可以阻止某些导航行为,直到特定条件满足。这对于需要确认用户是否丢失未保存数据的场景非常有用。例如,在用户离开当前页面时弹出确认框。
  • listen:允许开发者监听历史记录的变化,例如页面路径的改变,可以精确地捕捉到任何历史记录的变化。

原生的 window.history API 没有类似的机制,开发者需要自己在 popstate 事件中实现这些逻辑。


React Router v4、v5 和 v6 的差异

特性React Router v4React Router v5React Router v6
路由组件<Route component={...}><Route render={...}><Route element={...}>
动态路由使用 render 和 children使用 render 和 children使用 element 属性
路由切换容器<Switch> <Switch> <Routes>
嵌套路由支持支持支持支持
重定向组件<Redirect /> <Redirect /> <Navigate />
history.push使用 history.push()使用 history.push()使用 useNavigate()
exact 匹配模式需要设置 exact需要设置 exact默认精确匹配,无需设置 exact
权限控制手动实现手动实现更加简洁,直接在路由配置中处理
组件支持支持不再支持

history和hash模式区别

特性history 模式hash 模式
URL 结构使用标准的 URL(没有 #),例如 /page1使用 # 字符,例如 /page1#about
浏览器行为不会导致页面刷新,支持 SEO(如果配置了服务器端路由)URL 中的 # 部分变化不会引起页面刷新,历史记录不干扰页面加载
浏览器支持需要浏览器支持 HTML5 History API(现代浏览器基本都支持)兼容所有浏览器,支持老版浏览器
服务器要求需要服务器配置支持单页应用路由(服务器需要处理 404)不需要特殊服务器配置,直接可以运行
SEO 支持有较好的 SEO 支持,服务器可以返回正确的 HTML 页面对 SEO 支持差,因为 URL 中的 # 部分不被搜索引擎索引
URL 变化时页面刷新不刷新页面,完全由 JavaScript 控制路由变化不刷新页面,只有 hash 部分发生变化
历史记录管理更强大的历史记录控制,支持 pushState 和 replaceState简单的历史记录管理,hashchange 事件
用户体验URL 规范,更像传统的网页应用,支持前进和后退URL 通过 # 跳转,可能影响用户体验