Привет, Хабр! Меня зовут Алексей Сингур, я — фронтенд-разработчик в проекте KICS (Kaspersky Industrial CyberSecurity) for Networks «Лаборатории Касперского». Если коротко, то наш продукт защищает промышленные инфраструктуры и сети от киберугроз: анализирует трафик для выявления отклонений и обнаружения признаков сетевых атак, чтобы обеспечивать предприятию непрерывность процессов.

image

Одной из фичей KICS for Networks является генерация отчетов о сканировании инфраструктуры в формате PDF. При разработке этой фичи пришлось погрузиться в вопрос верстки и рендеринга PDF на Node.js. Речь пойдет об использовании для этих целей библиотеки React-pdf (в нашем проекте мы пока используем версию 2.1.1.), которая может показаться весьма экзотичной, если судить по количеству статей и отзывов в Интернете :)

Пост будет полезен веб-разработчикам для расширения кругозора в области инструментов рендеринга PDF, а также заинтересованным в генерации PDF-документов на стороне клиента или сервера.

1. Обзор библиотеки


Итак, давайте рассмотрим этого зверя поближе. Согласно статье о шагах рендеринга в документации, React-pdf стоит на двух китах:
  • Yoga — для стилизации и расположения контента на страницах
  • Pdfkit — для рендеринга PDF в среде Node.js или браузере

Что, конечно же, накладывает свои ограничения. Yoga накладывает ограничения на позиционирование элементов на странице, а компоненты из React-pdf внутри себя имеют вызовы Pdfkit, что и позволяет им отрендериться в документе, поэтому можно использовать только предоставленные библиотекой компоненты, а не привычный HTML. Опять же, если вы соберете из имеющихся примитивов свои React-компоненты, то можете беспрепятственно их использовать.

В принципе, библиотека предоставляет все необходимые компоненты-примитивы для успешной верстки PDF-документа, что позволяет довольно гибко и удобно создавать различные шаблоны. Вот некоторые примеры страниц PDF-отчета, который является результатом работы с React-pdf.
image

2. Причины использования и архитектурные решения


Эту библиотеку мы затянули в проект для рендеринга PDF-отчетов. Предвосхищая вопросы, почему просто не поднять headless-браузер и не распечатать html в pdf, отвечу, что это ограничения со стороны бизнеса, которые как раз и подтолкнули нас на поиск сторонних библиотек. Из не столь широкого выбора React-библиотек для рендеринга PDF наш выбор пал на React-pdf, потому что она имеет убедительные 11.8k звезд на Github (что говорит о вполне живом сообществе и актуальной поддержке — latest commit на момент написания статьи), предоставляет вполне удобные React-компоненты для сбора шаблонов страниц и возможности стилизации этих компонентов. Более подробное описание выделенных нами преимуществ данной библиотеки приведено ниже. Опять же, как ограничения (а все сторонние библиотеки накладывают какие-то ограничения), React-pdf не переваривает обычный HTML, только предоставленные самой библиотекой примитивы, и также знает далеко не все CSS-свойства, что иногда заставляет поприседать.

React-pdf позволяет отрендерить отчет на стороне сервера, что открывает возможность встроить его как отдельное апи для рендеринга PDF-документов в свое приложение.

Так как рендеринг PDF не подразумевает взаимодействие с пользователем, то не требуются оптимизации в виде мемоизации, управления стейтом и т. д. Но нужно было продумать организацию модулей приложения, чтобы избежать prop-drilling'а и переиспользовать типовые компоненты.

В плане композиции все осталось в рамках «традиционного» реакт-приложения. То есть сущности в приложении разделились на несколько уровней:
  • Компоненты, отвечающие за определенную логику отображения (таблицы, графики и т. д)
  • Стилизованные компоненты (виджеты, со специфичным отображением, в зависимости от места применения)
  • Компоненты лейаута страниц (колонтитулы. титульные страницы и т. д.)
  • И агрегирующие компоненты отчетов, которые маппят данные в необходимый для каждого виджета вид, располагают виджеты в нужном порядке и пробрасывают им пропсы

Что в итоге позволило собирать отчеты, как конструктор из нужных модулей. Приведу пример:

Полный отчет состоит из двух блоков: титульной страницы и контента.
export const FullReport: FC<IFullReportProps> = ({ generatedAt, period, serverName, version, ...widgetsData }) => (
    <>
        <FullReportTitle generatedAt={generatedAt} period={period} serverName={serverName} version={version} />
        <FullReportContent generatedAt={generatedAt} period={period} widgetsData={widgetsData} />
    </>
);

Code Block 1 FullReport.tsx

В свою очередь, контент этого отчета — это набор различных элементов (содержание, виджеты, заголовки разделов и т. д.), обернутых в layout-компоненты; виджеты, в свою очередь, содержат внутри стилизованные компоненты таблиц, графиков и т. д.
export const FullReportContent: FC<IFullReportContentProps> = ({ generatedAt, period, widgetsData }) => (
    <Layout generatedAt={generatedAt} period={period}>
        <PageLayout>
            <FullReportContentsPage />
        </PageLayout>
 
        <PageLayout>
            <PointInTimeDataTitle />
            <IndustrialNetworkCompositionChapterTitle />
 
            <DevicesByTypesWidget {...widgetsData.devicesByTypes} />
            <WidgetSplitter />
            <DevicesByVendorsWidget {...widgetsData.devicesByVendors} />
        </PageLayout>
 
        <PageLayout>
            <DevicesByOperationSystemsWidget {...widgetsData.devicesByOperationSystems} />
            <WidgetSplitter />
            <DevicesByTagsWidget {...widgetsData.devicesByTags} />
        </PageLayout>
 
        <PageLayout lastPage>
            <RiskScoreLevelCountersWidget {...widgetsData.risksByScoreLevels} />
        </PageLayout>
    </Layout>
);

Code Block 2 FullReportContent.tsx

3. Преимущества


В этом разделе я постараюсь перечислить все преимущества, которые выделяют данную библиотеку при работе с ней.

3.1 Удобство и простота


React-pdf предоставляет из коробки удобный набор инструментов для рендеринга PDF как на клиенте, так и на сервере

Например, для рендеринга документа на стороне Node.js предоставлено несколько API, которые позволяют рендерить документ в файл, строку или Node Stream. А на стороне клиента также предоставлены следующие возможности: отображение PDF, генерация и скачивание по ссылке и возможность получения документа в виде Blob-объекта без отображения на экране. Это добавляет удобство при разработке, потому что после каждого изменения верстки можно просто посмотреть отображение документа в браузере, не генерируя файл.

3.2 Yoga-layout


Благодаря Yoga управление лейаутом на страницах действительно удобное. Yoga предоставляет управление Flex-лейаутом, что дает удобное управление позиционированием блоков в документе, т. к. поддерживаются все основные свойства.

3.3 Динамический рендеринг


Также есть вполне понятный механизм рендеринга динамического контента в зависимости от номера страницы

В примере ниже отображается номер страницы во втором блоке Text, а сам блок View, благодаря пропсу fixed, является фиксированным для каждой страницы. В итоге получаем автоматическое проставление номеров страниц. View и Text являются стандартными компонентами, предоставляемыми React-pdf, подробнее о них можно узнать в документации.
export const LayoutFooter: FC<ILayoutFooterProps> = ({ generatedDate, period }) => (
    <View style={styles.footerContent} fixed>
        <Text style={styles.date}>{useFooterDates(period, generatedDate)}</Text>
        <Text render={({ pageNumber }) => pageNumber} style={styles.pageCounter} />
    </View>
);

Code Block 3 LayoutFooter.tsx

3.4 И еще немного вкуснятины


  • Возможность задать фиксированный контент для всех страниц (например, колонтитулы)
  • Удобное подключение шрифтов, что позволяет сразу легко и быстро добавить нужный шрифт (но были проблемы со шрифтами в svg, об этом расскажу дальше)
  • Понятная и небольшая документация
  • Довольно низкий порог вхождения для начала работы

4. Ограничения и боли


