https://habr.com/company/ruvds/blog/414079/- Разработка веб-сайтов
- Node.JS
- JavaScript
- Блог компании RUVDS.com
Если вы занимались разработкой для платформы node.js, то вы, наверняка, слышали об 
express.js. Это — один из самых популярных легковесных фреймворков, используемых при создании веб-приложений для node.
 
Автор материала, перевод которого мы сегодня публикуем, предлагает изучить особенности внутреннего устройства фреймворка express через анализ его исходного кода и рассмотрение примера его использования. Он полагает, что изучение механизмов, лежащих в основе популярных опенсорсных библиотек, способствует более глубокому их пониманию, снимает с них завесу «таинственности» и помогает создавать более качественные приложения на их основе.
Возможно, вы сочтёте удобным держать под рукой исходный код express в процессе чтения этого материала. Здесь использована 
эта версия. Вы вполне можете читать эту статью и не открывая код express, так как здесь, везде где это уместно, даются фрагменты кода этой библиотеки. В тех местах, где код сокращён, используются комментарии вида 
// ...
Базовый пример использования express
Для начала взглянем на традиционный в деле освоения новых компьютерных технологий «Hello World!»-пример. Его можно найти на официальном сайте фреймворка, он послужит отправной точкой в наших исследованиях.
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!'))
Этот код запускает новый HTTP-сервер на порту 3000 и отправляет ответ 
Hello World! на запросы, поступающие по маршруту 
GET /. Если не вдаваться в подробности, то можно выделить четыре стадии происходящего, которые мы можем проанализировать:
- Создание нового приложения express.
- Создание нового маршрута.
- Запуск HTTP-сервера на заданном номере порта.
- Обработка поступающих к серверу запросов.
Создание нового приложения express
Команда 
var app = express() позволяет создать новое приложение express. Функция 
createApplication из файла 
lib/express.js является функцией, экспортируемой по умолчанию, именно к ней мы обращаемся, выполняя вызов функции 
express(). Вот некоторые важные вещи, на которые тут стоит обратить внимание:
// ...
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;
}
Объект 
app, возвращённый из этой функции  — это один из объектов, используемых в коде нашего приложения. Метод 
app.get добавляется с использованием функции 
mixin библиотеки 
merge-descriptors, которая ответственна за назначение 
app методов, объявленных в 
proto. Сам объект 
proto импортируется из 
lib/application.js.
Создание нового маршрута
Взглянем теперь на 
код, который ответственен за создание метода 
app.get из нашего примера.
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;
  };
});
Интересно отметить, что, помимо семантических особенностей, все методы, реализующие действия HTTP, вроде 
app.get, 
app.post, 
app.put и подобных им, в плане функционала, можно считать одинаковыми. Если упростить вышеприведённый код, сведя его к реализации лишь одного метода 
get, то получится примерно следующее:
app.get = function(path, handler){
  // ...
  var route = this._router.route(path);
  route.get(handler)
  return this
}
Хотя у вышеприведённой функции 2 аргумента, она похожа на функцию 
app[method] = function(path){...}. Второй аргумент, 
handler, получают, вызывая 
slice.call(arguments, 1).
Если в двух словах, то 
app.<method> просто сохраняет маршрут в маршрутизаторе приложения, используя его метод 
route, а затем передаёт 
handler в 
route.<method>.
 
