邮件中加入 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 也是需要费一番功夫的。

相关推荐
web行路人3 分钟前
React中类组件和函数组件的理解和区别
前端·javascript·react.js·前端框架
超雄代码狂25 分钟前
ajax关于axios库的运用小案例
前端·javascript·ajax
长弓三石33 分钟前
鸿蒙网络编程系列44-仓颉版HttpRequest上传文件示例
前端·网络·华为·harmonyos·鸿蒙
小马哥编程35 分钟前
【前端基础】CSS基础
前端·css
嚣张农民1 小时前
推荐3个实用的760°全景框架
前端·vue.js·程序员
周亚鑫1 小时前
vue3 pdf base64转成文件流打开
前端·javascript·pdf
Justinc.1 小时前
CSS3新增边框属性(五)
前端·css·css3
neter.asia2 小时前
vue中如何关闭eslint检测?
前端·javascript·vue.js
~甲壳虫2 小时前
说说webpack中常见的Plugin?解决了什么问题?
前端·webpack·node.js
嚣张农民2 小时前
JavaScript中Promise分别有哪些函数?
前端·javascript·面试