邮件中加入 Chart 图表的实现

需求

最近接到个周报的需求,需要定期给上面的领导发送邮件汇报各项目组的情况。并且想在邮件中插入图表的形式。类似下图:

说干就干,以上这种形式的报表明显是用 HTML 邮件的形式实现的,所以先要调研下邮件里写 HTML 都有哪些坑~

关于邮件 HTML

参考阮一峰老师的 HTML Email 编写指南 以及其他一些资料,简单归纳下:

  • HTML DOM 元素:邮件 HTML 中的 html head body 标签可能是无效的,一些邮箱会直接忽略这些,直接取 <body></body> 中的内容。另外邮件也会忽略很多带交互的 DOM 元素,如 <select>,具体可以参考 Email HTML tags & attributes ignored by Outlook
  • 邮件内容布局:邮件中页面结构布局最好使用 table、tr、td 这些表格元素来实现。(PS:然后我去自己邮箱里翻了翻,发现果然如此。都是用嵌套 table 的方式来实现布局的。)
  • 外部资源引用:引用各种外部资源,如字体、样式、JS 文件等。唯一可以引用的就是图片资源了。且图片的显示也需要用户手动授权才可以显示。
  • 样式:最好直接写行内样式,class 类的写法在某些邮箱会失效。且 CSS 属性也不可以用太过高级的。可以去网上查一下邮件支持的 CSS 元素,类似Universally Supported CSS and HTML for Email Designs这种。

总而言之,尽量用最原始的方式去实现邮件 HTML。

在邮件中添加 Chart 图表

回到需求,想想解决方案。

纯前端实现

在大概了解了邮件 HTML 的开发后,就可以得知邮件 HTML 中是无法引用外部 JS 文件,同时也不支持 JavaScript 脚本的(应该是出于安全考虑,否则邮件可以干的事儿太多了。)

那么通过前端手段使用 VChartg2 或者 Echart 这类前端图表库实现的路就堵死了。

后端实现

所以,只能通过后端来做了。其实方案类似于服务器渲染。

获取图表数据

首先,我们需要有一串图的数据。

js 复制代码
const chartValues = [
  {
    legend: "新登设备",
    x: "2024-02-27(二)",
    y: 35680.0,
  },
  {
    legend: "新登设备",
    x: "2024-02-28(三)",
    y: 29651.0,
  },
  {
    legend: "新登设备",
    x: "2024-02-29(四)",
    y: 18106.0,
  },
  {
    legend: "新登设备",
    x: "2024-03-01(五)",
    y: 22782.0,
  },
  // 此处省略 N 条数据
];

绘制图表

然后,我们使用 VChart 图表库将 Chart 绘制出来。

js 复制代码
const fs = require("fs");
const VChart = require("@visactor/vchart");
const Canvas = require("canvas");

// 生成 chart
function genChart(values) {
  // 正常的图表 spec 配置
  const spec = {
    type: "line",
    width: 200,
    height: 100,
    data: [
      {
        id: "Map",
        values,
      },
    ],
    xField: "x",
    yField: "y",
    seriesField: "legend",
    point: {
      visible: false,
    },
    axes: [
      {
        orient: "left",
        visible: false,
      },
      {
        orient: "bottom",
        visible: false,
      },
    ],
  };
  const cs = new VChart.default(spec, {
    // 声明使用的渲染环境以及传染对应的渲染环境参数
    mode: "node",
    modeParams: Canvas,
    animation: false, // 关闭动画
  });

  cs.renderSync();

  // 导出图片
  const buffer = cs.getImageBuffer();

  // buffer 转 base64
  function bufferToBase64(imageBuffer) {
    return (
      "data:image/png;base64," +
      Buffer.from(imageBuffer, "binary").toString("base64")
    );
  }


  // fs.writeFileSync(`./line-chart.png`, buffer);

  return bufferToBase64(buffer);
}

其实,很多成熟的 chart 库都提供了服务器渲染方案,可以直接通过 node 来做到图表的服务器渲染。我们可以打开 fs.writeFileSync('./line-chart.png', buffer); 看下输出的图片。

HTML 输出

HTML 模板我就直接用了 HTML Email 编写指南 中的现成模板了。

