本文介绍为免费评论系统 Valine.js 增加新回复邮件提醒的方法。使用 node.js 借助 nodemailer 实现。引擎使用 Valine 所使用的 LeanCloud Serverless 云引擎。

对于不会使用 node.js 的用户也没关系,只需要把本文中 编写 LeanCloud 云函数 一节中的代码简单修改(加入您的邮箱和网址),再按下一节的指引操作部署,就可以实现有新评论时,评论者和站长都收到邮件提醒。

什么是 Valine.js

Valine.js 是我非常喜欢的评论系统,正在本站中使用。这个系统有以下几个优点

  1. 不需要用户登录即可评论
  2. 中国大陆可用
  3. 基本免费,无广告
  4. 没有繁杂的后端管理系统
  5. 界面现代美观且可订制
  6. 部署非常方便

在具有以上优点的同时,也随之而来一些缺点

  1. 没有评论审核,会产生垃圾评论,需要用户手动处理
  2. 只适合小型网站,评论量不大的情况
  3. 没有评论提醒功能,需要到数据存储中查看

下面针对第 3 点问题,提出解决方案,即为 Valine 评论系统加入邮件提醒功能。

Valine 的工作原理

Valine 的数据存储在 LeanCloud 上。LeanCloud 是一个 Serverless 服务平台,可提供强有力的 Serverless 后端支持。其开发版本的数据存储和云引擎功能在一定条件下是免费的。而这个免费的额度足够一个小型网站使用。因此说 Valine 是基本免费的。

关于 Valine 的配置,请移步其 官网文档 ,这里不再赘述。

在 Valine 配置完成后,我们会发现,它在 LeanCloud 的云存储中建了一个新数据表,名为 Comment ,里面记录了每一条评论的信息,主要包括

1
2
3
4
5
6
7
8
9
{
comment: string, //评论内容,html格式
nick: string, //用户名
mail: string, //用户邮箱
link: string, //用户网址,url格式
rid: string, //评论项id,hash格式
pid: string, //被回复的评论id,hash格式
url: string, //网页地址
}

这些信息可以通过 LeanCloud 提供的 API 来获取。LeanCloud 的 cloud-storage API 提供了多种语言的调用方法,详细使用方法可以参考其官方文档,这里可以暂时跳过。

使用 LeanCloud 云引擎

上一节介绍了 Valine 使用的云存储。下面我们来介绍云引擎。云引擎是 LeanCloud 提供的真正 Serverless 后端,同样支持多种语言。

云引擎还支持多种调用方法,包括钩子(Hook)函数和定时函数。其中钩子函数是在云存储发生改变的时候激发的。定时函数是在定时器达到条件时激发的。下面就使用云引擎的这两种调用方法,来实现评论的邮件提醒功能。这里我们以 node.js 为例。

使用 nodemailer 发送邮件

本节介绍 nodemailer 的使用方法。如果您不感兴趣,或者本地没有安装 node.js,可以直接跳到下一节,不会影响您的应用部署。

nodemailer 是一个 node.js 的邮件发送引擎。下面我们介绍其使用方法。

在使用之前,先来找一个发送邮件使用的 smtp 服务器。这里介绍使用 QQ 邮箱 smtp 服务器的方法。

登录 QQ 邮箱,进入 设置 > 账户 ,其中有一项“POP3/IMAP/SMTP/Exchange/CardDAV/CalDAV 服务”,在 IMAP/SMTP 服务一项,选择 开启 。开启之后,在下方找到 生成授权码 ,根据提示,生成授权码,并复制下来。这个授机码就是在 app 中使用的邮箱密码。假设它是”shouquanma”。这时,IMAP/SMTP 服务设置完成。

下面,在系统中配置好了 node 和 npm 之后,安装 nodemailer

1
npm install nodemailer

下面我们来测试一下。创建一个文件 app.js ,写入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
function app(request) {
// 引入nodemailer
const nodemailer = require('nodemailer');
// 创建smtp服务器对象
const transporter = nodemailer.createTransport({
host: 'smtp.qq.com', // 由邮件商提供
port: 465, // SSL加密的smtp都用此端口
secure: true, // SSL加密
auth: {
user: '123456789@qq.com', // 更换为你的邮箱地址
pass: 'shouquanma', // 授权码
},
});
// 创建邮件内容
const mail = {
from: '123456789@qq.com',
to: '123456789@qq.com, other@qq.com',
subject: '测试邮件',
html: '<p>这是一封<strong>html</strong>格式的测试邮件</p>',
};
// 返回发送邮件的Promise
return transporter.sendMail(mail);
}

