【Node.js】基于EventProxy模块的顺序异步调用
[TOC]
Tag:Node.js、WebStorm、Bmob、EventProxy
背景
最近2个多月,由于笔者的老手机iPhone 6 plus 刷了iOS11的测试版,在公司的网络下,不挂VPN基本无法正常打开网址、图片、超链等。笔者还穷酸,不舍得花钱买收费的VPN,偶然的机会遇到一个好心人架设的免费VPN。每日签到送流量。对于我这种低流量用户来说,够用了。为了保证账户的激活状态,加上偏执症发作,每日签到。自己写了一个简单的iOS应用,一键签到领流量。无奈舍不得花钱购买开发者账号,所以在测试机模式下,居然要每7天安装一次。又是懒癌发作,动手自己写一个循环定时签到的服务吧。
本来昨天和今天打算用Python写一个每日签到领流量的小定时器服务。结果折腾了一天,发现笔者电脑上的Python可能是由于版本的问题,无法通过网站的SSL验证。无奈之下,再次祭起Node.js和Bmob。又是折腾了一天,期间犯了很多低级错误。
流程
云逻辑及云数据表
笔者很喜欢的一个服务平台Bmob,收费后,免费版功能大减,但是偶尔写写小东西还是不错的,也够用。
这里也不多说了,挺简单的。创建一个应用,在新应用中左面的工具栏选择云逻辑(基于Node.js,但是封装了自己的一部分模块,并且不太支持第三方其他模块)。添加一个方法icafe。
由于密码是以明文存在的,所以笔者不想直接在代码中暴露用户名和密码,因此先把用户名和密码存在Bmob上云数据库中。
创建一张云数据库表app_icafe_info,新添加两列email和passwd,分别用于存放登录邮箱和密码。请读者将自己的icafe用户名和密码存在这张表里。
数据库读取
此部分的云逻辑代码如下:
//往http://bmob.cn/save发起POST请求
db.findOne({
"table":"app_icafe_info", //表名
"objectId":"这里替换掉主键" //记录的objectId
},function(err,dbdata){//回调函数
if (!err) {
var dataObject= JSON.parse(dbdata);
}
});
因为这条数据记录是人工录入进去的,并且数据表中也只有这一条,所以使用的是
findOne方法。如果有其他的请自行查看Bmob的开发文档。
爱咖啡这个网站,签到需要两步。先登录,再签到。
登录API
https://icafe.tech/login
就是这个地方,由于笔者的Python问题,无法验证https,因此更换的Bmob的nodejs。
需要传参用户名和密码
{
"email": "此处填写登录邮箱",
"passwd": "此处填写登录密码"
}
由于密码传输未做任何加密或者编码,是明文。所以建议用POST一个form。
此部分的云逻辑代码如下:
http.post('https://icafe.tech/login', {form:{'email': dataObject.email, 'passwd': dataObject.passwd}}, function (error, loginres, loginbody) {
if (!error && loginres.statusCode == 200) {
}
});
可补充一下Node.js的代码实现:
// request Request
(function(callback) {
'use strict';
const httpTransport = require('https');
const responseEncoding = 'utf8';
const httpOptions = {
hostname: 'icafe.tech',
port: '443',
path: '/login',
method: 'POST',
headers: {"Content-Type":"application/x-www-form-urlencoded; charset=utf-8"}
};
httpOptions.headers['User-Agent'] = 'node ' + process.version;
// Paw Store Cookies option is not supported
const request = httpTransport.request(httpOptions, (res) => {
let responseBufs = [];
let responseStr = '';
res.on('data', (chunk) => {
if (Buffer.isBuffer(chunk)) {
responseBufs.push(chunk);
}
else {
responseStr = responseStr + chunk;
}
}).on('end', () => {
responseStr = responseBufs.length > 0 ?
Buffer.concat(responseBufs).toString(responseEncoding) : responseStr;
callback(null, res.statusCode, res.headers, responseStr);
});
})
.setTimeout(0)
.on('error', (error) => {
callback(error);
});
request.write("email=这里替换登录用户名&passwd=这里替换密码")
request.end();
})((error, statusCode, headers, body) => {
console.log('ERROR:', error);
console.log('STATUS:', statusCode);
console.log('HEADERS:', JSON.stringify(headers));
console.log('BODY:', body);
});
在登录成功之后,即response为一下之后,才可以进行签到操作。
{
"code": 0,
"msg": "请求成功",
"data": "登录成功!"
}
如果用户名密码错误的话,则会收到一下response
{
"code": 1,
"msg": "用户密码错误!",
"data": []
}
签到API
https://icafe.tech/checkIn
此部分的云逻辑代码如下:
http.post('https://icafe.tech/checkIn', function (error, checkInres, checkInbody) {
if (!error && checkInres.statusCode == 200) {
var str = JSON.parse(checkInbody);
var regs = str.msg.match('\\d+\\.?\\d*');
var flow_capacity = regs instanceof Array?regs[0]:"0"
}
});
可补充一下Node.js的代码实现:
// request Request
(function(callback) {
'use strict';
const httpTransport = require('https');
const responseEncoding = 'utf8';
const httpOptions = {
hostname: 'icafe.tech',
port: '443',
path: '/checkIn',
method: 'POST'
};
httpOptions.headers['User-Agent'] = 'node ' + process.version;
// Paw Store Cookies option is not supported
const request = httpTransport.request(httpOptions, (res) => {
let responseBufs = [];
let responseStr = '';
res.on('data', (chunk) => {
if (Buffer.isBuffer(chunk)) {
responseBufs.push(chunk);
}
else {
responseStr = responseStr + chunk;
}
}).on('end', () => {
responseStr = responseBufs.length > 0 ?
Buffer.concat(responseBufs).toString(responseEncoding) : responseStr;
callback(null, res.statusCode, res.headers, responseStr);
});
})
.setTimeout(0)
.on('error', (error) => {
callback(error);
});
request.write("")
request.end();
})((error, statusCode, headers, body) => {
console.log('ERROR:', error);
console.log('STATUS:', statusCode);
console.log('HEADERS:', JSON.stringify(headers));
console.log('BODY:', body);
});
成功的话,会收到如下response
{
"code": 0,
"msg": "恭喜您获得 702MB 流量",
"data": []
}
如果每天重复签到的话,会收到如下response
{
"code": 1,
"msg": "您今天已经签过到了,明天再来吧~",
"data": []
}
数据库存储
笔者希望将每日签到的结果存入数据表中备查。同时,也将签到得到的流量分离出来,将来可以用于统计分析。Bmob用代码进行数据表存储,可以不先创建数据表。代码是执行时会自动进行查找判断,如果存在表,则进行记录操作;如果表不存在,则按照代码中的数据记录对象(json)自动生成对应的表结构,再进行相应的操作。
此部分的云逻辑代码如下:
db.insert({
"table":"app_icafe_records", //表名
"data":{"msg":str.msg,"flow_capacity":str.flow_capacity} //需要更新的数据,格式为JSON
},function(err,data){
response.send(data + err);//显示结果
});
因此自动生成的表结构如下图所示:
那么问题来了,这里需要四次操作才可以完成一次签到操作。
- 从数据库中读取用户名和密码;
- 使用用户名和密码进行登录验证;
- 调用签到API;
- 签到成功后,将结果存入数据库中。
在nodejs中,四者都是异步操作,并且是一环套一环的有先后顺序。所以需要进行深度callback嵌套……笔者开始纠结了。
因此,
解决回调嵌套
笔者在网络中偶然查找到下面的EventProxy,据说正是用于解决回调嵌套的。
https://github.com/JacksonTian/eventproxy
EventProxy 仅仅是一个很轻量的工具,但是能够带来一种事件式编程的思维变化。有几个特点:
- 利用事件机制解耦复杂业务逻辑
- 移除被广为诟病的深度callback嵌套问题
- 将串行等待变成并行等待,提升多异步协作场景下的执行效率
- 友好的Error handling
- 无平台依赖,适合前后端,能用于浏览器和Node.js
- 兼容CMD,AMD以及CommonJS模块环境
此处主要使用了all和emit两个方法。
使用方法和其他API请自行查阅官方的API和开发文档。
全部代码
下面是全部云逻辑代码:
function onRequest(request, response, modules) {
/**
*发起Post请求
*/
//获取Http模块
var http = modules.oHttp.defaults({jar: true});
//eventproxy模块,解决异步回调的问题
var ep = modules.oEvent;
//获取数据库对象
var db = modules.oData;
ep.all('step3 checkin',function(str){ //任务3完成时执行
db.insert({
"table":"app_icafe_records", //表名
"data":{"msg":str.msg,"flow_capacity":str.flow_capacity} //需要更新的数据,格式为JSON
},function(err,data){
response.send(data + err);//显示结果
});
})
ep.all('step2 login',function(){ //任务2完成时执行
http.post('https://icafe.tech/checkIn', function (error, checkInres, checkInbody) {
if (!error && checkInres.statusCode == 200) {
var str = JSON.parse(checkInbody);
var regs = str.msg.match('\\d+\\.?\\d*'); //使用正则表达式来获取中文字符串中的数字
str.flow_capacity= regs instanceof Array?regs[0]:"0" // 重复签到时会在数据表中的流量列写入0
ep.emit('step3 checkin',str);
}
});
});
ep.all('step1 db',function(dataObject){ //任务1完成时执行
http.post('https://icafe.tech/login', {form:{'email': dataObject.email, 'passwd': dataObject.passwd}}, function (error, loginres, loginbody) {
if (!error && loginres.statusCode == 200) {
ep.emit('step2 login'); //异步任务2完成
}
});
});
//往http://bmob.cn/save发起POST请求
db.findOne({
"table":"app_icafe_info", //表名
"objectId":"8tXE2225" //记录的objectId
},function(err,dbdata){//回调函数
if (!err) {
var dataObject= JSON.parse(dbdata);
ep.emit('step1 db',dataObject); //异步任务1完成
}
});
}
设置定时任务
具体设置方式如下图所示:
0 0 9 * * *表示每天的9时0分0秒,触发定时器一次。
基本上就是这些了。文中未尽事宜,读者如有疑问,可以留言或者发邮件。笔者尽能力一一解答。
完结撒花。
以上。










网友评论