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

我做了个什么货色呢?

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

这个性能干什么用呢?

举个栗子:某位产品负责人很关怀上线的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 = '';//贮存strdata.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')// 生成图片 base64function 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配置,调用截图办法getScreenshotmodule.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代码如下:

//引入模块 nodemailerconst 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