Skip to content

Instantly share code, notes, and snippets.

@JamesMGreene
Last active February 20, 2018 18:10
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save JamesMGreene/5693546 to your computer and use it in GitHub Desktop.
Save JamesMGreene/5693546 to your computer and use it in GitHub Desktop.
A brief example of the latest idea for communicating with PhantomJS from the client-side.

A brief example of the latest idea for communicating with PhantomJS from the client-side.

This is the next revision of ideas discussed in a previous Gist:
    https://gist.github.com/JamesMGreene/3716654

This example demonstrates shimming in the new communication channels under discussion via existing methods (WebPage#onCallback and window.callPhantom).

The example does, however, take advantage of a yet-to-be-implemented EventEmitter API for core modules in the PhantomJS outer context. This API would add the following methods to core modules as appropriate:

  • on / addEventListener
  • off / removeEventListener
  • emit (strictly for triggering handlers within the PhantomJS context)
// PURPOSE:
// This code snippet shims new hooks into the WebPage (client-side) with existing functionality.
// Override some functionality within the "create" function of the "webpage"
// core module to immediately attach signal handlers within "create":
page.on("initialized", function() {
page.evaluate(function() {
// TODO: This needs to happen in frames as well!
(function(top) {
// Create a `phantom` object based on the prototype of `top.parent` (`top`)
var phantom = Object.create(top.parent);
// Override the `top.parent.dispatchEvent` method to communicate with Phantom
phantom.dispatchEvent = function(evt) {
// Reformat
var message = {
name: evt.type,
data: evt.detail
};
// Send to Phantom
// TODO: Use an equivalent future implementation to connect instead of `callPhantom`
var response = window.callPhantom(message);
// `dispatchEvent` is supposed to return a Boolean
// We will invoke client-side handlers to get that (if any)
var responseEvt = new CustomEvent(evt.type, { "detail": response });
var result = top.dispatchEvent(responseEvt);
// Keeping up appearances
if (evt.cancelable === true && !result) {
return false;
}
return true;
};
// Everything else will remain intact as if it were actually the `top` object
// Override the `top.parent` to create the wormhole connection to Phantom
top.parent = phantom;
})(top);
});
// Delegate to another handler, e.g. the `topSecret` event handler
page.on("callback", function(message) {
return page.emit(message.name, message.data);
};
}
// PURPOSE:
// This code snippet adds a handler for the `"topSecret"` event.
page.on("topSecret", function(data) {
if (data && data.secret === "sauce") {
return "Roger!"
}
// else `return undefined;`
});
<!DOCTYPE html>
<html>
<!--
PURPOSE:
This page demonstrates utilizing the new hooks to achieve bi-directional
communication with Phantom from the WebPage (client-side).
-->
<head>
<title>Test PhantomJS client messaging API</title>
</head>
<body>
<div>
<h1>Testing special concept of leveraging `top.parent` for PhantomJS client messaging API</h1>
</div>
<script type="text/javascript">
(function() {
// Grab a reference to this hooked PhantomJS context for convenience
var phantom = top.parent;
// Create custom event
var evt = new CustomEvent("topSecret", {
"detail": {
"secret": "sauce"
}
});
// Create a listener callback
var callback = function(evt) {
if (evt.detail === "Roger!") {
alert("Secret received.");
}
else {
alert("UH OH! Secret compromised!");
}
};
// Add a listener for the same event (i.e. PhantomJS response to message)
phantom.addEventListener(evt.type, callback, false);
// Send a client-to-PhantomJS message.
phantom.dispatchEvent(evt);
})();
</script>
</body>
</html>
<!DOCTYPE html>
<html>
<!--
PURPOSE:
This page demonstrates utilizing the new hooks to achieve bi-directional
communication with Phantom from the WebPage (client-side).
-->
<head>
<title>Test PhantomJS client messaging API</title>
</head>
<body>
<div>
<h1>Testing special concept of leveraging `top.parent` for PhantomJS client messaging API</h1>
</div>
<script type="text/javascript">
(function() {
// Wrap this with a `callPhantom` convenience method for demonstration
// purposes. This wrapper would be excellent for one-time communications
// but less so for events that are received multiple times as it removes
// the handler each time.
var callPhantom = function(msg, callback) {
// Grab a reference to this hooked PhantomJS context for convenience
var phantom = top.parent;
// Handle a PhantomJS-to-client response.
// TODO: Should the name always be `msgName`, `msgName + ".response"`,
// or fully customizable via the `msgData` sent?
if (typeof callback === "function") {
var wrappedCallback = function() {
phantom.removeEventListener(msg.name, wrappedCallback, false);
callback.apply(this, arguments);
};
phantom.addEventListener(msg.name, wrappedCallback, false);
}
// Send a client-to-PhantomJS message.
var evt = new CustomEvent(msg.name, { "detail": msg.data });
phantom.dispatchEvent(evt);
};
var msg = {
"name": "topSecret",
"data": {
secret: "sauce"
}
};
var callback = function(evt) {
// `evt.type` === `msgName` + ".response"
// `evt.detail` === response
if (evt.detail === "Roger!") {
alert("Secret received.");
}
else {
alert("UH OH! Secret compromised!");
}
};
// Invoke!
callPhantom(msg, callback);
})();
</script>
</body>
</html>
@ariya
Copy link

ariya commented Jun 3, 2013

I would argue that we may need not any kind of window.top.parent override (my suggestions was either and not both parent and CustomEvent). Perhaps we can get away by having a special name of the custom event, e.g.:

dispatchEvent(new CustomEvent('phantomjs', { secretAnswer: 42 });

which means that the event can be emitted in any place, wherever dispatchEvent is available.

Any other kind of mapping for the convention, e.g. phantomjs:foo -> page.on("foo", ...), is also a further possibility.

@JamesMGreene
Copy link
Author

Follow-up Questions:

  1. Would it make sense to call dispatchEvent on any object other than a Window object (window, window.parent, window.opener, top, self, etc.) if trying to communicate with PhantomJS, though?

  2. I'd be fine with doing just the CustomEvent approach, likely with an event naming convention of phantomjs:foo (or phantom:foo, if you're still interested in dropping the "JS") like you mentioned. However, I haven't been able to find any existing signals or callbacks for connecting to such from the Qt side. As such, it seems like the best we could do [if allowing the CustomEvent to be dispatched on any object] is either to:

    1. add a capturing event listener on the frame that will call back to Phantom; or
    2. add an bubbling event listener on the frame that will call back to Phantom AND require the user to specify that the event can bubble (which is not the default)

    Or am I missing something?

@ariya
Copy link

ariya commented Jul 16, 2013

I'm not sure dispatchEvent on other than window makes sense. We just have to play with different code examples and see what works.

I think extra hook is still necessary somewhere in QtWebKit bridge to let us detect and process those custom events. I don't think Qt provides an API for that yet. Also, I like the idea of the capture event listener.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment