Skip to content

Instantly share code, notes, and snippets.

@caridy
Last active December 20, 2015 07:19
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save caridy/6092619 to your computer and use it in GitHub Desktop.
Save caridy/6092619 to your computer and use it in GitHub Desktop.
express-routes-notes

What is this?

This is a prototype for the low-level api to serialize express routes into the client side.

Installation

npm install
node simple.js
node complex.js

How does it works?

helper.js is a simple node module that holds a set of utility functions to analyze and transpile the routes from an express application.

As a result, those new transpiled routes can be sent to the client side as a json object or by using express-state in case you need to serialize functions and regular expressions.

Examples

  • simple.js uses the very low level express api to response with a json object with the serialized routes that contains the annotations for the express routes. With that, you should be able to use that information in any way you want in the client side, for example, using YAF to redefine those routes, and get an application that can move between pages where those pages are defined as express routes. Pretty cool right?
  • complex.js uses express-state to support more advanced cases where we serialize functions, replicate functions, optimize functions, and expose regular express. The principle is the same, but this time it just spills a javascript blob that contains the state of the app from the server side to the client.

PNM routes

Option 1

Adding annotations to existing routes instead of adding a fixed mapping in public/js/app.js:

app.get('/', helper.annotate(routes.index, 'locate'));

app.get('/places/:id/', helper.annotate(routes.places.load, 'handlePlace'), helper.annotate(routes.places.render, 'showGrid'));

app.get('/photos/:id/', helper.annotate(routes.photos.load, 'handlePhoto'), helper.annotate(routes.photos.render, 'showLightbox'));

Option 2

Alternative, if those functions under routes object are named correctly, then there is not need to replicate it here. It will be something like this:

app.get('/', helper.annotate(routes.index));

app.get('/places/:id/', helper.annotate(routes.places.load), helper.annotate(routes.places.render));

app.get('/photos/:id/', helper.annotate(routes.photos.load), helper.annotate(routes.photos.render));

Option 3

Another alternative, is to do the annotations directly when creating the routes object, in which case, the result will be:

app.get('/', routes.index);

app.get('/places/:id/', routes.places.load, routes.places.render);

app.get('/photos/:id/', routes.photos.load, routes.photos.render);

while routes.photos.load will look like:

exports.load = helper.annotate(function handlePhoto(req, res, next) {
    // ...
});

Option 4

app.get('/', app.id('index'), app.render);
app.get('/places/:id/', app.id('places'), app.load, app.render);
app.get('/photos/:id/', app.id('photos'), app.load, app.render);

While we can have some sugar to add those simple methods into app:

app.id = function (id) {
	return helper.annotate(function(req, res, next) {
		req.id = id;
		res.expose(helper.extractAll(req.app.routes), 'routes');
		res.expose(id, 'activeRoute');
		next();
	}, id);
};
app.load = function (req, res, next) {
	if (req.id && route[req.id]) {
		route[req.id].load(req, res, next);
	} else {
		next();
	}
};
app.render = function () {
	var fn = req.id ? route[req.id].render : route.index;
	fn(req, res, next);
};

Of course, nothing really force you to use app.load and app.render, you could still do the same we have today:

