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, третий на инфраструктуру
(база, конфиг). Плагин умеет четыре вещи, и в этом вся его сила:
- регистрировать роуты;
- регистрировать другие плагины (вложенность);
- принимать
options(переиспользуемость с разной конфигурацией); - создавать область инкапсуляции с собственными хуками и префиксом роутинга.
Четвёртый пункт, главный, и ему посвящён § 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
onRequestlogTiming · локально
- decorate
- hook
preHandlerrequireAuth · локально
- hook
- plugin config · decorate
Зачем такая изоляция:
- нет глобального состояния: плагин
articlesне может случайно затоптать хук плагинаusers, они в разных областях; - локальность рассуждения: глядя на плагин, ты видишь ровно те хуки и декораторы, что действуют внутри него, и не держишь в голове весь глобальный конвейер;
- переиспользуемость: плагин, не зависящий от глобального состояния, можно поднять в другом приложении как есть.
дальше после входа
Урок целиком открыт всем, кто вошёл. Войди по своему email, чтобы читать дальше.
Вход пока по приглашению. Если хочешь учиться, напиши Vassiliy.