Hello everyone, I am Cat Xiaobai. This article brings me a small project that I have completed in the last few days. I hope it can help students with the same needs.

What have I made?

In fact, very simple: by calling the interface to return the data into a table or text regularly send mail to the specified person! .

What does this function do?

For example, a product owner is very concerned about how many new users register for an app every day. He may not open the system background to check it very often, but just want to check it by email every day. For example, when dealing with other emails, he can take a look at it and get a good idea.

Or, someone in a management system needs to check the information in time when others initiate similar orders and review the order in the first time, which can be reminded by email.

Some people think of SMS alerts, but they are charged.

Technical premise:

As we all know, email content can add HTML strings, and both class and inline styles work, but javascript code cannot be run. So Echarts cannot render dynamically into the message, but we can somehow convert it to Base64 and insert it into the message as an image.

Comb the realization process of the whole procedure

Create a new empty folder and run itnpm initSet up the project.

The main process is as follows:

  1. Create a new HTML file and write the style and layout locally (It can be previewed locally through plug-ins such as live-server), replace the column with a variable after the code snippet to be replaced is deleted as follows:{{body}},{{date}},{{img}}.
  2. throughaxiosThe module gets the interface data, provided here by my colleague.
  3. Concatenate the response string with interface data and replace the corresponding variable in the template, for example:<tr><td>balaba</td><td>lalala</td></tr>Replace the table in the template{{body}}
  4. Render from interface dataechartsAnd screenshot conversion tobase64Replace the SRC attribute of the IMG tag, which is in the replace template{{img}}The variable.
  5. Configure and send mail
  6. Complete the timer function, monitor the time, reach the set time to send.

The whole process looks cumbersome, in fact, when the implementation of the program to write the following steps will be clear, we see step by step how to achieve.

Step 1: Create new HTML and write the style and layout

This step is not much to say, and the email does not need to be fancy, a form and a few words to do it, you can do it in minutes.

After writing the HTML, assign the style code inside the style tag and the code inside the body to a JS file and export it for later use.

Template.js: template. Js: template.

