SleepyHollow enables events between Node and PhantomJS

John Weis recently posted a pretty fancy jig for setting up events between node and PhantomJS. I wanted to share it because I think it deserves some props…

In a nutshell, sleepyhollow spawns a Phantom instance in your node app and implements a bare-bones event buss over stdin/stdout/stdErr channels.

Installation: I already have a global installation of PhantomJS

  $ phantomjs --version
  1.9.7

and sleepyhollow is installed in two parts…

$ npm install sleepyhollow-node
$ npm install sleepyhollow-phantom

I prototyped Sleepyhollow as a route inside an express project. Here is the script for that…

'use strict';

var request = require('request');
var stampy = require('stampy');//<-- bare bones time stamping object

module.exports = function (router) {

	//===========SLEEPYHOLLOW EXAMPLE=============
	router.get('/', function (req, res) {

		var ts = new stampy();//get perf object 
		ts.start('reqIn');//log start time


		var sleepyhollow = require('sleepyhollow-node');
		
		//this module spawns a PhantomJS process and sets up events
		//also names the script Phantom will run it's local scope
		//arg1: is phantoms config argument
		//arg2: named script that runs in the Phantom scope
		//arg3: a node config object, used in the node child-process spawn() method
		var drjekyll = sleepyhollow(
			"--ignore-ssl-errors=true"
			,"./phantom/phantom_job.js"
			,{cwd:"/"}
		);

		//by default sleepyhollow publishes any Phantom console.log messages 
		//as an error event.  This sets up a node echo of that.
		drjekyll.on("error", function(data) {
			console.log("STDOUT --> "+data);
		});

		//this event is called when PhantomJS is loaded and asked to call a page
		drjekyll.on("loading", function(data) {
			ts.lap("phantom_page_loading");
		});

		//called after a successful load
		drjekyll.on("loaded", function(phantomOut) {
			console.log("PAGE WAS LOADED vvv n" + phantomOut);
			res.send(phantomOut);
			ts.stop("resOut",console.log);
		});

		//this publishes a load to Phantom
		drjekyll.emit("load", "http://localhost:8000");

	});//router.get()

};//module.exports

So, the above runs when a call comes in for data. On the PhantomJS side we have this… ./phantom/phantom_job.js which runs in the Phantom context. Since you can’t share objects directly, all communication is done with events.

// NOTE:  this is PhantomJS code, not Node.js!

console.log('INSIDE PHANTOM!')


// PHANTOM TIMEOUT
var timeout = 10000;//ms


//vvv THE PHANTOM JOB vvv

//relative to the calling script
var sleepyhollow = require('../node_modules/sleepyhollow-phantom/index.js');

var mrhyde = sleepyhollow();

mrhyde.on('load', function(url) {
	mrhyde.emit("loading");//just before loading a page
	var page = require('webpage').create();
	page.open(url, function(status) {
			//page is loaded
			mrhyde.emit("loaded",page.content);
			page.close();
			console.log("PhantomJS: SUCCESS");
			phantom.exit();
	});
});


//this timeout will cleanup any hanging PhantomJS instances in case of an unhandeled situation
var doTimeout = setTimeout(function(){
	console.log("PhantomJS: TIMEDOUT");
	phantom.exit();
}, timeout);

Here’s the result…

STDOUT --> INSIDE PHANTOM!

PAGE WAS LOADED vvv

<!DOCTYPE html><html lang="en"><head><meta charset="utf-8"><title>WTF krak'd</title></head><body><div id="wrapper"><h1>Hello, Mr. Garris @ Sat Jul 12 2014 11:40:52 GMT-0700 (PDT)!</h1></div><script data-main="/js/app" src="/components/requirejs/require.js"></script></body></html>

[ { id: 'reqIn', run: 0, diff: 0, ts: 1405190450689 },
  { id: 'phantom_page_loading', run: 2040, diff: 2040, ts: 1405190452729 },
  { id: 'resOut', run: 2110, diff: 70, ts: 1405190452799 } ]

STDOUT --> PhantomJS: SUCCESS

Screen Shot 2014-07-12 at 11.50.00 AM

There is a lot of room for iteration here — for starters, you may have noticed the 2 second bootstrap time for Phantom. In general though, there is a lot of activity in this server-side DOM rendering space. I think SleepyHollow is a novel approach worth looking at for those of us working on the Node-PhantomJS problem.