I wanted to see the implementation of express.Router.get.
I looked at the express source on git, starting with the project's index.js.
The index has module.exports = require('./lib/express'). That file has var Router = require('./router'). That location is a directory, so I checked the index.js file in that directory. Sure enough it has:
var proto = module.exports = function(options) {
var opts = options || {};
function router(req, res, next) {
router.handle(req, res, next);
}
// mixin Router class functions
setPrototypeOf(router, proto)
router.params = {};
router._params = [];
router.caseSensitive = opts.caseSensitive;
router.mergeParams = opts.mergeParams;
router.strict = opts.strict;
router.stack = [];
return router;
};
That's the code that returns the router function. However, there is no .get function defined anywhere. Where is the actual function defined?
Note: I'm asking about the get function you would use like:
router.get('/', ...).
Browsing Express v4.15.2 I found the following in express/lib/application.js (line 468)
/**
* Delegate `.VERB(...)` calls to `router.VERB(...)`.
*/
methods.forEach(function(method){
app[method] = function(path){
if (method === 'get' && arguments.length === 1) {
// app.get(setting)
return this.set(path);
}
this.lazyrouter();
var route = this._router.route(path);
route[method].apply(route, slice.call(arguments, 1));
return this;
};
});
where this._router is coming from express/lib/router/index.js
From there depending on what you are looking for you have either:
express/lib/router/layer.js::match (line 110) that checks if this route matches path, if so populate .params
express/lib/router/route.js::dispatch (line 98) that dispatchs req, res to that route
Related
I've had no trouble testing my own route handlers but in this case I want to test express's static handler. I can't for the life of me figure out why it's hanging. Clearly there's some callback I'm missing or some event I need to emit.
I tried to make the smallest example I could.
var events = require('events');
var express = require('express');
var stream = require('stream');
var util = require('util');
function MockResponse(callback) {
stream.Writable.call(this);
this.headers = {};
this.statusCode = -1;
this.body = undefined;
this.setHeader = function(key, value) {
this.headers[key] = value;
}.bind(this);
this.on('finish', function() {
console.log("finished response");
callback();
});
};
util.inherits(MockResponse, stream.Writable);
MockResponse.prototype._write = function(chunk, encoding, done) {
if (this.body === undefined) {
this.body = "";
}
this.body += chunk.toString(encoding !== 'buffer' ? encoding : undefined);
done();
};
function createRequest(req) {
var emitter = new events.EventEmitter();
req.on = emitter.on.bind(emitter);
req.once = emitter.once.bind(emitter);
req.addListener = emitter.addListener.bind(emitter);
req.emit = emitter.emit.bind(emitter);
return req;
};
describe('test', function() {
var app;
before(function() {
app = express();
app.use(express.static(__dirname));
});
it('gets test.js', function(done) {
var req = createRequest({
url: "http://foo.com/test.js",
method: 'GET',
headers: {
},
});
var res = new MockResponse(responseDone);
app(req, res);
function responseDone() {
console.log("done");
done();
}
});
});
Setup,
mkdir foo
cd foo
mkdir test
cat > test/test.js # copy and paste code above
^D
npm install express
npm install mocha
node node_modules/mocha/bin/mocha --recursive
it just times out.
What am I missing?
I also tried making the request a Readable stream. No change
var events = require('events');
var express = require('express');
var stream = require('stream');
var util = require('util');
function MockResponse(callback) {
stream.Writable.call(this);
this.headers = {};
this.statusCode = -1;
this.body = undefined;
this.setHeader = function(key, value) {
this.headers[key] = value;
}.bind(this);
this.on('finish', function() {
console.log("finished response");
callback();
});
};
util.inherits(MockResponse, stream.Writable);
MockResponse.prototype._write = function(chunk, encoding, done) {
if (this.body === undefined) {
this.body = "";
}
this.body += chunk.toString(encoding !== 'buffer' ? encoding : undefined);
done();
};
function MockMessage(req) {
stream.Readable.call(this);
var self = this;
Object.keys(req).forEach(function(key) {
self[key] = req[key];
});
}
util.inherits(MockMessage, stream.Readable);
MockMessage.prototype._read = function() {
this.push(null);
};
describe('test', function() {
var app;
before(function() {
app = express();
app.use(express.static(__dirname));
});
it('gets test.js', function(done) {
var req = new MockMessage({
url: "http://foo.com/test.js",
method: 'GET',
headers: {
},
});
var res = new MockResponse(responseDone);
app(req, res);
function responseDone() {
console.log("done");
done();
}
});
});
I've still been digging. Look inside static-server I see it creates a Readable stream by calling fs.createReadStream. It does effectively
var s = fs.createReadStream(filename);
s.pipe(res);
So trying that myself works just fine
it('test stream', function(done) {
var s = fs.createReadStream(__dirname + "/test.js");
var res = new MockResponse(responseDone);
s.pipe(res);
function responseDone() {
console.log("done");
done();
}
});
I thought maybe it's something about express waiting for the input stream to finish but that doesn't seem to be it either. If I consume the mock input stream with the response it works just fine
it('test msg->res', function(done) {
var req = new MockMessage({});
var res = new MockResponse(responseDone);
req.pipe(res);
function responseDone() {
console.log("done");
done();
}
});
Any insight what I might be missing would be helpful
Note: while suggestions for 3rd party mocking libraries are appreciated I'm still really looking to understand what I'm missing to do it myself. Even if I eventually switch to some library I still want to know why this isn't working.
I found two issues that prevent the finish callback from being executed.
serve-static uses send module which is used to create file readstream from the path and pipe it to res object. But that module uses on-finished module which checks if finished attribute is set to false in response object, otherwise it destroys the file readstream. So filestream never gets a chance to emit data event.
express initialization overwrites the response object prototype. So the default stream methods like end() method is overwritten by http response prototype:
exports.init = function(app){
return function expressInit(req, res, next){
...
res.__proto__ = app.response;
..
};
};
To prevent this, I added another middleware right before static middleware to reset it back to MockResponse prototype:
app.use(function(req, res, next){
res.__proto__ = MockResponse.prototype; //change it back to MockResponse prototype
next();
});
Here are the changes made to make it work with MockResponse:
...
function MockResponse(callback) {
...
this.finished = false; // so `on-finished` module doesn't emit finish event prematurely
//required because of 'send' module
this.getHeader = function(key) {
return this.headers[key];
}.bind(this);
...
};
...
describe('test', function() {
var app;
before(function() {
app = express();
//another middleware to reset the res object
app.use(function(req, res, next){
res.__proto__ = MockResponse.prototype;
next();
});
app.use(express.static(__dirname));
});
...
});
EDIT:
As #gman pointed out, it is possible to use direct property instead of prototype method. In that case the extra middleware to overwrite prototype isn't necessary:
function MockResponse(callback) {
...
this.finished = false; // so `on-finished` module doesn't emit finish event prematurely
//required because of 'send' module
this.getHeader = function(key) {
return this.headers[key];
}.bind(this);
...
//using direct property for _write, write, end - since all these are changed when prototype is changed
this._write = function(chunk, encoding, done) {
if (this.body === undefined) {
this.body = "";
}
this.body += chunk.toString(encoding !== 'buffer' ? encoding : undefined);
done();
};
this.write = stream.Writable.prototype.write;
this.end = stream.Writable.prototype.end;
};
It appears my answer is not complete. For some reason the app works only if the file is not found. First thing to debug is do the following in your shell (or cmd):
export DEBUG=express:router,send
then run the test, you'll get more info.
Meanwhile I am still looking into this, for now, ignore my answer below.
----------- ignore this till I verify that it does work -----------
It seems like express static does not favor the absolute path you give it (__dirname).
Try:
app.use(express.static('.'));
and it will work. Note that your current dir for the mocha runner is 'test/'
I have to admit this is quite a mistery. I tried 'fulling' it by doing:
app.use(express.static(__dirname + '/../test')
but still it didn't work. Even specifying a full path did not solve this. Strange.
I'd like to have some advice on how to call different database queries based on the path in the url using express.js. Here's a working example code but I'm not sure if that is the good way of doing it:
server.js
var express = require('express'),
app = express(),
Promise = require("bluebird"),
db = require('./db/managedb'); // database modules for sequelize.js
app.get('/p/:section/:optional?', function(req, res){
var section = req.params["section"];
var optional = req.params["optional"];
if(section == "index"){
Promise.props({
main: db.db.query('CALL sp1()'),
second: db.db.query("CALL sp2()")
}).then(function(obj){
res.render(section+'.html',obj)
}).catch(function (error) {
})
}else if(section == "overviews"){
var page = req.query.page || 0;
Promise.resolve(db.db.query('CALL sp3(:page)',{page:page})).then(function(d){
res.render(section+'.html',d)
})
}else if(section == "reviews"){
var page = req.query.page || 0;
var review_id = req.query.review_id || 1;
Promise.resolve(db.db.query('CALL sp4(:review_id,:page)',{review_id:review_id,page:page})).then(function(d){
res.render(section+'.html',d)
})
}
})
Is it an okay solution? My concern is that if I kept adding more conditions for new sections, it might be quite messy, but is there a better way to call different database queries based on the path? Any suggestion is appreciated.
I would simply use different routing handlers, like this:
var express = require('express'),
app = express(),
Promise = require('bluebird'),
db = require('./db/managedb'); // database modules for sequelize.js
app.get('/p/index/:optional?', function(req, res) {
Promise.props({
main: db.db.query('CALL sp1()'),
second: db.db.query('CALL sp2()')
}).then(function(obj) {
res.render('index.html', obj);
}).catch(function(error) {
});
});
app.get('/p/overviews/:optional?', function(req, res) {
var page = req.query.page || 0;
Promise.resolve(db.db.query('CALL sp3(:page)', {page: page})).then(function(d) {
res.render('overviews.html', d);
});
});
app.get('/p/reviews/:optional?', function(req, res) {
var page = req.query.page || 0;
var review_id = req.query.review_id || 1;
Promise.resolve(db.db.query('CALL sp4(:review_id,:page)', {review_id: review_id, page: page})).then(function(d) {
res.render('reviews.html', d);
});
});
If you have more shared code between the routes, you could use multiple routing handlers using next() callbacks. For example, you could write your content from d somewhere into req and then just call next() instead of res.render(…). You then add another routing handler with the same signature as your old one (matching all routes) below those three and call res.render(section + '.html', req.d) (or other code) in there.
In the past, you could have done something like this:
app.get(['/', '/dashboard'], (req, res, next) => { ... });
in order to have multiple routes using the same route handler.
Several Stack Overflow answers indicate that this behavior is deprecated and should not be used anymore.
Aside from an ugly regex or a semi-hack like putting the function into a variable and passing it to 2 distinct app.get() calls, is there an elegant alternative to just passing an array as the first argument?
Do something like this:
var express = require('express');
var app = express();
var originalExpressGet = app.get;
app.get = (function() {
var expressApp = app;
function turnToArray(args){
var array = [];
for(var i = 0; i < args.length; i++){
array.push(args[i]);
}
return array;
}
return function(routes, callback) {
var args = turnToArray(arguments);
if(args.length <= 1){
return originalExpressGet.apply(app, arguments);
}
if(!Array.isArray(routes)){
routes = [routes];
}
args.shift();
routes.forEach(function(route){
originalExpressGet.apply(app, [route].concat(args));
});
}
return app;
}());
That way you are still using express's app.get, but it allows you to use [] for multiple routes. For example, this works:
app.get(['/', '/same'], function(req, res){
res.send(true);
});
And so does this:
app.get('/another route', function(req, res){
res.send(true);
});
I have put the server setting script into a separate js file called server.js. My problem is that I don't know how to get the value of cookie_key from the express middleware function and pass it back to the index.js file.
server.js:
var express = require('express'),
app = express(),
http = require('http').createServer(app),
cookie = cookieParser = require('cookie-parser'),
url = require('url');
module.exports = {
use_app : function(){
app.use(function (req, res, next) {
var cacheTime = 86400000*7; // 7 days
if (!res.getHeader('Cache-Control'))
res.setHeader('Cache-Control', 'public, max-age=' + (cacheTime / 1000));
next();
});
},
get_app : function(callback){
app.use(cookieParser());
app.get('/id/:tagId', function(req, res){ // parse the url parameter to get the file name
function getkey(err,data){ // get users' session cookie
var cookie_key;
if(err)
{
callback(err);
}
cookie_key = req.cookies["session"];
callback(cookie_key);
}
var filename = req.param("tagId");
res.sendFile(filename+'.html');
});
}
}
index.js:
var server = require('./server'),
server.use_app();
server.get_app(); // how to get the cookie_key when calling this function?
console.log(show_cookie_key_from the module);
if(cookie_key !== undefined)
{
// do something
}
I tried to write a callback function to fetch the cookie key but I don't think it's working.
Update from A.B's answer:
var server = require('./server');
server.use_app();
server.get_app(function(cookie){
if(cookie !== undefined)
{
// do something
}
});
But I still think there is something strange about this setup, what exactly are you trying to accomplish with splitting the app up like this?
Since you are using callback function and that is being poplulated with cookie value , you can get this like following:
var server = require('./server');
server.use_app();
server.get_app(function(cookie){
cookie_key= cookie
if(cookie_key !== undefined)
{
// do something
}
});
I am recently learning express.js. The code below is copied from the router lib of express.js.
var proto = module.exports = function(options) {
options = options || {};
function router(req, res, next) {
router.handle(req, res, next);
}
// mixin Router class functions
router.__proto__ = proto;
router.params = {};
router._params = [];
router.caseSensitive = options.caseSensitive;
router.strict = options.strict;
router.stack = [];
return router;
};
My question is what is returned if I call
var Router = require('./router');
var _router = new Router(...);
What is _router? Is it the function router(req, res, next)? If yes, can I call _router(req, res, next);?
If I am wrong, could someone please explain what does the code do?
If yes, why don't they just do it like:
var proto = module.exports = function(options) {
options = options || {};
var router = {};
// mixin Router class functions
router.__proto__ = proto;
router.params = {};
router._params = [];
router.caseSensitive = options.caseSensitive;
router.strict = options.strict;
router.stack = [];
return router;
};
For your first question:
var Router = require('./router');
var _router = new Router(...);
var Router is a object of created by the function router(req, res, next), and your var router is a new object of Router. In javascript almost everything is an object. You can read more here.
If they use your aproach they won't have a constructor. And they use the constructor to do router.handle(req, res, next); I don't know why they need the handle, you can study more the code or ask the developers. But you probably can use the var router new Router(req,res,next); if you know what the params do.