app().then(
() => {
console.log('发送成功');
},
(err) => {
console.log({ err });
}
);

这里先定义了一个函数 app ,这个函数返回了一个 Promise 。对于 javascript 异步操作不熟悉的朋友,这里推荐Promise 迷你书。然后在主进程中调用这个函数返回的 Promise。我们来测试一下,在控制台中输入

1
node app

可看到,控制台中提示发送成功,邮箱里收到了发送的测试邮件。如果不成功,请注意是否修改了正确的发送邮箱,并查看控制台输出的错误原因。

以上是 nodemailer 最简单的使用方法。对于更多的使用方法,请参考其 官网

学会了使用 nodemailer 后,我们来写云函数。

编写 LeanCloud 云函数

下面我们创建 LeanCloud 支持的云函数,如果您对 node.js 不熟悉,不需要理解其内容,只需要复制下来,把发件邮箱、邮件内容相应修改即可。直接看代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
var sendEmail = function(request) {
// 打印日志
console.log("sendEmail is triggered.");
// 引入nodemailer
const nodemailer = require('nodemailer');
// 创建smtp服务器对象
const transporter = nodemailer.createTransport({
host: 'smtp.qq.com', // 由邮件商提供
port: 465, // SSL加密的smtp都用此端口
secure: true, // SSL加密
auth: {
user: '123456789@qq.com', // 更换为你的邮箱地址
pass: 'shouquanma', // 授权码
},
});
// 获取新评论的rid和pid
let commentObject = request.object;
let rid = commentObject.get('rid'); // 发起者
let pid = commentObject.get('pid'); // 被回复者
// 生成邮件内容
let receiver = '12345678@qq.com'; // 所有回复都发送给站长
let subject = '您的评论有了新回复';
let content = `<strong>评论内容:</strong>
<br>${commentObject.get('comment')}<br>
<a href="http://your.site${commentObject.get('url')}">
点击查看</a>`;
// 创建一个返回邮件内容的函数,receiver后面根据查询组装
let email = function (receiver) {
return {
from: '"张三" <12345678@qq.com>', // 自动解析发件人名字和邮箱
to: receiver,
subject: subject,
html: content,
};
};
if (!(rid && pid)) {
// 新评论项,只给站长发送
// 返回发送邮件的Promise
return transporter.sendMail(email(receiver));
} else {
// 回复评论,给站长和相关人同时发送
let query1 = new AV.Query('Comment'); // 创建对Comment表的查询
query1.get(rid).then(
(cmt) => {
// 通过rid查询,得到记录cmt
let mail1 = cmt.get('mail'); // 得到记录中的mail项
mail1 ? (receiver = mail1 + ', ' + receiver) : null; // 如果存在,加入收件人中。
if (rid === pid) {
// 如果pid和rid相同,返回发送邮件的Promise
return transporter.sendMail(email(receiver));
} else {
// 如果pid和rid不同,再找pid的记录
let query2 = new AV.Query('Comment');
query2.get(pid).then(
(cmt) => {
let mail2 = cmt.get('mail');
mail2 && mail2 != mail1
? (receiver = mail2 + ', ' + receiver)
: null;
// 返回发送邮件的Promise
return transporter.sendMail(email(receiver));
},
(err) => {
console.log('bad pid');
}
);
}
},
(err) => {
console.log('bad rid');
}
);
}
}

其中,AV 是 LeanCloud 的云存储提供的 SDK 所封装的对象,它提供了查询方法

1
Query(class_ : string).get(objectId : string) -> Promise

等一系列云存储的 API 方法,可参考文档

request 是函数的参变量, request.object 代表发送来要存储的那条记录。

下面把以上代码部署到云引擎中。

部署发送邮件的云函数 Hook

云函数是云引擎调用的函数。它支持使用 git 部署,也支持在线编辑。这里我们为了方便,选择在线编辑。

首先,登录到 LeanCloud。在配置 Valine 时,已经创建了一个名为 vcomment 的 app,在网站控制台的卡片中点击这个 app 的“云引擎”图标,进入云引擎的管理页,找到 部署 选项卡,看到部署状态为“未部署”。有两种部署模式可以选择,我们选择 在线编辑 ,点击进入。

