Если вы занимались разработкой для платформы node.js, то вы, наверняка, слышали об express.js. Это — один из самых популярных легковесных фреймворков, используемых при создании веб-приложений для node.
![](https://habrastorage.org/webt/bs/np/1r/bsnp1rphnfiib_l1tjzaauvcs48.jpeg)
Автор материала, перевод которого мы сегодня публикуем, предлагает изучить особенности внутреннего устройства фреймворка express через анализ его исходного кода и рассмотрение примера его использования. Он полагает, что изучение механизмов, лежащих в основе популярных опенсорсных библиотек, способствует более глубокому их пониманию, снимает с них завесу «таинственности» и помогает создавать более качественные приложения на их основе.
Возможно, вы сочтёте удобным держать под рукой исходный код express в процессе чтения этого материала. Здесь использована эта версия. Вы вполне можете читать эту статью и не открывая код express, так как здесь, везде где это уместно, даются фрагменты кода этой библиотеки. В тех местах, где код сокращён, используются комментарии вида
Для начала взглянем на традиционный в деле освоения новых компьютерных технологий «Hello World!»-пример. Его можно найти на официальном сайте фреймворка, он послужит отправной точкой в наших исследованиях.
Этот код запускает новый HTTP-сервер на порту 3000 и отправляет ответ
Команда
Объект
Взглянем теперь на код, который ответственен за создание метода
Интересно отметить, что, помимо семантических особенностей, все методы, реализующие действия HTTP, вроде
Хотя у вышеприведённой функции 2 аргумента, она похожа на функцию
Если в двух словах, то
Метод маршрутизатора
Неудивительно то, что объявление метода
У каждого маршрута может быть несколько обработчиков, на основе каждого обработчика конструируется переменная типа
И
При создании объектов типа
У каждого объекта типа
Вспомним, что происходит при создании маршрута с использованием метода
В итоге все обработчики хранятся внутри экземпляра
![](https://habrastorage.org/getpro/habr/post_images/4a9/e8c/5b0/4a9e8c5b056ef1e8ce1e36503ab8c0f0.png)
Объекты типа Layer в стеке маршрутизатора и в стеке маршрутов
Поступающие HTTP-запросы обрабатываются в соответствии с этой логикой. Мы поговорим о них ниже.
После настройки маршрутов надо запустить сервер. В нашем примере мы обращаемся к методу
Похоже, что
После понимания того, что, в итоге, всё, что даёт нам express.js, может быть сведено к весьма интеллектуальной функции-обработчику, фреймворк выглядит уже не таким сложным и таинственным, как раньше.
Теперь, когда мы знаем, что
Сначала запрос поступает в функцию
Потом он идёт в метод
Метод
Если описать происходящее в двух словах, то функция
Так же, как и в случае с маршрутизатором, при обработке каждого маршрута осуществляется перебор слоёв, которые есть у этого маршрута, и вызов их методов
Здесь, наконец, HTTP-запрос попадает в область кода нашего приложения.
![](https://habrastorage.org/getpro/habr/post_images/afb/0d1/5fb/afb0d15fbe71236ed615f88fe61bf062.png)
Путь запроса в приложении express
Здесь мы рассмотрели лишь основные механизмы библиотеки express.js, те, которые ответственны за работу веб-сервера, но эта библиотека обладает и многими другими возможностями. Мы не останавливались на проверках, которые проходят запросы до поступления их в обработчики, мы не говорили о вспомогательных методах, которые доступны при работе с переменными
Надеемся, этот материал помог вам разобраться в основных особенностях устройства express, и теперь вы, при необходимости, сможете понять всё остальное, самостоятельно проанализировав интересующие вас части исходного кода этой библиотеки.
Уважаемые читатели! Пользуетесь ли вы express.js?
![](https://habrastorage.org/webt/bs/np/1r/bsnp1rphnfiib_l1tjzaauvcs48.jpeg)
Автор материала, перевод которого мы сегодня публикуем, предлагает изучить особенности внутреннего устройства фреймворка 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
, которые находятся в стеке маршрутизатора:![](https://habrastorage.org/getpro/habr/post_images/4a9/e8c/5b0/4a9e8c5b056ef1e8ce1e36503ab8c0f0.png)
Объекты типа 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-запрос попадает в область кода нашего приложения.
![](https://habrastorage.org/getpro/habr/post_images/afb/0d1/5fb/afb0d15fbe71236ed615f88fe61bf062.png)
Путь запроса в приложении express
Итоги
Здесь мы рассмотрели лишь основные механизмы библиотеки express.js, те, которые ответственны за работу веб-сервера, но эта библиотека обладает и многими другими возможностями. Мы не останавливались на проверках, которые проходят запросы до поступления их в обработчики, мы не говорили о вспомогательных методах, которые доступны при работе с переменными
res
и req
. И, наконец, мы не затрагивали одну из наиболее мощных возможностей express. Она заключается в использовании промежуточного программного обеспечения, которое может быть направлено на решение практически любых задача — от разбора запросов до реализации полноценной системы аутентификации.Надеемся, этот материал помог вам разобраться в основных особенностях устройства express, и теперь вы, при необходимости, сможете понять всё остальное, самостоятельно проанализировав интересующие вас части исходного кода этой библиотеки.
Уважаемые читатели! Пользуетесь ли вы express.js?
![](https://habrastorage.org/files/1ba/550/d25/1ba550d25e8846ce8805de564da6aa63.png)
Sirion
Пользуюсь. Так вышло, что это наиболее часто используемый мной сервер.