Зарегистрируйтесь, чтобы продолжить обучение

Middlewares JS: Express

До сих пор мы пользовались фреймворком Express как чёрным ящиком. Что, кстати, характеризует его как хорошую абстракцию. Но помимо очевидного внешнего поведения у микрофреймворков есть ещё одно интересное свойство. Давайте зададим себе вопрос: какими качествами должен обладать хороший фреймворк?

Самое очевидное и важное — это концептуальный дизайн, который определяет то, с какими абстракциями мы имеем дело. Бывают фреймворки, в которых абстракции не очень удачные, и разработка на них обрастает случайной сложностью. С другой стороны, в вебе более-менее выработан единый подход к организации серверных фреймворков. Доминирующей является архитектура MVC, с которой мы и работаем. Controller - контроллеры это наши обработчики, View - это шаблоны, а Model - это наши сущности и бизнес-логика.

mvc

Замечание. Исторически 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 пока не наткнётся на терминальную мидлвару.

pipline

В свою очередь каждая мидлвара принимает на вход три параметра: 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, и это тоже паттерн.


Дополнительные материалы

  1. Connect

Аватары экспертов Хекслета

Остались вопросы? Задайте их в разделе «Обсуждение»

Вам ответят команда поддержки Хекслета или другие студенты

Открыть доступ

Курсы программирования для новичков и опытных разработчиков. Начните обучение бесплатно

  • 130 курсов, 2000+ часов теории
  • 1000 практических заданий в браузере
  • 360 000 студентов
Отправляя форму, вы принимаете «Соглашение об обработке персональных данных» и условия «Оферты», а также соглашаетесь с «Условиями использования»

Наши выпускники работают в компаниях:

Логотип компании Альфа Банк
Логотип компании Aviasales
Логотип компании Yandex
Логотип компании Tinkoff

Используйте Хекслет по-максимуму!

  • Задавайте вопросы по уроку
  • Проверяйте знания в квизах
  • Проходите практику прямо в браузере
  • Отслеживайте свой прогресс

Зарегистрируйтесь или войдите в свой аккаунт

Отправляя форму, вы принимаете «Соглашение об обработке персональных данных» и условия «Оферты», а также соглашаетесь с «Условиями использования»