做了个网站后台Web API

前言

事情的起因是上个月本站依赖的提供访问量统计和评论后台存储功能的LeanCloud由于某些原因域名故障了几天。

之后虽说是回复了,但是必须需要实名认证才能继续使用他们的服务了。所以说我一想,反正之前在LeanCloud上跑的业务也十分简单,也就是统计下阅读量,保存下博客评论啥的,好像完全可以托管在自己的服务器上嘛,同时也更灵活,想对自己的数据做什么直接就可以操作,不用再去套一层SDK了。

开工

说干就干。

接口设计

由于我并不是web相关的程序员,于是上网随便搜索了几篇文章,简单看了下,首先所有的url都以https://api.inspoy.cc/开头,https+专属二级域名,既美观又显得专业233,为了之后的扩展性,这次定了以下几个:

  • [GET]/v1/blog/article/article_key - 获取指定文章的阅读量
  • [PATCH]/v1/blog/article/article_key - 更新指定文章的阅读量,使数据库中的记录+1
  • [GET]/v1/blog/comment/article_key - 获取指定文章的所有评论
  • [POST]/v1/blog/comment/article_key - 在指定文章中发表评论
  • [POST]/v1/blog/bugreport - 用于接收前端网页上报的报错信息

参考了阮一峰大佬的这篇文章大致是遵循了RESTful API的设计准则。目前是API的第一个版本,且只有博客的功能,所以URI都是以/v1/blog/开头的233

fig01

技术选型

选用了Node.js的Express框架来开发该web应用程序,之后通过Nginx的反向代理功能,把来自api.inspoy.cc443端口的访问请求转发至Node.js应用程序(之前一直用Apache嫌迁移起来太麻烦就一直没搞,最近用了Nginx才发现真的是好用hhh)。

然后数据库依旧使用了之前部署WordPress时用的MySQL,单独建了一个新数据库用来存储这些API需要的业务数据,以及一个只有访问这个数据库的权限的用户。

实际在编写代码的时候,使用TypeScript进行开发,为的就是TS的强类型声明,这样才舒服~部署的时候执行tsc;node .即可。

实现细节

这个后台api我打算一直用下去的,之后除了博客相关可能会增加更多的业务,所以考虑到之后可能的扩展,大概把整体的结构设计成了这样:

fig02

为了避免太高的并发量(我的乞丐服务器也撑不住)在最顶层(即图中“统一入口”)设置了同一个IP对同一个URL的访问在一定时间只会响应一次,CD时间内重复访问会直接返回429 TooManyRequests,以防对数据库的访问压力过大。

除了需要返回的客户端的数据查询,请求不会等待那些额外的数据操作,直接返回结果,那些额外的操作(追加访问日志,评论邮件提醒之类的)慢慢做就行。

同样是在最顶层统一入口处,我会给每个请求分配一个唯一的ID,主要用于打印日志,以便查看对应的请求的返回值是什么(因为所有的操作都是异步的,当一个请求尚未返回时,就已经可能有其他请求又来了,此时在日志中加上ID识别起来就会很方便)。

对于一个URI,可能会多个HTTP动词,或者说操作方式,比如对于/v1/blog/article/这个URI来说,就同时存在GETPATCH两种操作方式,为了统一管理,我给每个URI设计了一个类RouteItem,其中包含了所有可能的操作方法,注册路由时只需找到非空的字段即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class RouteItem {
public MethodGet: (realIp: string, req: Request, res: Response, complete: () => void, rid: number) => void = null;
public MethodPost: (realIp: string, req: Request, res: Response, complete: () => void, rid: number) => void = null;
public MethodPut: (realIp: string, req: Request, res: Response, complete: () => void, rid: number) => void = null;
public MethodPatch: (realIp: string, req: Request, res: Response, complete: () => void, rid: number) => void = null;
public MethodDelete: (realIp: string, req: Request, res: Response, complete: () => void, rid: number) => void = null;
private mRoutePath: string = "";

public constructor(path: string) {
this.mRoutePath = path;
}
public GetRoutePath() {
return this.mRoutePath;
}
}

之后手动把所有的RouteItem整合到一个数组里,依次遍历找到非空的Method*用express的方式注册即可

1
2
3
4
5
6
7
8
const routers: RouteItem[];
for (const item of routers) {
const router: express.IRoute = app.route(item.GetRoutePath());
if (item.MethodGet != null) {
router.get(MethodWrapper(item.MethodGet));
}
// 以此类推
}

上面出现的MethodWarpper就是之前多次提到的顶层统一入口了,我会在这个方法里面分配请求ID(request ID或简称rid),限制访问频率,解析真实IP,设置响应头部,打印统一格式的日志等。

其中解析真实IP一项可以单独拿出来说说。上面说过我的Node.js应用是由Nginx反向代理到Node应用的端口的,其req.ip始终为127.0.0.1,所以需要在Nginx处给这个请求手动加一个真实IP的Header:

1
proxy_set_header X-Real-IP $remote_addr;

这样我们就可以通过解析这个Header来获取客户端的真实IP地址啦~

邮件提醒

这是个重要的功能,之前用WordPress的时候没配邮件提醒,以致于有读者看了我的文章发表了评论,我可能很久之后才会看到,然后我回复之后读者也不会收到邮件提醒,基本上是没办法持续地交流的。

我使用了npm上的nodemailer模块,使用它只需要很简单的配置就可以发邮件了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const transporter = nodemailer.createTransport({
host: "SMTP服务器",
port: "SMTP端口",
secure: true, // 如果使用SSL的话就设为true
auth: {
user: "用户名",
pass: "密码",
},
});
const mailOptions = {
from: "博客自动提醒<xxx@xxx.xx>",
to: "收件人名字<receiver@xxx.xx>",
subject: "标题",
html: "<p>Yo~</p>",
};
transporter.sendMail(mailOptions, (err,info) => { });

关于SMTP服务器,可以用你自己的邮箱,当然最好是专门注册一个用来发这种推送类的邮件。而我为了显得专业(😎),使用的邮箱是自己的域名,也就是@inspoy.cc结尾的邮箱。不过自己搭建邮件服务器不太靠谱(首先阿里云默认没有开放SMTP和POP3需要的接口,必须要专门申请才能开放;其次是为了防范垃圾邮件,似乎存在某种白名单的机制,我这种不知名的IP地址发出来的邮件很有可能会被各大邮箱直接丢到垃圾邮件里),所以我用的是网易的企业邮箱(之前用的是QQ企业邮箱,但是某次修改规则之后要求每个账号都要绑定手机,而且一个手机只能绑定一个账号,我自己的加上几个机器人账号就完全没法用了,所以放弃QQ转投网易),可以绑定自己的域名。

配置好之后,每当用户提交一条评论,首先要给我自己发邮件提醒有新评论;然后如果评论作者回复了另一条评论,后者的作者也会收到邮件提醒,告诉他你的评论有人回复你啦~

接下来……

关于博客评论的提醒,除了常规的邮件提醒之外,还有更方便的操作,下篇博客继续~(又水一篇)

微信订阅号

扫码关注我的个人订阅号,可以获得最新的博客文章推送

wxqr