下面点击 创建函数 ,弹出在线编辑对话框。类型选择 Hook ,Class 选择 Comment ,表示每次调用函数时,就发送一次 email。然后在内容部分,可以看到,函数的定义部分已经写好:

1
2
3
AV.Cloud.afterSave('Comment', async function(request) {
...
}

我们把上面定义的函数(去掉首行和尾行)复制进去。点击 创建 ,云函数创建完成。然后点击 部署 ,弹出日志对话框,提示部署完成后,发送邮件的功能就已经配置好了,可以到网页中尝试。

由于 LeanCloud 的免费版云引擎有休眠策略,每半小时都会休眠一次,而且休眠唤醒时需要数秒钟时间,所以如果不使用付费版的话,需要进一步处理。这里提出两种解决方案。

第一种是在浏览器端绑定点击事件。当用户点击了评论输入框或回复按钮时,向服务器发送一个请求,以唤醒服务器。

第二种方案是设置定时任务,每隔一定时间,如 1 小时,或每 1 天,检查一次是否有新的评论产生。如果有新的评论,把它们按收件人汇总后,统一发送邮件。

第一种方案适用于评论用户比较少的情况,第二种适用于评论较多的情况。我的网站采用的是前者。下面对两种方案一一介绍。

使用 HTTP 请求唤醒服务器

首先创建一个可调用的云函数,与前面创建方法相似,不过在函数类型中选择 Function ,用于浏览器调取。这里我们把它命名为 wakeUp 。内容很简单

1
2
console.log('Cloud function wakeUp is triggered.');
return 'Hello!';

这样,在调用函数时,返回一个 “Hello” 字符串。然后我们在使用了 Valine 的网页中,为评论区域添加一个事件,当评论框被第一次点击的时候,调用这个云函数,以唤醒服务器。在页面的 html 中加入以下 script 标签

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<script>
var vcomment = document.getElementById('vcomment'); // 换成您为Valine绑定的id。
if (vcomment) {
var onclickHist = vcomment.onclick;
vcomment.onclick = () => {
fetch('https://your.app.url/1.1/functions/wakeUp', {
// your.app.url换成您的API服务器地址
body: '{}',
headers: {
'X-LC-Id': 'your-app-id', // 换成您的AppId
'X-LC-Key': 'your-app-key', // 换成您的AppKey
'Content-Type': 'application/json',
},
method: 'POST',
mode: 'cors',
})
.then((response) => {
return response.json();
})
.then((json) => {
console.log(json.result);
vcomment.onclick = onclickHist;
});
};
}
</script>

基本思路为,为Valide所在的

添加 onclick 事件,当第一次点击时,调取一次 wakeUp 云函数,即激活了服务器。这时,您的邮件系统可以完全正常运行了。如果服务器休眠,只要用户用心评论(撰写时间达到数秒),也不会受到影响。

使用定时任务部署 LeanCloud 云函数

免费版的云引擎不但会自动休眠,而且每天的工作时间不能超过 18 小时。这对小流量网站来说足够使用了。但是如果评论数很多,可以选择使用定时任务部署,比如每隔一个小时,或在每天的早上 9 点,把之前的所有评论发到邮箱中,或者每月把统计数据发到站长邮箱中。这里给出方法,请有需要站长们自行补全。

先定义一个云函数,用和上一节相似的方法。函数体中一个主要变化就是需要根据日期查询所有的评论,这时我们借助 Moment.js,在每天早上查询前一天的所有评论。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
AV.Cloud.define('archiveComments', async function (request) {
const moment = require('moment');
let endDate = moment().hour(0).minute(0).second(0).millisecond(0);
let beginDate = moment(endDate).subtract(10, 'd');
let query = new AV.Query('Comment');
query.greaterThanOrEqualTo('createdAt', beginDate.utc().toDate());
query.lessThan('createdAt', endDate.utc().toDate());
query.find().then((cmds) => {
cmds.forEach((cmd) => {
// 在这里处理邮件内容
});
// 在这里创建transporter 并return其sendEmail方法
});
});

这个函数创建完成后,再到网页控制台中,找到 定时任务 ,单击 创建定时任务 ,选择刚才的函数,在 Cron 表达式中输入

1
0 0 9 * * ?

6 个项目分别表示:0 秒,0 分,9 时,任意日,任意月,任意星期。 Cron 表达式可参考这篇文档。保存,即可。

如果定时任务也遇到了休眠的问题,可以参考上一节,在定时任务执行之前几分钟,再增加一个定时唤醒任务 wakeUp ,问题就解决了。