As you know, postMessage is a common way to communicate between different pages:

The window.postMessage() method can safely implement cross-source communication. In general, scripts with two different pages can only communicate with each other if the page executing them is on the same protocol (usually HTTPS), port number (443 is the default value for HTTPS), and host (the module document.domain of the two pages is set to the same value). The window.postmessage () method provides a controlled mechanism to circumvent this limitation and is safe as long as it is used correctly.

PostMessage provides the underlying communication capabilities, and there are a number of excellent open source libraries that have been packaged on top of it to make it easier for developers to use. For example, postmate of 1.4K Start provides a promise-based encapsulation of postMessage communication capabilities between parent pages and iframe child pages.

This article introduces another new open source PostMessage library: PostMessagejs (NPM package name: Postmessage-Promise).

One might ask: why not use Postmate and build another wheel? Take your time and listen to me.

One, the postMessage

PostMessage is divided into message sender and message receiver. The sender shall use the following methods:

otherWindow.postMessage(message, targetOrigin, [transfer]);
Copy the code

Related objects and parameters are described as follows:

  • OtherWindow: A reference to another window, such as the contentWindow property of iframe, the window object returned by executing window.open, or the window.frames named or numeric index.
  • TargetOrigin: Specifies which Windows can receive message events via the origin property of the window. The value can be a string “*” (for unrestricted) or a URI.
  • Message: Data to be sent to another window. It will be serialized by a structured cloning algorithm. This means you can safely pass data objects to the target window without having to serialize them yourself.

The receiver receives the message using the window.addeventListener method:

window.addEventListener("message", receiveMessage, false);
function receiveMessage(event)
{
  var origin = event.origin
  if(origin ! = ="http://example.org:8080")
    return;
  // ...
}
Copy the code

2. Postmessage-promise

2.1 What is inconvenient about postMessage?

  1. PostMessage is a one-time, one-way event, that is, the action of sending a message is done, and there is no concept of a message response.
  2. PostMessage does not categorize and manage events. This is perfectly fine as an underlying mechanism, but there are some inconveniences and possible side effects in business use.

2.2 What problems do postmate and other open source libraries solve?

Postmate’s official description is:

A powerful, simple, promise-based postMessage iFrame communication library.

It has the following characteristics:

  1. Is a library that addresses the communication between parent pages and iFrame child pages.
  2. Promise-base = Handshake = then = parent/ Child = handshake = promise-base = handshake = then = then On the other hand, the parent page calls the child.get method to retrieve the returned data using then.
  3. Events are classified and managed internally:
this.parent.postMessage({
  postmate: 'emit'.type: messageType,
  ...
Copy the code

2.3 What are the shortcomings of postmate library?

Despite the Postmate library, some of these issues were not fully resolved:

  1. Postmate only supports iframe pages
  2. Postmate supports message response (THEN) only with the child.get method, and the data content of get needs to be defined at initialization
  3. Postmate establishes connections at iframe.onload and makes up to five connection attempts every 500ms, which is problematic when there is a delay in page logical unit preparation (e.g. React/Vue component instantiation, etc.).
 if (this.frame.attachEvent) {
  this.frame.attachEvent('onload', loaded)
} else {
  this.frame.addEventListener('load', loaded)
}
Copy the code
  1. The classification and management of message events are non-specialized. Although separated by the Postmate attribute, they are common across postmate pages and all messages are interoperable. This can be beneficial in some scenarios, but in others it can have unintended consequences, such as message interference.

2.4 Why is postmessage-Promise needed?

Postmessage-promise refers to the originator of the message as client and the listener as server.

  1. Sometimes the logical unit of the server page is not ready as soon as the Document is loaded, so we need a method to start a listener when the logical unit is ready
  2. Sometimes we need to wait for a response to a message before sending the next message

Postmessage-promise foundation

postmessage-promise is a client-server like, WebSocket like, full Promise syntax supported postMessage library.

3.1 features

  • Support iframe and window.open open Windows
    • PostMessage itself supports iframe and window.open Windows. The Postmessage-Promise decouples window objects from functionality, calledserverObject.
  • Client -server mode, WebSocket mode
    • Postmessage-promise refers to the originator of a message as a client and the listener as a server. After the connection is successful, the client can send a message to the server and wait for a response. Meanwhile, the server can actively send messages to the client and wait for the response.
  • The client side
    • usecallServerMethod to create a server (create an iframe or open a new window) and then try to connect to the server until it times out. You can use the same one if you wantserverObjectTo create a new server-caller.
  • The server side
    • usestartListeningMethod to enable a listener that can connect to only one client. You can also enable multiple listeners if desired.
  • Full Promise support, ES2017 async await syntax support
    • Full Promise support means that you can use THEN to wait for a message response when the connection is successful and after each message is sent. Naturally, both client and server can use async syntax

3.2 How to Use it

client (iframe case)

import { callServer, utils } from "postmessage-promise";
const { getIframeServer } = utils;
// Load iframe and return serverObject
const iframeRoot = document.getElementById("iframe-root");
const serverObject = getIframeServer(iframeRoot, "/targetUrl"."iname"['iframe-style']);
// Initiate a connection
const connection = callServer(serverObject, {});
connection.then(e= > {
  // Send messages to the server
  e.postMessage('getInfo'.'any payload').then(e= > {
    console.log("response from server: ", e);
  });
  // Listen for messages from the server
  e.listenMessage((method, payload, response) = > {
    // Respond to the server's message
    response('any response to server');
  });
});
Copy the code

Async written

const asyncCaller = async function () {
  const { postMessage, listenMessage, destroy } = await callServer(serverObject, options);
  const info = await postMessage('getAnyInfo');
  const secondResult = await postMessage('secondPost');
};
asyncCaller();
Copy the code

client (window.open case)

import { callServer, utils } from "postmessage-promise";
const { getOpenedServer } = utils;
// Change to getOpenedServer
const serverObject = getOpenedServer("/targetUrl");
const options = {}; 
const connection = callServer(serverObject, {});
// ...
Copy the code

server

import { startListening } from "postmessage-promise";
// Start a listener
const listening = startListening({});
listening.then(e= > {
  // Listen for messages from the client
  e.listenMessage((method, payload, response) = > {
      // Respond to the client's message
      response('any response to client');
  });
  // Send a message to the client
  e.postMessage('toClient'.'any payload').then(e= > {
    console.log("response from client: ", e);
  });
});
Copy the code

Async written

const asyncListening = async function () {
  const { postMessage, listenMessage, destroy } = await startListening(options);
  listenMessage((method, payload, response) = > {
    response('anyInfo')}); }; asyncListening();Copy the code

Four, postmessage- Promise implementation principle and source code analysis

Below begins to talk about the implementation principle and source code parsing, if you see here, please give PostMessagejs a star.

4.1 Establishing a Connection

  • serverObject

    • Utils has two built-in helper methods: getOpenedServer and getIframeServer, which return serverObject. The embedding of an iframe and the opening of a new window are actually done within this method.
    • ServerObject Contains Server, Origin, and destroy. Server can be the contentWindow property of iframe, a window object returned by executing window.open, or a named or numeric index window.frames.
  • client

    • After calling the callServer method, the client first creates a MessageProxy (see below).
    • Then, the client starts a timer every 100mshand-shakeMessage attempts to connect until timeout (default 20 * 1000ms).
    • When the server responds concurrentlyhand-shakeThe connection is considered successful and a MessageChannel is created (see below).
  • server

    • The server starts listening for messages only after startListening is called.
    • After receiving the first postmessage-promisehand-shakeAfter that, create a MessageProxy and a MessageChannel.
    • Identifies the source of this message as a client for this channel and responds with onehand-shakeMessage (sent by MessageProxy).

4.2 Message broker and Channel

  • MessageProxy

MessageProxy is a proxy for sending and receiving messages. ChannelId is the id of the channel after the connection is successful. Both parties communicate on this channel to avoid message interference.

  1. Filter message events when receiving messages:
    const listener = function listener(event) {
      if(event.origin ! == _this.origin || event.source ! == _this.source || ! event.data || event.data[IDENTITY_KEY] ! == identityMap[_this.type].accept || event.data.channelId ! == _this.channelId || ! _this.eventFilter(event) || ! event.data.method) {return;
      }
      const { eventId, method, payload } = event.data;
      fn(method, eventId, payload);
    };
Copy the code
  1. To inject channel information when sending a message:
    this.source.postMessage({
      [IDENTITY_KEY]: identityMap[this.type].key,
      channelId: this.channelId,
      eventId,
      method,
      payload
    }, this.origin);
Copy the code
  • MessageChannel

MessageChannel offer:

  1. PostMessage method, which contains the bearer of the messageResponse (messageResponse) :
  postMessage = (method, payload) = > {
    return new Promise((resolve, reject) = > {
      let ctimer = null;
      const reswrap = value= > {
        clearTimeout(ctimer);
        resolve(value);
      };
      const eventId = Math.random().toString().substr(3.10);
      this.doPost({
        resolve: reswrap, reject, eventId
      }, method, payload);
      ctimer = setTimeout(() = > {
        delete this.messageResponse[eventId];
        reject(new Error('postMessage timeout'));
      }, this.timeout || (20 * 1000));
    });
  }
Copy the code
  1. The listenMessage method, eventually used in receiveMessage (listener), mainly distinguishes message responses from normal messages:
  receiveMessage = (method, eventId, payload) = > {
    if (method === responseMap[this.type].receive) {
      if (eventId && this.messageResponse[eventId]) {
        const response = this.messageResponse[eventId];
        delete this.messageResponse[eventId]; response(payload); }}else {
    	const response = pload= > {
        this.messageProxy.request(responseMap[this.type].post, eventId, pload);
        };
       this.listener(method, payload, response); }}Copy the code

It is worth noting that the hosting of the message response is cleared at timeout to prevent memory leaks in the event of a server exception. In particular, when multiple CallServers and startListening are called, multiple message brokers and channels are created that do not interfere with each other.

4.3. Message sending, receiving and response

MessageChannel is used to send, receive, and respond messages, using a unified communication model:

{
  postMessage: (. args) = > {
    if (messageChannel) {
      returnmessageChannel.postMessage(... args); }return Promise.reject();
  },
  listenMessage: (. args) = > {
    if(messageChannel) { messageChannel.listenMessage(... args); } }, destroy, }Copy the code

Because the channel has been isolated, so to ensure the message non-interference and reliability.

4.4 Destroying Connections

When calling the exposed destroy interface, the client and server execute messagechannel.destroy (); , the client also executes serverObject.destroy(); In addition, there is a periodic daemon on the client that checks whether the server is available and performs cleanup if not:

  function watch() {
    if(! server || server.closed) {clearInterval(watcher);
      if (messageChannel) {
        messageChannel.destroy();
      }
    }
  }
  watcher = setInterval(watch, 1000);
Copy the code

Write at the end

I originally wanted to use page-to-page communication in a business implementation, but found that the open source library did not meet the business needs well, and ended up writing the implementation myself. I decided that the library would serve the community, so I re-implemented a pure postMessage library in three spare nights. The original plan was to use PostMessagejs as the name, but it turned out to conflict with an NPM package with a similar name, so postmessage-Promise was changed. Everyone is welcome to use, make suggestions and contribute code. And don’t forget the star.