🎧 羽森 / Kent

iframe 污染浏览器历史记录的问题与解决方案2025-01-05

问题描述

在视频通话应用中,走的充值会跳转新的页面,此时回来会中断视频,改为使用 iframe 模拟弹窗支付后,原先router.back(-1),退出功能失效。用户点击退出按钮后,页面停留在空白状态,无法正常返回。

问题分析

现象

通过日志发现,iframe 打开前后 history.length 发生了变化:

[Payment] iframe 打开前 history.length: 47
[Payment] iframe 关闭前 history.length: 50

历史记录被污染了 3 条。

A → B → iframe → back → B   ❌
A → B → iframe → back → A   ✅

从工程上来说,iframe的打开是是“模态行为”,不是“导航行为”

原因

虽然 iframe 是弹窗样式,不涉及路由跳转,但浏览器在加载外部 URL(如 Stripe 支付页面)时,仍然会在主窗口的历史记录中添加条目。

具体流程:

  1. 进入视频通话页面时:history.length = 47
  2. iframe 打开并加载支付页面:history.length = 50(不一定50,看iframe的跳转次数)
  3. 退出时使用 router.back(-1):只能回退 1 步,无法清除被污染的历史记录

其他方案的局限性

方案一:锁定“返回目标”,使用router.replace()进行跳转

具体是在跳转视频通话的时候带一个_entry参数,close的时候根据参数来replace,虽然能退出,但会导致用户在返回时需要点击两次才能回到上一页。因为 replace 会覆盖当前历史记录,导致历史记录变成 A → A(B 被 replace 成A),用户看似是返回了,但如果A还是有返回键的,第一次 back 仍然停在 A,就需要再点击一次了

方案二:记录污染数量并清除

记录 iframe 打开前后的历史记录长度差,退出时使用 router.go() 清除。但 router.go() 在某些情况下不稳定,且需要维护 localStorage,逻辑复杂。

解决方案

核心思路

在进入页面时记录历史记录基线,退出时直接回退到基线位置。这样无论 iframe 污染了多少条历史记录,都能精准回退。

方案优势

  1. 不新建历史记录:使用 history.go() 直接回退,不产生新记录
  2. 不覆盖历史记录:避免 replace 导致的"返回要点两次"问题
  3. 精准回退:直接回到进入页面前的历史节点,不受 iframe 污染影响
  4. 有兜底方案:如果没有基线记录,仍使用 router.back(-1) 作为后备
  5. 代码简洁:逻辑清晰,不需要维护 localStorage

技术要点

1. 使用 history.state 存储基线

Vue Router 4 支持 state 参数,不会出现在 URL 中,适合存储临时状态:

router.push({
  path: '/video',
  query: { ... },
  state: {
    __historyBase: historyBase // 不会进入 URL
  }
})

2. 计算回退步数

const delta = window.history.length - historyBase;
if (delta > 0) {
  window.history.go(-delta); // 回退 delta 步
}

实现

1. 进入页面时记录基线

在跳转到视频通话页面时,记录当前历史记录长度:

navigateToVideoPage(cid, otherId, isMatchIn, aid) {
  // 记录进入1v1前的历史记录基线
  const historyBase = window.history.length  
  router.push({
    path: '/video',
    query: {...},
    state: {
      __historyBase: historyBase // 使用 state,不会进入 URL
    }
  })
}

2. 退出时精准回退

退出时,计算当前历史记录长度与基线的差值,使用 history.go() 回退相应步数:

const executeHangupDirectly = () => {
  nextTick(() => {
    // 使用 history.go() 精准回退到进入1v1前的历史记录节点
    const historyBase = window.history.state?.__historyBase;
    if (typeof historyBase === 'number') {
      const delta = window.history.length - historyBase;
      if (delta > 0) {
        // 直接回退到进入1v1前的历史记录节点
        window.history.go(-delta);
        return;
      }
    }
    // 兜底:如果没有记录基线,使用 back
    router.back(-1);
  });
};

总结

iframe 虽然不涉及路由跳转,但加载外部 URL 时仍会污染浏览器历史记录。通过记录历史基线并使用 history.go() 精准回退,可以解决这个问题。

该方案既保证了退出功能的正常,又不会影响用户的返回体验。需要注意的是,需要确保所有退出路径都使用相同的逻辑,以保证一致性。

参考