Хабрахабр

[Перевод] Особенности работы и внутреннего устройства express.js

Если вы занимались разработкой для платформы 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 /. Если не вдаваться в подробности, то можно выделить четыре стадии происходящего, которые мы можем проанализировать:

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

Создание нового приложения express

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

// ...
var mixin = require('merge-descriptors');
var proto = require('./application'); // ... function createApplication() ; // ... // Функция `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:

  1. В маршрутизаторе приложения (this._router) создаётся маршрут.
  2. Метод маршрута dispatch назначается в качестве метода-обработчика соответствующего объекта Layer, и этот объект помещают в стек маршрутизатора.
  3. Обработчик запроса передаётся объекту 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?

Теги
Показать больше

Похожие статьи

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

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть