Пример серверного рендеринга: прокачиваем email-рассылку при помощи React

email

Рассказывает Альберто Рэстифо, JS-разработчик


Пару недель назад менеджер нашего проекта сообщил, что в ближайшие несколько спринтов мы будем заниматься электронными письмами. Автоматически генерируемые email’ы надо было сделать отзывчивыми, что привело бы к усложнению вёрстки.

Что делает веб-разработчик, когда сталкивается с таким заданием?

Верно. Он передаёт задачу коллеге и берёт двухнедельный отпуск.

Но, возможно, решение есть. На тот момент мы уже использовали React для упрощения создания PDF-файлов и решили применить этот опыт в создании email’ов. Подход состоит из двух этапов: вёрстка письма при помощи React-компонентов и формирование этого письма в Node.js.

Прим. перев. Если вы только начали знакомство с React, то вам стоит изучить это руководство — оно познакомит вас с основами использования библиотеки.

Создание разметки письма

Идея заключается в использовании серверного рендеринга React для отправки пользователю готового HTML-файла. Я создал простой проект, который вы можете использовать для собственных рассылок. Его исходный код доступен на GitHub.

Так выглядит шаблонное письмо

Используйте строчные стили

Хотя современной тенденцией является добавление стилей в <head>, для максимальной совместимости рекомендуется использовать строчные стили. React позволяет делать это с лёгкостью:

import React from 'react';

const style = {

  title: {
    fontSize: '24px',
    fontWeight: 'bold',
    marginTop: '5px',
    marginBottom: '10px',
  },

};

function Title({ children }) {
  return (
    <h1 style={style.title}>
      {children}
    </h1>
  );
}

export default Title;

Не обращайтесь к DOM

Вы должны помнить, что письмо будет отрисовываться на сервере и доступа к DOM у него не будет. Поэтому вы не можете полагаться на:

Компоненты вам помогут

Наверное, самой сложной частью создания электронных писем является написание HTML-таблиц. Чтобы сделать код чище, лучше использовать Grid:

import React from 'react';

function Cell({ children }) {
  return <td>{children}</td>;
}

function Row({ children }) {
  return (
    <tr>
      {React.Children.map(children, (el) => {
        if (el.type === Cell) return el;
       
        return <td>{el}</td>;
      })}
    </tr>
  );
}

function Grid({ children }) {
  return (
    <table>
      <tbody>
        {React.Children.map(children, (el) => {
          if (!el) return;

          if (el.type === Row) return el;

          if (el.type === Cell) {
            return <tr>{el}</tr>;
          }

          return (
            <tr>
              <td>{el}</td>
            </tr>
          );
        })}
      </tbody>
    </table>
  );
}

Grid.Row = Row;
Grid.Cell = Cell;

export default Grid;

Это позволит существенно сократить код:

Слева: React-код. Справа: скомпилированный HTML-код.

Займёмся отзывчивостью

Хорошие новости: все почтовые клиенты, поддерживающие медиазапросы, также поддерживают стили, добавленные в <head>. Это здорово, потому что медиазапросы строчным кодом не напишешь.

Чтобы уменьшить заголовок Title на мобильных устройствах, нужно добавить к элементу класс:

<h1 style={style.title} className="title-heading">
       {children}
</h1>

И импортировать таблицу стилей в index.js:

@media only screen and (max-width: 650px) {

  .title-heading {
    font-size: 18px !important;    /* !important тоже нужен  */
    text-align: center !important; /* переопределим inline-стили */
  }

}
import React from 'react';
import ReactDOM from 'react-dom';

import './inlined.css';

Важно добавить таблицу стилей именно в index.js, а не напрямую в Title.js, поскольку в процессе разработки импорт CSS доступен только через Webpack.

Чтобы добавить всё в скомпилированное письмо, лучше пойти другим путём.

Создание письма при помощи Node.js

Прежде чем заняться генерацией письма, необходимо транспилировать React-файлы, воспользовавшись Babel. Если во время запроса файла компонента возникнет ошибка, то избавьтесь от JSX и ES6-кода.

Я рекомендую собирать этот проект как отдельный git-репозиторий и устанавливать его через npm в виде зависимости. Такой подход позволит запускать процесс транспиляции при помощи npm install.

Учитывая, что проект создавался с помощью create-react-app, необходимо изменить package.json следующим образом:

 {
   "name": "react-emails-example",
   "version": "0.1.0",
   "private": true,
   "main": "./server/createEmail.js",
   "devDependencies": {
   "babel-cli": "^6.24.1",
   "babel-preset-react-app": "^2.2.0",
   "react-scripts": "0.9.5"
   },
   "dependencies": {
     "react": "^15.5.4",
     "react-dom": "^15.5.4"
   },
   "scripts": {
     "install": "babel src --out-dir lib --presets=react-app",
     "build": "babel src --out-dir lib --presets=react-app",
     "start": "react-scripts start",
     "test": "react-scripts test --env=jsdom",
     "eject": "react-scripts eject"
   }
 }

Папка server содержит два файла:

server
├── createEmail.js  # точка входа
└── email.html

Содержимое файла createEmail.js:

const fs = require('fs');
const Path = require('path');

const React = require('react');
const ReactDOMServer = require('react-dom/server');

const Email = require('../lib/Email').default;

const STYLE_TAG = '%STYLE%';
const CONTENT_TAG = '%CONTENT%';

/**
 * Функция getFile получает файл по относительному пути
 * @param {String} relativePath
 * @return {Promise.<string>}
 */
function getFile(relativePath) {
  return new Promise((resolve, reject) => {
    const path = Path.join(__dirname, relativePath);

    return fs.readFile(path, { encoding: 'utf8' }, (err, file) => {
      if (err) return reject(err);
      return resolve(file);
    })
  });
}

/**
 * Функция createEmail рендерит приложение React на основе входных данных.
 * Возвращает промис, который является HTML кодом для e-mail.
 * @param {Object} data
 * @return {Promise.<String>}
 */
function createEmail(data) {
  return Promise.all([
    getFile('../src/inlined.css'),
    getFile('./email.html'),
  ])
    .then(([style, template]) => {
      const emailElement = React.createElement(Email, { data });
      const content = ReactDOMServer.renderToStaticMarkup(emailElement);

      // Данные шаблона заменяются на контент
      let emailHTML = template;
      emailHTML = emailHTML.replace(CONTENT_TAG, content);
      emailHTML = emailHTML.replace(STYLE_TAG, style);

      return emailHTML;
    });
}

module.exports = createEmail;

Содержимое файла email.html:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
  <head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0"/>

    <style>
      %STYLE%
    </style>
  </head>
  <body style="width:100% !important; -webkit-text-size-adjust:100%; -ms-text-size-adjust:100%; margin:0; padding:0;">
    %CONTENT%
  </body>
</html>

Процесс создания письма предельно прост:

  1. Перенос в HTML-скелет CSS-стилей.
  2. Отрисовка React-приложения.
  3. Замена HTML-кода шаблона на отрисованные элементы.

Стоит отметить:

  • при импорте основного содержимого письма нужно указывать default, поскольку транспилированный файл экспортируется в виде объекта:
    const Email = require('../lib/Email').default;
  • лучше применять метод renderToStaticMarkup, не использующий в сгенерированном HTML характерные для React элементы (например, идентификаторы и комментарии), иначе сформированное письмо рискует быть распознанным как спам;
  • в данном примере используются промисы, но это не обязательно.

Заключение

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

Перевод статьи «How to build emails with React»