This article understands middleware from the Express source code, and can have a deeper understanding of Express
preface
What tasks can middleware functions perform?
- Execute any code.
- Make changes to the request and response objects.
- End the request/response loop.
- Call the next middleware function in the stack.
We start with an app.use and work our way up to the next middleware function execution.
Initializing the server
Start by downloading the Express source code from Github.
Create a file named test.js, import the index.js file from the root directory, instantiate Express, and start the server.
let express = require('.. /index.js');
let app = express()
function middlewareA(req, res, next) {
console.log('A1');
next();
console.log('A2');
}
function middlewareB(req, res, next) {
console.log('B1');
next();
console.log('B2');
}
function middlewareC(req, res, next) {
console.log('C1');
next();
console.log('C2');
}
app.use(middlewareA);
app.use(middlewareB);
app.use(middlewareC);
app.listen(8888, () = > {console.log("The server has started accessing http://127.0.0.1:8888");
})
Copy the code
Start the server, access http://127.0.0.1:8888, open the terminal, and view the terminal log execution sequence.
From the log, we can see that after next(), the next middleware functions will be called in sequence, and A1,B1 and C1 will be printed in sequence according to the execution order. At this time, the middleware has been called, and C2,B2 and A2 will be printed in sequence.
The directory structure
--lib
|__ middleware
|__ init.js
|__ query.js
|__ router
|__ index.js
|__ layer.js
|__ route.js
|__ application.js
|__ express.js
|__ request.js
|__ response.js
|__ utils.js
|__ view.js
Copy the code
By instantiating Express, we can see that the index.js file is actually a file that exposes lib/ Express.
Instantiation express
Express, which inherits appLication through mixins and initializes the appLication.
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;
}
Copy the code
Mixin is the merge-descriptorsnpm module. Merge objects using descriptors.
Open the application.js file and find an instantiation of Express from var app = exports = module.exports = {}. A further search for app.use finds app.use, which in turn is just a Proxy that adds middleware to the application router.
/** * Proxy `Router#use()` to add middleware to the app router. * See Router#use() documentation for details. * * If the _fn_ parameter is an express app, then it will be * mounted at the _route_ specified. * * @public */
app.use = function use(fn) {
var offset = 0;
var path = '/';
// Default path is '/'
// app.use([fn])
// Determine if app.use is passed as a function
if (typeoffn ! = ='function') {
var arg = fn;
while (Array.isArray(arg) && arg.length ! = =0) {
arg = arg[0];
}
// The first argument is the path
// Take the first argument and assign the first argument to path.
if (typeofarg ! = ='function') {
offset = 1; path = fn; }}// Slice. call(arguments,offset), slice can change the class array with length by converting slice to data.
// Arguments is an array object of class.
// Handle multiple middleware usage modes.
// app.use(r1, r2);
// app.use('/', [r1, r2]);
// app.use(mw1, [mw2, r1, r2], subApp);
var fns = flatten(slice.call(arguments, offset));//[funtion]
// Throw an error
if (fns.length === 0) {
throw new TypeError('app.use() requires a middleware function')}/ / set the router
this.lazyrouter();
var router = this._router;
fns.forEach(function (fn) {
// Handle applications that are not Express by calling route.use directly.
if(! fn || ! fn.handle || ! fn.set) {//path default to '/'
return router.use(path, fn);
}
debug('.use app under %s', path);
fn.mountpath = path;
fn.parent = this;
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);
});
});
// app Mounted Triggers emit
fn.emit('mount'.this);
}, this);
return this;
};
Copy the code
Define the default parameters Offer and PATH. We then deal with different types of fn parameters. Convert the parameters used by different types of middleware into flat arrays and assign values to FNS.
ForEach checks whether the fn, fn.handle, and fn.set parameters do not exist, and returns router.use(path, fn).
Otherwise, run router.use.
callhandle
Function that executes middleware.
The code is as follows:
/** * dispatches a REq, RES pair to the application. Middleware execution begins. * If no callback is provided, the default error handler will respond * when an error bubbles up in the stack. * /
app.handle = function handle(req, res, callback) {
var router = this._router;
// Handle error at last.
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);
};
Copy the code
Lazily add the Router.
As you can see from the above code, app.use is actually a mid-tier proxy that passes various application functions to the Router.
Also, the default router is lazily added by calling this.lazyRouter () in app.use.
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')));
// Initialize the router
this._router.use(middleware.init(this)); }};Copy the code
The Router is instantiated, the basic options are set, caseSensitive is caseSensitive, and strict is strict.
The Router is initialized as follows:
/** * Initializes a new router with the given "options". * * @param {Object} [options] [{caseSensitive: false, strict: false* @}]return {Router} which is an callable function
* @public
*/
var proto = module.exports = function(options) {
var opts = options || {};
functionrouter(req, res, next) { router.handle(req, res, next); } // Mix router class functionssetPrototypeOf(router, proto)
router.params = {};
router._params = [];
router.caseSensitive = opts.caseSensitive;
router.mergeParams = opts.mergeParams;
router.strict = opts.strict;
router.stack = [];
return router;
};
Copy the code
When app.use is called, the parameters are passed to router.use, so open the router/index.js file and look for router.use.
/** * uses the given middleware function with an optional path, which defaults to "/". * Use(e.g. '.all ') will be used for any HTTP methods, but will not add handlers for those methods, so option requests are not considered ". Use "* functions, even though they can respond. * Another difference is that _route_ path is stripped and not visible * to handler functions. The main purpose of this feature is to install * whatever the "prefix" is, handlers can manipulate * pathnames without changing any code. * * @public */
proto.use = function use(fn) {
var offset = 0;
var path = '/';
// Default path '/'
Router. Use ([fn])
// Check whether it is a function
if (typeoffn ! = ='function') {
var arg = fn;
while (Array.isArray(arg) && arg.length ! = =0) {
arg = arg[0];
}
// The first argument is a function
if (typeofarg ! = ='function') {
offset = 1; path = fn; }}// Convert arguments to an array and flatten the multidimensional array
var callbacks = flatten(slice.call(arguments, offset));
// If there is no transfer function in the callbacks, an error is thrown
if (callbacks.length === 0) {
throw new TypeError('Router.use() requires a middleware function')}// Loop through the Callbacks array
for (var i = 0; i < callbacks.length; i++) {
var fn = callbacks[i];
if (typeoffn ! = ='function') {
throw new TypeError('Router.use() requires a middleware function but got a ' + gettype(fn))
}
Query and expressInit
// Add middleware
// Anonymous function
debug('use %o %s', path, fn.name || '<anonymous>')
var layer = new Layer(path, {
sensitive: this.caseSensitive, // Case sensitive // Default is false
strict: false./ / strict
end: false / / end
}, fn);
layer.route = undefined;
this.stack.push(layer);
}
return this;
}
Copy the code
The main function of router.use is to add some methods to handle errors and requests through Layer instantiation of functions passed from app.use for subsequent calls. At the same time, pass the path, through the path-to-regexp module into the regular expression (this.regexp), call this.regexp.exec(path), to extract the parameters.
Layer code is more, there is no post code, can refer to express/lib/router/Layer. Js.
Processing middleware.
The new Layout([options],fn) will be placed in this,stack, and executed in sequence.
proto.handle = function handle(req, res, out) {
var self = this;
debug('dispatching %s %s', req.method, req.url);
var idx = 0;
// Get the protocol and URL
var protohost = getProtohost(req.url) || ' '
var removed = ' ';
// Whether to add a slash
var slashAdded = false;
var paramcalled = {};
// Store the requested options
// Only for option requests
var options = [];
// Middleware and routing
var stack = self.stack;
// Manage inter-router variables
//req.params request parameters
var parentParams = req.params;
var parentUrl = req.baseUrl || ' ';
var done = restore(out, req, 'baseUrl'.'next'.'params');
// Set the next layer
req.next = next;
// For option requests, if there is no other response, the default response is used
if (req.method === 'OPTIONS') {
done = wrap(done, function(old, err) {
if (err || options.length === 0) return old(err);
sendOptionsResponse(res, options, old);
});
}
// Set the basic req value
req.baseUrl = parentUrl;
req.originalUrl = req.originalUrl || req.url;
next();
function next(err) {
var layerError = err === 'route'
? null
: err;
// Whether to add slashes default to false
if (slashAdded) {
req.url = req.url.substr(1);
slashAdded = false;
}
// Restore changes to req.url
if(removed.length ! = =0) {
req.baseUrl = parentUrl;
req.url = protohost + removed + req.url.substr(protohost.length);
removed = ' ';
}
// Exit router signal
if (layerError === 'router') {
setImmediate(done, null)
return
}
// No longer matches layers
if (idx >= stack.length) {
setImmediate(done, layerError);
return;
}
// 获取路径pathname
var path = getPathname(req);
if (path == null) {
return done(layerError);
}
// Find the next matching layer
var layer;
var match;
var route;
while(match ! = =true && idx < stack.length) {
layer = stack[idx++];
//try layer.match(path) catch err
// Search path matchLayer has two states: Boolean and string.
match = matchLayer(layer, path);
route = layer.route;
if (typeofmatch ! = ='boolean') {
layerError = layerError || match;
}
if(match ! = =true) {
continue;
}
if(! route) {// Handle 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);
}
// Reassign router.
if (route) {
req.route = route;
}
// Merge parameters
req.params = self.mergeParams
? mergeParams(layer.params, parentParams)
: layer.params;
var layerPath = layer.path;
// Process parameters
self.process_params(layer, paramcalled, req, res, function (err) {
if (err) {
return next(layerError || err);
}
if (route) {
return layer.handle_request(req, res, next);
}
// Handle req.url and layerPath, and add tryCatch to error and handle_ERROR in layer.
trim_prefix(layer, layerError, layerPath, path);
});
}
Copy the code
Execute some core code in the while loop of proto.handle middleware. Each call to next() in the app.use callback will increment the IDX by one and stack[IDx ++]; Assign to layer, call layer.handle_request, and then call trim_prefix(layer, layerError, layerPath, path) to add some error handling.
The trim_prefix function is as follows:
function trim_prefix(layer, layerError, layerPath, path) {
if(layerPath.length ! = =0) {
// Validate path breaks on a path separator
var c = path[layerPath.length]
if(c && c ! = ='/'&& c ! = ='. ') return next(layerError)
// // Deletes the part of the URL that matches the route
// middleware (.use stuff) needs to have the path stripped
debug('trim prefix (%s) from url %s', layerPath, req.url);
removed = layerPath;
req.url = protohost + req.url.substr(protohost.length + removed.length);
// Ensure leading slash
if(! protohost && req.url[0]! = ='/') {
req.url = '/' + req.url;
slashAdded = true;
}
// Set base URL (no trailing slash)
req.baseUrl = parentUrl + (removed[removed.length - 1= = ='/'
? removed.substring(0, removed.length - 1)
: removed);
}
debug('%s %s : %s', layer.name, layerPath, req.originalUrl);
if (layerError) {
layer.handle_error(layerError, req, res, next);
} else{ layer.handle_request(req, res, next); }}};Copy the code
conclusion
The above is the implementation of the middleware function router.handle step by step after the app.use call.
The next core code is very simple, but there are many scenarios that need to be considered. Through this reading of the source code, you can further understand the core functions of Express.
While the Express framework is rarely, if ever, used for projects, Koa or Egg is the norm, Egg is the norm for projects of some size.
But the Express framework is undeniably a classic one.
The above code is purely personal understanding, if there is any inappropriate place, please leave a message in the comment section.