Итак, теперь перейдем к самому интересному. К тому, с чем нам пришлось столкнуться при работе с этой библиотекой. Потому что на примере с их сайта все выглядит легко и красиво, но на деле приходилось сталкиваться с различными трудностями, изобретая свои кастомные решения, потому что на просторах Интернета, как оказалось, никто не сталкивался с этим раньше или сталкивался, но не писал про это.

4.1 Таблицы


Сама React-pdf не предоставляет компонент таблицы, поэтому пришлось искать библиотеку, которая предоставляет такой функционал. И на удивление не нашлось популярного общепринятого решения. Удалось найти на гитхабе парочку библиотек, имеющих приблизительно по 100 звезд, что является аргументом против того, чтобы затягивать эту библиотеку в энтерпрайз-проект. Также найденные реализации таблиц предлагали довольно простой функционал, который недолго реализовать самостоятельно, и недостаточно гибкие возможности стилизации.

В итоге пришлось делать собственную реализацию, и вот тут я вспомнил о Yoga layout, которая предоставляет только флексы. Скажем так, не самый приятный опыт — верстать таблицу на флексах с нуля, зато мы получили вариант, реализованный полностью под наши задачи.

4.2 Проблемы с svg-иконками


Так как React-pdf предоставляет отдельные компоненты для отрисовки svg, то не прокатит рендеринг svg-иконок.

Как решение данной проблемы — мы придумали механизм верстки иконок в React-компонентах, где просто заменены стандартные теги svg на теги, предоставленные React-pdf.

4.3 Проблемы со шрифтами в svg


Одним из преимуществ данной библиотеки было легкое добавление шрифтов. Так оно и есть, но при использовании шрифтов в svg возникают трудности с изменением размера шрифта. Если быть точным, размер шрифта не изменялся от слова «совсем» и приходилось подбирать нужный размер и позиционирование через scale и translate, что, конечно же, добавило неудобств при разработке.

4.4 Проблемы с рендерингом графиков на D3.js


Аналогичное ограничение накладывается на графики, которые мы рендерим при помощи D3.js. Поскольку на выходе мы получаем стандартный svg, то возникла необходимость в написании компонента, который при помощи react-html-parser рендерит график, трансформируя его при помощи функции-конвертера в формат, понятный для React-pdf.

4.5 Отладка


Это, наверное, самая большая и часто ощущаемая боль. Потому что в основном сталкиваться приходится с версткой PDF-документов, и верстать нужно согласно макетам.

В случае с HTML на помощь пришла бы вкладка Elements из Dev-tools, но так как мы имеем дело с отрендеренным PDF, Dev-tools становятся бесполезными, и расположение элементов, отступы и прочее мы можем увидеть только при помощи пропа debug на конкретном элементе.

И как вы, наверное, уже поняли, причесать стили в Dev-tools, а потом перенести в код — не получится.

Только исправления и инкрементная сборка — только хардкор.

4.6 Единицы измерения


Еще один не самый приятный момент — в React-pdf дефолтными единицами измерения являются pt, а px вообще не существует. Так что нужно учитывать этот момент и предупредить вашего дизайнера.

4.7 Работа с rgba


Также боли добавила прозрачность. Потому что полноценной поддержки rgba нет. На Github можно найти Issue, где они вроде добавили поддержку, но это только для backgroundColor. Issue для borderColor на момент написания статьи открыто, поэтому приходится подкладывать дополнительные слои со стилизованными границами через position: absolute.

5. Выводы


Использование React-pdf является неплохой альтернативой html-to-pdf-рендерингу, так что если вы рассматриваете варианты для рендеринга PDF, то рекомендую обратить внимание на эту библиотеку. Несмотря на перечисленные выше ограничения и то, что придется разобраться с предоставляемым API, можно довольно быстро сверстать и отрендерить документ без дополнительных манипуляций (например, использования headless-браузера). Также React-pdf предоставляет удобные инструменты работы именно с PDF-документом из коробки (нумерация страниц, колонтитулы, добавление закладок и т. д.).

Если вам тоже интересны такие оптимизационные хуки, то приходите к нам во фронтенд-команду «Лаборатории Касперского». Процесс найма у нас максимально упрощен, так что уже через пару дней сможем делать подобные изыскания вместе :)

Комментарии (7)