Если вы занимались разработкой для платформы node.js, то вы, наверняка, слышали об express.js. Это — один из самых популярных легковесных фреймворков, используемых при создании веб-приложений для node.
Автор материала, перевод которого мы сегодня публикуем, предлагает изучить особенности внутреннего устройства фреймворка express через анализ его исходного кода и рассмотрение примера его использования. Он полагает, что изучение механизмов, лежащих в основе популярных опенсорсных библиотек, способствует более глубокому их пониманию, снимает с них завесу «таинственности» и помогает создавать более качественные приложения на их основе.
Возможно, вы сочтёте удобным держать под рукой исходный код express в процессе чтения этого материала. Здесь использована эта версия. Вы вполне можете читать эту статью и не открывая код express, так как здесь, везде где это уместно, даются фрагменты кода этой библиотеки. В тех местах, где код сокращён, используются комментарии вида
Для начала взглянем на традиционный в деле освоения новых компьютерных технологий «Hello World!»-пример. Его можно найти на официальном сайте фреймворка, он послужит отправной точкой в наших исследованиях.
Этот код запускает новый HTTP-сервер на порту 3000 и отправляет ответ
Команда
Объект
Взглянем теперь на код, который ответственен за создание метода
Интересно отметить, что, помимо семантических особенностей, все методы, реализующие действия HTTP, вроде
Хотя у вышеприведённой функции 2 аргумента, она похожа на функцию
Если в двух словах, то
Метод маршрутизатора
Неудивительно то, что объявление метода
У каждого маршрута может быть несколько обработчиков, на основе каждого обработчика конструируется переменная типа
И
При создании объектов типа
У каждого объекта типа
Вспомним, что происходит при создании маршрута с использованием метода
В итоге все обработчики хранятся внутри экземпляра
Объекты типа Layer в стеке маршрутизатора и в стеке маршрутов
Поступающие HTTP-запросы обрабатываются в соответствии с этой логикой. Мы поговорим о них ниже.
После настройки маршрутов надо запустить сервер. В нашем примере мы обращаемся к методу
Похоже, что
После понимания того, что, в итоге, всё, что даёт нам express.js, может быть сведено к весьма интеллектуальной функции-обработчику, фреймворк выглядит уже не таким сложным и таинственным, как раньше.
Теперь, когда мы знаем, что
Сначала запрос поступает в функцию
Потом он идёт в метод
Метод
Если описать происходящее в двух словах, то функция
Так же, как и в случае с маршрутизатором, при обработке каждого маршрута осуществляется перебор слоёв, которые есть у этого маршрута, и вызов их методов
Здесь, наконец, HTTP-запрос попадает в область кода нашего приложения.
Путь запроса в приложении express
Здесь мы рассмотрели лишь основные механизмы библиотеки express.js, те, которые ответственны за работу веб-сервера, но эта библиотека обладает и многими другими возможностями. Мы не останавливались на проверках, которые проходят запросы до поступления их в обработчики, мы не говорили о вспомогательных методах, которые доступны при работе с переменными
Надеемся, этот материал помог вам разобраться в основных особенностях устройства express, и теперь вы, при необходимости, сможете понять всё остальное, самостоятельно проанализировав интересующие вас части исходного кода этой библиотеки.
Уважаемые читатели! Пользуетесь ли вы express.js?
Автор материала, перевод которого мы сегодня публикуем, предлагает изучить особенности внутреннего устройства фреймворка 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?
Sirion
Пользуюсь. Так вышло, что это наиболее часто используемый мной сервер.