@actualwave/messageport-dispatcher

Cross-domain EventDispatcher for MessagePort interface

Usage no npm install needed!

<script type="module">
  import actualwaveMessageportDispatcher from 'https://cdn.skypack.dev/@actualwave/messageport-dispatcher';
</script>

README

MessagePortDispatcher

Build Status Coverage Status

MessagePortDispatcher is extended API for cross-origin communication. It utilizes MessagePort API available on window object to send custom events into/from <IFRAME/> or other target that implements MessagePort interface. MessagePortDispatcher uses two EventDispatcher's for incoming and outgoing events internally.

Demo with two <iframe/>'s talking to each other

Installation

Easy to install with npm package manager

npm install --save @actualwave/messageport-dispatcher

with yarn package manager

yarn add @actualwave/messageport-dispatcher

Usage

Note: MessagePortDispatcher distribution package contains dist/ folder with package wrapped into UMD wrapper, so it can be used with any AMD module loader, nodejs require() or without any.

To start using EventDispatcher, just instantiate it

const dispatcher = new MessagePortDispatcher(iframe.contentWindow);

As first argument its constructor accepts object that implements messaging methods of MessagePort interface.

Window object or Dedicated Worker can be used, to communicate with other side of communication channel(send event to script in IFRAME or from IFRAME or to Worker). To have custom events working on both sides, MessagePortDispatcher instances should be created from both sides of communication channel. In outer document pass IFRAME's window object

const frameDispatcher = new MessagePortDispatcher(iframeNode.contentWindow);
frameDispatcher.addEventListener('initialized', () => {
    console.log('Ok, we can start communication.');
});

In IFRAME use window.self

const dispatcher = MessagePortDispatcher.self();
dispatcher.dispatchEvent('initialized');

Instances returned from MessagePortDispatcher.self(), MessagePortDispatcher.parent() and MessagePortDispatcher.top() are cached internally, so will always return same instance.

Its possible to write an adapter for any object and pass it into MessagePortDispatcher

const target = {
    postMessage: (data, origin) => {
        console.log('Message sent', data);
        window.postMessage(data, origin);
    },
    addEventListener: (eventType, handler) => {
        console.log('Event listener added to ', eventType);
        window.addEventListener(eventType, handler);
    },
    removeEventListener: (eventType, handler) => {
        console.log('Event listener removed from ', eventType);
        window.removeEventListener(eventType, handler);
    }
};
const dispatcher = new MessagePortDispatcher(target);

Once its instance was created, you can send events into iframe

dispatcher.dispatchEvent('someEvent', {someData: 'anything here'});

and catch it on other side

dispatcher.addEventListener('someEvent', (event) => {
console.log('Data received', event.data);
});

When MessagePortDispatcher.dispatchEvent() called, it actually calls postMessage() method to pass message to other side. So instead of using postMessage and listening to message event, with MessagePortDispatcher you can send and receive custom events.

When MessagePortDispatcher instantiated, it creates two EventDispatcher's, one for incoming events and second for outgoing. Since Window object fires same message event for both sides, under the hood MessagePortDispatcher adds own ID to each event and if received event has same ID, it will be fired viasender(outgoing event) EventDispatcher, otherwise via receiver(incoming event). This will not work, event someEvent will be fired on other side but not for this dispatcher:

dispatcher.addEventListener('someEvent', () => {
    console.log('Some Event Received!');
});
dispatcher.dispatchEvent('someEvent');

If you want to listen for outgoing events, use sender:

dispatcher.sender.addEventListener('someEvent', () => {
    console.log('Some Event Received!');
});
dispatcher.dispatchEvent('someEvent');

Using same event types on both sides of communication channel will not mix them, since they will be fired from different dispatchers.

MessagePortDispatcher has exposed methods from receiver EventDispatcher for easier usage and custom dispatchEvent() method that sends events using MessagePort.postMessage(). These two calls are equivalent:

dispatcher.addEventListener('someEvent', () => {});
dispatcher.receiver.addEventListener('someEvent', () => {});

But these lines do different things:

dispatcher.dispatchEvent('someEvent');
dispatcher.sender.dispatchEvent('someEvent');

sender.dispatchEvent() will just fire event from sender EventDispatcher, but MessagePortDispatcher.dispatchEvent() will actually send message to other side via postMessage().

Since MessagePortDispatcher passes data between origins, it can send only simple data(i.e. nothing can be sent by reference) that can be converted to JSON. Before sending event, it checks its data property value. If this value has method toJSON(), it will use it and send returned data as is. In other case the value will be converted to JSON string before being sent and converted back when received. When using toJSON() method its developer's responsibility to look for nested data objects and convert everything to transferable simple objects.

Project contains example in example folder, it shows how to use MessagePortDispatcher when communicating with frames.

API

MessagePortDispatcher constructor arguments

  • target:Object - Requred, target object, should have postMessage(), addEventListener(), removeEventListener() methods, asdescribed in MessagePort docs.
  • customPostMessageHandler:Function - will be used to call target.postMessage()
  • receiverEventPreprocessor:Function - Optional, allows pre-processing of events and their data before firing event.
  • senderEventPreprocessor:Function - Optional, , allows pre-processing of events and their data before passing them to postMessage or customPostMessageHandler.

MessagePortDispatcher instance members

  • targetOrigin:String
  • sender:EventDispatcher - fires outgoing events that are passed to postMessage()
  • receiver:EventDispatcher - fires incoming events received from other origin
  • target:Object - target object that is used for communication
  • dispatcherId:String - unique ID of current MessagePortDispatcher instance
  • addEventListener(eventType:String, listener:Function):void - method copied from receiver EventDispatcher for easier access
  • hasEventListener(eventType:String):Boolean - method copied from receiver EventDispatcher for easier access
  • removeEventListener(eventType:String, listener:Function):void - method copied from receiver EventDispatcher for easier access
  • removeAllEventListeners(eventType:String):void - method copied from receiver EventDispatcher for easier access
  • dispatchEvent(event:Object):void - does not fire event, it sends event to postMessage(). Can be used with two arguments:
    • dispatchEvent(eventType:String, data?:Object):void

MessagePortTarget

A class that is used as a surrogate target for MessagePortDispatcher, it is useful when you have two objects for sending and receiving messages. For example, when you have an iframe with content from another origin and you can set iframe.contentWindow as sender object and own window as receiver. Sender object must contain postMessage() method and receiver object -- addEventListener() and removeEventListener() methods. Pass both sender and receiver into MessagePortTarget constructor, then it's instance can be provided for MessagePortDispatcher.

const frameDispatcher = new MessagePortDispatcher(new MessagePortTarget(iframeNode.contentWindow, window));

It also accepts lists of senders and receivers for mass sending and receiving.

const frameDispatcher = new MessagePortDispatcher(new MessagePortTarget([
  iframe1.contentWindow,
  iframe2.contentWindow,
  iframe3.contentWindow
  ], window));