/** * Email template */
const template = `
      

{{date}}XXXX

< th > header field 2 < / th > < th > header field 3 < / th > < thead > < tbody > {{tbody1}} < / tbody > < / table > < img SRC = "{{img1}}" Alt = "" > < / div > < / div > < p style="text-align: left; margin-top: 10px;" > Statistical time: {{datelong}}

'
exports.template = template; Copy the code

Note that the key content is replaced with {{variable name}}, convenient to fill their own content; I have {{tbody1}} of the table content, picture {{img1}} and statistical time {{datelong}}.

Step 2: Get the interface data through the AXIOS module

Create index.js, install AXIos, and encapsulate a function to fetch data from the back-end interface.

Step 3: Concatenate strings and replace variables in the template

Start by importing the template written above in index.js.

const {
    template
} = require("./template.js"); / / template
Copy the code

Loop through the interface data to generate a or < TD > string, roughly as follows:

let html = ' ';/ / store the STR
data.forEach((item, index) = > {
    html += ` <tr>
        <td>${index+1}</td>
        <td>${item['Start time']}${item['End time']}</td>
        <td>${item['Compliance rate']}</td>
    </tr>`
});
Copy the code

After concatenating, we replace the variables in the template with the string substitution function replace() :

The code is as follows:

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

Step 4: Render echarts through the interface and convert the screenshot to Base64 and replace{{img}}The variable.

This step is a lot more difficult than the other steps (for the past), I thought it was just a screenshot at first, there are a lot of mature frameworks on the web, so I easily found a library called Node-echarts. The name and function is aptly not ~ github link

I thought, that simple? But look at downloads week dozens and last maintained time: Published 3 years ago.

It’s also easier to use:


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)
Copy the code

Instead of writing code right away, I looked at its dependencies and found that the core is a library of Node-Canvas that I use. To put it bluntly, this library generates a Canvas object, just like the canvas on the browser side. We have this canvas object so we can use the Echarts library.

Use the following code:

const {
    createCanvas
} = require('canvas')
const echarts = require('echarts')
// Generate the image 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);
    // Returns base64 as a string
    return `data:image/png; base64,` + chart.getDom().toBuffer().toString('base64');
}

Copy the code

In a few lines we have the base64 encoding for the screenshot we want, adding it to the SRC property of img to test that the image will display properly in the browser.

So I have completed the program, ready to go online, I am local Windows system development and testing. However, the company server runs on Linux, so I started my journey to deploy on Linux and ended up crashing. The reason is that Node-Canvas runs on Windows and Linux with different dependencies. Not only NPM install is done.

Different dependencies need to be installed on different systems:

After installing a dependency, the following error is reported:

May be a lack of personal ability, online to find a lot of articles, the whole day, and finally did not solve ~.

After working on this all afternoon, I came up with the idea of using the puppeteer headless browser to load my local HTML file and take screenshots.

Puppeteer is a Node library that provides a high-level API for controlling Chrome or Chromium via the DevTools protocol. Puppeteer runs headless by default, but can be configured to run full (non-headless) Chrome or Chromium.

What to do:

  • Generate screen captures and PDF of the page.
  • Grab SPA (single-page application) and generate pre-rendered content (that is, “SSR” (server-side rendering)).
  • Automated form submission, UI testing, keyboard entry, and more.
  • Create the latest automated test environment. Run tests directly in the latest version of Chrome using the latest JavaScript and browser features.
  • Capture the timeline trace of your site to help diagnose performance problems.
  • Test the Chrome extension.

What we need is to draw echarts in HTML. After drawing, we can use this plug-in to take screenshots and export them as Base64 characters.

So without further ado, let’s get started

Let’s look at the HTML code

Create a random HTML file in your project:

<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');// The second argument is the style theme name. Can don't
    var myChart2 = echarts.init(dom2, 'chalk');// The second argument is the style theme name. Can don't
    // Register the global method, which will be called later
    window.loadEcharts = function (option) {
      myChart1.setOption(option[0]);
      myChart2.setOption(option[1]);
    }
  </script>
</body>
Copy the code

Since my requirement was to generate two diagrams, I created two div containers.

Echarts.js is copied locally and loads faster. Chalk. Js is a theme plugin for Echarts, not required. Echarts-theme.js is called echarts-theme.js and can be found on NPM.

Ignore the window.loadecharts method for now. We’ll explain why we wrote this global method next.

Then implement the core code of screenshots in screenshot.js:

/** * Use the puppeteer headless browser, open the local HTML, and call the method passing in the option parameter to load the echarts graphics and screenshot as base64 *@param {Object} Opt1 Figure 1 option parameter *@param {Object} Opt2 figure 2 option parameter *@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) = > { // Call the HTML global method loadEcharts to pass the argument
        loadEcharts(arr)// Take arguments to execute Windows global methods in HTML
    }, [opt1, opt2]);// Arguments must be passed in here
    console.log('wait for 1 s'); // Wait for echarts graphics to finish rendering
    await page.evaluate(async() = > {await new Promise(function (resolve) {
            setTimeout(resolve, 1000)}); });// Get two separate Echarts screenshots
    const el1 = await page.$('#container1');
    const el2 = await page.$('#container2');
    // Base64 is captured
    let img1 = `data:image/png; base64,` + await el1.screenshot({
        encoding: 'base64'
    });
    let img2 = `data:image/png; base64,` + await el2.screenshot({
        encoding: 'base64'
    });
    console.log('Screenshot taken successfully');
    await browser.close();
    return [img1, img2]
}
// Figure 1 configuration
let option_rate = {
    xAxis: [{
        type: 'category'.boundaryGap: false.data: []}],yAxis: [{
        type: 'value'}].series: [{
        type: 'line'.data: []}};// The configuration in Figure 2
let option_type = {
    legend: {
        top: 'bottom'
    },
    series: [{
        type: 'pie'.radius: ['40%'.'70%'].avoidLabelOverlap: false.data: [].}};// Use the getScreenshot method to put together the Option configuration of echarts from the interface data
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);
}

Copy the code

Description:

  1. opt1andopt2, respectively,echartstheoptionConfiguration item parameters.
  2. {args: ['--no-sandbox', '--disable-setuid-sandbox']}Parameter cannot be omitted, otherwise inlinuxAn error is reported in.
  3. Page. Goto (' http://127.0.0.1:3001/html/index.html ')localhtmlNeed to usehttpService access, available withnginxorexpressWait to build one. I used itexpressBuild (port 3001).
  4. page.evaluate(()=>{},option)Execute the page method, which you can get in thereHTML pageThe Windows global method ofThe above mentionedtheloadEchartsNotice the second parameteroptionYou need to pass in ‘.
  5. page.evaluate()Method makes the program wait 1s, waitechartsTake screenshots after rendering.
  6. const el1 = await page.$('#container1');Is to get specificdomFinished the screenshot, so I split it up here twice.
  7. After the transformationbase64Strings need to be prefixed:data:image/png; base64,To be asimgthesrcAttribute usage.

HTML and screenshot.js are all ready, just need a mail sending module nodemail.js

The nodeMailer module is chosen here, which is actually quite simple to use. Click on this article to learn more.

The nodemail.js code is as follows:

// Import the module nodemailer
const nodemailer = require('nodemailer')

const {
    timeFormat
} = require("./tools.js"); // Utility functions
const config = {
    host: 'smtp.exmail.qq.com'./ / port
    port: 465.secureConnection: true.auth: {
        // Sender email account
        user: '[email protected]'.// Sender's email authorization code
        pass: 'xxxx'}}const mail = {
    // Sender email 'nickname < sender email >'
    from: 'Automatic detection program 
      
       '
      @qq.com>./ / theme
    subject: 'XXXXXX Daily Statistics _'.// The recipient's email can be other email, not necessarily qq email
    to: '[email protected]'.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()) + '(auto send)'
        }, function (error, info) {
            if (error) {
                rej('Send failed' + error);
                return console.log(error);
            }
            transporter.close()
            console.log('mail sent:', info.response) res(); })})}Copy the code

Once the mail module is configured, let’s just import the entire module in the entry file.

const {
    sendEmail
} = require("./nodemailer.js"); // Send mail module

Copy the code

Ok, we have all the modules and tools in place, but there is one small requirement: emails are not sent immediately, but timed. My requirement is to deliver it every day at 9:00 sharp.

My timing function looks like this:

let SENDWeekDAY = -1; // negative daily positive number 1-7 Monday to Sunday
let SENDHOUR = 9; // Send it at 9:00
let SENDMINUTES = 00; / / minute
let sendList = {};

/** * Determine whether the sending time is reached, ensure that one data per day *@returns * /
function isSendTime() {
    const now = new Date(a);let sendDateStr = timeFormat(now);
    if ((now.getDay() == SENDWeekDAY || SENDWeekDAY == -1) && now.getHours() == SENDHOUR && now.getMinutes() == SENDMINUTES) {
        if(! sendList[sendDateStr]) { sendList[sendDateStr] =true;
            return true; }}}Copy the code

SENDWeekDAY, SENDHOUR, SENDMINUTES, you can simply set the sending time. You can implement a timing function yourself.

Ok, we have all the functions we need, the core code of the import and export file is posted below:

In the entry file index.js:

const { GET, timeFormat, parseTime, } = require("./tools.js"); Const {sendEmail} = require("./ nodemail.js "); Const {template} = require("./template.js"); // the template introduces const {getImgs,} = require("./ screenshot.js ")// the module introduces let SENDWeekDAY = -1; // negative daily positive number 1-7 Monday to Sunday let SENDHOUR = 9; // send let SENDMINUTES = 00; // min let sendList = {}; const express = require('express'); // load express const path = require('path'); // Open a local static service const app = express(); app.use('/html', express.static(path.join(__dirname, 'html'))); app.listen(3001); Function () {//test(); SetInterval (() => {if (isSendTime()) {// Test whether to send test(); Returns */ function isSendTime() {const now = new Date(); 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 is a wrapped request function based on axios Promise. All ([GET(url_rate), GET(url_type_calc)]).then(([rateArr, typesArr]) => { let html1 = ''; // table 1 string let html2 = ''; // Table 2 string let option1_series = []; // Let option2_series = []; // Series data for Figure 2 //.... Replace ('{{tbody1}}', html1).replace('{{tbody2}}', 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) = > {/ / replace the picture content = content. the replace (' {{img1}} 'img1). Replace (' {{img2}}, img2); return content; }); }).then(res => { send(res); / / email}). The catch (err = > {the console. The log (' server error, err); Function send(content) {content &&sendemail (content).then(() => {console.log('-- '+ timeFormat(new) Date()) + 'Report sent successfully '); })}Copy the code

The above code has been deleted and cannot be run directly. You need to clarify the main ideas and realize it by yourself.

Linux again encountered problems installing Puppeteer

Some people have the same problem in the issue, but they have not been well solved. The difficulty lies in the different systems and versions of each person, so the errors reported are often different.

Then I found a big guy’s blog here

My server environment is Centos7

NPM install puppeteer, and a number of dependencies need to be installed to enable puppeteer to run properly.

X86_64 libxcursor.x86_64 libxdamage.x86_64 libxext.x86_64 libxi.x86_64 yum install pango. 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 -yCopy the code

Again, the initialization needs to add parameters{args: ['--no-sandbox', '--disable-setuid-sandbox']}

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

This may not solve your problem, because our system may be different, you can find ~ in the issue

I think the screenshot is ok now. Indeed, the screenshot is ok, but it’s not that simple

There’s something wrong with the font again

Linux does not support Chinese fonts by default, so the Chinese characters in the echarts will show garbled characters. The problem is one after another. Look at the picture:

Now that we have all come, let’s finish. Generally speaking, it’s not difficult. I won’t expand it here. But there is a point to note that when copying the local font, if you find a font installed or not, then change a font such as: Microsoft Yahei, Song Typeface, simplified, it is best not to choose the English name of the font. Click here to learn how to install Chinese fonts for Linux

After installing the font, we are done. The font still feels a little strange, but now we can meet our needs.

How do I keep executing after closing the execution window?

As we all know in Windows, when we close the CMD window, Node automatically closes the service. How do you keep the Node service running on Linux? Click on this article to learn about pM2 installation

Pm2 allows the Node application to continue running without closing the window. Unless its command is used to execute the shutdown operation.

Installation:

npm install pm2 -g
Copy the code

Start the Node service:

First of all bycdCommand to enter the root directory of your project.

pm2 start index.js // Run the entry file index.js or your own app.js
// or --name with name for easy distinction
pm2 start index.js --name send2. 0
Copy the code

View a list of running programs:

pm2 list
Copy the code

View node logs:

Pm2 log ID (program ID or name)Copy the code

Delete the specified program:

pm2 deleteId (program ID or name)Copy the code

Restart program:

Pm2 restart ID (program ID or name)Copy the code

The above.

Feel every problem can be an article, because this is a niche demand, so the online related articles are not many, encounter problems only their own documents, or change the way of thinking maybe smooth.

This article hopes to help you ~

Ken, please don’t forget to give me a thumbs-up, comments, favorites.

Past highlights:

Module. Exports, exports, and exports are all exports. What’s the difference?

2. [Bao Zhen] My first Webpack optimization, first screen rendering from 9s to 1s