Iframe 父子页面消息传递实战

iframe postMessage Vue Router

iframe 可在当前页面内嵌独立子应用,父子页面可分别使用不同技术栈,只要约定好消息格式即可协作。本文记录一次实际踩坑:父页面嵌入使用 Vue Router Hash 路由的子应用,浏览与后退正常,但刷新后子路由丢失、回到首页。下文给出背景、方案与完整实现。

背景

进入首页后,依次点击 Go to Apple、Go to Banana、Go to Orange,下方路由会正确渲染,浏览器后退也正常。停留在 Orange 路由并刷新页面时,iframe 内的路由状态丢失,被重置为首页。

iframe 父子页面通信示意

目标:刷新后子页面路由保持原有状态。

方案

整体流程可概括为「子页上报 → 父页缓存 → 刷新后下发 → 子页恢复」:

  1. 子页面监听路由变化,通过 postMessage 通知父页面
  2. 父页面接收消息,将路由写入 localStorage
  3. 页面刷新后,父页面在 iframe 加载完成时读取缓存
  4. 父页面将缓存路由 postMessage 给子页面
  5. 子页面监听消息,执行 router.push 跳转到对应路由
子页面路由变化
    ↓ postMessage
父页面写入 localStorage
    ↓ 刷新
父页面 iframe onload
    ↓ postMessage
子页面 router.push 恢复路由

实现

父页面

父页面负责两件事:接收子页面的路由更新并持久化,以及在 iframe 就绪后把缓存路由发回子页面。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>hash模式主页面</title>
    <style>
      html,
      body {
        width: 90%;
        height: 90%;
      }
      iframe {
        width: 90%;
        height: 90%;
      }
    </style>
  </head>
  <body>
    <h1>测试 HASH 模式子路由保持功能</h1>
    <iframe src="http://localhost:3000/child" id="iframeId" name="iframeName"></iframe>
    <script>
      // 接收子页面路由消息,写入 localStorage
      window.addEventListener("message", (e) => {
        console.log("接收子页面消息:", e.data);
        window.localStorage.setItem("child_route", e.data);
      });
    </script>
    <script>
      // iframe 加载完成后,将缓存路由发回子页面
      document.getElementById("iframeId").onload = function () {
        const child_route = window.localStorage.getItem("child_route");
        if (child_route) {
          console.log("向子页面发送消息:", child_route);
          iframeName.window.postMessage(child_route, "http://localhost:3000/child");
          window.localStorage.removeItem("child_route");
        }
      };
    </script>
  </body>
</html>

子页面

子页面使用 Vue 3 + Vue Router(Hash 模式)。在 beforeEach 里上报路由,并监听父页面下发的恢复消息。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Hash模式子页面</title>
    <script src="https://unpkg.com/vue@3"></script>
    <script src="https://unpkg.com/vue-router@4"></script>
  </head>
  <body>
    <div id="app">
      <h1>Hello App!</h1>
      <p>
        <router-link to="/">主页</router-link>
        <router-link to="/apple">Go to Apple</router-link>
        <router-link to="/banana">Go to Banana</router-link>
        <router-link to="/orange">Go to Orange</router-link>
      </p>
      <p>下方为路由页面</p>
      <router-view></router-view>
    </div>
    <script>
      const Home = { template: "<div>水果</div>" };
      const Apple = { template: "<div>Apple</div>" };
      const Banana = { template: "<div>Banana</div>" };
      const Orange = { template: "<div>Orange</div>" };

      const routes = [
        { path: "/", component: Home },
        { path: "/apple", component: Apple },
        { path: "/banana", component: Banana },
        { path: "/orange", component: Orange },
      ];

      const router = VueRouter.createRouter({
        history: VueRouter.createWebHashHistory(),
        routes,
      });

      // 路由变化时通知父页面(首页不上报,避免覆盖有效缓存)
      router.beforeEach((to, from) => {
        if (to.fullPath === "/") return;
        console.log("向主页面发送消息:", to.fullPath);
        window.top.postMessage(to.fullPath, "http://localhost:3000");
      });

      // 接收父页面下发的路由,并跳转恢复
      window.addEventListener("message", (e) => {
        console.log("接收主页面消息:", e.data);
        router.push(e.data);
      });

      const app = Vue.createApp({});
      app.use(router);
      app.mount("#app");
    </script>
  </body>
</html>

注意点

  • postMessagetargetOrigin:示例写死了 http://localhost:3000,生产环境应换成实际域名,并在 message 事件里校验 e.origin,避免恶意页面注入消息。
  • 恢复后清除缓存:父页面下发路由后会 removeItem,防止每次 iframe 重载都重复跳转。
  • 首页不上报:子页面在 fullPath === "/" 时跳过上报,否则刷新后可能把有效路由覆盖成首页。
  • 时序依赖:恢复逻辑放在 iframe.onload 中,确保子页面脚本已加载、能接收消息。若子应用初始化较慢,可在子页面主动 postMessage 告知「已就绪」,再由父页面下发缓存。

小结

通过 postMessage 在父子页面间同步路由,并由父页面用 localStorage 持久化,即可在刷新后恢复 iframe 内子应用的路由状态。完整可运行示例见 GitHub