javascript中 Iframe 处理多端通信、鉴权

一、核心优化背景与目标

在项目 A 内嵌项目 B 页面的场景中,单纯的基础 iframe 嵌入存在样式冲突、通信安全、性能损耗、Token 管理混乱、用户体验差等问题。

本次优化围绕「兼容性、安全性、性能、可维护性、用户体验」五大维度展开,覆盖从嵌入方式到异常处理的全流程。

核心优化目标

  1. 消除父子页面样式/JS冲突,保证独立运行
  2. Token存储、更新逻辑,如何适配401场景

二、iframe

iframe:https://developer.mozilla.org/zh-CN/docs/Web/HTML/Reference/Elements/iframe

在A项目中嵌入Iframe时需要注意:项目 A 和项目 B 往往使用不同 UI 框架 / 全局样式,直接嵌入会导致样式覆盖、JS 变量污染,需做三层隔离:

javascript 复制代码
<!-- 项目A中嵌入项目B页面的优化写法 -->
<iframe
  id="projectB-iframe"
  src="https://projectB-domain.com/target-page"
  frameborder="0"
  scrolling="auto"
  sandbox="allow-scripts allow-same-origin allow-top-navigation allow-forms"
  style="width: 100%; height: 100%; border: none; display: none;"
  loading="lazy"
></iframe>
属性 作用 优化点
sandbox 沙箱隔离 精准授权(仅开放必要权限),防止 B 页面篡改 A 页面 DOM/JS,避免 XSS 风险;禁止allow-popups(非必要时)、allow-modals(按需开放)
loading="lazy" 懒加载 非首屏 iframe 延迟加载,降低 A 页面首屏渲染压力
display: none 初始隐藏 避免 B 页面加载过程中闪烁,加载完成后再显示
frameborder="0" 移除边框 视觉统一,提升体验

三、核心问题

问题场景

通信采用postMessage

iframe 内部同时发起多个接口请求,若 Token 过期所有请求均返回 401,会导致:

  • 多次通知父组件刷新 Token,造成接口冗余请求
  • 刷新 Token 后,失败的请求无法自动重试,需用户手动操作
  • 跨域通信无校验 / 超时机制,存在安全风险或页面卡死问题
核心机制 作用
刷新锁(isRefreshing) 保证仅向父组件发送一次 Token 刷新请求
失败请求队列(failedRequestQueue) 收集刷新期间所有失败的 401 请求,刷新后批量重试
跨域通信校验 校验 event.origin,仅处理可信域名的消息
通信超时机制 防止父组件无响应导致 iframe 卡死

四、完整实现方案

(1)通信协议规范化

定义统一的通信格式,避免通信混乱,便于后期维护和扩展:

javascript 复制代码
// 通信协议常量(A/B项目统一引入)
export const IFRAME_MESSAGE_PROTOCOL = {
  // 消息类型
  TYPE: {
    TOKEN_UPDATE: 'TOKEN_UPDATE', // A向B同步Token
    TOKEN_EXPIRED: 'TOKEN_EXPIRED', // B通知A Token过期
    ROUTE_CHANGE: 'ROUTE_CHANGE', // A通知B跳转路由
    LOADING: 'LOADING', // B通知A加载状态
    ERROR: 'ERROR', // 异常通知
    INIT_COMPLETE: 'INIT_COMPLETE' // B初始化完成
  },
  // 状态码
  CODE: {
    SUCCESS: 200,
    FAIL: 500,
    TIMEOUT: 408
  },
  // 生成标准消息体
  createMessage(type, payload = {}, code = 200) {
    return {
      version: '1.0', // 协议版本,便于后续兼容
      type,
      payload,
      code,
      timestamp: Date.now(),
      nonce: Math.random().toString(36).substr(2, 10) // 随机串,防止重复处理
    };
  },
  // 验证消息合法性
  validateMessage(message) {
    return message.version === '1.0' && message.type && message.timestamp > Date.now() - 5 * 60 * 1000; // 5分钟内有效
  }
};
(2)通信工具封装(A/B 通用)

