Note: This will be a contrived example, but hopefully illustrates some real-world trade-offs.
Example scenario: Suppose you're an independent web developer, and a client asks you to prototype a redesign of their website header. You'll be paid for your time, and if the client likes it, you'll be hired to do the full implementation. Your incentive is to show the client a quick, functional demo of the updated header. The problem is that quick and functional tend to be mutually-exclusive.
If the need is for speed, you could just create a mockup. Better yet, save a static snapshot of a page, modify it, and then demo it back to the client. The downside here is that it's a non-functional demo. What does it feel like to navigate around the site with the new header? How does it look on the product feature page, or the login page?
Alternatively, you could download the repo for the website, populate the database with representative information, and instantiate the whole thing locally. Then you have a fully functional instance you can modify and demo to the client. The downside here is the time it takes getting everything set up. In some cases it might be such a sprawling, complex, and/or undocumented system that a functional demo simply isn't possible.
The basic idea is that you can launch a fully-functional staging server of the client's website by using a reverse proxy that re-writes the website on-the-fly. Hoxy is an HTTP hacking proxy API for Node.js with a few tools for doing this sort of thing.
In a folder, type npm install hoxy
, then create a file called proxy.js
.
In that file, create a reverse proxy server like so:
// proxy.js
var hoxy = require('hoxy');
var proxy = hoxy.createServer({
reverse: 'http://www.clientwebsite.com'
}).listen(8080);
You've just created a reverse proxy to your client's website.
If you run node proxy.js
and visit http://localhost:8080/
, you should see an exact mirror of the site.
Now, let's programmatically modify that mirror. There are multiple ways to do this using Hoxy, but in this example we'll use Cheerio, which is a lightweight server-side jQuery clone that Hoxy supports by default. To do this, we'll add an interceptor to the proxy. An interceptor grabs the response mid-flight, operates on it, then sends it on its way.
// add an interceptor
proxy.intercept({
phase: 'response',
mimeType: 'text/html',
as: '$'
}, function(req, resp) {
resp.$('title').text('Unicorns!');
});
This code is explained in more detail below.
Suffice to say, if you visit http://localhost:8080/
you'll still see the client's website, but all titles will have been rewritten to "Unicorns!".
It's a silly example, but it might start being obvious where this is going.
Create an HTML file named header.html
and save it in the same folder.
Also, type npm install ugly-adapter
(a promise shim for callbacks) in that folder since we'll be doing some async stuff.
Now update your interceptor like this:
var fs = require('fs');
var adapt = require('ugly-adapter');
var readFile = adapt.part(fs.readFile);
proxy.intercept({
phase: 'response',
mimeType: 'text/html',
as: '$'
}, function(req, resp) {
return readFile('./header.html', 'utf8')
.then(function(header) {
resp.$('#header').html(header);
});
});
This basically injects header.html
into the #header
element of every page, before it reaches your browser.
Navigating around the site, every page should have the new version of the header.
This could then be demo'd to the client straightaway, or you could go on to use similar techniques to inject CSS and JavaScript too.
Congrats, you've just made a fully functional demo without needing to replicate your client's server environment.
Let's take a look at proxy.js
in its entirety.
Comments are added to help explain all the various pieces:
// proxy.js
var fs = require('fs');
var hoxy = require('hoxy');
var adapt = require('ugly-adapter');
var readFile = adapt.part(fs.readFile); // promise shim
// create and launch a reverse proxy
var proxy = hoxy.createServer({
reverse: 'http://www.clientwebsite.com'
}).listen(8080);
// Add our interceptor function.
proxy.intercept({
// Run this interceptor during the response phase.
phase: 'response',
// Only intercept html pages (not images, etc).
mimeType: 'text/html',
// Expose the response body as a mutable cheerio object.
as: '$'
}, function(req, resp) {
// Return a promise from the interceptor,
// signaling that this is an async operation.
return readFile('./header.html', 'utf8')
.then(function(header) {
// Read `header.html` from disk and swap
// it in place of the existing site header.
resp.$('#header').html(header);
});
});
Promises are supposed help avoid "callback hell", but I personally think then()
-based code is only incrementally better.
Since returning a promise from an interceptor signals Hoxy to behave in an async fashion, you could remedy this by using async functions, which also return promises:
proxy.intercept({
phase: 'response',
mimeType: 'text/html',
as: '$'
}, async function(req, resp) {
var header = await readFile('./header.html', 'utf8');
resp.$('#header').html(header);
});
Unfortunately, async functions aren't a fully-fledged standard yet, and in any case require transpiling on all current engines.
Thus, Hoxy supports generators (technically, returning an iterator over promises), which amounts to a very similar capability, as long as you're using Node 0.11 or higher with the --harmony
flag, or any version of io.js.
proxy.intercept({
phase: 'response',
mimeType: 'text/html',
as: '$'
}, function*(req, resp) {
var header = yield readFile('./header.html', 'utf8');
resp.$('#header').html(header);
});
This gist shows a contrived use case, but it hopefully illustrates the kinds of things that are possible.
As of version 3.0, Hoxy supports HTTPS proxying and generator-based intercepts. For a full broadside of Hoxy's capabilities, e.g. altering headers, throttling, or how the HTTPS proxying bits work internally, check out the documentation site. Also check out the github repo. Contributions and bug reports help this project suck even less, so keep them coming!