До сих пор мы пользовались фреймворком Express как чёрным ящиком. Что, кстати, характеризует его как хорошую абстракцию. Но помимо очевидного внешнего поведения у микрофреймворков есть ещё одно интересное свойство. Давайте зададим себе вопрос: какими качествами должен обладать хороший фреймворк?
Самое очевидное и важное — это концептуальный дизайн, который определяет то, с какими абстракциями мы имеем дело. Бывают фреймворки, в которых абстракции не очень удачные, и разработка на них обрастает случайной сложностью. С другой стороны, в вебе более-менее выработан единый подход к организации серверных фреймворков. Доминирующей является архитектура MVC
, с которой мы и работаем. Controller
- контроллеры это наши обработчики, View
- это шаблоны, а Model
- это наши сущности и бизнес-логика.
Замечание. Исторически MVC, который принят в вебе, сильно отличается от первоначального MVC, основное применение которого было толстые клиенты. В литературе можно встретить обозначение "MVC v2" для веб версии.
Если предположить, что с дизайном всё в порядке, то на сцену выходят более утилитарные качества:
- Гибкость
- Расширяемость
- Модульность
В этом месте мы поговорим о расширяемости, которая в свою очередь приводит к модульности. Рассмотрим самый простой пример - функции. Как можно расширить поведение функции?
Wrapping
const f1 = x => x + 5;
const f2 = x => f1(x * 2);
const f3 = x => f2(x - 10) - 3;
f3(20); // ((((20 - 10) * 2) + 5) - 3) = 22
const nextF = (/* args */) => {
// preprocessing
const result = prevF(/* updatedArgs */);
// afterprocessing
return /* newResult */;
};
Нет ничего проще, чем расширять поведение функции. Нужно написать новую функцию, в которой используется первоначальная. Единственное условие, которое нужно соблюсти, это совпадение входов этих функций (количество и тип аргументов) и выходов (тип выхода). В таком случае код, использующий вашу обёрнутую функцию, даже не сможет догадаться о том, что она обёрнута, ну а главное, что его не нужно переписывать, ведь интерфейс функции не поменялся, хотя и появилось новое поведение. Такой способ так же называют декорированием, и в справочниках по шаблонам проектирования описывают как "паттерн декоратор".
По похожей идее устроен Express, а точнее connect
, который является ядром микрофреймворка Express.
Connect
import Connect from 'connect';
const app = new Connect();
const logger = morgan('combined');
app.use(methodOverride('_method'));
app.use(logger);
app.use(bodyParser.urlencoded({ extended: false }));
// respond to all requests
app.use((req, res) => {
res.end('Hello from Connect!');
});
Middleware
Connect
представляет из себя механизм, который расширяется функциями, называемыми middleware
. Каждый раз, когда мы используем use
, очередная middleware
добавляется в общую очередь. В конечном счёте получается объект, наполненный мидлварами. Каждый запрос, отправляемый на обработку в connect
, проходит через цепочку этих middleware
пока не наткнётся на терминальную мидлвару.
В свою очередь каждая мидлвара принимает на вход три параметра: request
, response
и next
. Она может поменять их и в конце должна вызвать next
для передачи управления следующей по списку мидлваре. В этом и заключается вся мощь микрофреймворков. Удачный дизайн позволяет легко разбивать систему на модули-мидлвары и расширять за счёт мидлвар, которые, в большом количестве, пишут сторонние разработчики.
app.use((req, res, next) => {
req.newProperty = 'hello from my middleware';
next();
});
// вызов methodOverride возвращает функцию вида (req, res, next) => ...
app.use(methodOverride('_method'));
В Connect нет ничего кроме метода use
добавляющего очередную мидлвару в стек.
Mount middleware
Самое интересное в Connect, что обработчики конкретных маршрутов — это тоже, всего-навсего, мидлвары. Их особенностью является привязка к конкретному маршруту, в отличие от мидлвар, которые выполняются для всех запросов.
app.use('/foo', (req, res, next) => {
// req.url starts with "/foo"
next();
});
app.use('/bar', (req, res, next) => {
// req.url starts with "/bar"
next();
});
Такие мидлвары позволяют реализовывать базовый роутинг без привязки к конкретному глаголу http и без поддержки динамических маршрутов. В Express роутинг реализован без привязки к Mount Middlewares.
Terminate
Но далеко не всегда мы хотим двигаться вглубь. Более того, в какой-то момент одна из мидлвар должна взять обработку на себя.
// connect
app.use((req, res) => {
res.end('Hello from Connect!');
});
// express
app.use((req, res) => {
res.send('Hello from Express!');
});
У такого поведения, когда есть цепочка функций и любая из них в процессе обработки может принять решение остановки цепочки и возврата ответа, есть имя. Такие цепочки называют chain responsibility
, и это тоже паттерн.
Дополнительные материалы
Остались вопросы? Задайте их в разделе «Обсуждение»
Вам ответят команда поддержки Хекслета или другие студенты