In daily work, message communication is a very common scenario. For example, you are familiar with the B/S structure, in which the browser and server communicate messages based on the HTTP protocol:

However, in addition to HTTP protocol, we will use WebSocket protocol to complete message communication in some scenarios with high requirements on real-time data:

For these two scenes, I believe everyone is familiar with. Next, Po goes through another scenario of message communication, which is between a parent page and a child page loaded by an iframe.

Read Po’s recent popular articles (thanks to Digg friends for their encouragement and support 🌹🌹🌹) :

  • 77.9K Star’s Axios project (1018+ 👍)
  • Using these ideas and techniques, I have read several excellent open source projects (724+ 👍).
  • Have you mastered these advanced functional techniques (629+ 👍)

Why suddenly write about this topic? This is because in a recent project, Arbog needed to implement message communication between the parent page and the child page loaded by iframe. On the other hand, I was writing a source code analysis project recently, so I went to Github to search 🔍 and found a good project — Postmate.

After reading the Postmate source code, He decided that some of the project’s design ideas were worth learning, so he wrote this post to share with you. After reading this article, you will know the following:

  • The function of handshake in message system and how to realize handshake;
  • The design of message model and how to realize message verification to ensure communication security;
  • The use of postMessage and how to use it to achieve message communication between father and son pages;
  • Design and implementation of message communication API.

Well, without further ado, let’s take a quick look at Postmate.

In addition to Postmate, FrominXu postMessagejs is also a good choice.

Postmate introduction

Postmate is a powerful, simple, Promise – based postMessage library. It allows the parent page to communicate with child iframes across domains at minimal cost. The library has the following features:

  • Promise based apis for elegant and simple communication;
  • Message validation is used to secure the communication of bidirectional parent <-> child messages.
  • Child objects expose retrievable model objects accessible by the parent object;
  • The child object can send events that the parent object has listened to.
  • Parent objects can call functions in child objects;
  • Zero dependencies. Provide custom polyfills or abstractions for the Promise API if needed;
  • Lightweight, about 1.6 KB (minified & gzipped) in size.

We’ll look at the Postmate library from three aspects: how to shake hands, how to implement two-way messaging, and how to disconnect. In addition, some good design ideas of Postmate project will be introduced during this period.

Pay attention to the “full stack of immortal Road” read Po ge original 3 free e-books and more than 50 “re-learn TS” tutorial.

How to shake hands

The TCP connection requires a three-way handshake. Similarly, when a parent page communicates with a child page, Postmate “shakes hands” to ensure normal communication. Since Postmate communication is based on postMessage, let’s take a quick look at the postMessage API before getting into how to shake hands.

2.1 introduction of postMessage

Scripts with two different pages can only communicate with each other if the page executing them is on the same protocol, port number, and host. The window.postmessage () method provides a controlled mechanism to circumvent this limitation and is safe as long as it is used correctly.

2.1.1 postMessage () syntax
otherWindow.postMessage(message, targetOrigin, [transfer]);
Copy the code
  • OtherWindow: A reference to another window, such as the contentWindow property of iframe, the window object returned by executing window.open, etc.
  • Message: Data to be sent to another window, which will be serialized by a structured clone algorithm.
  • 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.
  • Transfer (Optional) : A string of Transferable objects that are transferred at the same time as Message. Ownership of these objects is transferred to the receiver of the message, and ownership is no longer retained by the sender.

The sender sends messages through the postMessage API, and the receiver can add message processing callbacks by listening for message events, as follows:

window.addEventListener("message", receiveMessage, false);

function receiveMessage(event) {
  let origin = event.origin || event.originalEvent.origin; 
  if(origin ! = ="http://semlinker.com") return;
}
Copy the code

2.2 Postmate handshake implementation

In telecommunications and microprocessor systems, the term Handshake has the following meanings:

  • In data communication, a sequence of events managed by hardware or software that requires mutual agreement on the state of the mode of operation before information can be exchanged.
  • The process of establishing communication parameters between receiving and transmitting stations.

For communication systems, the handshake is after the communication circuit is established and before the transmission of information begins. Handshakes are used to agree parameters such as message transfer rate, alphabet, parity, interrupt procedure, and other protocol features.

For the Postmate library, the handshake is to ensure the normal communication between the parent page and the iframe child page. The corresponding handshake process is as follows:

In Postmate, the handshake message is initiated by the parent page. To initiate the handshake message in the parent page, we need to create a Postmate object:

const postmate = new Postmate({
  container: document.getElementById('some-div'), // Iframe container
  url: 'http://child.com/page.html'.// Contains the iframe subpage address of postmate.js
  name: 'my-iframe-name' // Set the name attribute of the iframe element
});
Copy the code

In the above code, we create the Postmate object by calling the Postmate constructor. Inside the Postmate constructor there are two main steps: setting the internal properties of the Postmate object and sending the handshake message:

The above flow chart corresponding to the code is relatively simple, here Po brother will not post detailed code. For those interested, read the SRC /postmate.js file. To be able to respond to the parent page’s handshake message, we need to create a Model object in the child page:

const model = new Postmate.Model({
  // Expose your model to the Parent. Property values may be functions, promises, or regular values
  height: () = > document.height || document.body.offsetHeight
});
Copy the code

The Postmate.Model constructor is defined as follows:

// src/postmate.js
Postmate.Model = class Model {
  constructor(model) {
    this.child = window;
    this.model = model;
    this.parent = this.child.parent;
    return this.sendHandshakeReply(); }}Copy the code

In the Model constructor, we can clearly see the call to sendHandshakeReply. Here we’ll just look at the core code:

Now let’s summarize the handshake process between parent and child pages: When the child page is loaded, the parent page sends the handshake message to the child page via the postMessage API. If the child page receives a Handshake message, the postMessage API is used to reply to the parent page with a Handshake reply message.

Also, note that a timer is started inside the sendHandshake method to ensure that the subpage receives the handshake message:

// src/postmate.js
class Postmate {
  sendHandshake(url) {
    return new Postmate.Promise((resolve, reject) = > {
      const loaded = () = > {
        doSend();
        responseInterval = setInterval(doSend, 500);
      };

      if (this.frame.attachEvent) {
        this.frame.attachEvent("onload", loaded);
      } else {
        this.frame.addEventListener("load", loaded);
      }
      
      this.frame.src = url; }); }}Copy the code

Of course, in order to avoid sending too many invalid handshake messages, the doSend method limits the maximum number of handshakes:

const doSend = () = > {
  attempt++;
  this.child.postMessage(
    {
      postmate: "handshake".type: messageType,
      model: this.model,
    },
    childOrigin
  );
  // const maxHandshakeRequests = 5;
  if (attempt === maxHandshakeRequests) {
     clearInterval(responseInterval); }};Copy the code

After the handshake is complete between the master and child applications, two-way message communication can take place. Let’s look at how to implement two-way message communication.

How to realize two-way message communication

After calling the Postmate and Postmate.Model constructors, a Promise object is returned. When the Promise object’s state changes from Pending to Resolved, ParentAPI and ChildAPI objects are returned:

Postmate

// src/postmate.js
class Postmate {
  constructor({
    container = typeofcontainer ! = ="undefined" ? container : document.body,
    model, url, name, classListArray = [],
  }) {
    // omit setting the internal properties of the Postmate object
    return this.sendHandshake(url);
  }
  
  sendHandshake(url) {
    // Omit some code
    return new Postmate.Promise((resolve, reject) = > {
      const reply = (e) = > {
        if(! sanitize(e, childOrigin))return false;
        if (e.data.postmate === "handshake-reply") {
          return resolve(new ParentAPI(this));
        }
        return reject("Failed handshake"); }; }); }}Copy the code

ParentAPI

class ParentAPI{
  +get(property: any) // Get the value of the property property on the Model object in the child page
  +call(property: any, data: any) // Invoke the method on the Model object in the child page
  +on(eventName: any, callback: any) // Listen for events distributed by child pages
  +destroy() // Remove event listener and delete iframe
}
Copy the code

Postmate.Model

