Рассказывает Альберто Рэстифо, 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 у него не будет. Поэтому вы не можете полагаться на:
componentDidMount
;- коллбеки
ref
; - SVG-изображения;
- canvas.
Компоненты вам помогут
Наверное, самой сложной частью создания электронных писем является написание 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;
Это позволит существенно сократить код:
Займёмся отзывчивостью
Хорошие новости: все почтовые клиенты, поддерживающие медиазапросы, также поддерживают стили, добавленные в <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>
Процесс создания письма предельно прост:
- Перенос в HTML-скелет CSS-стилей.
- Отрисовка React-приложения.
- Замена HTML-кода шаблона на отрисованные элементы.
Стоит отметить:
- при импорте основного содержимого письма нужно указывать
default
, поскольку транспилированный файл экспортируется в виде объекта:const Email = require('../lib/Email').default;
- лучше применять метод
renderToStaticMarkup
, не использующий в сгенерированном HTML характерные для React элементы (например, идентификаторы и комментарии), иначе сформированное письмо рискует быть распознанным как спам; - в данном примере используются промисы, но это не обязательно.
Заключение
Как видите, для решения этой задачи не потребовались большие библиотеки и сложный код. Хватило возможностей статического рендеринга React и слегка нестандартного мышления. Кроме того, в шаблон письма теперь можно добавить логику, облегчив бэкенд-код, да и вёрстка стала гораздо более читаемой.
Перевод статьи «How to build emails with React»