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.

callhandleFunction 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.