Contents of this series

1. GlobalI went to see

2. Routes are matchedI went to see

3. Error handlingI went to see

4. View templateI went to see

5. Design ideaI went to see

1. Review

Last time, we talked about the relationship between the four objects in Express, each of which does its job

  • App objects are responsible for exposing interfaces and managing application-level global variables such as global routers, global Settings, template engines, and so on

  • The Router object is responsible for the underlying route matching, that is, managing the relationship between the middleware and the request path, parsing the path, and invoking the appropriate middleware

  • The Route object is generally responsible for managing functions that eventually perform operations on reQ and RED objects, which can be understood as secondary or multi-level middleware, such as functions that return data, which are usually the most written functions

router.get('/user',function(req,res,next){
    res.end('hello world');
})
Copy the code
  • The layer object can be divided into three types according to the different types of its handle function. One type of handle is middleware function, which represents middleware and resides in the global router.stack. The other type of handle is Route. dispatch, which is responsible for waking up route. The last type of layer handle is the function written above. This layer usually has a specific method and only works if the path and method match

In this article, we will look at Express from the perspective of route matching how to handle control middleware and route matching, as well as dynamic path parsing and other related issues


2. Use

Before to see the source code, familiar with documents and use should be the premise, because when you meet the so-called black magic will raise curiosity to find why this is, so before really began to analyze routing is how to match, review, use, at the interface level, is how to set the routing and the corresponding relationship between layer, Express documents come fast!

2.1 Normal Routing

//This route path will match requests to the root route, /.
app.get('/'.function (req, res) {
  res.send('root')})Copy the code

She’s very delicate, very nice!

2.2 Normal Dynamic Routing

//To define routes with route parameters, simply specify the route parameters in the path of the route as shown below.
app.get('/users/:userId/books/:bookId'.function (req, res) {
  res.send(req.params)
})
Copy the code

Ok, can endure, can read, can barely write

3. Euclidean abnormal routing (blind name)

app.get('/ab? cd'.function (req, res) {
  res.send('ab? cd')})//This route path will match abcd, abbcd, abbbcd, and so on.
app.get('/ab+cd'.function (req, res) {
  res.send('ab+cd')})//This route path will match abcd, abxcd, abRANDOMcd, ab123cd, and so on.
app.get('/ab*cd'.function (req, res) {
  res.send('ab*cd')})//This route path will match /abe and /abcde.
app.get('/ab(cd)? e'.function (req, res) {
  res.send('ab(cd)? e')})Copy the code

Me:?????

In fact, no matter what kind of matching path is written, the internal matching operation is the same. Therefore, this article analyzes the two kinds of routes mentioned above, which are set by the same API, no matter which Settings, app.use,app.get, etc., the internal implementation is similar, we take app[method] as an example to analyze. [method] app[method]I quick go to

Post the API documentation for the rest of the process

Router. route passes the path to the layer and route for initialization. Router. route passes the path to the layer and route. So the path of layer initialization is the path passed in by app[method]

Everything is all but the east wind, the east wind!

3. The deep Layer

The Layer object is closely related to route matching. Only by understanding the properties of the Layer object can we understand exactly how to match.

3.1 Attributes and Initialization

! Note: The path in the figure above is not the path passed in for initialization

As you can see, when you initialize a layer and pass in a path, the path is passed to the pathRegexp method and the return value is given to this.regexp. The pathRegexp method is the key to implementing the various dynamic routing functions described above

3.2. The path – to – the regexpI quick go to

Yes, yes, this is a third package, obviously look at the source of this third party package is not rational, because I am the most dizzy regular expression, so in the NPM package online to find the use of a simple look

First of all, there are only four methods in this package, and the new Layer is the first one. Obviously, in addition to path, there are also keys and options

const { pathToRegexp, match, parse, compile } = require("path-to-regexp");
// pathToRegexp(path, keys? , options?)
// match(path)
// parse(path)
// compile(path)
Copy the code

I know your English is not good, Google translate, will see

  • Path: string, string array, or regular expression
  • Keys: An array filled with keys found in the path.
  • options
    • Sensitive if true, the regular expression is case sensitive (default: false) –
    • Strict if true, regEXP does not allow matching optional tail delimiters (delimiters, form Requirements dictionary). (Default: false)
    • If end is true, regexp matches the end of the string. (Default: true)

You don’t understand? I don’t understand either. Relax. I’ve got examples

/ / 1 with chestnuts
const keys = [];
const regexp = pathToRegexp("/foo/:bar", keys);
// regexp = /^\/foo(? : \ / ([^ # \ \ /?] +? ) / # \ \ /? ? $/i
// keys = [{ name: 'bar', prefix: '/', suffix: '', pattern: '[^\\/#\\?]+?', modifier: '' }]

/ / 2 with chestnuts
const regexp = pathToRegexp("/:foo/:bar");
// keys = [{ name: 'foo', prefix: '/', ... }, { name: 'bar', prefix: '/', ... }]
 
regexp.exec("/test/route");
//=> [ '/test/route', 'test', 'route', index: 0, input: '/test/route', groups: undefined ]
Copy the code

Don’t dwell on it. Just summarize

3.2.1 The function passes path as a dynamic routing path and generates a regular expression that matches the dynamic routing path

Such as path ='/foo/:id'The generated regular expression matches'/foo/1'.'foo/2'
Copy the code

Each item is an object composed of a dynamic parameter key and some related attributes, which is used to obtain the real parameters passed in later

Such as path ='/foo/:id'The generated keys array is [{name:'id'. }], the length of the array is the number of dynamic parametersCopy the code

3.2.3 Options controls how the regular expression generated by 1 is matched, including whether case is ignored, whether matching ends, and so on

There are also two booleans, regexp. Fast_star, regexp. Fast_slash,

  • Regexp. fast_star: True if the path passed by the current layer is’ * ‘
  • Regexp. Fast_slash: true if path passed in by current layer is “/” and is global middleware

3. Match method

Match method is the method of layer matching routes, and its logic determines which middleware corresponds to which routes

Layer.prototype.match = function match(path) {
  var match
  if(path ! =null) {
    // fast path non-ending match for / (any path matches)
    if (this.regexp.fast_slash) {
      this.params = {}
      this.path = ' '
      return true
    }
    // fast path for * (everything matched in a param)
    if (this.regexp.fast_star) {
      this.params = {'0': decode_param(path)}
      this.path = path
      return true
    }
    // match the path
    match = this.regexp.exec(path)
  }
  // The path is empty
  if(! match) {this.params = undefined;
    this.path = undefined;
    return false;
  }

  // store values
  this.params = {};
  this.path = match[0]

  var keys = this.keys;
  var params = this.params;

  for (var i = 1; i < match.length; i++) {
    var key = keys[i - 1];
    var prop = key.name;
    var val = decode_param(match[i])

    if(val ! = =undefined || !(hasOwnProperty.call(params, prop))) {
      params[prop] = val;
    }
  }
  return true;
};
Copy the code

The summary is as follows:

  1. Params ={}, path= “”, params={}, path=” “

  2. Initialized with path as’/’ and global middleware, params={‘ 0 ‘: decodeparam(PATH)}, path = matching path

  3. If none of the above is satisfied, the re match begins

  4. No match: return false, params, path reset to undefined

  5. Matched: returns true, path = first matched all, params = params obtained by parsing the path

For example, the initialization path is /foo/:id and the matching path is /foo/1, so params is {'id': 1}, layer, path ='/foo'
Copy the code

4. Official match

We are ready to start the formal matching process. As mentioned in the previous article, when a request arrives, the app executes as a callback, and the router. Handle is finally executed internally, where all the details of the route matching take place.

Once again, the router.handle function is the manager of the whole process. It is a closure in which the living variables remain valid throughout the matching process

This function begins by declaring some global closure variables at the request level

In the whole process, these variables will be operated to carry out nested matching for the transformation and cutting of req. URL path. Here we only look at the general logic, because the next method is the middleware flow scheduling method, so the specific cutting transformation operation also takes place in it

4.1 The first step of matching is to get the request route (the third-party package is used here, do not care, just cache the req.url, and eventually return path)

// get pathname of request
var path = getPathname(req);
Copy the code

4.2 The second step is to find the matching layer, which is to use the above mentioned match method (source has been removed)

 while(match ! = =true && idx < stack.length) {
      layer = stack[idx++];
      match = matchLayer(layer, path);
      route = layer.route;
      if(match ! = =true) {
        continue;
      }
      if(! route) {// process non-route handlers normally
        continue; }}Copy the code

4.3. The third step deals with dynamic parameters. The purpose of this step is to mount the parsed dynamic parameters to req.params

// this should be done for the layer
self.process_params(layer, paramcalled, req, res, callback);
// For the sake of visualization, the transformation is performedThe callback =function (err) {
  if (err) {
    return next(layerError || err);
  }
  if (route) {
    return layer.handle_request(req, res, next);
  }
  trim_prefix(layer, layerError, layerPath, path);
}
Copy the code

Specific analysis parameter source code

proto.process_params = function process_params(layer, called, req, res, done) {
  var params = this.params;
  // captured parameters from the layer, keys and values
  var keys = layer.keys;

  // fast track
  if(! keys || keys.length ===0) {
    return done();
  }
  var i = 0;
  var name;
  var paramIndex = 0;
  var key;
  var paramVal;
  var paramCallbacks;
  var paramCalled;
  // process params in order
  // param callbacks can be async
  function param(err) {
    if (err) {
      return done(err);
    }

    if (i >= keys.length) {
      return done();
    }

    paramIndex = 0;
    key = keys[i++];


    name = key.name;
    paramVal = req.params[name];
    paramCallbacks = params[name];
    paramCalled = called[name];

    if (paramVal === undefined| |! paramCallbacks) {return param();
    }
    // param previously called with same value or error occurred
    if(paramCalled && (paramCalled.match === paramVal || (paramCalled.error && paramCalled.error ! = ='route'))) {
      // restore value
      req.params[name] = paramCalled.value;

      // next param
      return param(paramCalled.error);
    }
    called[name] = paramCalled = {
      error: null.match: paramVal,
      value: paramVal
    };
    paramCallback();
  }

  // single param callbacks
  function paramCallback(err) {
    var fn = paramCallbacks[paramIndex++];
    // store updated value
    paramCalled.value = req.params[key.name];
    if (err) {
      // store error
      paramCalled.error = err;
      param(err);
      return;
    }
    if(! fn)return param();
    try {
      fn(req, res, paramCallback, paramVal, key.name
        );
    } catch (e) {
      paramCallback(e);
    }
  }
  param();
};
Copy the code

4.4 The fourth step is to execute the callback of the previous step, which will remove the matching prefix of the global middleware layer, update the value of req.url to match nested routines, and finally execute the Handle of the layer

function trim_prefix(layer, layerError, layerPath, path) {
  // layerPath = undefined path = /user
  if(layerPath.length ! = =0) {
    // Validate path breaks on a path separator
    var c = path[layerPath.length]
    if(c && c ! = ='/'&& c ! = ='. ') return next(layerError)

    // Trim off 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;
    }

    // Setup 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

5. To summarize

Route matching is to match the request path with the corresponding layer

  1. During initialization, layer will generate a specific regular expression based on the passed path and generate an array of keys to manage the dynamic parameters in its own part of path

  2. The layer has a match method, whose logic is to verify whether the actual path matches the layer. If so, set the params and path of the layer

  3. Router. handle is a closure in which the entire middleware flow is completed. Therefore, some request-level closure variables, such as req.baseurl and req.url, are often saved

  4. Formal matching is to constantly find the layer that matches the path, execute the Handle method of the layer, and then return to the router.handle scope by using the closure of the next method, and repeat the process, during which params are mounted and route path prefix cutting are performed.