github: rocore.git oscgit: rocore.oscgit oscpage: coding nodejs async with generator
福利:traceur-compiler 目前想要运行ES6代码的话,可以用google/traceur-compiler将代码转译. 然后静待nodejs0.12. (这个代码是运行前完全编译的,所以不用担心解析性能问题)
单队列多任务自由切换?
如果你喜欢nodejs的干脆直接,又深陷异步回调的泥潭,不如试试ROCORE,一个采用生成器和惰性求值的轻量框架。
ROCORE更像是nodejs里的一个干净独立的模块,提供一组轻便好用性能不错的工具, 不会污染nodejs内核,也不进行任何封装。
任务和队列将是nodejs异步编程的主题,ROCORE提供了3个工具,使代码可以自由的在单队列多任务切换:
- yield停止当前任务
- next跳到下一个任务(如果有, 否则跳到最外部)
- ynext返回到当前任务
下面是一组我最近完成的邮箱找回密码代码,当请求时会检测用户email是否已经注册,然后往用户邮箱发送一个临时会话id。 你会看到,整套代码只有if,没有else,代码完全是扁平的,只有一层。
- 使用redis作为会话缓存服务器
- 使用mongodb作为数据服务器
路由文件:
// route.js
let R = require('rocore');
let userdb = require('../database/user');
let session = require('../session');
let validate = require('./validate');
let cparser = require('../cookie-parser');
let nodemailer = require('nodemailer');
const FIND_MAIL_HOST = 'smtp.163.com';
const FIND_MAIL_PORT = 465;
const FIND_MAIL_USER = 'wtaaa@163.com';
const FIND_MAIL_PWD = 'abc123';
const FIND_MAIL_FROM = 'wtaaa@163.com';
const FIND_MAIL_SUBJECT = 'Hello ✔';
const FIND_MAIL_BODY = '<h3>Hello world ✔</h3><p><a href="http://www.baidu.com/userback/{sid}">get back password</a></p>';
exports.send_mail = function* (ynext, next, req, res, mdb, rlci) {
let email = req.body.email;
let vd_email = validate.vemail(email);
// invalid email
if (!vd_email) {
res.writeHead(200, 'OK', { 'content-type':'text/plain' });
res.end('invalid email');
yield next();
}
// let yctx_ue = yield userdb.exists_bye(mdb, email, ynext);
let yctx_ue = yield mdb.collection('users').findOne({ email:email }, { fields:{_id:1} }, ynext);
let err_ue = yctx_ue[0];
let doc_ue = yctx_ue[1];
// mongo-server error
if (err_ue) {
res.writeHead(500);
res.end('server error');
yield next();
}
// email not exists
if (!doc_ue) {
res.writeHead(200, 'OK', { 'content-type':'text/plain' });
res.end('email not exists');
yield next();
}
let transporter = nodemailer.createTransport({
host: FIND_MAIL_HOST,
secure: true, // 使用 SSL
port: FIND_MAIL_PORT, // SMTP 端口
auth: {
user: FIND_MAIL_USER,
pass: FIND_MAIL_PWD
}
});
let yctx_sm = yield transporter.sendMail({
from : FIND_MAIL_FROM,
to : email,
subject : FIND_MAIL_SUBJECT,
html : FIND_MAIL_BODY.replace('{sid}', 'SID123456789ABCDEFGHIJKLMN')
}, ynext);
let err_sm = yctx_sm[0];
let info_sm = yctx_sm[1];
// send error
if (err_sm) {
res.writeHead(500);
res.end('error');
yield next();
}
// successful
res.writeHead(200, 'OK');
res.end('OK');
//console.log('Message sent: ' + info_sm.response);
yield next();
};
单元测试:
let assert = require('assert');
let R = require('rocore');
let http = require('http');
let url = require('url');
let qs = require('querystring');
let uroute = require('../../lib/route/user');
let cparser = require('../../lib/cookie-parser');
let rcli = require('redis').createClient();
let MongoClient = require('mongodb').MongoClient;
let server = http.createServer();
let app = R.Application();
let mdb = null;
server
.on('request', function (req, res) {
app.match(req, res);
})
;
app
.on('found', function (route, req, res) {
req.cookie = cparser.parse(req.headers.cookie);
var data = '';
req.setEncoding('utf8');
req.on('data', function (d) {
data += d;
});
req.on('end', function () {
req.body = qs.parse(data, '&', '=');
app.exec(route, req, res, mdb, rcli);
});
})
.post('/join', uroute.join)
.post('/userback', uroute.send_mail)
;
R.scc(function* (ynext) {
let yctx_mc = yield MongoClient.connect('mongodb://127.0.0.1:31000/test', { "poolSize":10 }, ynext);
let err_mc = yctx_mc[0];
if (err_mc) { throw err_mc; }
mdb = yctx_mc[1];
server.listen(8000);
// join user with full info
let res_jf = (yield R.request({
hostname: '127.0.0.1',
port: 8000,
path: '/join',
headers: {
'Content-Type': 'application/application/x-www-form-urlencoded'
},
method: 'post',
body: {
username: 'wt',
password: '123456',
password2: '123456',
email: 'wtaaa@163.com',
sex: 'male',
language: 'en'
}
}, ynext))[0];
let res_ue = (yield R.request({
hostname: '127.0.0.1',
port: 8000,
path: '/userback',
headers: {
'Content-Type': 'application/application/x-www-form-urlencoded'
},
method: 'post',
body: {
email: 'wtaaa@163.com'
}
}, ynext))[0];
assert.strictEqual(res_ue.body, 'OK');
process.exit(0);
});
服务器如何配置呢?
遵循nodejs设计思想,rocore.Application提供事件注册的机制,服务器配置将会如下:
let R = require('rocore');
let http = require('http');
let url = require('url');
let qs = require('querystring');
let uroute = require('../../lib/route/user');
let cparser = require('../../lib/cookie-parser');
let rcli = require('redis').createClient();
let MongoClient = require('mongodb').MongoClient;
let server = http.createServer();
let app = R.Application();
let mdb = null;
server
.on('request', function (req, res) {
app.match(req, res);
})
;
app
.on('found', function (route, req, res) {
req.cookie = cparser.parse(req.headers.cookie);
var data = '';
req.setEncoding('utf8');
req.on('data', function (d) {
data += d;
});
req.on('end', function () {
req.body = qs.parse(data, '&', '=');
app.exec(route, req, res, mdb, rcli);
});
})
.on('notfound', function (req, res) {
res.writeHead(404);
res.end('Could not found ' + url.parse(req.url).pathname);
})
.get('/', function* (ynext, next, req, res) {
res.writeHead(200, 'OK', { 'Content-Type':'text/html' });
res.end('/');
})
.post('/join', uroute.join)
.post('/login', uroute.login)
.post('/logout', uroute.is_login, uroute.logout)
;
R.scc(function* (ynext) {
let yctx_mc = yield MongoClient.connect('mongodb://127.0.0.1:31000/test', { "poolSize":10 }, ynext);
let err_mc = yctx_mc[0];
if (err_mc) { throw err_mc; }
mdb = yctx_mc[1];
server.listen(8000);
});
更有趣的队列实验?
想要在队列的多任务中来去自如,那么下面的代码很有代表性:
let app = require('../lib/rocore').Application();
let assert = require('assert');
let stack = [];
function test(x, callback) {
process.nextTick(function () {
callback(x);
});
}
app
.on('found', function (route, req, res) {
app.exec(route, req, res);
})
.on('finish', function (ynext, req, res) {
if (typeof ynext === 'function') {
ynext({ '0': 0 });
}
})
.post(
'/:user/ttt',
function* (ynext, next, req, res) {
let a = yield test(1, ynext); stack.push(a[0]);
let b = yield next(ynext); stack.push(b[0][0]);
let c = yield test(2, ynext); stack.push(c[0]);
assert.deepEqual(stack, [ 1, 3, 5, 0, 6, 6, 4, 4, 2 ]);
process.exit(0);
},
function* (ynext, next, req, res) {
let a = yield test(3, ynext); stack.push(a[0]);
let b = yield next(ynext); stack.push(b[0][0]);
let c = yield test(4, ynext); stack.push(c[0]);
},
function* (ynext, next, req, res) {
let a = yield test(5, ynext); stack.push(a[0]);
let b = yield next(ynext); stack.push(b[0][0]);
let c = yield test(6, ynext); stack.push(c[0]);
}
)
;
// 1, 3, 5, 0, 6, 6, 4, 4, 2
更有利的异步任务控制工具?
- rocore.scc(generator, [callback])
- rocore.mcc(generator, [callback])
使用这两个工具可以帮助你随时完成任意的异步代码:
let assert = require('assert');
let R = require('../lib/rocore');
function fA(a, callback) {
setTimeout(function () {
callback(a, 'aaa');
}, 1000);
}
function fB(b, callback) {
setTimeout(function () {
callback(b, 'bbb');
}, 1000);
}
R.scc(function* (ynext) {
let A = yield fA('a1', ynext);
assert.strictEqual(A[0], 'a1');
assert.strictEqual(A[1], 'aaa');
let B = yield fB('b1', ynext);
assert.strictEqual(B[0], 'b1');
assert.strictEqual(B[1], 'bbb');
let C = yield R.mcc(function* (ynext) {
yield fA('a2', ynext('a'));
yield fB('b2', ynext('b'));
}, ynext);
assert.strictEqual(C[0]['a'][0], 'a2');
assert.strictEqual(C[0]['a'][1], 'aaa');
assert.strictEqual(C[0]['b'][0], 'b2');
assert.strictEqual(C[0]['b'][1], 'bbb');
process.exit(0);
});