// src/postmate.js
Postmate.Model = class Model {
  constructor(model) {
    this.child = window;
    this.model = model;
    this.parent = this.child.parent;
    return this.sendHandshakeReply();
  }

  sendHandshakeReply() {
    // Omit some code
    return new Postmate.Promise((resolve, reject) = > {
      const shake = (e) = > {
        if (e.data.postmate === "handshake") {
          this.child.removeEventListener("message", shake, false);
          return resolve(new ChildAPI(this));
        }
        return reject("Handshake Reply Failed");
      };
      this.child.addEventListener("message", shake, false); }); }};Copy the code

ChildAPI

class ChildAPI{
  +emit(name: any, data: any)
}
Copy the code

3.1 Child Page -> Parent Page

3.1.1 Sending Messages on sub-pages
const model = new Postmate.Model({
  // Expose your model to the Parent. Property values may be functions, promises, or regular values
  height: () = > document.height || document.body.offsetHeight
});

model.then(childAPI= > {
  childAPI.emit('some-event'.'Hello, World! ');
});
Copy the code

In the above code, a child page can send a message via the emit method provided by the ChildAPI object, which is defined as follows:

export class ChildAPI {
  emit(name, data) {
    this.parent.postMessage(
      {
        postmate: "emit".type: messageType,
        value: {
          name,
          data,
        },
      },
      this.parentOrigin ); }}Copy the code
3.1.2 The parent page listens for messages
const postmate = new Postmate({
  container: document.getElementById('some-div'), // Iframe container
  url: 'http://child.com/page.html'.// Contains the iframe subpage address of postmate.js
  name: 'my-iframe-name' // Set the name attribute of the iframe element
});

postmate.then(parentAPI= > {
  parentAPI.on('some-event'.data= > console.log(data)); // Logs "Hello, World!"
});
Copy the code

In the above code, the parent page can register the event handler via the on method provided by the ParentAPI object, which is defined as follows:

export class ParentAPI {
  constructor(info) {
    this.parent = info.parent;
    this.frame = info.frame;
    this.child = info.child;

    this.events = {};

    this.listener = (e) = > {
      if(! sanitize(e,this.childOrigin)) return false;
			// Omit some code
      if (e.data.postmate === "emit") {
        if (name in this.events) {
          this.events[name].forEach((callback) = > {
            callback.call(this, data); }); }}};this.parent.addEventListener("message".this.listener, false);
  }

  on(eventName, callback) {
    if (!this.events[eventName]) {
      this.events[eventName] = [];
    }
    this.events[eventName].push(callback); }}Copy the code

3.2 Message Verification

In order to ensure the security of communication, Postmate will verify the message during message processing, and the corresponding validation logic is encapsulated in sanitize method:

const sanitize = (message, allowedOrigin) = > {
  if (typeof allowedOrigin === "string"&& message.origin ! == allowedOrigin)return false;
  if(! message.data)return false;
  if (typeof message.data === "object" && !("postmate" in message.data))
    return false;
  if(message.data.type ! == messageType)return false;
  if(! messageTypes[message.data.postmate])return false;
  return true;
};
Copy the code

The corresponding verification rules are as follows:

  • Verify that the source of the message is valid;
  • Verify the presence of a message body;
  • Verify that the message body containspostmateProperties;
  • Verify that the message type is"application/x-postmate-v1+json";
  • In the validation message bodypostmateWhether the corresponding message type is valid;

Here are the types of messages supported by Postmate:

const messageTypes = {
  handshake: 1."handshake-reply": 1.call: 1.emit: 1.reply: 1.request: 1};Copy the code

To implement message validation ahead of time, we also need to define a standard message body model:

{
   postmate: "emit"./ / required: "request" | "call" and so on
   type: messageType, // Required: "application/x-postmate-v1+json"
   // Custom attributes
}
Copy the code

Now that you know how the child communicates with the parent and how message validation is performed, let’s take a look at how the parent communicates with the child.

3.3 Parent Page -> Child page

3.3.1 Calling methods on subpage model objects

In the page, we can invoke methods on the child page model object via the call method provided by the ParentAPI object:

