Recharts With HTML Email

尽管 Email 中的 HTML 与我们在平时写的 HTML 并没有本质上的区别,但由于受到不同客户端渲染方式不同以及邮件安全性限制的影响,让我们在编写邮件模版时,一夜之间回到解放前。只能使用 Table 来布局,不能运行 JavaScript 脚本,不能使用外联的 CSS,图片是唯一可以引用的外部资源等等。当然这些并不是本文讨论的重点,如果感兴趣,可以参考下列文章或互联网上的更多资源。

创建坚如磐石的HTML邮件
HTML Email 编写指南

最近,我需要编写的邮件模版并不是 EDM (以营销为目的的电子邮件),而是系统自动发送的统计报表邮件。因此并不需要非常绚丽,但是由于承载了一段时间内重要数据的显示,单纯的数字显得过于苍白,便希望插入一些图表来丰富。上文已经提到过了,电子邮件最多可以插入图片,平时用来制作图表 SVG、Canvas 就英雄无用武之地了。

从 svg 到 静态图片

我们开始考虑是否可以在服务器端将图表转化为静态图片后插入到邮件,尽管图片化的图表无法进行交互,但依旧能够展现数据的一些特点。在日常业务中,我们普遍使用了 recharts,其主要采用 SVG 来绘制图表,那么第一步就是将 SVG 图表转化为静态图片。由于我们需要在服务器端完成整个图片生成的流程,任何借用浏览器的实现方式被统统枪毙,但好在 PhantomJS 留下了一线生机。PhantomJS 是一个基于 Webkit 的服务端 JavaScript API,能在服务器端支持 Web 标准,如 DOM 操作,CSS 选择,Canvas、SVG 渲染。

我们使用的 svg2png 正是基于 PhantomJS 将 SVG 转化成了 PNG,阅读了一下它的源码。首先将需要转化的 svg 以 Buffer 的形式传入 svg2png 中,svg2png 新建子进程启动 PhantomJS,并将 Buffer 转化为 utf8 字符串交给它。PhantomJS 通过 system.stdin 模块读取字符串,利用 webpage 模块,将 svg 渲染到页面上,确定 svg 的显示区域后,利用 renderBase64 将页面转换为 base64 编码,最后通过 system.stdout 输出,核心代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
var webpage = require("webpage");
var system = require("system");
var HTML_PREFIX = "<!DOCTYPE html><style>html, body { margin: 0; padding: 0; } " + "svg { position: absolute; top: 0; left: 0; }</style>";
function convert(options) {
var page = webpage.create();
var source = "";
while (!system.stdin.atEnd()) {
source += system.stdin.readLine() + "\n";
}
// 当页面加载完成
page.onLoadFinished = function (status) {
try {
// 若 option 中传入了 width 或 height,设置 svg 的 widht 或 height
if (options.width !== undefined || options.height !== undefined) {
setSVGDimensions(page, options.width, options.height);
}
// 获取 svg 真实 width 与 height
var dimensions = getSVGDimensions(page);
// 根据真实 width 或 height,设置 svg 的 width 与 height
setSVGDimensions(page, dimensions.width, dimensions.height);
// 设置页面窗口大小
page.viewportSize = {
width: dimensions.width,
height: dimensions.height
};
// 设置截图范围
page.clipRect = {
top: 0,
left: 0,
width: dimensions.width,
height: dimensions.height
};
} catch (e) {
phantom.exit();
return;
}
// 输出图片的 base64 编码
var result = "data:image/png;base64," + page.renderBase64("PNG");
system.stdout.write(result);
phantom.exit();
};
// PhantomJS will always render things empty if you choose about:blank, so that's why the different default URL.
// PhantomJS's setContent always assumes HTML, not SVG, so we have to massage the page into usable HTML first.
// 将内容渲染到页面上
page.setContent(HTML_PREFIX + source, options.url || "http://example.com/");
}

从 recharts 到 svg

能顺利将 svg 转化为静态图片后,下一步就是考虑如何将 recharts 转化为 svg,得到 HTML 字符串即可。recharts 是基于 react 的图表库,自然而然,我们想到了 react 的 server 端渲染。在 react server render 中,有两个API,renderToStaticMarkup 与 renderToString 相比,少了大批在 DOM 上的 reactid。对于我们的场景毫不犹豫选择 renderToStaticMarkup。

1
2
3
4
5
6
7
8
9
10
require('babel-register');
const ReactDOMServer = require('react-dom/server');
const React = require('react');
const markup = ReactDOMServer.renderToStaticMarkup(
React.createElement(rechartsComponent, props);
);
const sourceBuffer = new Buffer(markup);

当然 node 是无法直接执行 recharts 代码的,需要通过 babel 进行转译。这里注意 babel-register 实时对 require 加载的文件进行转译的,因此在生产环境需要放置 babel 转译完成后的组件代码。

highcharts 生成图表就是一个完整的 SVG 不同,recharts 其有部分是使用普通的 HTML 标签来实现的,比如 Legend。recharts 图表的 renderToStaticMarkup 结果最外层是 <div class="recharts-wrapper">...</div>,那是否 svg2png 应该无法使用呢?答案是否定的,svg2png 使用 PhantomJS 的原理在于渲染页面后截图,输出图片的base64编码,并不需要外层一定为 <svg>。svg2png 由于专门为单个 SVG 元素定制,例如添加了 svg { position: absolute; top: 0; left: 0; } 这样的代码,便会造成 recharts 图表中的 Legend 发生样式错误,后续笔者会针对 recharts,编写专门的 PhantomJS 脚本,读者亦可自行实现。对比其他图表库的纯 sever 端静态图片渲染,recharts 配合 react server 端渲染与 PhantomJS 应该是一种较为简单的实现方式,提供 简单Demo chart-to-pic-example

示例图