html 复制代码
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
  <head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    <title>HTML Email编写指南</title>
    <meta name="robots" content="noindex, nofollow" />
    <meta name="GENERATOR" content="MSHTML 8.00.6001.18876" />
  </head>

  <body style="margin: 0; padding: 0">
    <table border="0" cellspacing="0" cellpadding="0">
      <tbody>
        <!-- SSR_AREA -->
      </tbody>
    </table>
  </body>
</html>

其中的 <!-- SSR_AREA --> 注释是用来被生成的 HTML 替换的区域。

js 复制代码
function genHtml(values) {
  const base64Url = genChart(values);

  var html = fs.readFileSync("./templete.html", { encoding: "utf-8" });

  var content = "";
  content += "<thead><tr>";
  values.forEach((value) => {
    content += `<td>${value.x}</td>`;
  });
  content += "<td>图表</td>";
  content += "</tr></thead>";

  content += "<tr>";
  values.forEach((value) => {
    content += `<td>${value.y}</td>`;
  });
  content += `<td><img src="${base64Url}" /></td>`; // 插入 Chart 图片
  content += "</tr>";

  html = html.replace("<!-- SSR_AREA -->", content);

  // fs.writeFileSync(`./line-chart.html`, html);
  return html;
}

const html = genHtml(chartValues);

发送邮件

node 邮件的发送我用的是 nodeMailer

js 复制代码
const nodemailer = require("nodemailer");

const transporter = nodemailer.createTransport({
  host: "smtp.163.com",
  port: 465,
  secure: true, // Use `true` for port 465, `false` for all other ports
  auth: {
    user: "violetjack@163.com",
    pass: "6666666", // 这里需要去邮箱开通 SMTP 并拿到授权码
  },
});

async function sendMail(htmlValue) {
  try {
    // send mail with defined transport object
    const info = await transporter.sendMail({
      from: '"VioletJack" <violetjack@163.com>', // sender address
      to: "hello@qq.com", // list of receivers
      subject: "HTML 模板", // Subject line
      // text: "Hello world?", // plain text body
      html: htmlValue, // html body
    });

    console.log("邮件发送成功: %s", info.messageId);
  } catch (error) {
    console.log("邮件发送失败: %s", error);
  }
}

sendMail(html);

改进

由于只是前期的技术调研,所以并没有往下做。后续的一些想法:

  • 把传入图表数据输出图片 Base64 做成一个后端服务,方便使用。
  • 不知道 base64 的图片对于邮件的支持会不会有坑,后续还是得存后端,然后通过 URL 来访问图片。

最后

邮件 HTML 为了兼容不同平台、不同形式、不同厂家的邮箱应用、为了兼容老的历史版本邮箱,也出于安全方面的考虑。所以限制颇多。想要写好邮箱 HTML 也是需要费一番功夫的。

相关推荐
极客密码5 小时前
感谢雷总!Mimo大模型价值¥659/月的 MAX 套餐,让我免费领到了!
前端·ai编程·claude
深念Y6 小时前
我明白为什么B站没法在浏览器开直播了——Windows Chrome推流踩坑全记录
前端·chrome·webrtc·浏览器·srs·直播·flv
zhangxingchao6 小时前
AI应用开发七:可以替代 RAG 的技术
前端·人工智能·后端
Sun@happy6 小时前
现代 Web 前端渗透——基础篇(1)
前端·web安全
希冀1236 小时前
【CSS学习第十一篇】
前端·css·学习
隔窗听雨眠7 小时前
doctype、charset、meta如何控制整个渲染流水线
java·服务器·前端
kyriewen7 小时前
写组件文档写到吐?我用AI自动生成Storybook,同事以后直接抄
前端·javascript·面试
excel7 小时前
🧠 Prisma 表名大写 vs SQL 导出小写问题深度解析(附踩坑与解决方案)
前端·后端
周淳APP7 小时前
【前端工程化原理通识:从源头到运行时的理论阐述】
前端·编译·打包·前端工程化
五点六六六8 小时前
你敢信这是非Native页面写出来的渐变效果吗🌝(底层原理解析
前端·javascript·面试