关于node.js:实战linux环境用node自动发送邮件含echarts图赶紧收藏

65次阅读

共计 11598 个字符,预计需要花费 29 分钟才能阅读完成。

大家好,我是猫小白,本文带来我最近几天闭关修炼,游走与各大技术论坛最终实现的一个小我的项目,心愿能帮到有雷同需要的同学。

我做了个什么货色呢?

其实很简略:通过调用接口把返回的数据转换成表格或者文字定时发送邮件到指定的人!

这个性能干什么用呢?

举个栗子:某位产品负责人很关怀上线的 app 每天有多少注册的新用户,他可能不会常常关上零碎后盾去查看,只是想每天通过邮件查看,比方在解决其它邮件的时候,顺便看一下,做到成竹在胸。

又或者,某个管理系统的人,须要在他人发动相似订单的时候及时核查信息并且第一工夫审核该订单,那能够通过邮件的形式揭示它。

有的人会想到短信揭示,然而毕竟是免费的。

技术前提:

家喻户晓、邮件内容是能够退出 html 字符串的,并且 class 款式和行内款式都能失效,然而无奈运行 javascript 代码。所以 echarts 无奈动静渲染到邮件中,不过咱们能够通过某些伎俩转换成 base64,把它作为图片插入到邮件中。

梳理下整个程序的实现流程

在之前新建一个空文件夹,运行 npm init 把我的项目搭建起。

主流程如下:

  1. 新建一个 html 文件,先在本地把款式和布局写好(能够通过 live-server 等插件本地预览),须要替换的代码片段删除后用变量代替列如:{{body}}{{date}}{{img}}
  2. 通过 axios 模块获取接口数据,这里是我共事提供的。
  3. 通过接口数据拼接响应的字符串并替换模板中的对应变量,例如:<tr><td>balaba</td><td>lalala</td></tr>替换模板中表格的{{body}}
  4. 通过接口数据渲染出 echarts 并截图转换为 base64,替换 img 标签的 src 属性,也就是替换模板中的{{img}} 变量。
  5. 配置并发送邮件
  6. 实现定时器函数,监听工夫,达到设定的工夫才发送。

整个流程看起来比拟繁琐,其实在实现程序的时候写着写着前面的步骤就清晰了,咱们一步一步看如何实现的。

第一步:新建 html,写好款式和布局

这一步没啥好说的,邮件又不须要多花哨,一个表格一些文字就搞定了,大家分分钟就搞定了。

写好 html 后,把 style 标签外面的款式代码和 body 外面的代码赋值到一个 js 中导出备用。

取名:template.js
大抵内容如下(被我删减了一些,看下意思就行):

/**
 * 邮箱模板
 */
const template = `<div>
<style>
    
    p {
        font-size: 13px;
        line-height: 27px;
        padding: 0;
        margin: 0;

        color: #333;
    }

    .content {
        display: flex;
        /* justify-content: center; */
        flex-direction: column;
        align-items: left;
    }

    .img {margin-top: 10px;}
    table {border: 1px solid rgb(206, 206, 206);
        border-collapse: collapse;
        width: 100%;
        max-width: 500px;
        margin-bottom: 16px;
    }
</style>
    <p> 各位领导、共事好,</p>
    <p style="text-indent:30px">{{date}}XXXX 统计信息如下:</p>
    <div class="content">
        <div>
            <table>
                <thead>
                    <th> 表头字段 1 </th>
                    <th> 表头字段 2 </th>
                    <th> 表头字段 3 </th>
                </thead>
                <tbody>
                    {{tbody1}}
                </tbody>
            </table>
           <img src="{{img1}}" alt="">
        </div>
    </div>
    <p style="text-align: left;margin-top: 10px;">
        统计工夫:{{datelong}}
    </p>
`
exports.template = template;

留神其中的要害内容用 {{变量名}} 的形式替换,不便填充本人的内容; 我这里有表格内容的 {{tbody1}}、图片{{img1}} 还有统计工夫{{datelong}}

第二步:通过 axios 模块获取接口数据

新建 index.js,装置 axios、封装一个函数去拿后端接口的数据,这里老司机都懂,就不再赘述。

第三步:拼接字符串并替换模板中的变量

首先在 index.js 中引入下面写好的模板。

const {template} = require("./template.js"); // 模板

通过循环接口数据,生成 <tr> 或者 <td> 字符串,大抵代码如下:

let html = '';// 贮存 str
data.forEach((item, index) => {
    html += ` <tr>
        <td>${index+1}</td>
        <td>${item['开始工夫']} 至 ${item['完结工夫']}</td>
        <td>${item['合规率']}</td>
    </tr>`
});

拼接好后咱们替换模板中的变量,通过字符串替换函数replace()

代码如下:

let content = template.replace('{{tbody1}}', html)
     .replace('{{tbody2}}', html2)
     .replace('{{datelong}}', parseTime(new Date()))
     .replace('{{date}}', timeFormat(new Date(), '-'));

第四步:通过接口渲染出 echarts 并截图转换为 base64,并替换 {{img}} 变量。

这一步比起其余步骤来说,要艰难很多(对之前来说),开始我想不就是截个图吗,网上应该有很多比拟成熟的框架吧,于是我就轻易的搜到了一个叫做 node-echarts 的库。这个名字和性能很贴切是不是~ github 链接

心想,这么简略?然而一看下载量周几十和上次保护工夫:Published 3 years ago

应用也比较简单:


var node_echarts = require('node-echarts');
var config = {
    width: 500, // Image width, type is number.
    height: 500, // Image height, type is number.
    option: {}, // Echarts configuration, type is Object.
    //If the path  is not set, return the Buffer of image.
    path:  '', // Path is filepath of the image which will be created.
    enableAutoDispose: true  //Enable auto-dispose echarts after the image is created.
}
node_echarts(config)

然而我并没有立刻开始写代码,而是看下它的依赖,发现外围是应用的 node-canvas 的一个库。说白了这个库能够生成一个 canvas 对象,就像在浏览器端的 canvas 一样。咱们有这个 canvas 对象就能够应用 echarts 的库了。

应用代码如下:

const {createCanvas} = require('canvas')
const echarts = require('echarts')
// 生成图片 base64
function generateImage(options, width = 800, height = 600, theme = 'chalk') {const canvas = createCanvas(width, height)
    const ctx = canvas.getContext('2d')
    ctx.font = '12px'
    echarts.setCanvasCreator(() => canvas)
    const chart = echarts.init(canvas, theme)
    options.animation = false
    chart.setOption(options);
    // 返回 base64 字符串
    return `data:image/png;base64,` + chart.getDom().toBuffer().toString('base64');
}

是不是也挺简略的,几行就曾经生成了咱们想要的截图 base64 编码,退出到 imgsrc属性中,测试在浏览器中能够失常显示出图片。

等我曾经实现了程序,筹备上线了,我本地是用 windows 零碎开发测试的。然而公司服务器是 linux 零碎的,于是开始了我在 linux 零碎部署的旅程,后果翻车了~
起因是 node-canvas 运行在 windows 零碎和 linux 所需的依赖是不同的。不仅仅是 npm install 就完事了。

插件官网外面有说到,在不同零碎中须要先装置不同的依赖:

依照文档装置后依赖后,还是报错:

可能是集体能力有余,网上找了许多文章,整了一天,最终还是没解决~。

搞了一下午之后,我想到换一种思路,应用 puppeteer 无头浏览器去加载我本地 html 文件,而后截图即可。

Puppeteer 是一个 Node 库,它提供了一个高级 API 来通过 DevTools 协定管制 Chrome 或 Chromium。Puppeteer 默认运行[无头],但能够配置为运行残缺(非无头)Chrome 或 Chromium。

能够做什么:

  • 生成页面的屏幕截图和 PDF。
  • 抓取 SPA(单页应用程序)并生成预渲染内容(即“SSR”(服务器端渲染))。
  • 自动化表单提交、UI 测试、键盘输入等。
  • 创立最新的自动化测试环境。应用最新的 JavaScript 和浏览器性能间接在最新版本的 Chrome 中运行测试。
  • 捕捉您网站的工夫线轨迹以帮忙诊断性能问题。
  • 测试 Chrome 扩大程序。

咱们的需要就是把 echartshtml中绘制好,等绘制实现后用这个插件截图,导出为 base64 字符即可。

话不多说,那咱们开始吧

先看下 html 代码

本人在我的项目中轻易建一个 html 文件:

<body style="height: 1300px; margin: 0">
  <div id="container1" style="width: 800px;height: 600px;"></div>
  <div id="container2" style="width: 800px;height: 600px;margin-top: 50px;"></div>

  <script type="text/javascript" src="./js/echarts.js"></script>
  <script type="text/javascript" src="./js/chalk.js"></script>

  <script type="text/javascript">
    var dom1 = document.getElementById("container1");
    var dom2 = document.getElementById("container2");
    var myChart1 = echarts.init(dom1, 'chalk');// 第二个参数是款式主题名称。能够不要
    var myChart2 = echarts.init(dom2, 'chalk');// 第二个参数是款式主题名称。能够不要
    // 注册全局办法,前面会调用到
    window.loadEcharts = function (option) {myChart1.setOption(option[0]);
      myChart2.setOption(option[1]);
    }
  </script>
</body>

因为我的需要是要生成 2 张图篇,所以创立了 2 个 div 容器。

echarts.js复制到本地,加载速度快一些。chalk.jsecharts 的主题插件,非必须。叫做 echarts-theme.jsnpm下面可搜到,应用办法也简略。

先疏忽 window.loadEcharts办法。咱们上面会介绍为什么要写这个全局办法。

其次在 screenShot.js 中实现 截图 的外围代码:

/**
 * 通过 puppeteer 无头浏览器,关上本地 html,调用办法传入 option 参数 加载 echarts 图形并截图为 base64
 * @param {Object} opt1  图形 1 的 option 参数
 * @param {Object} opt2  图形 2 的 option 参数
 * @returns 
 */
async function getScreenshot(opt1, opt2) {const browser = await puppeteer.launch({args: ['--no-sandbox', '--disable-setuid-sandbox']})
    // const browser = await puppeteer.launch();
    const page = await browser.newPage();
    await page.goto('http://127.0.0.1:3001/html/index.html');
    // await page.goto(path.resolve(__dirname, './html/line-simple.html'));
    await page.evaluate((arr) => { // 调用 html 全局办法 loadEcharts 把参数传过来
        loadEcharts(arr)// 承受参数 执行 html 中的 windows 全局办法
    }, [opt1, opt2]);// 参数必须在这里传入
    console.log('期待 1s'); // 期待 echarts 图形渲染实现
    await page.evaluate(async () => {await new Promise(function (resolve) {setTimeout(resolve, 1000)
        });
    });
    // 离开获取 2 个 echarts 截图
    const el1 = await page.$('#container1');
    const el2 = await page.$('#container2');
    // 截图成 base64
    let img1 = `data:image/png;base64,` + await el1.screenshot({encoding: 'base64'});
    let img2 = `data:image/png;base64,` + await el2.screenshot({encoding: 'base64'});
    console.log('截图胜利');
    await browser.close();
    return [img1, img2]
}
// 图 1 的配置
let option_rate = {
    xAxis: [{
        type: 'category',
        boundaryGap: false,
        data: []}],
    yAxis: [{type: 'value'}],
    series: [{
        type: 'line',
        data: []}]
};
// 图 2 的配置
let option_type = {
    legend: {top: 'bottom'},
    series: [{
        type: 'pie',
        radius: ['40%', '70%'],
        avoidLabelOverlap: false,
        data: [],}]
};
// 对外裸露接口 依据接口数据 拼接 echarts 的 option 配置,调用截图办法 getScreenshot
module.exports.getImgs = async function (results1, results2) {let xAxisArr = [];
    let seriesArr = [];
    results1.forEach(result => {xAxisArr.push(result.name)
        seriesArr.push(result.value)
    });
    option_rate.xAxis[0].data = xAxisArr;
    option_rate.series[0].data = seriesArr;

    option_type.series[0].data = results2;
    return await getScreenshot(option_rate, option_type);
}

阐明:

  1. opt1opt2 别离是 echartsoption配置项参数。
  2. {args: ['--no-sandbox', '--disable-setuid-sandbox']}参数不能省略,不然在 linux 中会报错。
  3. page.goto('http://127.0.0.1:3001/html/index.html') 本地 html 须要用 http 服务拜访,能够用 nginx 或者 express 等搭建一个,我采纳的 express 搭建(3001 端口)。
  4. page.evaluate(()=>{},option)执行页面办法,外面能够拿到 html 页面 的 windows 全局办法也就是 下面提到 loadEcharts,留神第二个参数 option 须要传入 `。
  5. page.evaluate()办法让程序期待 1s,期待 echarts 渲染实现后再截图。
  6. const el1 = await page.$('#container1');是获取特定的 dom 实现截图,所以我这里离开截了 2 次。
  7. 转换后 base64 字符串须要加上前缀:data:image/png;base64,能力作为 imgsrc属性应用。

htmlscreenShot.js 都筹备好了,还差一个邮件发送模块 nodemailer.js

这里抉择用 nodemailer 模块,应用起来其实很简略。能够点这篇文章理解详情。

nodemailer.js代码如下:

// 引入模块 nodemailer
const nodemailer = require('nodemailer')

const {timeFormat} = require("./tools.js"); // 工具函数
const config = {
    host: 'smtp.exmail.qq.com',
    // 端口
    port: 465,
    secureConnection: true,
    auth: {
        // 发件人邮箱账号
        user: 'xxxxxxxxx@qq.com',
        // 发件人邮箱的受权码
        pass: 'xxxx'
    }
}
const mail = {
    // 发件人 邮箱  '昵称 < 发件人邮箱 >'
    from: '自动检测程序 <xxxxxxxxx@qq.com>',
    // 主题
    subject: 'XXXXXX 每日统计_',
    // 收件人 的邮箱 能够是其余邮箱 不肯定是 qq 邮箱
    to: 'xxxxxxxx@163.com',
    html: '',
}

exports.sendEmail = function (content) {const transporter = nodemailer.createTransport(config);
    if (content) mail.html = content;
    return new Promise((res, rej) => {
        transporter.sendMail({
            ...mail,
            subject: mail.subject + timeFormat(new Date()) + '(主动发送)'
        }, function (error, info) {if (error) {rej('发送失败' + error);
                return console.log(error);
            }
            transporter.close()
            console.log('mail sent:', info.response)
            res();})
    })
}

邮件模块配置好了,咱们在入口文件中引入整个模块就好。

const {sendEmail} = require("./nodemailer.js"); // 发送邮件模块

好了,该有的模块和工具咱们都写好了,然而还有一个小需要:邮件不是执行立刻发送的,而是 定时发送
我的需要是 每天 9 点整发送

我的定时函数是这样:

let SENDWeekDAY = -1; // 正数每天  负数 1 -7 周一到周日
let SENDHOUR = 9; // 9 点发送
let SENDMINUTES = 00; // 分钟
let sendList = {};

/**
 * 判断是否达到发送工夫,保障每天发送一条数据
 * @returns 
 */
function isSendTime() {const now = new Date();
    let sendDateStr = timeFormat(now);
    if ((now.getDay() == SENDWeekDAY || SENDWeekDAY == -1) && now.getHours() == SENDHOUR && now.getMinutes() == SENDMINUTES) {if (!sendList[sendDateStr]) {sendList[sendDateStr] = true;
            return true;
        }
    }
}

SENDWeekDAY、SENDHOUR、SENDMINUTES、能够简略配置发送的机会。你能够本人实现一个定时函数。

好了,咱们所有须要的性能都曾经有了,上面贴出入口文件的外围代码:

在入口文件 index.js 中:

const {
    GET,
    timeFormat,
    parseTime,
} = require("./tools.js"); // 工具函数 可本人封装

const {sendEmail} = require("./nodemailer.js"); // 发送邮件模块

const {template} = require("./template.js"); // 模板引入
const {getImgs,} = require("./screenShot.js")// 截图模块引入

let SENDWeekDAY = -1; // 正数每天  负数 1 -7 周一到周日
let SENDHOUR = 9; // 9 点发送
let SENDMINUTES = 00; // 分钟
let sendList = {};

const express = require('express');// 加载 express 
const path = require('path');
// 开启一个本地动态服务
const app = express();
app.use('/html', express.static(path.join(__dirname, 'html')));
app.listen(3001);

// 主函数
(function () {//test();// 测试应用
   setInterval(() => {if (isSendTime()) {// 检测是否发送
            test();// 发送}
    }, 45 * 1000)
})()

/**
 * 判断是否达到发送工夫,保障每天发送一条数据
 * @returns 
 */
function isSendTime() {const now = new Date();
    let sendDateStr = timeFormat(now);
    if ((now.getDay() == SENDWeekDAY || SENDWeekDAY == -1) && now.getHours() == SENDHOUR && now.getMinutes() == SENDMINUTES) {if (!sendList[sendDateStr]) {sendList[sendDateStr] = true;
            return true;
        }
    }
}

/**
 * 申请数据组装模板
 */
function test() {
    //GET 是封装的申请函数基于 axios
    Promise.all([GET(url_rate), GET(url_type_calc)]).then(([rateArr, typesArr]) => {
        let html1 = ''; // 表格 1 字符串
        let html2 = ''; // 表格 2 字符串
        let option1_series = [];// 图表 1 的 series 数据
        let option2_series = [];// 图表 2 的 series 数据
       
        
        //.... 省略数据处理和 html 代码拼接
        
        // 用字符串 replace 办法替换变量
        let content = template.replace 办法替换变量('{{tbody1}}', html1)
            .replace('{{tbody2}}', html2)
            .replace('{{datelong}}', parseTime(new Date()))
            .replace('{{date}}', timeFormat(new Date(), '-'));
        // 获取图表 Base64 img
        return getImgs(option1_series, option2_series).then(([img1, img2]) => {
             // 替换图片
            content = content.replace('{{img1}}', img1).replace('{{img2}}', img2);
            return content;
        });
    }).then(res => {send(res); // 发送邮件
    }).catch(err => {console.log('服务器出错', err);
    })
}

// 发送邮件
function send(content) {content && sendEmail(content).then(() => {console.log('---' + timeFormat(new Date()) + '报告发送胜利');
    })
}

下面代码有删减,无奈间接运行,须要你理清次要思路本人实现。

linux 装置 Puppeteer 再次遇到问题

本人依照官网文档装置,死活运行不起,报错,在 issue 外面也有人有雷同的问题,但都没有很好的解决, 难点在于每个人的零碎不同、版本不同,报的谬误往往是不同的。

起初我找到一个大佬的博客 点这里

我的服务器环境是 centos7

简略来说就是npm install puppeteer,当前还须要装置一些列依赖能力失常运行。

# 依赖库
yum install pango.x86_64 libXcomposite.x86_64 libXcursor.x86_64 libXdamage.x86_64 libXext.x86_64 libXi.x86_64 libXtst.x86_64 cups-libs.x86_64 libXScrnSaver.x86_64 libXrandr.x86_64 GConf2.x86_64 alsa-lib.x86_64 atk.x86_64 gtk3.x86_64 -y

再者初始化的时候须要加上参数 {args: ['--no-sandbox', '--disable-setuid-sandbox']}

const browser = await puppeteer.launch({args: ['--no-sandbox', '--disable-setuid-sandbox']});

这个不肯定能解决你的问题,因为咱们零碎可能不同,能够到 issue 外面找~

我想这下截图看到没问题了吧,的确截图没问题了,不过没那么简略~

字体又出问题了

linux 上默认是不反对中文字体的,所以截的 echarts 图外面中文会显示乱码。这问题堪称是一波未平一波又起啊~
看图:

既然来都来了,那就整到底,总体来说不难,这里就不开展说了。但有一点要留神,复制本机字体的时候,如果发现一个字体装置好了还是不行,那就换一个字体例如:微软雅黑、宋体、简体,最好不要选英文名称的字体。点这里学习 linux 装置中文字体

装置好字体后终于功败垂成,尽管感觉字体还是有点奇怪,不过当初这样曾经能够满足需要了。

敞开执行窗口后,如何放弃执行?

家喻户晓,windows 中咱们敞开 cmd 窗口后,node 执行的服务就主动敞开 。那在linux 上如何放弃 node 服务始终执行呢?
点击这篇文章,学习 pm2 的装置

pm2 就是能够让 node 程序始终运行,敞开窗口也不会敞开。除非应用它的命令执行敞开操作。

装置:

npm install pm2 -g

启动 node 服务:

首先通过cd 命令进入本人我的项目的根目录。

pm2 start index.js // 运行入口文件 index.js 或者本人的 app.js
// 或者 --name 加名称不便区别
pm2 start index.js --name send2.0

查看运行的程序列表:

pm2 list

查看 node 日志:

pm2 log id(程序 id 或者 name)

删除指定程序:

pm2 delete id(程序 id 或者 name)

重启程序:

pm2 restart id(程序 id 或者 name)

以上。

感觉每一个问题都能够出一篇文章了,因为这是一个小众需要,所以网上相干文章并不是很多,遇到问题只有本人多翻文档,或者换个思路说不定就顺畅了。

这篇文文章心愿能帮到你~

肯请各位大佬, 不要忘了给我 点赞 评论 珍藏

往期精彩:

1.【混同系列】一问:module.exports、exports、export 都是导出,有何区别?

2.【包真】我的第一次 webpack 优化,首屏渲染从 9s 到 1s

正文完
 0