Introduction

Recently, I started work on a project in which I thought I might use an iframe within the application to handle some of the work.

As a result I investigated communication between a web page and contained iframe elements, hosted on the same domain.

Ultimately, this part of the project took a different turn, so this code is entirely unloved.

Posting messages between windows

A typical setup I was looking at would be something like:

<body>
 <iframe id='iframe' src='iframe.html'></iframe>
</body>

Given that both the parent page, and the iframe (loaded from iframe.html) have the same origin, it is possible to communicate between the two using Window.postMessage.

For example, sending from iframe to parent:

window.parent.postMessage('Hello!', window.location.origin);

and from parent to iframe:

let target = document.getElementById('iframe').contentWindow;
target.postMessage('Hello!', window.location.origin);

This is pretty straight forward, but a little bit fire-and-forget:

  • Did my message get through?
  • What was the result? Was there any response?

Something a little more workable…

It was evident this was going to be too basic for my needs.

What I really needed was the means to:

  • Call a function in the iframe/parent
  • Pass arguments to that function
  • Get a response from that function
  • Be alerted to errors, including where the called function does not exist
  • Set a timeout on the function execution

Phew! That’s quite a different set of requirements, but if I could implement that, things would then get quite a bit more elegant. I would be able to write code such as:

// Call the 'add' function in the parent window of this iframe
rpc.execute('parent', 'add', [1, 2])
    .then(result => {
      console.log(result);
    })
    .catch(error => {
      console.log('Error:' + error);
    });

That sounds quite nice. Hmmm maybe I could give that a shot?

Implementing function execution

Fortunately, the Promise object seemed perfect for this scenario.

Using a Promise allows a message to be passed in one direction to to call the function, and then wait asynchronously for the response, at which point the promise can be resolved.

A listener can be set up to listen for the response, specifically for a requestId associated with a given call. This is torn down on success:

let listener = window.addEventListener('message',
    event => {
      if (event.origin === window.location.origin &&
          event.data.requestId === message.requestId &&
          event.data.targetId === this.sourceId_ &&
          event.data.sourceId === message.targetId &&
          event.data.type === 'response') {
        // Remove the listener - it was just for this 'function call'
        window.removeEventListener('message', listener);
        // Remove any alerting to RPC timeout
        clearTimeout(fail);
        if (event.data.responseType === 'success') {
          resolve(event.data.response);
        } else {
          reject(event.data.response);
        }
      }
    });

Finally, a timer can be set, whereby if no response is received, the Promise can be rejected (causing it to fail), which allows a timeout to be implemented:

let fail = setTimeout(() => {
  window.removeEventListener('message', listener);
  reject('No response to request for: ' + message);
}, this.timeout_);

Listening for function calls

The other side of this exchange, listening for incoming messages requesting execution, is a simple event hander:

window.addEventListener('message', event => this.messageHandler(event));

This simply performs some basic checks before attempting to execute the function and return the results:

try {
  let func = this.context_[data.functionName];
  message.response = func.apply(this.context_, data.argumentList);
  message.responseType = 'success';
} catch (e) {
  message.response = e.message;
  message.responseType = 'error';
}

Conclusion

This was pretty fun to write, and ultimately of absolutely zero use to me as the project took a different path. Nevertheless, it has shown me that it is relatively simple to get function execution across the page/iframe boundary, and that using a Promise is an ideal way to simplify this.

The code, for what it is worth, lives at https://github.com/plemont/iframe-rpc