iT邦幫忙

2022 iThome 鐵人賽

DAY 28
0
Modern Web

這些那些你可能不知道我不知道的Web技術細節系列 第 28

你可能不知道的Web API -- postMessage

前言

postMessage()是少數可以讓兩個不同頁面交換訊息的方式。如其名,傳遞訊息,postMessage()接收一段文字訊息,將這個文字訊息傳遞給通知的對象。通知的對象可以監聽message事件獲取訊息。

關於postMessage()

實際上現在瀏覽器網頁API上,存在的postMessage()API不只一個。有window.postMessage()Worker.postMessage()BroadcastChannel.postMessage()Client.postMessage(),它們有著類似的使用方式。除了不同頁面溝通外,對於建立的Web Worker執行緒也有相似方式傳遞訊息,除Worker.postMessage()外,在Web Worker環境下還可以建立BroadcastChannel物件,使用BroadcastChannel.postMessage()方法。至於Client.postMessage()是在Service Worker使用的,有在寫PWA(Progressive Web Application,漸進式網頁應用程式)才比較會用到。

本節主要討論的是最基本的window.postMessage()

基本用法

基本上的用法可以傳遞一個字串作為訊息發送出去,像是window.postMessage("msg")。然後監聽message事件。所以我們可以做一個簡單的例子。

現在建立兩個頁面index.html作為主畫面,sub.html作為用iframe嵌入在主畫面的子畫面。

主畫面內容如下,除了一個發送訊息的文字話框外,還有一個接收訊息的文字畫框,以及一個發送訊息的按鈕。

<!------ index.html ------>
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8"/>
    <title>[DEMO] postMessage (Main)</title>
  </head>
  <body>
    <form>
      <textarea cols="30" id="msg" name="" rows="10"></textarea>
      <br/>
      <textarea cols="30" id="rev" name="" rows="10" disabled></textarea>
      <br/>
      <button>send</button>
    </form>

    <hr/>

    <iframe frameborder="0" src="sub.html"></iframe>

    <script type="text/javascript" src="main.js"></script>
  </body>
</html>

相對來說子畫面就簡單一些,只有一個接受訊息的地方。它會將接受到的訊息原封不動的回傳回去。

<!------ sub.html ------>
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8"/>
    <title>[DEMO] postMessage (Sub)</title>
  </head>
  <body>
    <form>
      <textarea cols="30" id="rev" name="" rows="10" disabled></textarea>
      <br/>
    </form>
    <script type="text/javascript" src="sub.js"></script>
  </body>
</html>

reciveMessage()是最基本的訊息事件監聽處理器。它一接收到訊息,確定來源正確,除了將訊息打印在接收文字話框外,就又將訊息向來源發送回去。

  function reciveMessage(event) {
    let {data, origin, source} = event;
    if(origin !== window.location.origin)
      return;

    rev.value += data;
    rev.value += `
--------------
`

    if(data.match(/^ECHO/))
      return;

    source.postMessage(`ECHO(Sub): ${data}`);
  }

這麼一來,要建立子畫面的JavaScript腳本也就容易了,以下是sub.js的部分內容,即監聽message事件。

/*** sbu.js ****/
window.addEventListener('load', (event) => {
  let rev = document.querySelector('#rev');

  function reciveMessage(event) { /*** 中略 ***/ }

  window.addEventListener('message', reciveMessage);
})

主畫面除了上面幾本內容外,還需要在點擊發送訊息按鈕後,將訊息傳遞給子畫面:

  send.addEventListener('click', (event) => {
    event.preventDefault();
    let _msg = msg.value;
    iframe.contentWindow.postMessage(_msg, window.location.origin);
  })

所以下面是更完整的main.js內容:

/*** main.js ****/
window.addEventListener('load', () => {
  let msg = document.querySelector('#msg');
  let rev = document.querySelector('#rev');
  let send = document.querySelector('button');
  let iframe = document.querySelector('iframe');

  function reciveMessage(event) { /*** 中略 ***/ }

  window.addEventListener('message', reciveMessage);
    
  send.addEventListener('click', (event) => {
    event.preventDefault();
    let _msg = msg.value;
    iframe.contentWindow.postMessage(_msg, window.location.origin);
  })
})

DEMO

window.postMessage()的基本用法

window.postMessage()的第二個參數可以指名對象的origin,避免將訊息傳遞到不該接受訊息的頁面,洩漏訊息。origin包含協議(HTTP或HTTPs)、domain、port等部分,必須完全一致才會真的將訊息傳遞出去,否則將會報錯。如果並不在意訊息傳遞何處,允許任意頁面處理非敏感訊息,可以直接用萬用字元*表示。

實際上第一個參數也不是只有接受字串類型,多數的JavaScript物件都可以,它會自動將物件透過結構化複製(The structured clone algorithm)序列化,然後在接收端反序列化為物件。

比如現在有一個bob的簡單物件:

var bob = { name: "Bob" }

將接收端收到的訊息,設定到全域變數的data上:

iframe.contentWindow.addEventListener('message', 
                                      (event) => globalThis.data = event.data);

然後將物件發送出去:

iframe.contentWindow.postMessage(bob);

現在globalThis包含data屬性,長的和bob一模一樣:

console.log(globalThis.data); // {name: bob}

但是bobdata是不同物件:

data === bob; // false

要注意的是,多數JavaScript類型都可以,但有少部分類型無法進行結構化複製,像是ES6後的基本類型Symbol,故使用時仍需多加留意。

message事件

message事件除了包含傳遞進來的資料data外,還有發送來源的origin。可以透過檢查origin是否為可信任的來源。除此之外,還有一個source物件,用以回應訊息。

也就是說,在主頁面傳遞訊息給子頁面後,子頁面可以保留source物件,建立雙向的溝通連線。在上例中只是簡單的將訊息重複而已,你可以建立更為複雜的溝通流程。

傳遞所有權

實際上postMessage還有一個我非常少使用到而經常被忽略的參數transfer。如上所說,多數資料(data)傳遞方式是透過「複製」,「複製」存在一定的成本,尤其是針對非常大的物件的時候。如果可以直接轉移所有權,就可以減少複製的動作,提升效能、保護地球。

不過也不是所有物件都可以轉移的,必須是Transferable Objects,如:

  • ArrayBuffer
  • MessagePort
  • ReadableStream
  • WritableStream
  • TransformStream
  • AudioData
  • ImageBitmap
  • VideoFrame
  • OffscreenCanvas
  • RTCDataChannel

其中最簡單的就是ArrayBuffer。以同樣上面的實驗環境,同樣將接受到的物件儲存到globalThis

iframe.contentWindow.addEventListener('message', 
                                      (event) => globalThis.data = event.data);

不同的是現在要傳遞的物件是一個ArrayBuffer

var buff = new ArrayBuffer(8);
console.log(buff.byteLength); // 8

然後用類似方式將物件發送出去,不同的是這次申明了轉移所有權:

iframe.contentWindow.postMessage(buff, "*", [buff]);

在最後的結果會發現buffbyteLength變成了0

console.log(globalThis.data.byteLength); // 8
console.log(           buff.byteLength); // 0

所以這可以做什麼?

建立兩個Web Worker之間的通訊

MessagePort是一個可轉移類型。並且不是只有window.postMessage(),更有用的地方或許在Web Worker--Worker.postMessage()。可以用於建立兩個worker之間的溝通。

// 以下參考自: http://xiefeng.tech/Web%20Worker.html#messagechannel
const channel = new MessageChannel();

const workerA = new Worker('./worker.js');

const workerB = new Worker('./worker.js');

workerA.postMessage('workerA', [channel.port1]);

workerB.postMessage('workerB', [channel.port2]);

將大資料的運算交由Web Worker處理

儘管Worker.postMessage()window.postMessage()的用法不全然相同。但透過轉移大資料,比如上傳的檔案、相機擷取到的影像等,如果需要進行大量運算,會交由Web Worker處理,減少UI的卡頓。但如果使用複製方式,資料量大的話一樣容易造成畫面UI卡頓問題。使用轉移方式可以降低成本、提升效率。

題外話

突然想到的。其實這有點像是Go語言的Channel:

// https://gobyexample.com/channels
package main

import "fmt"

func main() {

    messages := make(chan string)

    go func() { messages <- "ping" }()

    msg := <-messages
    fmt.Println(msg)
}

channel的這種設計方式,也可以算是一種設計模式。儘管Go語言中的主要是兩個gorounting,而不是傳統意義上的thread,但這種設計可以簡化異步(含不同threat之間)溝通的複雜情況。現在在C#也可以使用System.Threading.Channels

本文同時發表於我的隨筆


上一篇
你可能不知道的CSS Injection
下一篇
你可能不知道的(Web)API--FinalizationRegistry(GC)
系列文
這些那些你可能不知道我不知道的Web技術細節33
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言