Раздел 8 · Frameworks и Libs

Fastify: плагины и инкапсуляция

middle~30 мин

без входаВойди, чтобы отмечать прогресс, вести заметки и решать задачи в редакторе. войти

Fastify: плагины и инкапсуляция

В уроке 01 прозвучал тезис: Fastify это не “Express, только быстрее”, а эффективная система плагинов над HTTP-сервером. Сейчас разберём, что это значит. Плагин и инкапсуляция, сердце Fastify: всё приложение, от одного роута до боевого монолита, собирается из плагинов. Поймёшь эту модель, и остальное (декораторы, хуки, схемы из следующих уроков) ляжет на неё без усилий.

1. Что такое плагин

Плагин это функция, которой Fastify передаёт инстанс приложения, чтобы она что-нибудь на нём зарегистрировала: роуты, другие плагины, декораторы, хуки. Регистрируем плагин через app.register.

import Fastify from 'fastify';

const app = Fastify({ logger: true });

// плагин в классическом колбек-стиле
app.register((instance, options, done) => {
  instance.get('/ping', async () => ({ pong: true }));
  done(); // без done плагин не завершит регистрацию
});

await app.listen({ port: 3000 });

Сигнатура (instance, options, done):

  • instance это инстанс Fastify, переданный плагину. Важно: это не тот же объект, что внешний app, а его дочерняя копия. Почему, разберём в § 3.
  • options это объект конфигурации, который передали вторым аргументом в register (см. § 2).
  • done это колбек завершения. Пока ты его не вызвал, Fastify считает, что плагин ещё грузится, и не переходит к следующему. done(error), чтобы сообщить об ошибке в классическом Node-стиле.

1.1 Async-плагин: так привычнее

Колбек done легко забыть, и тогда приложение зависнет при запуске, молча ожидая завершения плагина. Поэтому идиоматичнее async-форма: возвращаем промис, а done Fastify подставит сам.

app.register(async (instance, options) => {
  instance.get('/ping', async () => ({ pong: true }));
  // нет done: завершение это просто конец async-функции
});

Ошибку в async-плагине бросаем через throw, без done(error). Дальше в уроке используем только async-форму.

1.2 Зачем вообще плагины

Можно ведь повесить роуты прямо на app, без всякого register. Можно, и для трёх роутов так и делают. Но как только приложение растёт, плагин становится единицей, в которой удобно держать связную часть системы: один плагин на домен articles, другой на auth, третий на инфраструктуру (база, конфиг). Плагин умеет четыре вещи, и в этом вся его сила:

  1. регистрировать роуты;
  2. регистрировать другие плагины (вложенность);
  3. принимать options (переиспользуемость с разной конфигурацией);
  4. создавать область инкапсуляции с собственными хуками и префиксом роутинга.

Четвёртый пункт, главный, и ему посвящён § 3.

2. Options и префикс

Вторым аргументом register принимает объект, который придёт плагину в options. Это и есть способ переиспользовать один плагин с разной настройкой.

const ratePlugin = async (instance, options: { max: number }) => {
  instance.addHook('onRequest', async (request) => {
    request.log.info({ max: options.max }, 'rate check');
  });
};

app.register(ratePlugin, { max: 100 });

Особое поле options, prefix: оно сдвигает все роуты плагина под общий префикс пути.

app.register(
  async (instance) => {
    instance.get('/', async () => ({ list: [] })); // станет GET /articles
    instance.get('/:id', async () => ({ one: true })); // GET /articles/:id
  },
  { prefix: '/articles' },
);

Это базовый приём версионирования и доменной раскладки: плагин v1 под { prefix: '/v1' }, плагин v2 под { prefix: '/v2' }, оба со своими роутами и хуками, не пересекаясь.

3. Инкапсуляция: главная идея Fastify

Вот тот самый механизм, ради которого всё затевалось. Когда ты вызываешь register, Fastify создаёт новую область: дочерний инстанс, который наследует всё от родителя, но добавленное внутри него (декораторы и хуки) наружу не видно.

const app = Fastify();

app.register(async (instance) => {
  // этот хук виден только внутри области и её потомкам
  instance.addHook('onRequest', async () => {
    /* считаем что-то локальное */
  });
  instance.get('/inside', async () => ({ ok: true }));
});

app.get('/outside', async () => ({ ok: true }));
// onRequest-хук выше на /outside НЕ сработает: он заперт в своей области

Запрос на /inside пройдёт через хук, запрос на /outside, нет. Хук привязан к области, а не к глобальному списку. Сравни с Express из урока 01: там app.use(middleware) кладёт обработчик в один общий конвейер, и он крутится на каждом запросе, хочешь ты того или нет. У Fastify сквозная логика по умолчанию локальна: добавил хук в области articles, и он живёт только там.

3.1 Дерево областей

Области вкладываются: плагин внутри плагина создаёт область внутри области. Получается дерево, где наследование идёт сверху вниз, а видимость локального снизу не поднимается. Дочерняя область видит всё, что есть у предков, плюс своё. Родитель не видит того, что объявлено в потомке.

Покрути модель: кликни область и посмотри, какие декораторы и хуки тебе из неё доступны. Декоратор articleService объявлен локально в articles, и из users его не видно. А вот db, особый случай, о нём § 4.

Дерево областей и инкапсуляция
дерево плагинов (кликни область)
    • plugin config · decorate config · fp
      • decorate articleService · локально
      • hook onRequest logTiming · локально
      • hook preHandler requireAuth · локально
видно из области app

    Зачем такая изоляция:

    • нет глобального состояния: плагин articles не может случайно затоптать хук плагина users, они в разных областях;
    • локальность рассуждения: глядя на плагин, ты видишь ровно те хуки и декораторы, что действуют внутри него, и не держишь в голове весь глобальный конвейер;
    • переиспользуемость: плагин, не зависящий от глобального состояния, можно поднять в другом приложении как есть.

    дальше после входа

    Урок целиком открыт всем, кто вошёл. Войди по своему email, чтобы читать дальше.

    Вход пока по приглашению. Если хочешь учиться, напиши Vassiliy.