
Как работает 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. Вообще говоря, мы можем проанализировать четыре этапа:
- Создание нового экспресс-приложения
- Создание нового маршрута
- Запуск HTTP-сервера по данному номеру порта
- Обращение с запросом, когда оно приходит
Создание нового экспресс-приложения
Оператор 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>:
- Маршрут создается под маршрутизатором приложений (
this._router
) - Метод отправки маршрутов назначается как метод обработчика для слоя, и этот уровень переносится в стек маршрутизаторов.
- Сам обработчик запроса передается как метод обработчика на уровень, и этот слой переносится в стек маршрутов
В конце все ваши обработчики хранятся внутри экземпляра приложения как слои, находящиеся внутри стека маршрутов, методы отправки которых назначены уровням, находящимся внутри стека маршрутизаторов:
Обработка 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 является использование промежуточного программного обеспечения, которое может помочь с чем-либо от разбора тела запроса до полномасштабной проверки подлинности.
Надеюсь, этот пост помог вам понять важные аспекты исходного кода, которые вы можете использовать для понимания остальной части.
Если есть какие-либо библиотеки или рамки, чья внутренняя работа, которую вы чувствуете, заслуживает объяснения, сообщите мне в комментариях.