一、核心优化背景与目标
在项目 A 内嵌项目 B 页面的场景中,单纯的基础 iframe 嵌入存在样式冲突、通信安全、性能损耗、Token 管理混乱、用户体验差等问题。
本次优化围绕「兼容性、安全性、性能、可维护性、用户体验」五大维度展开,覆盖从嵌入方式到异常处理的全流程。
核心优化目标
- 消除父子页面样式/JS冲突,保证独立运行
- 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);
}
);