本文首發於個人部落格:Vince'Blog
專案原始碼:NodeMail,歡迎star,說不定哪天脫單了就能用到了
寫在前面
自從用郵箱註冊了很多賬號後,便會收到諸如以下類似的郵件,剛開始還以為是一張圖片,後來仔細一看不是圖片呀,好像還是HTML呀,於是好奇寶寶我Google一下,查閱多篇資料後總結出怎麼用前端知識和Node做一個這樣的“郵件網頁”。
確認主題
知道怎麼實現功能後,思考著我該寫什麼主題呢,用一個HTML模板隨便給小夥伴們發個郵件炫個技?不行,作為一個很cool的程式設計師怎麼能這麼low呢,最近天氣變化幅度大,溫度捉摸不定,女朋友總是抱怨穿少了又冷穿多了又熱,嗨呀,要不我就寫個每天定時給寶寶傳送天氣預報的郵件,另外想起寶寶喜歡看ONE·一個這個APP上的每日更新,要不發天氣預報的同時,再附贈一個“ONE的每日訂閱”?機智又浪漫,開始搬磚~
劇透
本來是想最後放效果圖的,怕你們看到一半就沒興趣了,就在前面劇透一下我最後做出來的效果圖吧~
待解決的問題
1. 如何獲取天氣預報和ONE上的data?
答:獲取data有兩種方法,第一種方法是獲取天氣預報和ONE的API,第二種是用node爬蟲獲取天氣預報和ONE網頁的資訊。後來找了下,發現ONE並沒有API介面,為了讓兩者統一,於是決定使用node上的一個外掛叫cheerio
,配合superagent
能夠很方便地爬取網頁上的資訊。
2. 如何做出HTML的這種郵件?
答:之前學過一段時間的express這個框架,接觸到模版引擎這個概念,傳入data便可獲得html檔案,再結合node的fs模組,獲取到這個html檔案,便可以結合node的郵件外掛傳送HTML郵件啦!
3. 如何用node傳送郵件?
感謝無私的開源開發者,開發了一款傳送郵件的Node外掛nodemailer
,相容主流的Email廠商,只需要配置好郵箱賬號和smtp授權碼,便可以用你的郵箱賬號在node指令碼上發檔案,很cool有沒有~
4. 如何做到每日定時傳送?
其實可以通過各種hack的方式寫這麼一個定時任務,但是既然node社群有這個定時的輪子,那我們直接用就好了,node-schedule
是一個有著各種配置的定時任務發生器,可以定時每個月、每個禮拜、每天具體什麼時候執行什麼任務,這正符合每天早晨定時給寶寶傳送郵件的需求。
一切準備就緒,開始做一次浪漫的程式設計師
編寫程式碼
網頁爬蟲
這裡我們使用到superagent
和cheerio
組合來實現爬蟲:
- 分析網頁DOM結構,如下圖所示:
- 用superagent來獲取指定網頁的所有DOM:
superagent.get(URL).end(function(err,res){
//
}
複製程式碼
- 用cheerio來篩選superagent獲取到的DOM,取出需要的DOM
imgUrl:$(todayOne).find('.fp-one-imagen').attr('src'),
type:$(todayOne).find('.fp-one-imagen-footer').text().replace(/(^\s*)|(\s*$)/g, ""),
text:$(todayOne).find('.fp-one-cita').text().replace(/(^\s*)|(\s*$)/g, "")
複製程式碼
以下就是爬取ONE的程式碼,天氣預報網頁也是一個道理:
const superagent = require('superagent'); //傳送網路請求獲取DOM
const cheerio = require('cheerio'); //能夠像Jquery一樣方便獲取DOM節點
const OneUrl = "http://wufazhuce.com/"; //ONE的web版網站
superagent.get(OneUrl).end(function(err,res){
if(err){
console.log(err);
}
let $ = cheerio.load(res.text);
let selectItem=$('#carousel-one .carousel-inner .item');
let todayOne=selectItem[0]; //獲取輪播圖第一個頁面,也就是當天更新的內容
let todayOneData={ //儲存到一個json中
imgUrl:$(todayOne).find('.fp-one-imagen').attr('src'),
type:$(todayOne).find('.fp-one-imagen-footer').text().replace(/(^\s*)|(\s*$)/g, ""),
text:$(todayOne).find('.fp-one-cita').text().replace(/(^\s*)|(\s*$)/g, "")
};
console.log(todayOneData);
})
複製程式碼
EJS模版引擎生成HTML
通過爬蟲獲取到了資料,那麼我們就能夠通過將date輸入到EJS渲染出HTML,我們在目錄下建立js指令碼和ejs模版檔案:
- app.js
const ejs = require('ejs'); //ejs模版引擎
const fs = require('fs'); //檔案讀寫
const path = require('path'); //路徑配置
//傳給EJS的資料
let data={
title:'nice to meet you~'
}
//將目錄下的mail.ejs獲取到,得到一個模版
const template = ejs.compile(fs.readFileSync(path.resolve(__dirname, 'mail.ejs'), 'utf8'));
//將資料傳入模版中,生成HTML
const html = template(data);
console.log(html)
複製程式碼
- mail.ejs
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<h1>
<%= title %>
</h1>
</body>
</html>
複製程式碼
用Node傳送郵件
這裡我們可以傳送純text也可以傳送html,注意的是郵箱密碼不是你登入郵箱的密碼,而是smtp授權碼,什麼是smtp授權碼呢?就是你的郵箱賬號可以使用這個smtp授權碼在別的地方發郵件,一般smtp授權碼在郵箱官網的設定中可以看的到,設定如下注釋。
const nodemailer = require('nodemailer'); //傳送郵件的node外掛
let transporter = nodemailer.createTransport({
service: '126', // 傳送者的郵箱廠商,支援列表:https://nodemailer.com/smtp/well-known/
port: 465, // SMTP 埠
secureConnection: true, // SSL安全連結
auth: { //傳送者的賬戶密碼
user: '賬戶@126.com', //賬戶
pass: 'smtp授權碼', //smtp授權碼,到郵箱設定下獲取
}
});
let mailOptions = {
from: '"傳送者暱稱" <地址@126.com>', // 傳送者暱稱和地址
to: 'like@vince.studio', // 接收者的郵箱地址
subject: '一封暖暖的小郵件', // 郵件主題
text: 'test mail', //郵件的text
// html: html //也可以用html傳送
};
//傳送郵件
transporter.sendMail(mailOptions, (error, info) => {
if (error) {
return console.log(error);
}
console.log('郵件傳送成功 ID:', info.messageId);
});
複製程式碼
Node定時執行任務
這裡我們用到了node-schedule
來定時執行任務,示例如下:
var schedule = require("node-schedule");
//1. 確定的時間執行
var date = new Date(2017,12,10,15,50,0);
schedule.scheduleJob(date, function(){
console.log("執行任務");
});
//2. 秒為單位執行
//比如:每5秒執行一次
var rule1 = new schedule.RecurrenceRule();
var times1 = [1,6,11,16,21,26,31,36,41,46,51,56];
rule1.second = times1;
schedule.scheduleJob(rule1, function(){
console.log("執行任務");
});
//3.以分為單位執行
//比如:每5分種執行一次
var rule2 = new schedule.RecurrenceRule();
var times2 = [1,6,11,16,21,26,31,36,41,46,51,56];
rule2.minute = times2;
schedule.scheduleJob(rule2, function(){
console.log("執行任務");
});
//4.以天單位執行
//比如:每天6點30分執行
var rule = new schedule.RecurrenceRule();
rule.dayOfWeek = [0, new schedule.Range(1, 6)];
rule.hour = 6;
rule.minute =30;
var j = schedule.scheduleJob(rule, function(){
    console.log("執行任務");
getData();
});
複製程式碼
思路與步驟
當所有的問題都解決後,便是開始結合程式碼成一段完整的程式,思路很簡單,我們來逐步分析:
- 由於獲取資料是非同步的,並且不能判斷出哪個先獲取到資料,這個是可以將獲取資料的函式封裝成一個Promise物件,最後在一起用Promise.all來判斷所有資料獲取完畢,再傳送郵件
// 其中一個資料獲取函式,其他的也是類似
function getOneData(){
let p = new Promise(function(resolve,reject){
superagent.get(OneUrl).end(function(err, res) {
if (err) {
reject(err);
}
let $ = cheerio.load(res.text);
let selectItem = $("#carousel-one .carousel-inner .item");
let todayOne = selectItem[0];
let todayOneData = {
imgUrl: $(todayOne)
.find(".fp-one-imagen")
.attr("src"),
type: $(todayOne)
.find(".fp-one-imagen-footer")
.text()
.replace(/(^\s*)|(\s*$)/g, ""),
text: $(todayOne)
.find(".fp-one-cita")
.text()
.replace(/(^\s*)|(\s*$)/g, "")
};
resolve(todayOneData)
});
})
return p
}
複製程式碼
- 將爬取資料統一處理,作為EJS的引數,傳送郵件模板。
function getAllDataAndSendMail(){
let HtmlData = {};
// how long with
let today = new Date();
let initDay = new Date(startDay);
let lastDay = Math.floor((today - initDay) / 1000 / 60 / 60 / 24);
let todaystr =
today.getFullYear() +
" / " +
(today.getMonth() + 1) +
" / " +
today.getDate();
HtmlData["lastDay"] = lastDay;
HtmlData["todaystr"] = todaystr;
Promise.all([getOneData(),getWeatherTips(),getWeatherData()]).then(
function(data){
HtmlData["todayOneData"] = data[0];
HtmlData["weatherTip"] = data[1];
HtmlData["threeDaysData"] = data[2];
sendMail(HtmlData)
}
).catch(function(err){
getAllDataAndSendMail() //再次獲取
console.log('獲取資料失敗: ',err);
})
}
複製程式碼
- 傳送郵件具體程式碼
function sendMail(HtmlData) {
const template = ejs.compile(
fs.readFileSync(path.resolve(__dirname, "email.ejs"), "utf8")
);
const html = template(HtmlData);
let transporter = nodemailer.createTransport({
service: EmianService,
port: 465,
secureConnection: true,
auth: EamilAuth
});
let mailOptions = {
from: EmailFrom,
to: EmailTo,
subject: EmailSubject,
html: html
};
transporter.sendMail(mailOptions, (error, info={}) => {
if (error) {
console.log(error);
sendMail(HtmlData); //再次傳送
}
console.log("Message sent: %s", info.messageId);
});
}
複製程式碼
安裝與使用
如果你覺得這封郵件的內容適合你傳送的物件,可以按照以下步驟,改少量引數即可執行程式;
- git clone https://github.com/Vincedream/NodeMail
- 開啟main.js,修改配置項
//紀念日
let startDay = "2016/6/24";
//當地拼音,需要在下面的墨跡天氣url確認
const local = "zhejiang/hangzhou";
//傳送者郵箱廠家
let EmianService = "163";
//傳送者郵箱賬戶SMTP授權碼
let EamilAuth = {
user: "xxxxxx@163.com",
pass: "xxxxxx"
};
//傳送者暱稱與郵箱地址
let EmailFrom = '"name" <xxxxxx@163.com>';
//接收者郵箱地
let EmailTo = "like@vince.studio";
//郵件主題
let EmailSubject = "一封暖暖的小郵件";
//每日傳送時間
let EmailHour = 6;
let EmialMinminute= 30;
複製程式碼
- 終端輸入
npm install
安裝依賴,再輸入node main.js
,執行指令碼,當然你的電腦不可能不休眠,建議你部署到你的雲伺服器上執行。
最後
冬天到了,是不是也該用程式設計師的專業知識給身邊的人帶來一些溫暖呢,原始碼與demo已經放到github上,要不試一試?
GitHub:github.com/Vincedream/…