封装通信方法,简化使用并统一处理安全校验、超时、异常:

javascript 复制代码
// 项目A/B通用 - iframe通信工具
export class IframeCommunicator {
  /**
   * 初始化通信器
   * @param {string} targetOrigin 目标域名(如https://projectB-domain.com)
   * @param {HTMLElement} iframeElement 仅A项目需要传,B项目传null
   */
  constructor(targetOrigin, iframeElement = null) {
    this.targetOrigin = targetOrigin;
    this.iframeElement = iframeElement;
    this.listeners = new Map(); // 监听回调映射
    this.pendingRequests = new Map(); // 待响应的请求(用于双向确认)

    // 初始化监听
    this.initListener();
  }

  // 初始化message监听
  initListener() {
    window.addEventListener('message', (event) => {
      // 1. 校验来源
      if (event.origin !== this.targetOrigin) return;
      const message = event.data;

      // 2. 校验协议合法性
      if (!IFRAME_MESSAGE_PROTOCOL.validateMessage(message)) return;

      // 3. 处理响应型消息(如A发请求,B回结果)
      if (message.nonce && this.pendingRequests.has(message.nonce)) {
        const { resolve, reject } = this.pendingRequests.get(message.nonce);
        message.code === IFRAME_MESSAGE_PROTOCOL.CODE.SUCCESS ? resolve(message.payload) : reject(message.payload);
        this.pendingRequests.delete(message.nonce);
        return;
      }

      // 4. 处理普通监听消息
      const callback = this.listeners.get(message.type);
      if (callback) callback(message.payload, event);
    });
  }

  /**
   * 发送消息
   * @param {string} type 消息类型
   * @param {any} payload 消息体
   * @param {boolean} needResponse 是否需要响应(双向确认)
   * @returns {Promise<any>} 仅needResponse为true时返回Promise
   */
  send(type, payload, needResponse = false) {
    const message = IFRAME_MESSAGE_PROTOCOL.createMessage(type, payload);
    const targetWindow = this.iframeElement ? this.iframeElement.contentWindow : window.parent;

    // 发送消息
    targetWindow.postMessage(message, this.targetOrigin);

    // 需要响应时,返回Promise
    if (needResponse) {
      return new Promise((resolve, reject) => {
        // 设置超时
        const timeout = setTimeout(() => {
          reject({ msg: '通信超时' });
          this.pendingRequests.delete(message.nonce);
        }, 5000);

        this.pendingRequests.set(message.nonce, {
          resolve,
          reject,
          timeout
        });
      });
    }
  }

  /**
   * 监听消息
   * @param {string} type 消息类型
   * @param {Function} callback 回调函数
   */
  on(type, callback) {
    this.listeners.set(type, callback);
  }

  /**
   * 移除监听
   * @param {string} type 消息类型
   */
  off(type) {
    this.listeners.delete(type);
  }

  // 销毁通信器(防止内存泄漏)
  destroy() {
    this.listeners.clear();
    this.pendingRequests.forEach(({ timeout }) => clearTimeout(timeout));
    this.pendingRequests.clear();
  }
}

3)项目 A 中使用通信工具

javascript 复制代码
// 项目A - 初始化通信器
const iframe = document.getElementById('projectB-iframe');
const communicator = new IframeCommunicator('https://projectB-domain.com', iframe);

// 1. 监听B项目的Token过期通知
communicator.on(IFRAME_MESSAGE_PROTOCOL.TYPE.TOKEN_EXPIRED, async (payload) => {
  try {
    // 刷新A项目的Token
    const newToken = await refreshToken();
    // 同步Token给B项目(需要B响应确认)
    await communicator.send(
      IFRAME_MESSAGE_PROTOCOL.TYPE.TOKEN_UPDATE,
      { token: newToken, refreshToken: newRefreshToken },
      true // 需要B确认接收成功
    );
  } catch (err) {
    // 刷新失败,跳转登录
    window.location.href = '/login';
  }
});

// 2. 监听B项目的加载状态
communicator.on(IFRAME_MESSAGE_PROTOCOL.TYPE.LOADING, (payload) => {
  // 显示/隐藏A项目的加载动画
  setLoading(payload.isLoading);
});

// 3. 通知B项目跳转路由
async function redirectProjectB(path) {
  try {
    await communicator.send(IFRAME_MESSAGE_PROTOCOL.TYPE.ROUTE_CHANGE, { path }, true);
    console.log('B项目路由跳转成功');
  } catch (err) {
    console.error('B项目路由跳转失败:', err);
  }
}

(4)项目 B 中使用通信工具

javascript 复制代码
// 项目B - 初始化通信器
const communicator = new IframeCommunicator('https://projectA-domain.com');

// 1. 初始化完成后通知A项目
communicator.send(IFRAME_MESSAGE_PROTOCOL.TYPE.INIT_COMPLETE, { status: 'ready' });

// 2. 监听A项目的Token更新
communicator.on(IFRAME_MESSAGE_PROTOCOL.TYPE.TOKEN_UPDATE, (payload) => {
  // 更新B项目本地Token
  localStorage.setItem('token', payload.token);
  localStorage.setItem('refreshToken', payload.refreshToken);
  // 通知A项目接收成功(响应)
  communicator.send(
    IFRAME_MESSAGE_PROTOCOL.TYPE.TOKEN_UPDATE,
    { msg: 'Token接收成功' },
    false
  );
  // 重试失败的请求
  retryFailedRequests();
});

// 3. 监听A项目的路由跳转指令
communicator.on(IFRAME_MESSAGE_PROTOCOL.TYPE.ROUTE_CHANGE, (payload) => {
  // B项目内部路由跳转(如vue-router)
  router.push(payload.path);
  // 响应A项目:跳转成功
  communicator.send(
    IFRAME_MESSAGE_PROTOCOL.TYPE.ROUTE_CHANGE,
    { msg: '路由跳转成功' },
    false
  );
});

// 4. 接口401时通知A项目(集成到axios拦截器)
axios.interceptors.response.use(
  (res) => res,
  async (err) => {
    const originalRequest = err.config;
    if (err.response?.status === 401 && !originalRequest._retry) {
      originalRequest._retry = true;
      // 通知A项目Token过期(需要A响应)
      try {
        await communicator.send(IFRAME_MESSAGE_PROTOCOL.TYPE.TOKEN_EXPIRED, {}, true);
        // Token已刷新,重试请求
        originalRequest.headers['Authorization'] = `Bearer ${localStorage.getItem('token')}`;
        return axios(originalRequest);
      } catch (refreshErr) {
        return Promise.reject(refreshErr);
      }
    }
    return Promise.reject(err);
  }
);
相关推荐
周淳APP2 小时前
【JS之闭包防抖节流,this指向,原型&原型链,数据类型,深浅拷贝】简单梳理啦!
开发语言·前端·javascript·ecmascript
ok_hahaha2 小时前
java从头开始-苍穹外卖day05-Redis及店铺营业状态设置
java·开发语言·redis
2501_933329552 小时前
舆情监测系统的技术演进:从数据采集到AI中台,Infoseek如何实现“监测+处置”一体化
开发语言·人工智能·自然语言处理·系统架构
kyriewen2 小时前
console.log 骗了我一整个通宵:原来它才是时间旅行者
前端·javascript·chrome
dgvri2 小时前
Windows上安装Go并配置环境变量(图文步骤)
开发语言·windows·golang
忆江南2 小时前
# iOS 动态库与静态库全面解析
前端
冴羽2 小时前
在浏览器控制台调试的 6 个秘密技巧
前端·javascript·chrome
青莲8432 小时前
查找算法详解
android·前端
前端Hardy2 小时前
别再手动调 Prompt 了!这款开源神器让 AI 输出质量提升 300%,支持 Claude、GPT、Gemini,还免费开源!
前端·javascript·面试