Skip to content

Instantly share code, notes, and snippets.

@shobhitsharma
Created February 5, 2019 20:45
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save shobhitsharma/544ea31f55776469e724ac4b13354e36 to your computer and use it in GitHub Desktop.
Save shobhitsharma/544ea31f55776469e724ac4b13354e36 to your computer and use it in GitHub Desktop.

IframeClient

Provides simple and reliable cross-origin JavaScript messaging between iframes and their host pages by wrapping window.postMessage in a transactional layer. IframeClient builds atop postMessage in the way that TCP builds atop IP. IframeClient provides request/response cycles with polling and timeouts to (better) guarentee that messages will be received and post back responses.

This repo is configured as an NPM module and a Ruby gem for integration into Node-based projects and Rails applications.

Install

Node

In package dependencies:

"iframe-client": "git@github.com:voxmedia/iframe-client.git"

Then install:

npm install

In project script:

var IframeClient = require('iframe-client');

Rails

In Gemfile:

gem 'iframe-client', :git => 'git@github.com:voxmedia/iframe-client.git'

In JavaScript manifest:

//= require iframe-client

Usage

1. Create client instances

First, create a new IframeClient instance on your host page and within each iframe window:

On http://my-host-page.com/index.html

var hostClient = IframeClient.create('myapp', 'http://my-embed.com');

On http://my-embed.com/embed.html

var embedClient = IframeClient.create('myapp', '*');

The IframeClient.create factory function accepts an application namespace and a frame origin that the new client may post messages to. It's very important that each window (host and iframes) build their own client instance with a common namespace so they may respond to relevant messages within their window environment.

2. Configure message handlers

Next, configure each client with the messages that it should respond to. Message handlers may be chained using calls to the .on() method. A handler function receives the message event and a payload of data from each message, and may return response data to pass back to the sender. After configuring all message handlers, call .listen() to begin monitoring communication.

embedClient
  .on('play', function(evt, data) { ... })
  .on('pause', function(evt, data) { ... })
  .on('getstuff', function(evt, data) { return 'stuff' })
  .listen();

3. Send messages

Messages may be posted or requested.

Using post, a client sends a one-time message attempt to the target window. This message is posted blindly at the target window, and provides no indication as to whether the message was actually received. Message posts will commonly fail if one window starts sending messages before another window is ready to receive them.

hostClient.post('#my-iframe', 'play', 'hello embed!');

Using request, a client initiates a full request/response cycle with the target window. A request will repeatedly send a message to the target window, and does not stop sending until the target responds or the request times out. This also allows windows to coordinate data passing, and for completed requests to trigger callbacks.

hostClient.request('#my-iframe', 'getstuff', 'hello embed!', function(err, res) {
  if (err) return console.log(err.message);
  console.log('Received response:', res);
});

API

IframeClient.isInIframe()

Checks if the current window environment is displayed in an iframe. Returns true when in an iframe.

var cli = IframeClient.create(appId, [allowedOrigin])

Creates a new IframeClient instance.

  • appId: required string. A keyword specifying an app-specific messaging channel. Clients across windows must share a common application identifier to respond to one another's messages.

  • [allowedOrigin]: optional string. Specifies an origin URI that the client is allowed to post messages to. Defaults to "*" (allow any origin) when omitted.

cli.on(message, handler, [context])

Registers a message handler on the client. Handlers will run when the specified message type is received within the window. Returns the client instance to support method chaining.

  • message: required string. Name of the message to respond to.
  • handler: required function. Handler function to run in response to the message. Accepts arguments (evt, value), where evt is the message event, and value is any data value that was sent with the message. This handler may return data to pass back in response to the sender.
  • [context]: optional object. Context in which to invoke the handler.

cli.listen()

Starts the client listening for incoming messages. Call this once after registering all message handlers. Returns the client instance to support method chaining.

cli.post(target, message, [value])

Posts a blind message to another window. This is a convenience wrapper for calling postMessage with some added data management. Messages sent via post may fail if the receiving window's client has not yet fully initialized. Use this method to send non-critical messages where loss is acceptible.

  • target: required string, iframe, or window element. Must specify an iframe or window element to post to, or else provide a selector for an iframe or window element.

  • message: required string. A message keyword that maps to registered message handlers in the target window.

  • [value]: optional object. Additional data to be sent as a payload with the message.

cli.request(target, message, [value], [callback])