Метод маршрутизатора 
route() объявлен в 
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;
};
Неудивительно то, что объявление метода 
route.get в 
lib/router/route.js похоже на объявление 
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;
  };
});
У каждого маршрута может быть несколько обработчиков, на основе каждого обработчика конструируется переменная типа 
Layer, представляющая собой слой обработки данных, которая потом попадает в стек.
Объекты типа Layer
И 
_router, и 
route используют объекты типа 
Layer. Для того чтобы разобраться в сущности такого объекта, посмотрим на его 
конструктор:
function Layer(path, options, fn) {
  // ...
  this.handle = fn;
  this.regexp = pathRegexp(path, this.keys = [], opts);
  // ...
}
При создании объектов типа 
Layer им передают путь, некие параметры, и функцию. В случае нашего маршрутизатора этой функцией является 
route.dispatch (подробнее о ней мы поговорим ниже, в общих чертах, она предназначена для передачи запроса отдельному маршруту). В случае с самим маршрутом, эта функция является функцией-обработчиком, объявленной в коде нашего примера.
У каждого объекта типа 
Layer есть метод 
handle_request, который отвечает за выполнение функции, переданной при инициализации объекта.
Вспомним, что происходит при создании маршрута с использованием метода 
app.get:
- В маршрутизаторе приложения (this._router) создаётся маршрут.
- Метод маршрута dispatchназначается в качестве метода-обработчика соответствующего объектаLayer, и этот объект помещают в стек маршрутизатора.
- Обработчик запроса передаётся объекту Layerв качестве метода-обработчика, и этот объект помещается в стек маршрутов.
В итоге все обработчики хранятся внутри экземпляра 
app в виде объектов типа 
Layer, которые находятся внутри стека маршрутов, методы 
dispatch которых назначены объектам 
Layer, которые находятся в стеке маршрутизатора:
Объекты типа Layer в стеке маршрутизатора и в стеке маршрутов
Поступающие HTTP-запросы обрабатываются в соответствии с этой логикой. Мы поговорим о них ниже.
Запуск HTTP-сервера
После настройки маршрутов надо запустить сервер. В нашем примере мы обращаемся к методу 
app.listen, передавая ему в качестве аргументов номер порта и функцию обратного вызова. Для того чтобы понять особенности этого метода, мы можем обратиться к файлу 
lib/application.js:
app.listen = function listen() {
  var server = http.createServer(this);
  return server.listen.apply(server, arguments);
};
Похоже, что 
app.listen — это просто обёртка вокруг 
http.createServer. Такая точка зрения имеет смысл, так как если вспомнить то, о чём мы говорили в самом начале, 
app — это просто функция с сигнатурой 
function(req, res, next) {...}, которая совместима с аргументами, необходимыми для 
http.createServer (сигнатурой этого метода является 
function (req, res) {...}).
После понимания того, что, в итоге, всё, что даёт нам express.js, может быть сведено к весьма интеллектуальной функции-обработчику, фреймворк выглядит уже не таким сложным и таинственным, как раньше.
Обработка HTTP-запроса
Теперь, когда мы знаем, что 
app — это всего лишь обработчик запросов, проследим за путём, который проходит HTTP-запрос внутри приложения express. Этот путь ведёт его в объявленный нами обработчик.
Сначала запрос поступает в функцию 
createApplication (
lib/express.js):
var app = function(req, res, next) {
    app.handle(req, res, next);
};
Потом он идёт в метод 
app.handle (
lib/application.js):
app.handle = function handle(req, res, callback) {
  // `this._router` - это место, где мы объявили маршрут, используя `app.get`
  var router = this._router;
  // ... 
  // Запрос попадает в метод `handle`
  router.handle(req, res, done);
};
Метод 
router.handle объявлен в 
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);
      }
      // ...
    });
  }
};
Если описать происходящее в двух словах, то функция 
router.handle проходится по всем слоям в стеке, до тех пор, пока не найдёт тот, который соответствует пути, заданному в запросе. Затем будет произведён вызов метода слоя 
handle_request, который выполнит заранее заданную функцию-обработчик. Эта функция-обработчик является методом маршрута 
dispatch, который объявлен в 
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);
    // ...
  }
};
Так же, как и в случае с маршрутизатором, при обработке каждого маршрута осуществляется перебор слоёв, которые есть у этого маршрута, и вызов их методов 
handle_request, которые выполняют методы-обработчики слоёв. В нашем случае это обработчик запроса, который объявлен в коде приложения.
Здесь, наконец, HTTP-запрос попадает в область кода нашего приложения.
Путь запроса в приложении express
 
Итоги
Здесь мы рассмотрели лишь основные механизмы библиотеки express.js, те, которые ответственны за работу веб-сервера, но эта библиотека обладает и многими другими возможностями. Мы не останавливались на проверках, которые проходят запросы до поступления их в обработчики, мы не говорили о вспомогательных методах, которые доступны при работе с переменными 
res и 
req. И, наконец, мы не затрагивали одну из наиболее мощных возможностей express. Она заключается в использовании промежуточного программного обеспечения, которое может быть направлено на решение практически любых задача — от разбора запросов до реализации полноценной системы аутентификации.
Надеемся, этот материал помог вам разобраться в основных особенностях устройства express, и теперь вы, при необходимости, сможете понять всё остальное, самостоятельно проанализировав интересующие вас части исходного кода этой библиотеки.
Уважаемые читатели! Пользуетесь ли вы express.js?
