post-me
is a library that facilitates two way communication between windows, for example between a parent and an iframe, a worker, a tab, or a popup.
Under the hood post-me
uses the low level postMessage
API.
post-me
was inspired by postmate
, and it provides several major improvements:
- Native
typescript
support to allow strong typings of method calls and event payloads during development. - Method calls can have both arguments and a return value.
- Both parent and child can expose methods and events (instead of child only).
- Exceptions that occur in a method call can be caught by the caller.
- Communicate between any two windows, not just iframes (e.g. workers, popups, and tabs).
- Create multiple concurrent connections.
In this live demo a parent window achieves two-way communication with its 5 children (4 iframes and 1 web worker).
To establish a connection between two windows follow the steps below:
- Initiate a handshake between the parent window and the child window by calling the
ParentHandshake()
andChildHandshake()
methods respectively. - The
methods
parameter contain the methods that each window will expose to the other. - The handshake returns a
Promise<Connection>
to the two windows. - Get a handle to the other window by calling the
connection.getRemoteHandle()
method.- Use
remoteHandle.call(methodName, ...args)
to call methods on the other window. It returns aPromise
of the result. - Use
remoteHandle.addEventListener(eventName, callback)
to listen to specific events dispatched by the other window. - Use
remoteHandle.removeEventListener(eventName, callback)
to remove listeners.
- Use
- Get a handle to the local window by calling the
connection.getLocalHandle()
method.- Use
localHandle.emit(eventName, payload)
to emit a specific event with the given payload.
- Use
Refer to the code snippet below as an example of these steps.
import { ParentHandshake } from 'post-me';
// Create the child window any way you like (iframe here, but could be popup or tab too)
const childFrame = document.createElement('iframe');
childFrame.src = './child.html';
const childWindow = childFrame.contentWindow;
// Define the methods you want to expose to the other window.
// Methods can either return values or Promises
const methods = {
foo: (s, x) => s.length * x,
bar: (x) => Promise.resolve(x * 2),
}
// Start the handshake
ParentHandshake(methods, childWindow, childWindow.origin);
.then((connection) => {
const localHandle = connection.localHandle();
const remoteHandle = connection.remoteHandle();
// Call a method on the child
remoteHandle.call('baz', 3)
.then((value) => {
console.log(value); // 9
})
// Listen for an event emitted by the child
remoteHandle.addEventListener('some-child-event', (payload) => {
console.log(payload) // 'Hi from child'
});
// Emit an evevent
localHandle.emit('some-parent-event', 'Hi from parent');
})
import { ChildHandshake } from 'post-me';
// Define the methods you want to expose to the other window.
// Methods can either return values or Promises
const methods = {
baz: (x) => x * 3,
}
// Start the handshake
// For safety it is strongly adviced to pass the explicit parent origin instead of '*'
const parentOrigin = '*';
ChildHandshake(methods, parentOrigin)
.then((connection) => {
const localHandle = connection.localHandle();
const remoteHandle = connection.remoteHandle();
// Call a method on the parent
remoteHandle.call('foo', 'ciao', 2)
.then((value) => {
console.log(value); // 8
})
// Listen for an event emitted by the child
remoteHandle.addEventListener('some-parent-event', (payload) => {
console.log(payload) // 'Hi from parent'
});
// Emit an evevent
localHandle.emit('some-child-event', 'Hi from child');
})
Thanks to post-me
typescript support, the correctness of the methods call arguments and event payloads can be statically enforced during development.
Ideally methods and events types should be defined in a third package that will be imported by both the parent and the child. This way, it will be ensured that both applications are working with up to date type definition.
Below a modified version of the previous example using typescript.
// common.ts
export type ParentMethods = {
foo: (s: string, x: number) => number;
bar: (x: number) => Promise<number>;
};
export type ParentEvents = {
'some-parent-event': string;
}
export type ChildMethods = {
baz: (x: number) => number;
};
export type ChildEvents = {
'some-child-event': string;
}
import { ParentHandshake, Connection } from 'post-me';
import { ParentMethods, ParentEvents, ChildMethods, ChildEvents} from '/path/to/common';
// Create the child window any way you like (iframe here, but could be popup or tab too)
const childFrame = document.createElement('iframe');
childFrame.src = './child.html';
const childWindow = childFrame.contentWindow;
// Define the methods you want to expose to the other window.
// Methods can either return values or Promises
const methods: ParentMethods = {
foo: (s, x) => s.length * x,
bar: (x) => Promise.resolve(x * 2),
}
// Start the handshake
ParentHandshake(methods, childWindow, childWindow.origin);
.then((connection: Connection<ParentEvents, ChildMethods, ChildEvents>) => {
const localHandle = connection.localHandle();
const remoteHandle = connection.remoteHandle();
// Call a method on the child
remoteHandle.call('baz', 3)
.then((value) => {
console.log(value); // 9
})
// Listen for an event emitted by the child
remoteHandle.addEventListener('some-child-event', (payload) => {
console.log(payload) // 'Hi from child'
});
// Emit an evevent
localHandle.emit('some-parent-event', 'Hi from parent');
})
import { ChildHandshake, Connection } from 'post-me';
import { ParentMethods, ParentEvents, ChildMethods, ChildEvents} from '/path/to/common';
// Define the methods you want to expose to the other window.
// Methods can either return values or Promises
const methods: ChildMethods = {
baz: (x) => x * 3,
}
// Start the handshake
// For safety it is strongly adviced to pass the explicit parent origin instead of '*'
const parentOrigin = '*';
ChildHandshake(methods, parentOrigin)
.then((connection: Connection<ChildEvents, ParentMethods, ParentEvents>) => {
const localHandle = connection.localHandle();
const remoteHandle = connection.remoteHandle();
// Call a method on the parent
remoteHandle.call('foo', 'ciao', 2)
.then((value) => {
console.log(value); // 8
})
// Listen for an event emitted by the child
remoteHandle.addEventListener('some-parent-event', (payload) => {
console.log(payload) // 'Hi from parent'
});
// Emit an evevent
localHandle.emit('some-child-event', 'Hi from child');
})
A minimal example of using post-me
with a web worker can be found in the demo source code.