Initiates a request/response cycle with the target window. The message is repeatedly sent to the target window until the window responds, or until the request times out. Use this method for better guarentee of critical message delivery, or to request a data response from another window.

  • target: required string, iframe, or window element. Must specify an iframe or window element to post to, or else provide a selector for an iframe or window element.

  • message: required string. A message keyword that maps to registered message handlers in the target window.

  • [value]: optional object. Additional data to be sent as a payload with the message.

  • [callback]: optional function. Callback to run after the request cycle is complete. Accepts arguments (err, res), where err is any error that was encountered, and res is response data sent back from the target window.

cli.dispose()

Stops listening and cancels all polling messages. Releases the client for garbage collection.

Testing

npm install
npm test

Or, open test/test.html in a browser after package installation.

Contributing

  1. Fork it ( https://github.com/voxmedia/iframe-client/fork )
  2. Create your feature branch (git checkout -b my-new-feature)
  3. Commit your changes (git commit -am 'Add some feature')
  4. Push to the branch (git push origin my-new-feature)
  5. Create a new Pull Request
/*!
* IframeClient v0.0.5
* Copyright 2015, Vox Media
* Released under MIT license
*/
(function(global, factory) {
/**
* IframeClient API
* All frames must install and create their own instance of this client.
* @example
* var client = IframeClient.create('myapp', 'http://aweso.me')
* .on('play', function(data) { ... }, this)
* .on('pause', function(data) { ... }, this)
* .on('host', function(data) { return document.domain; }, this)
* .listen();
*/
var IframeClient = {
debug: false,
/**
* Creates a new IframeClient instance.
* Seeds the client factory with parameters.
* @private
*/
create: function(app, origin) {
return factory(this, app, origin, global);
},
/**
* Tests if the current window environment is within an iframe.
*/
isInIframe: function() {
try {
return global.parent && global.self !== global.top;
} catch (e) {
return true;
}
},
/**
* Library error handling.
*/
error: function(err) {
if (this.debug) throw err;
}
};
/**
* Module definitions:
* Supports CommonJS and global namespace.
*/
if (typeof module === 'object' && module.exports) {
module.exports = IframeClient;
} else {
global.IframeClient = IframeClient;
}
})(window, function(lib, appId, originHost, global) {
/**
* Generates a random GUID (Globally-Unique IDentifier) component.
* @private
*/
function s4() { return Math.floor((1 + Math.random()) * 0x10000).toString(16).substring(1); }
// Constants
var CLIENT_GUID = s4() + s4() + '-' + s4() + '-' + s4() + '-' + s4() + '-' + s4() + s4() + s4();
var PROTOCOL_APP = '@app';
var PROTOCOL_APP_ID = appId || 'xframe';
var PROTOCOL_RESPONSE = '@res';
var POLL_INTERVAL = 200;
// Encapsulated state
var handlers = [];
var requests = {};
var responses = {};
var requestId = 0;
var currentPoll = null;
var listener = null;
// Client API:
return {
host: originHost || '*',
/**
* READONLY
* Specifies the GUID of the client instance.
*/
get id() {
return CLIENT_GUID;
},
/**
* Resolves the source of a client window.
* @param {String|Element|Window} target selector or iframe/window element.
* @returns {Window} a resolved window source.
* @private
*/
src: function(src) {
// Query for target when given a string:
if (typeof src === 'string') {
src = document.querySelector(src);
}
// Check target element (iframe) for a content window:
if (src && src instanceof Element && src.contentWindow) {
src = src.contentWindow;
}
// Return a resolved source frame:
return (src && typeof src.postMessage === 'function') ? src : null;
},
/**
* Posts a message and value to a source window.
* This is a blind post without guarentee that the message will go through
* (messages may fail if the source is not yet fully initialized).
* Use `request` to post a message with confirmation of receipt.
* @param {String|Element|Window} target selector or iframe/window element.
* @param {String|Object} message string or formatted object (w/ "message" key).
* @param {Any} [value] an optional value to send with the message.
*/
post: function(src, message, value) {
src = this.src(src);
if (!src || !message) return lib.error('invalid post');
// Generate the base data object:
var data = (typeof message === 'string') ? { message: message } : message;
// Assign a value, if one was provided:
if (value !== undefined) data.value = value;
// Validate message and protocol before sending:
if (data.message) {
data[PROTOCOL_APP] = PROTOCOL_APP_ID;
try {
src.postMessage(JSON.stringify(data), this.host);
} catch (e) {
lib.error(e);
}
}
},
/**
* Posts a message to a source with request for confirmation.
* This implementation polls the source frame with the posted message
* until the frame sends back a confirmation response.
* Use this method to (better) guarentee delivery.
* @param {String} message string to send.
* @param {Any} [value] an optional value to send with the message.
* @param {Function} [callback] function to call with the response data.
* @param {Number} [timeout] in milliseconds before request is aborted.
* @returns {String} id of the newly-created request.
*/
request: function(src, message, value, callback, timeout) {
src = this.src(src);
if (!src || !message) return lib.error('invalid request');
var self = this;
var id = CLIENT_GUID +'-'+ ('0000' + requestId++).slice(-4);
var req = requests[id] = {
timeout: (timeout || 15000),
attempts: 0,
src: src,
cb: callback,
data: {
message: message,
value: value,
id: id
}
};
// Runs a single polling cycle to push pending messages.
// Continues polling calls until the request queue is empty.
function poll() {
var running = false;
for (var id in requests) {
if (requests.hasOwnProperty(id)) {
var pending = requests[id];
if (pending.attempts++ < pending.timeout / POLL_INTERVAL) {
self.post(pending.src, pending.data);
running = true;
} else {
self.end(id, new Error('request timeout'));
}
}
}
currentPoll = running ? setTimeout(poll, POLL_INTERVAL) : null;
}
// Start polling, or else make an initial request:
if (!currentPoll) poll();
else this.post(src, req.data);
return id;
},
/**
* Ends a pending request.
* The request is deleted from the queue,
* and the pending request callback is fulfilled.
* @param {String|Object} message id or data object to conclude.
* @param {Error|Object} error or cancelation options.
* Pass "abort: true" to end without response.
* @example
* xframe.end('request-0001', new Error('boom'))
* xframe.end('request-0001', { abort: true })
* xframe.end({ id: 'request-0001', value: 'hello' })
*/
end: function(req, err) {
var id = req.id || req;
var isAbort = (err && err.abort);
if (requests.hasOwnProperty(id)) {
var pending = requests[id];
delete requests[id];
if (!isAbort && typeof pending.cb === 'function') {
pending.cb(err || null, req.value);
}
}
},
/**
* Attaches a response handler to this XFrame Client.
* Each handler may target a unique message type.
* @param {String} message name to respond to.
* @param {Function} handler function for responding.
* Handler receives any value that was sent with the message,
* and may return a value that gets sent back to requests.
* @param {Object} context in which to invoke the handler.
* @example
* var xframe = require('xframe');
* var client = xframe('volume', 'http://aweso.me');
* client
* .on('play', function(data) { ... }, this)
* .on('pause', function(data) { ... }, this)
* .on('host', function(data) { return document.domain; }, this)
* .listen();
*/
on: function(message, handler, context) {
handlers.push({ message: message, fn: handler, ctx: context });
return this;
},
/**
* Enables listening on the client.
* Call this once after configuring all request handlers.
*/
listen: function() {
if (!listener) {
var self = this;
// Loops through all handlers, responding to the message type:
// collects and returns an optional response value from handlers.
function handleMessage(evt, req) {
var res;
for (var i=0; i < handlers.length; i++) {
var handler = handlers[i];
if (handler.message === req.message) {
res = handler.fn.call(handler.ctx, evt, req.value);
}
}
return res;
}
// Handle events sent from `postMessage`.
// This listener delegates all requests and responses.
listener = function(evt) {
var origin = (self.host === '*' || String(evt.origin).indexOf(self.host) >= 0);
var req, res;
// Parse request data:
if (origin && /^\{.*\}$/.test(evt.data)) {
try { req = JSON.parse(evt.data) }
catch (e) { req = null }
}
// Abort for invalid origin, request, or protocol:
if (!origin || !req || req[PROTOCOL_APP] !== PROTOCOL_APP_ID) return;
if (req.id) {
// MESSAGE WITH ID (track request/response cycle)
// Check if message is a response to a previous request:
var isResponse = (req.message === PROTOCOL_RESPONSE);
// MESSAGE RESPONSE (conclude request/response cycle)
if (isResponse && requests.hasOwnProperty(req.id)) {
self.end(req);
}
// REQUEST FOR RESPONSE (handle message and send response)
else if (!isResponse && !responses.hasOwnProperty(req.id)) {
responses[req.id] = true;
self.post(evt.source, {
message: PROTOCOL_RESPONSE,
value: handleMessage(evt, req) || 'success',
id: req.id
});
}
} else {
// GENERIC MESSAGE (just handle locally)
handleMessage(evt, req);
}
};
global.addEventListener('message', listener);
}
return this;
},
/**
* Stops listening for message events.
* Call this while uninstalling the client.
*/
stopListening: function() {
if (listener) {
global.removeEventListener('message', listener);
listener = null;
}
return this;
},
/**
* Stops all pending requests.
* Call this while uninstalling the client.
*/
dispose: function() {
clearInterval(currentPoll);
this.stopListening();
}
};
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment