Привет, Хабр! Меня зовут Алексей Сингур, я — фронтенд-разработчик в проекте KICS (Kaspersky Industrial CyberSecurity) for Networks «Лаборатории Касперского». Если коротко, то наш продукт защищает промышленные инфраструктуры и сети от киберугроз: анализирует трафик для выявления отклонений и обнаружения признаков сетевых атак, чтобы обеспечивать предприятию непрерывность процессов.
Одной из фичей KICS for Networks является генерация отчетов о сканировании инфраструктуры в формате PDF. При разработке этой фичи пришлось погрузиться в вопрос верстки и рендеринга PDF на Node.js. Речь пойдет об использовании для этих целей библиотеки React-pdf (в нашем проекте мы пока используем версию 2.1.1.), которая может показаться весьма экзотичной, если судить по количеству статей и отзывов в Интернете :)
Пост будет полезен веб-разработчикам для расширения кругозора в области инструментов рендеринга PDF, а также заинтересованным в генерации PDF-документов на стороне клиента или сервера.
Итак, давайте рассмотрим этого зверя поближе. Согласно статье о шагах рендеринга в документации, React-pdf стоит на двух китах:
Что, конечно же, накладывает свои ограничения. Yoga накладывает ограничения на позиционирование элементов на странице, а компоненты из React-pdf внутри себя имеют вызовы Pdfkit, что и позволяет им отрендериться в документе, поэтому можно использовать только предоставленные библиотекой компоненты, а не привычный HTML. Опять же, если вы соберете из имеющихся примитивов свои React-компоненты, то можете беспрепятственно их использовать.
В принципе, библиотека предоставляет все необходимые компоненты-примитивы для успешной верстки PDF-документа, что позволяет довольно гибко и удобно создавать различные шаблоны. Вот некоторые примеры страниц PDF-отчета, который является результатом работы с React-pdf.
Эту библиотеку мы затянули в проект для рендеринга PDF-отчетов. Предвосхищая вопросы, почему просто не поднять headless-браузер и не распечатать html в pdf, отвечу, что это ограничения со стороны бизнеса, которые как раз и подтолкнули нас на поиск сторонних библиотек. Из не столь широкого выбора React-библиотек для рендеринга PDF наш выбор пал на React-pdf, потому что она имеет убедительные 11.8k звезд на Github (что говорит о вполне живом сообществе и актуальной поддержке — latest commit на момент написания статьи), предоставляет вполне удобные React-компоненты для сбора шаблонов страниц и возможности стилизации этих компонентов. Более подробное описание выделенных нами преимуществ данной библиотеки приведено ниже. Опять же, как ограничения (а все сторонние библиотеки накладывают какие-то ограничения), React-pdf не переваривает обычный HTML, только предоставленные самой библиотекой примитивы, и также знает далеко не все CSS-свойства, что иногда заставляет поприседать.
React-pdf позволяет отрендерить отчет на стороне сервера, что открывает возможность встроить его как отдельное апи для рендеринга PDF-документов в свое приложение.
Так как рендеринг PDF не подразумевает взаимодействие с пользователем, то не требуются оптимизации в виде мемоизации, управления стейтом и т. д. Но нужно было продумать организацию модулей приложения, чтобы избежать prop-drilling'а и переиспользовать типовые компоненты.
В плане композиции все осталось в рамках «традиционного» реакт-приложения. То есть сущности в приложении разделились на несколько уровней:
Что в итоге позволило собирать отчеты, как конструктор из нужных модулей. Приведу пример:
Полный отчет состоит из двух блоков: титульной страницы и контента.
Code Block 1 FullReport.tsx
В свою очередь, контент этого отчета — это набор различных элементов (содержание, виджеты, заголовки разделов и т. д.), обернутых в layout-компоненты; виджеты, в свою очередь, содержат внутри стилизованные компоненты таблиц, графиков и т. д.
Code Block 2 FullReportContent.tsx
В этом разделе я постараюсь перечислить все преимущества, которые выделяют данную библиотеку при работе с ней.
React-pdf предоставляет из коробки удобный набор инструментов для рендеринга PDF как на клиенте, так и на сервере
Например, для рендеринга документа на стороне Node.js предоставлено несколько API, которые позволяют рендерить документ в файл, строку или Node Stream. А на стороне клиента также предоставлены следующие возможности: отображение PDF, генерация и скачивание по ссылке и возможность получения документа в виде Blob-объекта без отображения на экране. Это добавляет удобство при разработке, потому что после каждого изменения верстки можно просто посмотреть отображение документа в браузере, не генерируя файл.
Благодаря Yoga управление лейаутом на страницах действительно удобное. Yoga предоставляет управление Flex-лейаутом, что дает удобное управление позиционированием блоков в документе, т. к. поддерживаются все основные свойства.
Также есть вполне понятный механизм рендеринга динамического контента в зависимости от номера страницы
В примере ниже отображается номер страницы во втором блоке
Code Block 3 LayoutFooter.tsx
Итак, теперь перейдем к самому интересному. К тому, с чем нам пришлось столкнуться при работе с этой библиотекой. Потому что на примере с их сайта все выглядит легко и красиво, но на деле приходилось сталкиваться с различными трудностями, изобретая свои кастомные решения, потому что на просторах Интернета, как оказалось, никто не сталкивался с этим раньше или сталкивался, но не писал про это.
Сама React-pdf не предоставляет компонент таблицы, поэтому пришлось искать библиотеку, которая предоставляет такой функционал. И на удивление не нашлось популярного общепринятого решения. Удалось найти на гитхабе парочку библиотек, имеющих приблизительно по 100 звезд, что является аргументом против того, чтобы затягивать эту библиотеку в энтерпрайз-проект. Также найденные реализации таблиц предлагали довольно простой функционал, который недолго реализовать самостоятельно, и недостаточно гибкие возможности стилизации.
В итоге пришлось делать собственную реализацию, и вот тут я вспомнил о Yoga layout, которая предоставляет только флексы. Скажем так, не самый приятный опыт — верстать таблицу на флексах с нуля, зато мы получили вариант, реализованный полностью под наши задачи.
Так как React-pdf предоставляет отдельные компоненты для отрисовки
Как решение данной проблемы — мы придумали механизм верстки иконок в React-компонентах, где просто заменены стандартные теги
Одним из преимуществ данной библиотеки было легкое добавление шрифтов. Так оно и есть, но при использовании шрифтов в
Аналогичное ограничение накладывается на графики, которые мы рендерим при помощи D3.js. Поскольку на выходе мы получаем стандартный
Это, наверное, самая большая и часто ощущаемая боль. Потому что в основном сталкиваться приходится с версткой PDF-документов, и верстать нужно согласно макетам.
В случае с HTML на помощь пришла бы вкладка Elements из Dev-tools, но так как мы имеем дело с отрендеренным PDF, Dev-tools становятся бесполезными, и расположение элементов, отступы и прочее мы можем увидеть только при помощи пропа
И как вы, наверное, уже поняли, причесать стили в Dev-tools, а потом перенести в код — не получится.
Только исправления и инкрементная сборка — только хардкор.
Еще один не самый приятный момент — в React-pdf дефолтными единицами измерения являются
Также боли добавила прозрачность. Потому что полноценной поддержки
Использование React-pdf является неплохой альтернативой html-to-pdf-рендерингу, так что если вы рассматриваете варианты для рендеринга PDF, то рекомендую обратить внимание на эту библиотеку. Несмотря на перечисленные выше ограничения и то, что придется разобраться с предоставляемым API, можно довольно быстро сверстать и отрендерить документ без дополнительных манипуляций (например, использования headless-браузера). Также React-pdf предоставляет удобные инструменты работы именно с PDF-документом из коробки (нумерация страниц, колонтитулы, добавление закладок и т. д.).
Если вам тоже интересны такие оптимизационные хуки, то приходите к нам во фронтенд-команду «Лаборатории Касперского». Процесс найма у нас максимально упрощен, так что уже через пару дней сможем делать подобные изыскания вместе :)
Одной из фичей 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.
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-документом из коробки (нумерация страниц, колонтитулы, добавление закладок и т. д.).
Если вам тоже интересны такие оптимизационные хуки, то приходите к нам во фронтенд-команду «Лаборатории Касперского». Процесс найма у нас максимально упрощен, так что уже через пару дней сможем делать подобные изыскания вместе :)