Express Source Analysis
1. What happened when express() was introduced and called?
- When using const express = require("express"), the default is to look for the index.js file under the express module
- In index.js, we found that:
module.exports = require('./lib/express');
- This refers to the express.js file under the lib directory
- In express.js, the first line: exports = module.exports = createApplication, which exports the createApplication function by default
- Take another look at createApplication:
function createApplication() { var app = function(req, res, next) { app.handle(req, res, next); }; mixin(app, EventEmitter.prototype, false); mixin(app, proto, false); // expose the prototype that will get set on requests app.request = Object.create(req, { app: { configurable: true, enumerable: true, writable: true, value: app } }) // expose the prototype that will get set on responses app.response = Object.create(res, { app: { configurable: true, enumerable: true, writable: true, value: app } }) app.init(); return app; }
- An Express example is returned from the createApplication, that is, when we call express(), it is equivalent to createApplication()
2. What was done at the bottom of Express when app.listen (port, host, callback) was executed?
- When creating the Express instance, we found that only request and response objects are mounted on the app instance, and there is no listen method. It is actually mounted through mixin(app, proto, false). proto is an object, which is defined in application.js
- The listen method was found in application.js with the following source code:
app.listen = function listen() { var server = http.createServer(this); return server.listen.apply(server, arguments); };
- Originally, the native HTTP module of NodeJS was called, a server was created, and listening was turned on. this in http.createServer points to app, which is equivalent to the following:
app.listen = function listen() { var server = http.createServer(function(req, res, next) { app.handle(req, res, next); }); return server.listen.apply(server, arguments); };
3. What does Express do when using the middleware app.use?
- App.use = function use (fn), the form of which is a function
- this.lazyrouter() is called in function;, To initialize the route, the source code is as follows:
//application.js app.lazyrouter = function lazyrouter() { if (!this._router) { this._router = new Router({ caseSensitive: this.enabled('case sensitive routing'), strict: this.enabled('strict routing') }); this._router.use(query(this.get('query parser fn'))); this._router.use(middleware.init(this)); } }; var fns = flatten(slice.call(arguments, offset)); if (fns.length === 0) { throw new TypeError('app.use() requires a middleware function') } this.lazyrouter(); var router = this._router; fns.forEach(function (fn) { // non-express app if (!fn || !fn.handle || !fn.set) { return router.use(path, fn); } debug('.use app under %s', path); fn.mountpath = path; fn.parent = this; // restore .app property on req and res router.use(path, function mounted_app(req, res, next) { var orig = req.app; fn.handle(req, res, function (err) { setPrototypeOf(req, orig.request) setPrototypeOf(res, orig.response) next(err); }); }); // mounted an app fn.emit('mount', this); }, this);
- Originally, when calling use to use the middleware, Express would create a router instance, then get an array of middleware, iterate through all the middleware matching the same middleware as the current parameter, and finally process it through the router, so a deeper level of detail is in the router.use() method
- The source code for router.use is as follows: under router/index.js
proto.use = function use(fn) { var offset = 0; var path = '/'; // default path to '/' // disambiguate router.use([fn]) if (typeof fn !== 'function') { var arg = fn; while (Array.isArray(arg) && arg.length !== 0) { arg = arg[0]; } // first arg is the path if (typeof arg !== 'function') { offset = 1; path = fn; } } var callbacks = flatten(slice.call(arguments, offset)); if (callbacks.length === 0) { throw new TypeError('Router.use() requires a middleware function') } for (var i = 0; i < callbacks.length; i++) { var fn = callbacks[i]; if (typeof fn !== 'function') { throw new TypeError('Router.use() requires a middleware function but got a ' + gettype(fn)) } // add the middleware debug('use %o %s', path, fn.name || '<anonymous>') var layer = new Layer(path, { sensitive: this.caseSensitive, strict: false, end: false }, fn); layer.route = undefined; this.stack.push(layer); } return this; };
- You can see:
- First the parameters are determined
- Get all registered callback functions in use
- Traverse through all callbacks, throw an error if there are parameters that are not of type function, and turn each callback function into a layer object
- Put layer s on the stack
4. When a user sends an http request, how is the registered middleware executed?
- Since the service is started, all requests are monitored, and callbacks during listening are performed:
var server = http.createServer(function(req, res, next) { app.handle(req, res, next); });
- Let's look at the app.handle method: it actually initializes a router and executes router.handle()
app.handle = function handle(req, res, callback) { var router = this._router; // final handler var done = callback || finalhandler(req, res, { env: this.get('env'), onerror: logerror.bind(this) }); // no routes if (!router) { debug('no routes defined on app'); done(); return; } router.handle(req, res, done); };
- Looking at the router.handle() method: Simply put, it initializes some parameters, takes out the global stack, and calls next()
var self = this; debug('dispatching %s %s', req.method, req.url); var idx = 0; var protohost = getProtohost(req.url) || '' var removed = ''; var slashAdded = false; var paramcalled = {}; // store options for OPTIONS request // only used if OPTIONS request var options = []; // middleware and routes var stack = self.stack; // manage inter-router variables var parentParams = req.params; var parentUrl = req.baseUrl || ''; var done = restore(out, req, 'baseUrl', 'next', 'params'); // setup next layer req.next = next; // for options requests, respond with a default if nothing else responds if (req.method === 'OPTIONS') { done = wrap(done, function(old, err) { if (err || options.length === 0) return old(err); sendOptionsResponse(res, options, old); }); } // setup basic req values req.baseUrl = parentUrl; req.originalUrl = req.originalUrl || req.url; next();
- Therefore, you need to look at the implementation of next:
function next(err) { var layerError = err === 'route' ? null : err; // remove added slash if (slashAdded) { req.url = req.url.substr(1); slashAdded = false; } // restore altered req.url if (removed.length !== 0) { req.baseUrl = parentUrl; req.url = protohost + removed + req.url.substr(protohost.length); removed = ''; } // signal to exit router if (layerError === 'router') { setImmediate(done, null) return } // no more matching layers if (idx >= stack.length) { setImmediate(done, layerError); return; } // get pathname of request var path = getPathname(req); if (path == null) { return done(layerError); } // find next matching layer var layer; var match; var route; while (match !== true && idx < stack.length) { layer = stack[idx++]; match = matchLayer(layer, path); route = layer.route; if (typeof match !== 'boolean') { // hold on to layerError layerError = layerError || match; } if (match !== true) { continue; } if (!route) { // process non-route handlers normally continue; } if (layerError) { // routes do not match with a pending error match = false; continue; } var method = req.method; var has_method = route._handles_method(method); // build up automatic options response if (!has_method && method === 'OPTIONS') { appendMethods(options, route._options()); } // don't even bother matching route if (!has_method && method !== 'HEAD') { match = false; continue; } } // no match if (match !== true) { return done(layerError); } // store route for dispatch on change if (route) { req.route = route; } // Capture one-time layer values req.params = self.mergeParams ? mergeParams(layer.params, parentParams) : layer.params; var layerPath = layer.path; // this should be done for the layer self.process_params(layer, paramcalled, req, res, function (err) { if (err) { return next(layerError || err); } if (route) { return layer.handle_request(req, res, next); } trim_prefix(layer, layerError, layerPath, path); }); }
-
Actually next() is just doing a few things
- Previously, when the user registered the middleware, the callbacks they needed to execute were stored in a global stack as layer objects, so when responding, they needed to find a layer that matched the path
- After finding the corresponding layer, call layer.handle_request(req, res, next), which is ultimately handled by the layer
-
Execution of the final response: layer.handle_request(), the source code is as follows:
Layer.prototype.handle_request = function handle(req, res, next) { var fn = this.handle; if (fn.length > 3) { // not a standard request handler return next(); } try { fn(req, res, next); } catch (err) { next(err); } };
- In this method:
- Determining the number of handle parameters
- If the parameters are OK, execute the callback function passed in by the user
- If an error occurs, call the middle of the execution error
5. Why can next() call the next middleware?
- idx++ in stack, will go to the stack to find the next matching middleware call!