前端和前端聯調的各種姿勢,了解一下
- 2019 年 12 月 17 日
- 筆記
平時前端都是和後台聯調,或者在內嵌webview的客戶端上和客戶端聯調,前端和前端聯調是什麼鬼?其實也是存在的,比如另一個前端寫了一個龐大的模塊(如遊戲、在線ide、可視化編輯頁面等需要沙盒環境的情況),此時引進來需要使用iframe來使用。在一個大需求裏面,按照模塊化分工的話,顯然iframe裏面的功能由一個人負責,主頁面由另一個人負責。不同的人負責的東西同時展示在頁面上交互,那麼兩個前端開發的過程中必然有聯調的過程
背景:父頁面index.html裏面有一個iframe,iframe的src為子頁面(另一個html的鏈接),下文都是基於此情況下進行
傳統方式——iframe的postmessage通信
// 父頁面的js document.querySelector("iframe").onload = () => { window.frames[0].postMessage("data from parent", "*"); }; // 子頁面的js window.addEventListener( "message", e => { console.log(e); // e是事件對象,e.data即是父頁面發送的message }, false ); 複製代碼
這個是比較傳統的方法了。注意的是,addEventListener
接收消息的時候,必須首先使用事件對象的origin和source屬性來校驗消息的發送者的身份,如果這裡有差錯,可能會導致跨站點腳本攻擊。而且需要iframe的onload觸發後才能使用postmessage
iframe的哈希變化通信
低門檻的一種手段,可以跨域
父頁面
const iframe = document.querySelector("iframe"); const { src } = iframe; // 把數據轉字符串,再通過哈希傳遞到子頁面 function postMessageToIframe(data) { iframe.src = `${src}#${encodeURIComponent(JSON.stringify(data))}`; } 複製代碼
子iframe頁面
window.onhashchange = e => { // 監聽到哈希變化,序列化json const data = JSON.parse(decodeURIComponent(location.hash.slice(1))); console.log(data, "data >>>>"); }; 複製代碼
打開父頁面,執行postMessageToIframe({ txt: 'i am lhyt' })
,即可看見控制台有子頁面的反饋:

反過來,子頁面給父頁面通信,使用的是parent:
// 子頁面 parent.postMessageToIframe({ name: "from child" }) // 父頁面, 代碼是和子頁面一樣的 window.onhashchange = () => { const data = JSON.parse(decodeURIComponent(location.hash.slice(1))); console.log(data, "data from child >>>>"); }; 複製代碼
注意:
- 父傳子hash通信,是沒有任何門檻,可以跨域、可以直接雙擊打開html
- 子頁面使用parent的時候,跨域會報錯
Uncaught DOMException: Blocked a frame with origin "null" from accessing a cross-origin frame.
onstorage事件
父子iframe頁面通信
localstorage
是瀏覽器同域標籤共用的存儲空間。html5支持一個onstorage
事件,我們在window對象上添加監聽就可以監聽到變化: window.addEventListener('storage', (e) => console.log(e))
需要注意 此事件是非當前頁面對localStorage進行修改時才會觸發,當前頁面修改localStorage不會觸發監聽函數!!!
// 父頁面 setTimeout(() => { localStorage.setItem("a", localStorage.getItem("a") + 1); }, 2000); window.addEventListener("storage", e => console.log(e, "parent")); // 子頁面 window.addEventListener("storage", e => console.log(e, "child")); 複製代碼
打印出來的storageEvent是這樣的:

更騷的操作,自己和自己通信
都是兩個頁面,要寫兩分html,有沒有辦法不用寫兩個html呢,只需要一個html呢?其實是可以的!
給url加上query
參數或者哈希,表示該頁面是子頁面。如果是父頁面,那麼創建一個iframe,src是本頁面href加上query
參數。父頁面html不需要有什麼其他標籤,只需要一個script即可
const isIframe = location.search; if (isIframe) { // 子頁面 window.addEventListener("storage", e => console.log(e, "child")); } else { // 父頁面,創建一個iframe const iframe = document.createElement("iframe"); iframe.src = location.href + "?a=1"; document.body.appendChild(iframe); setTimeout(() => { localStorage.setItem("a", localStorage.getItem("a") + 1); }, 2000); window.addEventListener("storage", e => console.log(e, "parent")); } 複製代碼
MessageChannel
MessageChannel
創建一個新的消息通道,並通過它的兩個MessagePort 屬性發送數據,而且在 Web Worker 中可用。MessageChannel
的實例有兩個屬性,portl1
和port2
。給port1發送消息,那麼port2就會收到。
// 父頁面 const channel = new MessageChannel(); // 給子頁面的window注入port2 iframe.contentWindow.port2 = channel.port2; iframeonload = () => { // 父頁面使用port1發消息,port2會收到 channel.port1.postMessage({ a: 1 }); }; // 子頁面,使用父頁面注入的port2 window.port2.onmessage = e => { console.error(e); }; 複製代碼
MessageChannel優點: 可以傳對象,不需要手動序列化和反序列化,而且另一個port收到的是對象深拷貝
SharedWorker
是worker的一種,此worker可以被多個頁面同時使用,可以從幾個瀏覽上下文中訪問,例如幾個窗口、iframe、worker。它具有不同的全局作用域——只有一部分普通winodow下的方法。讓多個頁面共享一個worker,使用該worker作為媒介,即可實現通信
worker的代碼
// 存放所有的連接端口 const everyPorts = []; onconnect = function({ ports }) { // onconnect一觸發,就存放到數組裏面 everyPorts.push(...ports); // 每次連接所有的端口都加上監聽message事件 [...ports].forEach(port => { port.onmessage = function(event) { // 每次收到message,對所有的連接的端口廣播,除了發消息的那個端口 everyPorts.forEach(singlePort => { if (port !== singlePort) { singlePort.postMessage(event.data.data); } }); }; }); }; 複製代碼
父頁面js代碼
const worker = new SharedWorker("./worker.js"); window.worker = worker; worker.port.addEventListener( "message", e => { console.log("parent:", e); }, false ); worker.port.start(); setTimeout(() => { worker.port.postMessage({ from: "parent", data: { a: 111, b: 26 } }); }, 2000); 複製代碼
iframe子頁面的js代碼:
const worker = new SharedWorker("./worker.js"); worker.port.onmessage = function(e) { console.log("child", e); }; worker.port.start(); setTimeout(() => { worker.port.postMessage({ data: [1, 2, 3] }); }, 1000); 複製代碼
正常情況下,postMessage發生的時機應該是全部內容onload後執行最好,不然對方還沒load完,還沒綁定事件,就沒有收到onmessage了
SharedWorker也是可以傳對象的哦
直接注入對象和方法
上面很多例子,都用了contentWindow,既然contentWindow是iframe自己的window,那麼我們就可以隨意注入任何內容,供iframe調用了。前端和客戶端聯調,常用的方法之一就是注入函數。子頁面調用父頁面的方法,因為有parent這個全局屬性,那麼父頁面的window也是可以拿到的了
// 父頁面 document.querySelector("iframe").contentWindow.componentDidMount = () => { console.log("iframe did mount"); }; // 子頁面 window.onload = () => { // 假設這裡有react一系列流程運行... setTimeout(() => { // 假設現在是react組件didmount的時候 window.componentDidMount && window.componentDidMount(); }, 1000); }; 複製代碼
下面,基於給iframe的window注入方法,來設計一個簡單的通信模塊
- 父頁面主動調子頁面, 子頁面被父頁面調
- 父頁面被子頁面調,子頁面調父頁面
父頁面下,給window掛上parentPageApis對象,是子頁面調用方法的集合。並給子頁面注入一個callParentApi的方法來調父頁面的方法。
const iframe = document.querySelector("iframe"); window.parentPageApis = window.parentPageApis || {}; // 父頁面自己給自己注入子頁面調用的方法 Object.assign(window.parentPageApis, { childComponentDidMount() { console.log("子頁面did mount"); }, changeTitle(title) { document.title = title; }, showDialog() { // 彈窗 } }); // 給子頁面注入一個callParentApi的方法來調父頁面 iframe.contentWindow.callParentApi = function(name, ...args) { window.parentPageApis[name] && window.parentPageApis[name].apply(null, args); }; iframe.contentWindow.childPageApis = iframe.contentWindow.childPageApis || {}; Object.assign(iframe.contentWindow.childPageApis, { // 父頁面也可以給子頁面注入方法 }); setTimeout(() => { // 調用子頁面的方法 callChildApi("log", "父頁面調子頁面的log方法打印"); }, 2000); 複製代碼
子頁面也給父頁面注入callChildApi方法,並把自己的一些對外的方法集合寫在childPageApis上
window.childPageApis = window.childPageApis || {}; Object.assign(window.childPageApis, { // 子頁面自己給自己注入方法 log(...args) { console.log(...args); } }); window.parent.window.callChildApi = function(name, ...args) { window.childPageApis[name] && window.childPageApis[name].apply(null, args); }; window.onload = () => { // 假設這裡有react一系列流程運行... setTimeout(() => { // 假設現在是react組件didmount的時候,告訴父頁面 window.callParentApi("childComponentDidMount"); }, 1000); }; 複製代碼
最後
以上的storage、SharedWorker的方案,也適用於「不同tab通信」這個問題。總的來說,SharedWorker比較安全,注入全局方法比較靈活,哈希變換通信比較簡單。postmessage、哈希變化、storage事件都是基於字符串,MessageChannel、SharedWorker可以傳遞任何「可拷貝的值」。全局注入就可以為所欲為了,但也是最危險的,需要做好防範