Соавтором этого поста является Хрича Кабир, мой коллега по Altizon Systems.

Мы работаем над продуктом IoT, ориентированным на обрабатывающую промышленность, и строим аналитические отчеты. В большинстве случаев дизайн отчетов и информация различаются в зависимости от роли пользователя, который будет их использовать.

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

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

Итак, нашей задачей было написать утилиту, которая периодически отправляет определенный отчет пользователю по электронной почте ежедневно или в определенное запланированное время. Для планирования мы используем Sidekiq, но наша основная проблема заключалась в том, как выполнить / рассчитать отчет на стороне сервера (не открывая браузер) и отправить его по электронной почте.

Во всех наших продуктах мы используем React в качестве внешнего интерфейса, и он выполняет тяжелую работу по отображению отчета в браузере. Чтобы прикрепить один и тот же отчет к электронному письму, нам нужно будет разработать шаблон HTML для каждого отчета. Это означает, что у нас было бы 2 представления каждого отчета, одно написано в HTML, а другое - на ReactJS.

У этого есть следующие недостатки:

1. База кода продукта будет содержать повторяющийся код, что нарушает принцип DRY при разработке программного обеспечения.

2. Увеличивает время на разработку отчета, который влияет на доставку продукта.

Мы решили посмотреть на другие возможные решения вместо того, чтобы дважды писать один и тот же код. И мы натолкнулись на следующие варианты и исследовали их один за другим, пока нам не удалось достичь нашей цели.

1. Печать экрана: Распечатайте отчет с помощью экрана печати и отправьте его как вложение.

2. Кнопка печати FrontEnd: в пользовательском интерфейсе отчета добавьте кнопку печати, которая преобразует компонент реакции в страницу HTML / PDF.

3. Ruby Gem: есть ruby ​​gem, который может отображать компонент отчета на стороне сервера в Rails, генерировать его HTML и отправлять его как тело сообщения электронной почты.

4. Рендеринг на стороне сервера с помощью Node server: Используйте рендеринг на стороне сервера с помощью ReactDOMServer и безголового протокола браузера для рендеринга HTML и JS и создания PDF-файла.

Теперь мы подробно рассмотрим каждое решение.

Печать экрана:

Это было самое простое решение, которое у нас было, и оно требует ввода данных пользователем. Пользователь должен присутствовать в системе, которая сделает снимок экрана в виде изображения и прикрепит его к почте. Но это не масштабируется. А что, если пользователю нужен отчет в запланированное время или периодически?

Пример : каждую неделю в 20:00. Человек не может этого сделать. Это также связано с невозможностью полностью захватить прокручиваемую страницу, поэтому нам пришлось отказаться от этой идеи.

Кнопка печати на передней панели:

Проведя поиск, мы обнаружили, что есть несколько доступных расширений для браузера, которые создают полное изображение открытой страницы. Пример: захват экрана всей страницы в Chrome. После добавления этого расширения в браузер мы сможем снимать любой экран в формате изображения (png / jpg).

Опять же, это решение не требует разработки, но страдает некоторыми из предыдущих проблем с масштабированием и планированием. В то же время для выполнения этого действия нам все равно потребуется войти в систему в браузере, что противоречит цели доставки электронной почты.

RubyGem:

Вскоре мы поняли, что наше решение не может требовать взаимодействия с браузером. Нам нужно будет использовать рендеринг на стороне сервера. Итак, мы начали изучать Ruby Gems, которые поддерживают рендеринг на стороне сервера с помощью React. Мы исследовали следующие драгоценные камни

3.1 rails_react_stdio:

Он основан на response-stdio, который поддерживает рендеринг на стороне сервера независимо от технологии на стороне сервера. Он действует как двоичный файл, который выполняет работу по рендерингу реагирующих компонентов. Для рендеринга компонента React на стороне сервера нам нужно передать путь к файлу компонента реакции и реквизиты, если это необходимо. Он вернет ответ JSON, который будет содержать HTML-код отчета. Далее мы можем отправлять HTML-контент по электронной почте.

Внутренне этот гем использует popen3 для выполнения команды рендеринга. Но это означает, что двоичный файл response-stdio должен присутствовать в контейнере докера, в котором работает наше приложение rails.

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

Пример : для визуализации отчета, в котором есть компонент TestComponent и путь к файлу app / assets / javascripts / components / TestComponent.jsx

Сначала включите гем в Gemfile:

gem ‘rails_react_stdio’, ‘~> 0.1.0’

Теперь из планировщика электронной почты вызовите метод gem, чтобы отобразить отчет и получить HTML-код из ответа. Затем отправьте этот ответ по электронной почте.

email_body = RailsReactStdio::React.render(‘app/assets/javascripts/components/TestComponent.jsx’, {city: “Pune”})

Мы пробовали использовать описанный выше метод, но безуспешно. Также репозиторий GitHub не поддерживался активно, и все его тестовые примеры не выполнялись. Поэтому мы решили двигаться дальше без этого.

3.2 react-rails:

Сообщество ReactJS создало этот драгоценный камень. Он использует ExecJS для выполнения рендеринга реагирующего компонента на стороне сервера. Нам просто нужно передать один флаг ‘prerender’: true.

<%= react_component(‘Dashboard’, {name: ‘Example’}, {prerender: true}) %>

У этого процесса предварительной отрисовки нет доступа к окну или документу, поэтому он не загружает JavaScript или CSS среды выполнения. Мы также используем JQuery для некоторых целей, поэтому он тоже не работает.

Есть альтернатива: у этого драгоценного камня есть другой класс для рендеринга на стороне сервера, ExecJSRenderer, который помогает использовать JavaScript для компонента на стороне сервера.