app.get('/', app.id('index'), routes.index);
app.get('/places/:id/', app.id('places'), routes.places.load, routes.places.render);
app.get('/photos/:id/', app.id('photos'), routes.photos.load, routes.photos.render);
/*jslint node:true, nomen: true*/
'use strict';
var express = require('express'),
expstate = require('express-state'),
helper = require('./helper.js'),
app = express();
function printRoutes(req, res) {
// exposing all routes thru express-state
res.expose(helper.extractAll(req.app.routes));
res.header("Content-Type", "text/javascript");
res.send(res.locals.state.toString());
}
// no annotation on this middleware
app.get('/r1', function (req, res, next) {
res.expose('r1 route', 'activeRoute');
next();
}, printRoutes);
// annotate one middleware by function name
var fnR2 = helper.annotate(function (req, res, next) {
res.expose('r2 route', 'activeRoute');
next();
}, function (something) {
// this method will be serialized and sent to the client side
});
app.get('/r2', fnR2, printRoutes);
// annotate one middleware with a reference to itself for client
var fnR3 = function (req, res, next) {
// the same function will be used at the server side and client
res.expose('r3 route', 'activeRoute');
next();
};
app.get('/r3', helper.annotate(fnR3, fnR3), printRoutes);
// annotate one middleware with a replica of itself for client
app.get('/r4', helper.replicate(function (req, res, next) {
// a replica of the function will be used at the client side
res.expose('r4 route', 'activeRoute');
next();
}), printRoutes);
// multiple functions in annotations
app.get('/r5', helper.annotate(function (req, res, next) {
res.expose('r5 route', 'activeRoute');
next();
}, {
foo: function (a) { return a * a; },
bar: function (b, c) { return b + c; }
}), printRoutes);
// minified function in annotations
app.get('/r6', helper.replicate(function (req, res, next) {
// since we pass `true` as a second argument
// to `helper.replicate()` it will optimize the
// function before serialized it to remove comments
// and spaces
res.expose('r6 route',
'activeRoute');
next();
}, true /* true here means minification of the function */), printRoutes);
// RegExp in annotations
var rangeRegExp = /^(\w+)\.\.(\w+)?$/;
app.get('/r7', helper.annotate(function (req, res, next) {
res.expose('r7 route', 'activeRoute');
next();
}, rangeRegExp), printRoutes);
// whilecard to print everything
app.get('*', printRoutes);
// listening for traffic only after locator finishes the walking process
app.listen(3000, function () {
console.log("Server listening on port 3000");
});
/*jslint nomen:true, node:true */
"use strict";
module.exports = {
/**
Minify the body of a function by removing spaces, tabs, comments, breaklines, etc.
This is specially useful to optimize the function before serializing it to send
it to the client side as a js blob.
@method minifyFunction
@param {Function} fn the function to minify
@return {Function} the minified function
**/
minifyFunction: function (fn) {
var fnString = fn.toString();
// removing comments (http://upshots.org/javascript/javascript-regexp-to-remove-comments)
fnString = fnString.replace(/(\/\*([\s\S]*?)\*\/)|(\/\/(.*)$)/gm, '');
// removing breaklines, tabs and spaces
fnString = fnString
.replace(/\r\n/g, '')
.replace(/\n/g, '')
.replace(/\r/g, '')
.replace(/\t/g, '')
.replace(/\ +/g, ' ');
return Function('return ' + fnString)();
},
annotate: function (fn, note) {
fn.note = note || fn.name;
return fn;
},
replicate: function (fn, minify) {
fn.note = minify ? this.minifyFunction(fn) : fn;
return fn;
},
extractAll: function (expressRoutes) {
var verbs = {},
verb,
routes;
for (verb in expressRoutes) {
routes = this.extract(expressRoutes, verb);
if (routes && routes.length) {
verbs[verb] = this.extract(expressRoutes, verb);
}
}
return verbs;
},
extract: function (expressRoutes, verb) {
var routes = [],
route, i;
if (expressRoutes[verb] && expressRoutes[verb].length) {
for (i = 0; i < expressRoutes[verb].length; i += 1) {
route = this.extractRoute(expressRoutes[verb][i]);
if (route) {
routes.push(route);
}
}
}
return routes;
},
extractRoute: function (expressRoute) {
var route, i;
if (expressRoute && expressRoute.path) {
route = {
path: expressRoute.path,
regexp: expressRoute.regexp,
keys: expressRoute.keys,
annotations: []
};
if (expressRoute.callbacks.length) {
for (i = 0; i < expressRoute.callbacks.length; i += 1) {
if (expressRoute.callbacks[i] && expressRoute.callbacks[i].note) {
route.annotations.push(expressRoute.callbacks[i].note);
}
}
}
}
return route && route.annotations.length ? route : null;
}
};
{
"name": "express-routes-notes",
"description": "TBD",
"version": "0.0.1",
"author": "Caridy Patino <caridy@yahoo-inc.com> (http://github.com/caridy)",
"contributors": [],
"keywords": [
"framework",
"yui",
"mojito",
"modown",
"express",
"routes",
"router"
],
"engines": {
"node": ">=0.8.x",
"npm" : ">=1.2.10"
},
"dependencies": {
"express": "3.x",
"express-state": "*",
"express-params": "*"
}
}
/*jslint node:true, nomen: true*/
'use strict';
var express = require('express'),
helper = require('./helper.js'),
app = express();
// no annotation on this middleware
app.get('/r1', function (req, res) {
res.json(helper.extractAll(req.app.routes));
});
// annotate one middleware with string
app.get('/r2', helper.annotate(function (req, res) {
res.json(helper.extractAll(req.app.routes));
}, 'annotation-on-r2-route'));
// annotate one middleware by function name
app.get('/r3', helper.annotate(function annotateR3ByName(req, res) {
res.json(helper.extractAll(req.app.routes));
}));
// annotate two middleware
app.get('/r4', helper.annotate(function fnR4One(req, res) {
res.locals.routes = helper.extractAll(req.app.routes);
}), helper.annotate(function (req, res) {
res.json(res.locals.routes);
}, 'fnR4Two'));
// route params
app.get('/r5/:foo', function (req, res) {
res.json(helper.extractAll(req.app.routes));
});
// custom params thru express-params
require('express-params').extend(app);
app.param('whatever', /^(\w+)\.\.(\w+)?$/);
app.get('/r6/:whatever', function (req, res) {
res.json(helper.extractAll(req.app.routes));
});
// post route
app.post('/r7', function (req, res) {
res.json(helper.extractAll(req.app.routes));
});
// object annotations
app.get('/r8', helper.annotate(function (req, res) {
res.json(helper.extractAll(req.app.routes));
}, { foo: 1, bar: '2' }));
// array annotations
app.get('/r9', helper.annotate(function (req, res) {
res.json(helper.extractAll(req.app.routes));
}, [1, '2']));
// extracting `get` routes only
app.get('/r10', function (req, res) {
res.json(helper.extract(req.app.routes, 'get'));
});
// extracting `post` routes only
app.get('/r11', function (req, res) {
res.json(helper.extract(req.app.routes, 'post'));
});
// whilecard
app.get('*', function (req, res) {
res.json(helper.extractAll(req.app.routes));
});
// listening for traffic only after locator finishes the walking process
app.listen(3000, function () {
console.log("Server listening on port 3000");
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment