一、開篇破局:被誤解的iframe,從未真正退場(chǎng)
在微前端大行其道的今天,很多人覺得 iframe 已經(jīng)過時(shí)了。但每當(dāng)業(yè)務(wù)遇到絕對(duì)的安全沙箱隔離、第三方老舊系統(tǒng)接入、跨域廣告/掛件嵌入時(shí),大家轉(zhuǎn)了一圈還是會(huì)乖乖回到 iframe 的懷抱——畢竟它是瀏覽器原生的、最徹底的隔離方案。
究其原因,無外乎它是瀏覽器原生支持、隔離性最徹底的方案,沒有之一。但凡事皆有兩面性,iframe的隔離有多極致,跨域通信就有多棘手,這也是無數(shù)開發(fā)者對(duì)它又愛又恨的核心原因。
但是,iframe 的隔離有多完美,它的跨域通信就有多讓人頭疼!
但凡用原生window.postMessage開發(fā)過稍復(fù)雜的跨域業(yè)務(wù),大概率都踩過這些讓人崩潰的坑,堪稱前端開發(fā)的“隱形絆腳石”:
- 回調(diào)地獄:發(fā)出去了消息,不知道對(duì)方收沒收到,只能滿屏幕寫 addEventListener 去匹配消息 ID。
- 時(shí)序問題:父頁面急著發(fā)數(shù)據(jù),子頁面還沒 onload,消息直接石沉大海。
- 惡心的雙滾動(dòng)條:子頁面內(nèi)容變多被撐開,父頁面無法感知,高度死活對(duì)不上。
- 狀態(tài)同步災(zāi)難:父頁面切了深色模式,子頁面還是亮瞎眼的白色,狀態(tài)完全割裂。
“原生長篇大論的事件監(jiān)聽代碼” vs “iframe-js 一行 await 代碼” 的對(duì)比截圖對(duì)比:
// 原生 postMessage 跨域獲取數(shù)據(jù)
function fetchRemoteData(userId) {
return new Promise((resolve, reject) => {
const messageId = 'req_' + Date.now();
// 1. 必須注冊(cè)全局監(jiān)聽器
const handler = (event) => {
// 安全第一:手動(dòng)死磕 origin 校驗(yàn)
if (event.origin !== 'https://target-domain.com') return;
// 必須通過唯一 ID 匹配,不然會(huì)串線
if (event.data?.id === messageId && event.data?.action === 'USER_INFO_RES') {
clearTimeout(timer);
window.removeEventListener('message', handler); // 極易忘寫導(dǎo)致內(nèi)存泄漏
resolve(event.data.result);
}
};
window.addEventListener('message', handler);
// 2. 發(fā)送請(qǐng)求
const targetIframe = document.getElementById('my-iframe').contentWindow;
targetIframe.postMessage({
action: 'USER_INFO_REQ',
id: messageId,
payload: { userId }
}, 'https://target-domain.com');
// 3. 手動(dòng)處理超時(shí)邏輯
const timer = setTimeout(() => {
window.removeEventListener('message', handler);
reject(new Error('跨域請(qǐng)求超時(shí)'));
}, 5000);
});
}
// 使用 iframe-js 的 RPC 遠(yuǎn)程調(diào)用
async function fetchRemoteData(userId) {
try {
// 就像調(diào)用本地異步函數(shù)一樣絲滑!
const userInfo = await iframeApp.callRemote('getUserInfo', { userId }, 5000);
return userInfo;
} catch (error) {
// 完美捕獲超時(shí)或?qū)Ψ綊伋龅漠惓? console.error('調(diào)用失敗:', error.message);
}
}
二、破局方案:iframe-js 2.2.1開源,降維打擊通信痛點(diǎn)
為了徹底消滅這些惡心人的痛點(diǎn),我重構(gòu)并開源了 iframe-js(目前最新版本 2.2.1)。它不是對(duì) postMessage 的簡單封裝,而是將 iframe 通信直接拉升到了現(xiàn)代前端工程化的標(biāo)準(zhǔn)。iframe-js 的四大殺手锏功能
他的核心思路就是拋棄傳統(tǒng)的發(fā)布訂閱,直接用現(xiàn)代前端的思維(RPC、狀態(tài)機(jī)、Promise 回執(zhí))去降維打擊這些痛點(diǎn)。今天開源出來,給大家分享一下。
三、四大核心功能:徹底解決iframe通信難題
1. 像調(diào)用本地函數(shù)一樣跨域:RPC 遠(yuǎn)程調(diào)用
這是我個(gè)人最喜歡的功能。以前你想讓子頁面去查個(gè)數(shù)據(jù),得先 postMessage 過去,子頁面查完再 postMessage 回來,邏輯被嚴(yán)重撕裂。
現(xiàn)在,你可以用 RPC (Remote Procedure Call) 模式,直接用 async/await 拿到跨域函數(shù)的返回值!
提供方(如父頁面):
// 暴露一個(gè)名為 'getUserInfo' 的異步服務(wù)
iframeApp.expose('getUserInfo', async (params) => {
const res = await fetch(`/api/user/${params.id}`);
return await res.json(); // 直接 return 即可!
});
調(diào)用方(如子頁面):
// 像調(diào)用本地函數(shù)一樣絲滑,天然支持超時(shí)控制和 try/catch 錯(cuò)誤穿透!
try {
const userInfo = await childApp.callRemote('getUserInfo', { id: 1001 }, 5000);
console.log('跨域拿到數(shù)據(jù)啦:', userInfo);
} catch(err) {
console.error('調(diào)用超時(shí)或報(bào)錯(cuò):', err);
}
2. 徹底告別雙滾動(dòng)條:自動(dòng)高度適應(yīng) (Auto Resize)
同域下我們可以直接讀 DOM 高度,跨域下怎么辦?iframe-js 內(nèi)置了基于現(xiàn)代瀏覽器 ResizeObserver 的高度同步機(jī)制。性能極致,零 CPU 輪詢消耗,甚至連 display: none 導(dǎo)致的 0px 高度塌陷陷阱都在底層幫你規(guī)避了。
父頁面一行代碼授權(quán):
iframeApp.enableAutoResize();
子頁面一行代碼開啟探測(cè):
// 當(dāng)內(nèi)部存在圖片懶加載、列表下拉導(dǎo)致 DOM 撐開時(shí),父頁面的 iframe 標(biāo)簽會(huì)自動(dòng)隨之伸縮!
childApp.startAutoResizer({ offset: 20 }); // 還能額外補(bǔ)償 20px 底部間距
3. 跨越 Iframe 的狀態(tài)機(jī):全局狀態(tài)共享 (State Sync)
業(yè)務(wù)里經(jīng)常遇到父子頁面需要共享上下文的情況(主題色、語言包、當(dāng)前登錄用戶信息)。與其用事件發(fā)來發(fā)去,不如直接用微縮版“Pinia/Vuex”。
不管子頁面加載有多慢,只要它一 onload,父頁面的最新狀態(tài)就會(huì)自動(dòng)全量同步過去。
// 父頁面隨時(shí)更新狀態(tài)
iframeApp.setState({ theme: 'dark', lang: 'zh-CN' });
// 子頁面響應(yīng)式監(jiān)聽
childApp.onStateChange((newState) => {
if (newState.theme === 'dark') {
document.body.classList.add('dark-mode');
}
});
4. 絕對(duì)可靠的送達(dá):Promise ACK 與內(nèi)置隊(duì)列
原生的 postMessage 是典型的“Fire-and-Forget(發(fā)后不理)”。
而在 iframe-js 中,你可以使用 emitWithAck。底層會(huì)自動(dòng)為你分配唯一 ID 并追蹤回執(zhí)。
// 如果返回 true,說明不僅發(fā)過去了,而且對(duì)方的代碼已經(jīng)成功執(zhí)行了業(yè)務(wù)邏輯!
const isSuccess = await parentApp.emitToChildWithAck('updateData', { a: 1 });
更絕的是內(nèi)置隊(duì)列機(jī)制:如果父頁面初始化后立刻發(fā)消息,而子頁面還沒準(zhǔn)備好,消息絕不會(huì)丟!
iframe-js 會(huì)自動(dòng)將消息存入內(nèi)存隊(duì)列,等子頁面打通連接的瞬間,依次重發(fā)。
怎么用?
四、極簡上手:開箱即用,全鏈路TS支持
iframe-js無需復(fù)雜配置,開箱即用,全面支持TypeScript類型推導(dǎo),兼顧開發(fā)效率與類型安全,一行命令即可安裝:
npm install iframe-js
五、Live Demo實(shí)測(cè):眼見為實(shí),上手即體驗(yàn)
文字描述再詳盡,不如直接上手實(shí)操。我針對(duì)核心功能打造了3大極限測(cè)試場(chǎng)景Demo,打開F12控制臺(tái)查看底層日志,更能直觀感受通信流程的絲滑:
- ?? 基礎(chǔ)通信與Promise ACK確認(rèn)機(jī)制 Demo
- ?? 自動(dòng)高度適配(Auto Resize)功能 Demo
- ?? 全局狀態(tài)同步+RPC遠(yuǎn)程調(diào)用 Demo
六、寫在最后
開發(fā)iframe-js的初衷,就是想讓開發(fā)者在處理微前端嵌套、低代碼平臺(tái)渲染區(qū)、第三方系統(tǒng)接入等場(chǎng)景時(shí),擺脫iframe跨域通信的繁瑣痛點(diǎn),少踩坑、少加班,專注核心業(yè)務(wù)開發(fā)。
跨域場(chǎng)景復(fù)雜多變,如果你在使用過程中遇到奇葩報(bào)錯(cuò),或是有點(diǎn)擊穿透攔截、快捷鍵透傳等個(gè)性化需求,歡迎前往GitHub倉庫提Issue交流,一起完善工具生態(tài)。
開源地址: https://github.com/1503963513/iFramejs,如果這款工具幫你解決了實(shí)際問題,歡迎點(diǎn)亮Star支持!