Как работает express.js — понимание внутренних функций экспресс-библиотеки

Если вы работали над разработкой веб-приложений в узле, вероятно, вы слышали о express.js. Express является одной из самых популярных облегченных веб-приложений для узла.

  • Просмотров |

Если вы работали над разработкой веб-приложений в узле, вероятно, вы слышали о express.js. Express является одной из самых популярных облегченных веб-приложений для узла.

В этом посте мы рассмотрим исходный код выражения и попытаемся понять, как он работает под капотом. Изучение того, как работает популярная библиотека с открытым исходным кодом, поможет нам улучшить использование приложений, а также уменьшит часть «magic», задействованной при ее использовании.

Возможно, вам будет полезно сохранить копию экспресс-кода удобным, пока мы проходим через сообщение. Мы используем эту версию. Даже если вы этого не сделаете, ссылки перед исходным кодом предоставляются перед каждым объяснением.
Этот комментарий: // … означает, что исходный код был скрыт для краткости

Пример «Hello World»

Давайте используем пример «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 и отправляет текстовый ответ «привет мир», когда мы нажимаем GET / route. Вообще говоря, мы можем проанализировать четыре этапа:

  1. Создание нового экспресс-приложения
  2. Создание нового маршрута
  3. Запуск HTTP-сервера по данному номеру порта
  4. Обращение с запросом, когда оно приходит

Создание нового экспресс-приложения

Оператор var app = express() создает для вас новое экспресс-приложение. Функция createApplication из файла lib/express.js является экспорт по умолчанию, который мы называем вызовом функции express()

Некоторые из важных бит:

// ...
var mixin = require('merge-descriptors');
var proto = require('./application');

// ...

function createApplication() {
  // This is the returned application variable, which we will get to later in the post. 
  //The important thing to remember is it's signtature of `function(req, res, next)`
  var app = function(req, res, next) {
    app.handle(req, res, next);
  };

  // ...

  // The `mixin` function assigns all the methods of `proto` as methods of `app`
  // One of the methods which it assigns is the `get` method which is used in the example
  mixin(app, proto, false);

 // ...

  return app;
}

Объект app, возвращенный из этой функции, является тем, который мы используем в нашем коде приложения. Метод app.get добавляется с помощью функции merge-descriptors libraries mixin, которая назначает методы, определенные в proto.

proto сам импортируется из lib/application.js.

Создание нового маршрута

Давайте теперь кратко рассмотрим код, который создает метод app.get, который мы используем в примере:

var slice = Array.prototype.slice;

// ...
/**
 * Delegate `.VERB(...)` calls to `router.VERB(...)`.
 */

