在把 H5 页面嵌入 PC 端 iframe 后,一个很容易被忽略的问题是:内嵌页面的路由跳转会写入浏览器历史栈,进而污染外层页面的前进后退行为。
例如,PC 页面打开一个 iframe,iframe 内从首页跳到列表,再跳到详情。用户点击浏览器返回时,浏览器可能先回到 iframe 的列表页,而不是回到 PC 页面真正想返回的位置。更麻烦的是,iframe 内的“返回”按钮和宿主页面的“返回”按钮开始共享同一套历史记录,两个页面的导航职责混在了一起。
这类问题的关键不在于再增加一个返回按钮判断,而在于先隔离两套导航历史:普通 H5 使用浏览器历史,PC iframe 使用 Vue Router 自己维护的内存历史。
普通 H5 与 PC iframe 使用不同历史实现
Vue Router 4 提供了两种适合这个场景的 history 实现:
| 运行场景 | history 实现 | 历史记录归属 |
|---|---|---|
| 普通 H5 | createWebHistory |
当前浏览器页面 |
| PC iframe | createMemoryHistory |
iframe 内的 Vue Router |
普通 H5 仍然需要浏览器地址栏、刷新和系统返回都能工作,所以继续使用 createWebHistory。PC iframe 则不应该把每次内部跳转同步给宿主浏览器,因此使用 createMemoryHistory,让 router.push() 和 router.go() 只操作内存中的路由队列。
路由创建可以集中封装成一个工厂函数:
const routerBase = import.meta.env.VITE_PUBLIC_PATH || '/platform-h5/';
const isPcIframe = () =>
window.parent !== window &&
new URLSearchParams(window.location.search).get('platform') === 'PC';
const createRouterHistory = () => {
if (!isPcIframe()) {
return createWebHistory(routerBase);
}
return createMemoryHistory(routerBase);
};
这样做的好处是,差异只存在于 history 的创建阶段,路由表、导航守卫、页面缓存和页面标题逻辑都可以继续复用。
为什么内存历史还要恢复首次地址
createMemoryHistory() 不会自动读取浏览器当前 URL。它的初始位置是一个空的起始位置,如果直接创建后等待应用启动,刷新详情页时,Vue Router 可能从根路径重新开始解析,而不是从浏览器当前地址进入详情页。
因此,创建内存 history 后,需要把当前页面地址放入它的初始位置:
const getInitialMemoryRoute = () => {
const normalizedBase = routerBase.replace(/\/$/, '');
const routePath = window.location.pathname.startsWith(normalizedBase)
? window.location.pathname.slice(normalizedBase.length) || '/'
: window.location.pathname;
return `${routePath}${window.location.search}${window.location.hash}`;
};
const history = createMemoryHistory(routerBase);
history.replace(getInitialMemoryRoute());
return history;
这里有两个细节:
- 部署目录可能是
/platform-h5/,路由匹配需要先移除这个 base,否则会把部署前缀当成业务路由的一部分。 search和hash不能丢失。像platform=PC、业务 id 或锚点信息,都可能参与页面初始化。
返回逻辑不能再只看 history.state.back
普通 Web History 会在浏览器 history state 中维护 back 等信息,因此原有代码常用下面的方式判断是否已经位于栈底:
if (!router.options.history.state.back) {
closeWebView();
} else {
router.go(-1);
}
但 createMemoryHistory() 的 state 并不提供 Web History 的 back 字段。PC iframe 如果继续走这段判断,就会被错误当成“没有上一页”,从而调用关闭 WebView 的原生接口。
配套修改应该把运行环境判断放在前面:
const isPcIframe =
globalStatusStore.platformName === 'PC' && window.parent !== window;
if (!isPcIframe && !router.options.history.state.back) {
platformApi.closeWebView({ callback: () => {} });
} else {
router.go(-1);
}
最终的行为变成:
| 场景 | 有上一页 | 没有上一页 |
|---|---|---|
| 普通 H5 | router.go(-1) |
关闭 WebView |
| PC iframe | router.go(-1) |
停留在内存历史首项,由宿主页面负责关闭 iframe |
这里的“PC iframe 栈底不关闭 WebView”是有意设计:iframe 本身通常不是一个应该主动关闭宿主 WebView 的页面,关闭动作应由 PC 容器决定。
需要同步检查的隐藏耦合
切换 history 后,不能只搜索 router.go(-1)。凡是直接读取浏览器历史状态的逻辑,都可能与内存 history 不兼容。
例如页面切换动画可能通过下面的代码判断是否发生了回退:
const isBack = fromRoutePosition > window.history.state.position;
在 PC iframe 中,Vue Router 的位置已经保存在内存队列里,而不是 window.history.state.position。这类代码需要改为使用 Vue Router 导航信息、在应用层维护自己的位置计数,或者针对 memory history 单独处理。否则路由虽然不再污染外层浏览器,动画判断仍可能失效,甚至在 window.history.state 为空时抛出异常。
这套方案的边界
这不是把所有 iframe 都切成内存路由,而是用明确的运行条件区分场景。platform=PC 的约定必须由宿主页面稳定传入,并且 iframe 检测不能只依赖 platform,否则普通页面带上同名参数时也可能错误切换 history。
另外,内存 history 只解决导航记录隔离,不会自动解决宿主页面和 iframe 之间的通信。若 PC 容器需要感知 iframe 当前路由,应通过 postMessage 或现有桥接协议显式同步,而不是重新共享浏览器 history。
总结
问题的根因是两套页面共享了同一份浏览器历史,而不是某个返回按钮少写了一个条件。
一个可维护的解决方案应当分三步:
- 普通 H5 使用
createWebHistory,PC iframe 使用createMemoryHistory。 - 创建内存 history 后恢复首次加载的完整路由地址。
- 返回逻辑和动画逻辑都不要假设 history 一定拥有 Web History 的
state.back或state.position。
这样,iframe 内部仍然保留完整的 Vue Router 导航体验,同时不会再把内部页面跳转泄漏到外层浏览器的历史栈中。
DLC:历史隔离成功了,返回动画却反了
上面的方案落地后,iframe 内部跳转已经不会再污染宿主页面,但很快又暴露出第二个问题:同一条返回路径,在普通 H5 中使用右滑动画,在 PC iframe 中却可能使用左滑动画,甚至完全没有进入特殊路由的动画判断。
路由本身确实回到了上一页,因此问题很容易被误判为 goBack() 少设置了一次动画方向。一个直觉式修复是在 PC iframe 分支中直接执行:
if (isPcIframe()) {
router.go(-1);
return;
}
这只能保证“返回动作发生”,不能保证返回后的动画仍然走原来的判断链。它还把 PC iframe 和普通 H5 拆成了两套返回流程:以后显式动画、iOS 点击标记或其他导航副作用发生变化时,两边很容易再次漂移。
真正的断点仍然是 history.state.position
页面布局原本通过浏览器历史位置判断当前导航是不是回退:
const isBack = fromRoutePosition > window.history.state.position;
fromRoutePosition = window.history.state.position;
这段判断位于动画决策的最前面,后面才会调用 special-routes.ts 中的 isPushAnimation() 和 isBackAnimation()。切换到 createMemoryHistory() 后,iframe 内的 push 和 go 不再更新 window.history.state.position。结果可能有两种:
- 浏览器位置始终不变,代码无法识别真实回退。
window.history.state为空,监听器直接抛错,后面的特殊路由判断根本没有执行机会。
所以,只修改返回按钮仍然修错了层级。既然动画依赖的是“当前路由在栈中的位置”,这个位置就应该由 history 层统一提供。
给内存 history 补上可观察的位置
Vue Router 没有公开 createMemoryHistory() 内部队列的 position,可以在创建 history 时同步维护当前位置和有效栈长度:
let memoryHistoryPosition = 0;
let memoryHistoryLength = 1;
export const getRouterHistoryPosition = () =>
isPcIframe()
? memoryHistoryPosition
: window.history.state?.position ?? 0;
const history = createMemoryHistory(routerBase);
history.replace(getInitialMemoryRoute());
const push = history.push.bind(history);
const go = history.go.bind(history);
history.push = (to, data) => {
memoryHistoryPosition += 1;
memoryHistoryLength = memoryHistoryPosition + 1;
push(to, data);
};
history.go = (delta, triggerListeners) => {
memoryHistoryPosition = Math.max(
0,
Math.min(memoryHistoryPosition + delta, memoryHistoryLength - 1),
);
go(delta, triggerListeners);
};
这里不能只维护一个不断加减的数字,还要记录有效栈长度。用户回退后再次 push 时,原来的 forward 记录会被截断;将长度更新为 memoryHistoryPosition + 1,才能和内存 history 的真实行为保持一致。go() 也需要限制上下界,避免在栈底继续返回后出现负数位置。
普通 H5 仍然读取浏览器维护的 state.position,PC iframe 则读取应用维护的内存位置。动画监听不再关心当前使用哪一种 history:
let fromRoutePosition = getRouterHistoryPosition();
watch(router.currentRoute, (to, from) => {
const currentRoutePosition = getRouterHistoryPosition();
const isBack = fromRoutePosition > currentRoutePosition;
fromRoutePosition = currentRoutePosition;
// 后续继续执行原有动画判断
});
保持的是整条动画优先级,而不是强制调用特殊路由表
原有动画逻辑通常不只有 special-routes.ts,而是存在明确的优先级:
- 调用方显式指定的动画方向。
- 根据 history position 确认的真实回退。
special-routes.ts中配置的特殊前进、返回关系。- 根据路由层级深度推断的默认方向。
真实回退一旦在第 2 步被识别,就会和普通 H5 一样直接使用右滑动画,不需要再查询特殊路由表。special-routes.ts 仍然负责无法单靠 position 或路径深度表达的路由关系。这里要恢复的是整条判断链和原有优先级,而不是让每一次返回都强制经过路由映射。
最后,goBack() 也可以重新合并为一条流程。PC iframe 只跳过“关闭原生 WebView”的栈底判断,实际回退继续和 H5 共用同一个出口:
if (!isPcIframe() && !window.history.state?.back) {
platformApi.closeWebView({ callback: () => {} });
} else {
if (animationDirection) {
globalStatusStore.setAnimationDirection(animationDirection);
}
router.go(-1);
}
这次补丁带来的结论是:替换路由 history 不只是更换存储介质,还必须迁移所有依赖该介质衍生状态的逻辑。 返回按钮、页面动画、导航守卫和缓存策略看似属于不同模块,只要它们读取过 window.history.state,就都处在这次迁移的影响范围内。