I have made a tool that is pretty useful for me. I have made it open source on github. It is a server that listens for requests and returns an HTML static version of a single page app. It works with a convention that when the title of the page changes means that the new page is rendered.
The way it works is very simple but the actual code its very ugly. Here is the main file of the project. I plan to refactor it soon, and it would be great to hear your ideas to improve modularity, response handling and concurrency.
'use strict';
const
phantom = require('phantom'),
cluster = require('cluster'),
express = require('express'),
async = require('async'),
app = express();
var SCRIPT_REGEX = /<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi;
var ASSET_REGEX = /\.(jpg|jpeg|png|gif|css|js|woff|\/img|\/css|\/js)/g;
var ESCAPED_REGEX = /escaped_fragment_=/g;
var ERR404_REGEX = /name="prerender-status-code/g;
var ORIGIN = 'http://localhost:8080/';
var pharguments = ["--load-images=false", "--ignore-ssl-errors=true", "--ssl-protocol=tlsv1"];
var processingQueue = [];
phantom.create(function(ph) {
ph.createPage(function(page) {
page.open(ORIGIN, function(status) {
console.log("Swayzee has awakened and is looking for " + ORIGIN + "...");
app.get('*', function(req, res) {
console.log("Receiving :", req.url)
var hasEscapedFragment = req.url.match(ESCAPED_REGEX);
var isAsset = req.url.match(ASSET_REGEX)
if (hasEscapedFragment && !isAsset) {
var hash = '#!' + req.url.split("_escaped_fragment_=")[1];
processingQueue.push({
hash: hash,
response: res,
page: page,
running: false
})
processNext();
} else {
var splited = req.url.split('/');
var redirect_url = ORIGIN + (splited[splited.length - 2]) + "/" + (splited[splited.length - 1])
res.redirect(redirect_url);
}
});
app.listen(1333, function() {
console.log("Swayzee is ready to receive requests.");
});
});
// HANDLE PHANTOM OUTPUT
page.set('onConsoleMessage', function(msg) {
// console.log("Phantom Console: " + msg);
if (msg.indexOf('PEF: ') > -1) {
var hash = msg.replace('PEF: ', '').split(' -@- ')[0];
var html = msg.replace('PEF: ', '').split(' -@- ')[1];
var task = processingQueue.shift();
if (hash == task.hash) {
while (SCRIPT_REGEX.test(html)) {
html = html.replace(SCRIPT_REGEX, "");
}
console.log("Response ready for ", hash);
if (html.match(ERR404_REGEX)) {
task.response.status(404).send(html);
} else {
task.response.status(200).send(html);
}
// delete pendingResponses[hash]
}
}
});
});
}, pharguments);
// FETCH AND RENDER PAGE (RECURSIVE),
var fetchAndRenderPage = function(page, hash) {
console.log("Swayzee has is now stalking ", hash);
page.evaluate(function(hash) {
var previousTitle = document.title;
var iteration = 0;
window.location.hash = hash;
function isRenderReady() {
// console.log(iteration+") Title : ",previousTitle, ' vs ',document.title);
if (previousTitle === document.title && iteration < 40) {
iteration++;
setTimeout(isRenderReady, 50);
} else {
setTimeout(function() {
console.log("PEF: " + hash + " -@- " + document.documentElement.innerHTML);
document.title = document.title + ' processed ' + Date.now()
return;
}, 900);
}
}
isRenderReady();
}, function(result) {}, hash);
}
//
var processNext = function() {
if (processingQueue.length > 0) {
var task = processingQueue[0];
if (task.running == false) {
fetchAndRenderPage(task.page, task.hash);
task.running = true;
}
}
}
// ON EXIT
process.on('SIGINT', function() {
console.log("Closing ");
require('child_process').spawn('pkill', ['phantomjs']);
ph.exit();
process.exit();
});