// `methods` is an array of HTTP methods, (something like ['get','post',...])
methods.forEach(function(method){
  // This would be the app.get method signature
  app[method] = function(path){
    // some initialization code

    // create a route for the path inside the applications router
    var route = this._router.route(path);

    // call the handler with the second argument provided
    route[method].apply(route, slice.call(arguments, 1));

    // returns the `app` instance, so that methods can be chained
    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.<method>

Метод route() маршрутизаторов определяется в lib/router/index.js:

// proto is the prototype definition of the `_router` object
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 определен аналогично app.get в lib/router/route.js :

methods.forEach(function (method) {
  Route.prototype[method] = function () {
    // `flatten` converts embedded arrays, like [1,[2,3]], to 1-D arrays ([1,2,3])
    var handles = flatten(slice.call(arguments));

    for (var i = 0; i < handles.length; i++) {
      var handle = handles[i];
      
      // ...
      // For every handler provided to a route, a layer is created
      // and pushed into the routes stack
      var layer = Layer('/', {}, handle);

      // ...

      this.stack.push(layer);
    }

    return this;
  };
});

Каждый маршрут может иметь несколько обработчиков и создает Layer из каждого обработчика, который затем нажимает на стек.

Слои

И _router, и route используют тип объекта, называемый Layer. Мы можем понять, что делает слой, увидев его определение конструктора:

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

Каждый слой имеет путь, некоторые параметры и функцию, которую нужно обрабатывать. В случае нашего маршрутизатора эта функция — route.dispatch (мы перейдем к тому, что этот метод делает в более позднем разделе. Это что-то вроде передачи запроса на индивидуальный маршрут). В случае самого маршрута эта функция является фактической функцией обработчика, определенной в нашем примере кода.

Каждый уровень также имеет метод handle_request, который фактически выполняет функцию, переданную во время инициализации слоев.

Давайте вспомним, что происходит при создании маршрута с помощью метода <code class=»language-text»>app.get</code>:

  1. Маршрут создается под маршрутизатором приложений (this._router)
  2. Метод отправки маршрутов назначается как метод обработчика для слоя, и этот уровень переносится в стек маршрутизаторов.
  3. Сам обработчик запроса передается как метод обработчика на уровень, и этот слой переносится в стек маршрутов

В конце все ваши обработчики хранятся внутри экземпляра приложения как слои, находящиеся внутри стека маршрутов, методы отправки которых назначены уровням, находящимся внутри стека маршрутизаторов:

Обработка HTTP-запроса после его ввода занимает аналогичную часть, и мы доберемся до этого немного

Запуск HTTP-сервера

После настройки маршрутов сервер должен быть запущен. В нашем примере мы вызываем метод <code class=»language-text»>app.listen</code> с номером порта и функцией обратного вызова в качестве аргументов. Чтобы понять этот метод, мы можем увидеть 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-запроса

Теперь, когда мы знаем, что приложение является просто обычным старым обработчиком запросов, давайте рассмотрим HTTP-запрос, поскольку он делает это через экспресс-приложение и, наконец, попадает внутрь обработчика, который мы определили.

Из функции 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` is where we declared the route using `app.get`
  var router = this._router;

  // ... 

  // The request goes on to the `handle` method
  router.handle(req, res, done);
};

Метод router.handler определен в lib/router/index.js:

proto.handle = function handle(req, res, out) {
  var self = this;
  //...
  // self.stack is where we pushed all our layers when we called 
  var stack = self.stack;
  // ...
  next();

  function next(err) {
    // ...
    // Get the path name from the request.
    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;
      }
      // ... some more validations to check HTTP methods, headers, etc
    }

   // ... more validations 
   
    // process params parses the requests parameters... not important for now
    self.process_params(layer, paramcalled, req, res, function (err) {
      // ...

      if (route) {
        // once the params are done processing, the `layer.handle_request` method is called
        // It is called with the request, as well as this `next` function as well
        // this means that `next` will bbe called all over again once the current layer is handled
        // so requests will move on the the next layer when the `next` function is called again
        return layer.handle_request(req, res, next);
      }
      // ...
    });
  }
};

Короче говоря, функция router.handle проходит через все слои в своем стеке, пока не найдет тот, который исправляет путь запроса. Затем AIt вызывает метод handle_request слоев, который выполняет предопределенную функцию обработчиков слоев. Эта функция-обработчик — это метод отправки маршрутов, определенный в lib/router/route.js:

Route.prototype.dispatch = function dispatch(req, res, done) {
  var stack = this.stack;
  // ...
  next();

  function next(err) {
    // ...
    var layer = stack[idx++];

    // ... some validations and error checks
    layer.handle_request(req, res, next);
    // ...
  }
};

Подобно маршрутизатору, каждый маршрут проходит через все его слои и вызывает их методы handle_request, которые выполняют описанный выше метод обработчика, который в нашем случае является обработчиком запросов, который мы определили в нашем коде приложения. Наконец, HTTP-запрос входит в сферу нашего кода приложения.

Все остальное

Хотя мы видели суть того, как экспресс-библиотека заставляет ваш веб-сервер работать, тем больше, что он предоставляет вам. Мы пропустили все проверки работоспособности и проверки, сделанные до того, как запрос передан вашему обработчику, мы также не прошли все вспомогательные методы, которые поставляются с переменными запроса и ответа запроса и ответа. Наконец, одной из самых мощных функций Express является использование промежуточного программного обеспечения, которое может помочь с чем-либо от разбора тела запроса до полномасштабной проверки подлинности.

Надеюсь, этот пост помог вам понять важные аспекты исходного кода, которые вы можете использовать для понимания остальной части.

Если есть какие-либо библиотеки или рамки, чья внутренняя работа, которую вы чувствуете, заслуживает объяснения, сообщите мне в комментариях.

Добавить комментарий

Этот сайт использует Akismet для борьбы со спамом. Узнайте как обрабатываются ваши данные комментариев.

Авторизация
*
*
Регистрация
*
*
*
Генерация пароля