Skip to content

Instantly share code, notes, and snippets.

@trevordixon
Last active May 9, 2016 16:59
Show Gist options
  • Save trevordixon/3061477 to your computer and use it in GitHub Desktop.
Save trevordixon/3061477 to your computer and use it in GitHub Desktop.
Very simple Express-like routing for PhantomJS
// Do things like:
var app = new Routes();
app.use(Routes.static('../htdocs'));
app.use(function(req, res, next) {
req.startTime = new Date().getTime();
next();
});
app.all(/.+/, function(req, res, next) {
console.log('Here comes a request.');
next();
});
app.get('/', function(req, res, next) {
res.send('Hello there.');
})
app.post('/', function(req, res, next) {
res.send(req.post.name);
});
app.get('/test', function(req, res, next) {
res.write('What a test.');
next();
});
app.get('/user/([a-zA-Z0-9]+)', function(req, res, next) {
res.write('You are user: ' + req.params[0]);
next();
});
app.get('/nonexistent', function(req, res) {
res.send(404);
});
app.get('/go_away', function(req, res) {
res.redirect('/');
});
app.use(function(req, res) {
console.log(new Date().getTime() - req.startTime);
res.close();
});
app.listen(8080);
var Routes = (function() {
var _ = {}, ctor = function(){};
_.bind = function bind(func, context) {
var bound, args, slice = Array.prototype.slice;
args = slice.call(arguments, 2);
return bound = function() {
if (!(this instanceof bound)) return func.apply(context, args.concat(slice.call(arguments)));
ctor.prototype = func.prototype;
var self = new ctor;
var result = func.apply(self, args.concat(slice.call(arguments)));
if (Object(result) === result) return result;
return self;
};
};
var Res = function Res(res) {
this.headers = res.headers || {};
this.res = res;
this.statusCode = res.statusCode;
};
Res.prototype.header = function(h, v) {
if (v) return this.headers[h] = v;
else return this.headers[h];
};
Res.prototype.send = function(body) {
if (arguments.length == 2) {
if (typeof body != 'number' && typeof arguments[1] == 'number') {
this.statusCode = arguments[1];
} else {
this.statusCode = body;
body = arguments[1];
}
}
if (typeof body == 'number') { this.statusCode = body; body = ''; }
else if (typeof body == 'object') body = JSON.stringify(body);
this.close(body);
return this;
};
Res.prototype.write = function(data) {
this.res.write(data);
return this;
};
Res.prototype.redirect = function(url) {
this.header('Location', url);
this.send(301);
};
Res.prototype.close = function(data) {
this.res.statusCode = this.statusCode;
this.res.headers = this.headers || {};
this.write(data);
this.res.close();
return this;
};
var R = function Routes() {
this.server = require('webserver').create();
this.routes = [];
};
R.prototype.preRoute = function(req, res) {
this.router.call(this, req, new Res(res));
};
R.prototype.router = function(req, res, i) {
var i = i || 0;
for (i; i < this.routes.length; i++) {
var route = this.routes[i];
if (route.method == 'ALL' || route.method == req.method) {
var match = req.url.match(route.route);
if (match) {
req.params = match.slice(1);
try {
return route.handler.call(this, req, res, _.bind(this.router, this, req, res, ++i));
} catch (err) {
console.log(err.stack);
return res.send(err.stack, 500);
}
}
}
}
res.send('Not found', 404);
};
R.prototype.addRoute = function(method, route, handler) {
if (!(route instanceof RegExp)) route = new RegExp("^" + route + "$");
this.routes.push({method: method, route: route, handler: handler});
};
R.prototype.all = function(route, handler) {
this.addRoute('ALL', route, handler);
};
R.prototype.get = function(route, handler) {
this.addRoute('GET', route, handler);
};
R.prototype.post = function(route, handler) {
this.addRoute('POST', route, handler);
};
R.prototype.head = function(route, handler) {
this.addRoute('HEAD', route, handler);
};
R.prototype.put = function(route, handler) {
this.addRoute('PUT', route, handler);
};
R.prototype.delete = function(route, handler) {
this.addRoute('DELETE', route, handler);
};
R.prototype.use = function(handler) {
this.addRoute('ALL', /.+/, handler);
};
R.prototype.listen = function(port) {
this.server.listen(port, _.bind(this.preRoute, this));
};
R.static = function(root) {
var fs = require('fs'),
root = fs.absolute(root);
return function(req, res, next) {
if (req.method != 'GET') return next();
var resource = req.url.slice(1),
path = root + '/' + resource;
if (resource && fs.isFile(path) && fs.isReadable(path)) {
var file = fs.read(path);
res.send(file);
} else {
next();
}
}
};
return R;
})();
if (module && module.exports) module.exports = Routes;
@ralphtheninja
Copy link

Looks really nice! A lot of meta stuff there with bind etc. Trying to figure out the magic but would like to know what npm module you are using when calling require('webserver')

@ralphtheninja
Copy link

I have a hard time trying to figure out lines 7 to 12 in Routes.js, are they there to make sure the bound function works regardless if the function was called with new or called directly?

@trevordixon
Copy link
Author

It's not actually an npm module, but a built-in PhantomJS module. http://code.google.com/p/phantomjs/wiki/Interface#WebServer_Module

I pulled the bind function out of underscore.js, and yes, it makes it possible to use the new keyword with a bound function. http://stackoverflow.com/questions/8552908/understanding-the-code-of-bind

@Aaronius
Copy link

Let's get some documentation up in haaaaaaaar!

@dongyuwei
Copy link

Bug report:

if we add a route /test, and then request /test?foo=bar. The url(/test?foo=bar) will not match the route: new RegExp("^" + "/test" + "$"), and the req.params will not be set correctly.

//curl -I http://127.0.0.1:3000/test?foo=bar
app.get('/test', function(req, res) {
   //...
}

but in expressjs, the route will work as expected.

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