export class ParentAPI {
	call(property, data) {
    this.child.postMessage(
      {
        postmate: "call".type: messageType,
        property,
        data,
      },
      this.childOrigin ); }}Copy the code

In the ChildAPI object, the call message type is processed as follows:

export class ChildAPI {
  constructor(info) {
		// Omit some code
    this.child.addEventListener("message".(e) = > {
      if(! sanitize(e,this.parentOrigin)) return;
      const { property, uid, data } = e.data;
      
      // The type of call message sent by the parent page to invoke the corresponding method on the Model object
      if (e.data.postmate === "call") {
        if (
          property in this.model &&
          typeof this.model[property] === "function"
        ) {
          this.model[property](data);
        }
        return; }}); }}Copy the code

The call message can only be used to call methods on the Model object of the child page, and does not get the return value of the method call. In some cases, however, we need to get the return value of a method call. Let’s look at how ParentAPI does this.

Call a method on a child page model object and get the return value

To get the return value of the call, we need to call the get method provided on the ParentAPI object:

export class ParentAPI {
	get(property) {
    return new Postmate.Promise((resolve) = > {
      // Get the data from the response and remove the listener
      const uid = generateNewMessageId();
      const transact = (e) = > {
        if (e.data.uid === uid && e.data.postmate === "reply") {
          this.parent.removeEventListener("message", transact, false); resolve(e.data.value); }};// Listen for response messages from child pages
      this.parent.addEventListener("message", transact, false);

      // Send requests to child pages
      this.child.postMessage(
        {
          postmate: "request".type: messageType,
          property,
          uid,
        },
        this.childOrigin ); }); }}Copy the code

If the parent page sends a request message, the child page will use the resolveValue method to get the return result, and then use postMessage to return the result:

// src/postmate.js
export class ChildAPI {
  constructor(info) {
    this.child.addEventListener("message".(e) = > {
      if(! sanitize(e,this.parentOrigin)) return;
      const { property, uid, data } = e.data;
      
      // Respond to the request message sent by the parent page
      resolveValue(this.model, property).then((value) = >
        e.source.postMessage(
          {
            property,
            postmate: "reply".type: messageType, uid, value, }, e.origin ) ); }); }}Copy the code

The resolveValue method in the above code is also simple:

const resolveValue = (model, property) = > {
  const unwrappedContext =
    typeof model[property] === "function" ? model[property]() : model[property];
  return Postmate.Promise.resolve(unwrappedContext);
};
Copy the code

3.4 Model extension mechanism

Postmate provides a very flexible Model extension mechanism that allows developers to extend the Model object of a child page as required:

The corresponding extension mechanism is not complicated to implement, and the specific implementation is as follows:

Postmate.Model = class Model {
  constructor(model) {
    // Omit some code
    return this.sendHandshakeReply();
  }

  sendHandshakeReply() {
    return new Postmate.Promise((resolve, reject) = > {
      const shake = (e) = > {
        // Omit some code
        if (e.data.postmate === "handshake") {
          // Use the model object provided by the parent page to extend the existing model object of the child page
          const defaults = e.data.model;
          if (defaults) {
            Object.keys(defaults).forEach((key) = > {
              this.model[key] = defaults[key];
            });
          }
          return resolve(new ChildAPI(this)); }}; }); }};Copy the code

Now that we’ve seen how Postmate shakes hands and implements two-way messaging, we’ll finish with how to disconnect.

How to disconnect

When the parent page has finished communicating with the child page, we need to disconnect. At this point we can call the destroy method on the ParentAPI object to disconnect.

// src/postmate.js
export class ParentAPI {
	destroy() {
    window.removeEventListener("message".this.listener, false);
    this.frame.parentNode.removeChild(this.frame); }}Copy the code

This article uses the Postmate library as an example to introduce how to implement elegant message communication between the parent page and iframe child page based on postMessage. If you don’t want more, check out Bob’s previous post on communication: How to Gracefully Communicate Messages. And websockets you don’t know about.

Pay attention to the “full stack of immortal Road” read Po ge original 3 free e-books and more than 50 “re-learn TS” tutorial.

5. Reference resources

  • MDN – postMessage
  • Github – postmate