Features of work and internal device express.js

If you were engaged in development for the node.js platform, then you, for certain, heard about express.js . This is one of the most popular lightweight frameworks used when building web applications for a node.



The author of the material, the translation of which we are publishing today, proposes to study the features of the internal structure of the express framework through the analysis of its source code and consideration of an example of its use. He believes that studying the mechanisms underlying popular open source libraries contributes to their deeper understanding, removes the veil of "mystery" and helps to create better applications based on them.

You may find it convenient to keep the express source code on hand as you read this material. This version is used here. You can read this article quite well without opening the express code, since here, wherever it is appropriate, fragments of the code of this library are given. In those places where the code is abbreviated, comments of the form // ... are used // ...

Basic example of using express


To begin, take a look at the traditional “Hello World!” In the development of new computer technologies - an example. It can be found on the framework’s official website; it will serve as a starting point in our research.

 const express = require('express') const app = express() app.get('/', (req, res) => res.send('Hello World!')) app.listen(3000, () => console.log('Example app listening on port 3000!')) 

This code starts a new HTTP server on port 3000 and sends a Hello World! Response Hello World! on inquiries coming along the route GET / . If you do not go into details, you can select four stages of what is happening, which we can analyze:

  1. Create a new express application.
  2. Creating a new route.
  3. Start the HTTP server on the specified port number.
  4. Processing incoming requests to the server.

Creating a new express application


The var app = express() command allows you to create a new express application. The createApplication function from the lib / express.js file is a function exported by default, and we refer to it by performing a call to the express() function. Here are some important things you should pay attention to here:

 // ... var mixin = require('merge-descriptors'); var proto = require('./application'); // ... function createApplication() { //    ,     . //     : `function(req, res, next)` var app = function(req, res, next) {   app.handle(req, res, next); }; // ... //  `mixin`    `proto`  `app` //     -  `get`,     . mixin(app, proto, false); // ... return app; } 

The app object returned from this function is one of the objects used in the code of our application. The app.get method app.get added using the mixin function of the merge-descriptors library, which is responsible for assigning app methods declared to proto . The proto object itself is imported from lib / application.js .

Creating a new route


Now let's take a look at the code that is responsible for creating the app.get method from our example.

 var slice = Array.prototype.slice; // ... /** *   `.VERB(...)` `router.VERB(...)`. */ // `methods`    HTTP, (  ['get','post',...]) methods.forEach(function(method){ //    app.get app[method] = function(path){   //     //          var route = this._router.route(path);   //        route[method].apply(route, slice.call(arguments, 1));   //   `app`,          return this; }; }); 

It is interesting to note that, in addition to semantic features, all methods that implement HTTP actions, such as app.get , app.post , app.put and the like, in terms of functionality, can be considered the same. If you simplify the above code, reducing it to the implementation of only one get method, you get something like this:

 app.get = function(path, handler){ // ... var route = this._router.route(path); route.get(handler) return this } 

Although the above function has 2 arguments, it is similar to the app[method] = function(path){...} . The second argument, handler , is obtained by calling slice.call(arguments, 1) .

In a nutshell, app.<method> simply stores the route in the application’s router using its route method, and then passes the handler to route.<method> .

The route() router method is declared in lib / router / index.js :

 // proto -     `_router` proto.route = function route(path) { var route = new Route(path); var layer = new Layer(path, {   sensitive: this.caseSensitive,   strict: this.strict,   end: true }, route.dispatch.bind(route)); layer.route = route; this.stack.push(layer); return route; }; 

Not surprisingly, the declaration of the route.get method in lib / router / route.js looks like an app.get :

 methods.forEach(function (method) { Route.prototype[method] = function () {   // `flatten`   ,  [1,[2,3]],      var handles = flatten(slice.call(arguments));   for (var i = 0; i < handles.length; i++) {     var handle = handles[i];         // ...     //   ,  ,    Layer,     //            var layer = Layer('/', {}, handle);     // ...     this.stack.push(layer);   }   return this; }; }); 

Each route can have several handlers; on the basis of each handler, a variable of type Layer constructed, which is a data processing layer, which then goes on the stack.

Layer Objects


Both _router and route use objects of type Layer . In order to understand the essence of such an object, let's look at its constructor :

 function Layer(path, options, fn) { // ... this.handle = fn; this.regexp = pathRegexp(path, this.keys = [], opts); // ... } 

When creating objects of type Layer they are given a path, certain parameters, and a function. In the case of our router, this function is route.dispatch (we'll talk about it in more detail below, in general terms, it is intended to send a request to a separate route). In the case of the route itself, this function is a handler function declared in the code of our example.

Each object of type Layer has a method handle_request , which is responsible for executing the function passed during object initialization.

Recall what happens when you create a route using the app.get method:

  1. In the application router ( this._router ) a route is created.
  2. The dispatch route method is assigned as the handler method of the corresponding Layer object, and this object is pushed onto the router's stack.
  3. The request handler is passed to the Layer object as a handler method, and this object is pushed onto the route stack.

As a result, all handlers are stored inside the app instance as Layer objects that are inside the route stack, whose dispatch methods are assigned to Layer objects that are on the router's stack:


Objects of type Layer in the stack of the router and in the stack of routes

Incoming HTTP requests are processed according to this logic. We will talk about them below.

Start HTTP server


After setting up the routes, you must start the server. In our example, we are app.listen method, passing in it the port number and the callback function as arguments. To understand the features of this method, we can refer to the lib / application.js file:

 app.listen = function listen() { var server = http.createServer(this); return server.listen.apply(server, arguments); }; 

Looks like app.listen is just a wrapper around http.createServer . This point of view makes sense, because if we recall what we said at the very beginning, the app is just a function with the signature function(req, res, next) {...} , which is compatible with the arguments required for http.createServer (the signature of this method is function (req, res) {...} ).

After understanding that, ultimately, everything that gives us express.js can be reduced to a very intelligent handler function, the framework does not look as complicated and mysterious as it used to be.

HTTP request processing


Now that we know that the app is just a request handler, let us follow the path that an HTTP request passes through the express application. This path leads it to the handler declared by us.

First, the request goes to the createApplication function ( lib / express.js ):

 var app = function(req, res, next) {   app.handle(req, res, next); }; 

Then it goes to the app.handle method ( lib / application.js ):

 app.handle = function handle(req, res, callback) { // `this._router` -  ,    ,  `app.get` var router = this._router; // ... //     `handle` router.handle(req, res, done); }; 

The router.handle method router.handle declared in lib / router / index.js :

 proto.handle = function handle(req, res, out) { var self = this; //... // self.stack -  ,      // Layer (  ) var stack = self.stack; // ... next(); function next(err) {   // ...   //        var path = getPathname(req);   // ...   var layer;   var match;   var route;   while (match !== true && idx < stack.length) {     layer = stack[idx++];     match = matchLayer(layer, path);     route = layer.route;     // ...     if (match !== true) {       continue;     }     // ...      HTTP,       }  // ...      // process_params    ,          self.process_params(layer, paramcalled, req, res, function (err) {     // ...     if (route) {       //       `layer.handle_request`       //         `next`       //  ,   `next`     ,              //  ,   `next`   ,            return layer.handle_request(req, res, next);     }     // ...   }); } }; 

If we describe what is happening in a nutshell, the router.handle function goes through all the layers in the stack until it finds one that matches the path specified in the request. Then the handle_request method of the layer will be called, which will execute the predefined handler function. This handler function is the dispatch route method, which is declared in lib / route / route.js :

 Route.prototype.dispatch = function dispatch(req, res, done) { var stack = this.stack; // ... next(); function next(err) {   // ...   var layer = stack[idx++];   // ...    layer.handle_request(req, res, next);   // ... } }; 

In the same way as in the case of a router, during the processing of each route, the layers are handle_request that the route has, and their handle_request methods are handle_request , which are performed by the layer handler methods. In our case, this is the request handler, which is declared in the application code.

Here, finally, the HTTP request falls into the code area of ​​our application.


The request path in the express application

Results


Here we covered only the basic mechanisms of the express.js library, those that are responsible for the operation of the web server, but this library has many other features. We didn’t stop at checks that pass requests before they are received by handlers, we didn’t talk about the helper methods that are available when working with the variables res and req . And finally, we did not touch on one of the most powerful express features. It consists in the use of middleware that can be aimed at solving virtually any task - from parsing requests to implementing a full-fledged authentication system.

We hope this material has helped you understand the main features of the express device, and now you, if necessary, can understand everything else by independently analyzing the parts of the source code of this library that you are interested in.

Dear readers! Do you use express.js?

Source: https://habr.com/ru/post/414079/


All Articles