Dian
10 min readSep 9, 2023

使用 promise 實作同步版 postMessage

2023/09/10 更新改用 MessageChannel 替代 messageId 區分每筆溝通

Photo by Michał Parzuchowski on Unsplash

前言:
postMessage 是一個可以跨頁面或是與各種 Worker, MessageChannel 等不在同一個頁面的 js 互相溝通的非同步 API,以下的文章將記錄透過 async/await 處理讓 postMessage 可以模擬成類同步 API,目的是簡化程式碼的架構。讓我們一步一步開始吧。

以下的範例都是用 main page 與 iframe page 溝通做說明。

第一步:讓 main 跟 iframe 互相收發訊息

// main page
window.addEventListener("message", (msgEvt)=>{
console.log(`[iframe-> main]: ${msgEvt.data}`);
});

const iframeWindow= document.querySelector("iframe").contentWindow;
iframeWindow.postMessage('hello iframe. Give me a number.');

// iframe page
window.addEventListener("message", (msgEvt)=>{
console.log(`[main -> iframe]: ${msgEvt.data}`);
msgEvt.postMessage(`Hi main, ${Date.now()}.`);
});

/*-------------------------------------*/
// console output
// [main -> iframe]: Hello iframe. Give me a number.
// [iframe -> main]: Hi main, 1694265150735.

在標準使用方式下,可以看到送訊息跟收訊息在程式碼的段落基本上是分開的,在程式碼的閱讀跟使用上都很不直覺。
假如 main 在短時間內送了兩次訊息, iframe 也會回傳兩次訊息但是無法分清楚是要針對那一個 main 訊息回傳的。

第二步:使用 MessageChannel 區分每次回傳的訊息

// main page
// 新增 messageId 區分訊息
let messageId = 0;
const iframeWindow = document.querySelector("iframe").contentWindow;

const msgChannel1 = new MessageChannel();
msgChannel1.port1.onmessage = (msgEvt) => {
const {message, messageId} = msgEvt.data;
console.log(`[iframe -> main][${messageId}]: ${message}`);
};
iframeWindow.postMessage({message:"Hello iframe. Give me a number.", messageId: `msg_${++messageId}`}, location.origin , [msgChannel1.port2]);

const msgChannel2 = new MessageChannel();
msgChannel2.port1.onmessage = (msgEvt) => {
const {message, messageId} = msgEvt.data;
console.log(`[iframe -> main][${messageId}]: ${message}`);
};
iframeWindow.postMessage({message:"Hello iframe. Give me a number.", messageId: `msg_${++messageId}`}, location.origin , [msgChannel2.port2]);


// iframe page
window.addEventListener("message", (msgEvt)=>{
const {message, messageId} = msgEvt.data;
console.log(`[main -> iframe][${messageId}]: ${message}`);
// 改用 message port 來回傳訊息,這點也比原本直接用 postMessage 方便。
const messagePort = msgEvt.ports?.[0];
messagePort?.postMessage({ message: `Hi main, ${Date.now()}.`, messageId });
});

/*-------------------------------------*/
// console output
// [main -> iframe][msg_1]: Hello iframe. Give me a number.
// [main -> iframe][msg_2]: Hello iframe. Give me a number.
// [iframe -> main][msg_1]: Hi main, It's 1694265150735.
// [iframe -> main][msg_2]: Hi main, It's 1694265150735.

原本是採用 messageId 區分每次的訊息來回,採用 MessageChannel 後實際上不再需要有 messageId,但是我還是保留在互相傳送的訊息中,單純為了是在驗證時比較方便。
另外是 messagePort 與 window 的 postMessage 參數不太相同,使用時需要注意一下。

註:後面的步驟 iframe 端就沒有變化所以就不再列出相關的程式碼

第三步,包裝 postMessage

在可以區分每次來回的 message 後,再做個幾個 function 把 postMessage 包裝一下,讓使用上簡單一些。

// main page
function genMessageChannel() {
return new MessageChannel();
}

function getMsgId(){
messageId += 1;
return "msg_" + messageId;
}

function postMessageWrap(msg){
const msgId = getMsgId();
const msgChannel = genMessageChannel();

msgChannel.port1.onmessage = (msgEvt) => {
const {message, messageId} = msgEvt.data;
console.log(`[iframe -> main][${messageId}]: ${message}`);
msgChannel.port1.close();
msgChannel.port2.close();
};

iframeWindow.postMessage({
message: msg,
messageId: msgId
},
location.origin,
[msgChannel.port2]
);
}

// 使用包裝過的 postMessage function
postMessageWrap("Hello iframe. Give me a number.");

第四步,讓 postMessageWrap 變成 async function

讓 postMessageWrap 回傳的一個 Promise,在 port1 收到訊息後再用 reslove 回傳 iframe 回傳的訊息,達成這次的主要目標將 postMessage 變成 可同步的 function。

// main page

const iframeWindow = document.querySelector("iframe").contentWindow;

// 新增 messageId 區分訊息
let messageId = 0;

function getMsgId(){
messageId += 1;
return "msg_" + messageId;
}

function genMessageChannel() {
return new MessageChannel();
}

// 把 function 加上 async 設定,以及回傳一個 promise
async function postMessageWrap(msg){
const msgId = getMsgId();
const msgChannel = genMessageChannel();

const p = new Promise((reslove) => {
msgChannel.port1.onmessage = (evt) => {
reslove(evt.data);
msgChannel.port1.close();
msgChannel.port2.close();
};
});

iframeWindow.postMessage(
{
type: "awaitppostmessage",
message: msg,
messageId: msgId,
},
location.origin
);

// 回傳 promise
return p;
}

// 使用包裝過的 postMessage function,加上 await 等待 callback 完成
const result1 = await postMessageWrap("Hello iframe. Give me a number.");
console.log(`[iframe-> main][${result1.messageId}]: ${result1.message}`);
const result2 = await postMessageWrap("Hello iframe. Give me a number.");
console.log(`[iframe-> main][${result2.messageId}]: ${result2.message}`);


/*-------------------------------------*/
// iframe console output
// [main -> iframe][msg_1]: Hello iframe. Give me a number.
// [iframe -> main][msg_1]: Hi main, 1694271556117.
// [main -> iframe][msg_2]: Hello iframe. Give me a number.
// [iframe -> main][msg_2]: Hi main, 1694271556118.

在最後的版本,已經將 postMessage 封裝成一個可以 await function,在程式碼的閱讀及使用上有比較友善的表現。

以上的程式碼有上傳到 github 做一個簡單的 Demo,提供大家參考及指教。
另外也打算做成更完整的 module,讓使用更模組化。

最後再次感謝 FB JavaScript.tw 社團中的 蔡牧村 提出改用 MessageChannel 的做法,讓程式可以更進一步的精簡及單純。

參考資料:
Window.postMessage
Worker
MessageChannel
MessageChannel.postMessage
Async
Await

Dian
Dian

Written by Dian

學習>吸收>實作>再學習

No responses yet