ExecJSRenderer Класс имеет 2 метода: before_render и after_render, которые предоставляют доступ к JavaScript, необходимому до и после рендеринга компонента. Но для поддержки серверного рендеринга в каждом контроллере потребовалось бы много изменений в существующей кодовой базе. Помимо этого, ExecJS не предоставляет информацию о песочнице и об ошибках времени выполнения. Мы все еще искали что-нибудь получше.

Отрисовка на стороне сервера с помощью Node server:

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

4.1 Решение на основе ReactDOMServer:

Здесь мы используем ReactDOMServer. Это предпочтительное решение для рендеринга на стороне сервера от команды React. Мы создали сервер узла, который вызывает метод renderToString () с компонентом реакции. Он возвращает обработанный контент, который мы объединяем с HTML и отправляем по электронной почте.

Пример :

server.get(‘/’, (req, res, next) => {
  /**
  * renderToString() will take our React app and turn it into a      string
  * to be inserted into our Html template function.
  */
  console.log(‘started’)
  const body = renderToString(<App />);
  const title = ‘Server side Rendering React Components’;
  var result = Html({ body, title })
}

Метод renderToString () возвращает строковый ответ. Мы передаем этот ответ в шаблон HTML и отправляем шаблон по почте.

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

Чтобы получить правильные изображения в электронном письме, нам нужно либо

  • Сохраняйте изображения в S3 и используйте исходный URL-адрес в отчете: Итак, теперь мы будем добавлять URL-адреса изображений S3 в электронное письмо, и почтовый сервер будет напрямую загружать изображения с сервера S3. Для хранения изображений потребуется дополнительное облачное пространство, а для загрузки из пункта назначения потребуется еще один сетевой вызов из почтового ящика.

Or

  • Отправить код изображения в формате base64 по почте: вместо URL-адреса изображения мы можем отправить код изображения в формате base64. Хотя это увеличивает полезную нагрузку сети, многие почтовые серверы, такие как Outlook и Gmail, блокируют изображения base64.

Так что нам все равно нужно что-то с этим делать.

4.2 Кукольник:

Изучив вышеуказанные методы, мы обнаружили сервис Puppeteer Headless Chrome. Puppeteer - это библиотека NodeJS от команды Google Chrome, используемая для сквозного тестирования. По умолчанию для этого используется браузер Chrome / Chromium. По сути, он имитирует все действия, которые пользователь может выполнять в браузере. Пример: ввод с клавиатуры, события мыши, отправка формы и т. Д.

Результатом запроса кукловода может быть HTML-страница, снимок экрана или PDF-файл. В случае HTML он отображает всю страницу на стороне сервера вместе со всеми изображениями, CSS и JavaScript. Или пользователь может отобразить страницу на стороне сервера и, при необходимости, сгенерировать снимок экрана или PDF-файл.

Если мы используем эту библиотеку с сервером Node, мы можем запланировать задачу в Sidekiq, которая будет делать запрос к этому серверу, отображать отчет и отправлять его по электронной почте. Puppeteer также имеет богатый набор API-интерфейсов, которые поддерживают отправку настраиваемых заголовков в запросе, которые помогают нам в аутентификации фонового запроса.

Это именно то, что нам нужно!

Итак, общий жизненный цикл запроса внутри кукловода должен выглядеть следующим образом:

1. Запускает браузер.

2. Создает страницу в браузере.

3. Выполните аутентификацию на сервере приложения отчетов.

4. Открывает URL отчета, который хочет пользователь, и возвращает отображаемое содержание страницы.

5. В зависимости от требований пользователя сохраняет HTML-код отображаемой страницы, делает снимок экрана или создает PDF-файл.

Мы можем использовать либо браузер по умолчанию (загружается при установке кукольника), либо указать конкретную версию браузера. Если мы дадим конкретную версию браузера, то нам нужно будет убедиться, что API кукловода совместимы с данным браузером.

Все вышеперечисленное может быть достигнуто с помощью следующих шагов.

1. Установить кукольник:

Он загружает браузер Chrome / Chromium по умолчанию. Итак, если мы хотим запустить браузер по умолчанию и установить кукольник:

npm install puppeteer

2. Подать запрос:

Создайте сервер, который будет получать запрос, реализовывать его жизненный цикл и возвращать желаемый результат.

Ниже приведен фрагмент кода, который мы используем для создания PDF-файла заданного URL-адреса страницы.

const puppeteer = require(“puppeteer”);
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.emulateMedia(“screen”);
await page.goto(‘https://www.google.com', {
      timeout: 30 * 1000, 
      waitUntil: “networkidle0”
});
await page.pdf(pdfOptions);
return page;

Как мы уже упоминали в шагах жизненного цикла выше, сначала запускается браузер. Затем он создает и открывает страницу в запущенном браузере. Здесь вместе с URL-адресом мы передали параметры timeout и waitUntil, которые задаются по следующим причинам:

  • тайм-аут: если мы хотим ограничить время запроса, мы можем передать его переменной тайм-аута.
  • waitUntil: если указан networkidle0, то следующий запрос не будет обслуживаться до тех пор, пока не будет выполнен текущий запрос.

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

Кроме того, мы вызвали page.emulateMedia («экран»); который применяет CSS к странице. Если мы не добавим emaulateMedia, наш PDF-файл не загрузит CSS.

Помимо того, что мы использовали, существует множество различных вариантов конфигурации с API Puppeteer. Посетите API docs для получения дополнительной информации.

Если вы нашли это полезным или у вас есть предложения, не стесняйтесь писать их в комментариях.

Это все, ребята.