Middleware паттерн в unified

Библиотека unified предоставляет гибкую архитектуру для работы с деревьями синтаксиса (AST) в экосистеме JavaScript. Центральной концепцией в этой архитектуре является middleware-подход, который позволяет строить конвейеры обработки данных в виде цепочки плагинов. Каждый плагин в этой цепочке выступает как middleware, изменяя или анализируя AST и передавая результат следующему звену.


Архитектура middleware

В основе подхода лежит идея цепочки обработки:

  1. Parser – преобразует исходный контент (например, Markdown) в дерево синтаксиса.
  2. Transformer – набор функций, которые последовательно изменяют или анализируют AST.
  3. Compiler – преобразует AST обратно в нужный формат, например, HTML.

Каждый трансформер можно рассматривать как middleware: он принимает AST, модифицирует его и передает дальше.

import { unified } from 'unified';
import remarkParse from 'remark-parse';
import remarkStringify from 'remark-stringify';

const processor = unified()
  .use(remarkParse)         // Парсер Markdown в AST
  .use(() => tree => {      // Простой middleware-трансформер
    tree.children.push({
      type: 'paragraph',
      children: [{ type: 'text', value: 'Добавленный текст.' }]
    });
  })
  .use(remarkStringify);    // Компилятор AST обратно в Markdown

Ключевая идея: каждый .use() регистрирует middleware, который будет вызван при обработке документа.


Функциональный паттерн middleware

Middleware в unified реализуются как функции с одной из двух сигнатур:

  1. Трансформер с доступом к AST:

    function transformer(tree, file) {
      // tree — AST документа
      // file — объект VFile с метаданными
    }
  2. Фабрика middleware (с настройками):

    function plugin(options) {
      return function transformer(tree, file) {
        // использование options для конфигурации
      }
    }

Это позволяет создавать параметризованные плагины, которые можно переиспользовать в разных проектах.


Асинхронные middleware

Unified поддерживает асинхронные плагины, что особенно важно при интеграции с внешними сервисами или при выполнении операций ввода-вывода.

import { unified } from 'unified';
import remarkParse from 'remark-parse';

async function asyncTransformer(tree, file) {
  await someAsyncOperation();
  tree.children.push({ type: 'paragraph', children: [{ type: 'text', value: 'Асинхронный текст' }] });
}

const processor = unified()
  .use(remarkParse)
  .use(asyncTransformer);

Middleware могут быть смешанными: одни синхронные, другие асинхронные. Unified корректно обрабатывает их в единой цепочке.


Контекст выполнения и объект file

Каждое middleware получает объект file (VFile), который содержит:

  • file.path — путь к исходному файлу.
  • file.contents — исходный текст.
  • file.data — объект для хранения промежуточных данных между middleware.

Пример использования:

function logTransformer(tree, file) {
  console.log('Обработка файла:', file.path);
  file.data.processed = true;
}

Middleware могут использовать file.data для передачи информации между собой, формируя сквозной контекст обработки.


Комбинация remark и rehype

Unified позволяет объединять разные экосистемы через конвертеры:

  • remark – Markdown → AST.
  • rehype – HTML → AST.
  • remark-rehype – конвертер Markdown AST в HTML AST.

Middleware цепочка может выглядеть так:

import remarkParse from 'remark-parse';
import remarkRehype from 'remark-rehype';
import rehypeStringify from 'rehype-stringify';

const processor = unified()
  .use(remarkParse)
  .use(myRemarkMiddleware)
  .use(remarkRehype)
  .use(myRehypeMiddleware)
  .use(rehypeStringify);

Важно понимать, что middleware, работающие с AST Markdown, не смогут напрямую работать с AST HTML, и наоборот. Конвертеры выступают границей между экосистемами.


Стратегии построения цепочек middleware

  1. Последовательная трансформация – каждый middleware вносит небольшие изменения в AST.
  2. Фильтрация и удаление узлов – middleware может модифицировать или удалять определённые узлы, формируя новый AST.
  3. Аналитика и метаданные – middleware может собирать статистику о документе, например, количество заголовков или ссылок.
  4. Условная активация – плагины могут применяться только к определённым файлам или условиям.

Пример условной активации:

function conditionalPlugin(options) {
  return (tree, file) => {
    if (file.path.endsWith('.md')) {
      tree.children.push({ type: 'paragraph', children: [{ type: 'text', value: options.text }] });
    }
  };
}

Отладка и визуализация AST

Middleware-паттерн упрощает пошаговую отладку:

  • Логирование структуры AST после каждого middleware.
  • Использование визуализаторов (например, unist-util-inspect) для проверки промежуточного состояния.
  • Добавление меток через file.data для отслеживания изменений.
import { inspect } from 'unist-util-inspect';

function debugTransformer(tree, file) {
  console.log(inspect(tree));
}

Взаимодействие с внешними системами

Middleware может интегрироваться с API, базами данных или другими сервисами. Асинхронные плагины позволяют:

  • Загружать данные для вставки в документ.
  • Проверять корректность ссылок.
  • Генерировать динамический контент на лету.
async function fetchDataMiddleware(tree) {
  const data = await fetchSomeData();
  tree.children.push({ type: 'paragraph', children: [{ type: 'text', value: data }] });
}

Итоговые принципы middleware в unified

  • Middleware обрабатывает AST по цепочке.
  • Поддерживаются синхронные и асинхронные плагины.
  • Используется объект file для передачи данных между плагинами.
  • Конвертеры связывают разные экосистемы (remark ↔︎ rehype).
  • Конфигурация через фабрики позволяет создавать универсальные и переиспользуемые плагины.

Middleware-подход обеспечивает модульность, масштабируемость и чистоту архитектуры, что особенно важно при построении сложных систем обработки текстового контента.