Webpack loaders для unified

Unified — это экосистема инструментов для обработки текстовых форматов через AST (Abstract Syntax Tree). Основные библиотеки в этой экосистеме — Remark для Markdown и Rehype для HTML. Для проектов на JavaScript и TypeScript важным аспектом является интеграция этих инструментов с Webpack, что позволяет обрабатывать контент как модули, автоматически превращая Markdown или HTML в готовый к использованию JSX, строки HTML или AST-объекты.


Основные принципы работы loaders

Webpack loader — это функция, которая получает содержимое файла и возвращает результат его трансформации. Для интеграции с unified создаются специализированные loaders, которые:

  1. Принимают исходный Markdown или HTML-файл.
  2. Пропускают содержимое через цепочку плагинов Remark или Rehype.
  3. Возвращают результат в формате, удобном для дальнейшего использования в проекте (например, как модуль JavaScript).

Ключевые моменты:

  • Синхронность и асинхронность: loaders могут работать как синхронно, так и асинхронно. Unified поддерживает асинхронные плагины, поэтому loader должен уметь обрабатывать промисы.
  • Поддержка source maps: для интеграции с Webpack source maps можно использовать this.callback внутри loader-а.
  • Кеширование: рекомендуется использовать this.cacheable() для оптимизации сборки.

Пример базового loader-а для Markdown через Remark

const remark = require('remark');
const remarkHtml = require('remark-html');

module.exports = function(source) {
  const callback = this.async();
  this.cacheable();

  remark()
    .use(remarkHtml)
    .process(source)
    .then(file => {
      callback(null, `export default ${JSON.stringify(String(file))};`);
    })
    .catch(err => {
      callback(err);
    });
};

Разбор кода:

  • this.async() создаёт асинхронный callback для loader-а.
  • this.cacheable() сообщает Webpack, что результат трансформации может быть закеширован.
  • remark().use(remarkHtml) превращает Markdown в HTML.
  • Результат экспортируется как ES-модуль (export default), чтобы его можно было импортировать в коде.

Loader для Rehype и HTML

Если проект работает с HTML или требует дополнительные преобразования после Markdown, используют Rehype:

const rehype = require('rehype');
const rehypeStringify = require('rehype-stringify');

module.exports = function(source) {
  const callback = this.async();
  this.cacheable();

  rehype()
    .data('settings', { fragment: true })
    .use(rehypeStringify)
    .process(source)
    .then(file => {
      callback(null, `export default ${JSON.stringify(String(file))};`);
    })
    .catch(err => callback(err));
};

Особенности:

  • fragment: true позволяет обрабатывать HTML-фрагменты без корневого тега <html>.
  • Rehype поддерживает множество плагинов для трансформации HTML, например, для добавления классов, оптимизации изображений или валидации структуры.

Цепочки loaders и интеграция с Webpack

Для сложных проектов часто создают цепочку loaders:

  1. Markdown loader (Remark + плагины) →
  2. Rehype loader (HTML-плагины) →
  3. Babel loader (если нужен JSX)

Пример конфигурации:

module.exports = {
  module: {
    rules: [
      {
        test: /\.md$/,
        use: [
          'babel-loader',
          path.resolve(__dirname, 'loaders/rehype-loader.js'),
          path.resolve(__dirname, 'loaders/remark-loader.js'),
        ],
      },
    ],
  },
};

Последовательность важна: сначала обрабатывается исходный Markdown через Remark, затем результат конвертируется Rehype, и наконец через Babel преобразуется в модуль JS/JSX.


Работа с плагинами Remark и Rehype

Плагины — основной механизм расширения функциональности:

  • Remark: remark-gfm, remark-slug, remark-autolink-headings.
  • Rehype: rehype-highlight, rehype-format, rehype-react (для преобразования в JSX).

Пример подключения плагина для подсветки синтаксиса:

const remark = require('remark');
const remarkHtml = require('remark-html');
const remarkGfm = require('remark-gfm');
const rehypeHighlight = require('rehype-highlight');

module.exports = function(source) {
  const callback = this.async();
  this.cacheable();

  remark()
    .use(remarkGfm)
    .use(remarkHtml)
    .process(source)
    .then(file => {
      return rehype()
        .use(rehypeHighlight)
        .process(String(file));
    })
    .then(file => {
      callback(null, `export default ${JSON.stringify(String(file))};`);
    })
    .catch(err => callback(err));
};

Особенности интеграции:

  • Remark и Rehype можно комбинировать, передавая данные между ними.
  • Для оптимизации производительности стоит использовать минимальное количество плагинов и асинхронные версии, если они доступны.
  • В больших проектах можно вынести loader-и в отдельные npm-пакеты, чтобы их легко использовать в нескольких сборках.

Рекомендации по оптимизации

  1. Кеширование: использовать this.cacheable() и кешировать результат process при неизменных файлах.
  2. Асинхронная обработка: избегать блокирующих операций внутри loader-а.
  3. Минификация: можно подключать плагины для минификации HTML на этапе Rehype.
  4. Разделение цепочки: при сложных проектах лучше создавать отдельные loader-и для Remark и Rehype, чтобы проще тестировать и поддерживать.

Поддержка TypeScript и JSX

Для использования с React или Vue:

  • Markdown → Remark → Rehype → rehype-react → JSX.
  • Loader возвращает готовый React-элемент, который можно сразу рендерить.

Пример интеграции с React через rehype-react:

const rehypeReact = require('rehype-react');
const React = require('react');

rehype()
  .use(rehypeReact, { createElement: React.createElement })
  .processSync('<h1>Hello</h1>')
  .result; // React элемент

Такой подход позволяет Markdown-файлы хранить как контент и динамически рендерить их в приложении, не превращая их в статические строки HTML.


Итоговая структура проекта с loader-ами

src/
  content/
    post1.md
  loaders/
    remark-loader.js
    rehype-loader.js
  components/
    MarkdownRenderer.jsx
webpack.config.js

Markdown-файлы становятся полноценными модулями, которые можно импортировать и использовать с любыми React/Vue компонентами.


Хотите, я могу подготовить следующую часть с реальными примерами цепочек Remark → Rehype → rehype-react для динамического рендеринга React компонентов с продвинутой обработкой плагинов и настроек Webpack?