<p>上次我们分析了Node Club的首页实现,了解了它的功能以及目前的实现方式。不过在我看来,如今使用EventProxy来辅助页面开发并没有解决部分异步编程中的主要问题。甚至可以说,就目前的EventProxy的使用方式而言,即便不借助任何类库,单纯基于JavaScript也可以得到有过之而无不及的编程体验。这次我们便来使用尝试使用<a href=“https://github.com/JeffreyZhao/jscex”>Jscex</a>来改进首页的逻辑。</p>
<h1>最后准备</h1>
<p>还记得首页的逻辑吗?其中可是用到了许多异步方法:</p>
tag_ctrl.get_all_tags(function (err, tags) { ... });
topic_ctrl.get_topics_by_query(..., function (err, topics) { ... });
topic_ctrl.get_topics_by_query(..., function (err, hot_topics) { ... });
user_ctrl.get_users_by_query(..., function (err, users) { ... });
user_ctrl.get_users_by_query(..., function (err, tops) { ... });
topic_ctrl.get_topics_by_query(..., function (err, no_reply_topics) { ... });
topic_ctrl.get_count_by_query(..., function (err, all_topics_count) { ... });
<p>如果要用Jscex来实现首页,这些方法都必须变成Jscex异步方法。之前有朋友说,在一个现成的项目中使用Jscex代价太高,因为每个函数都必须Jscex化。但事实上,我们完全可以在一个现有的项目逐步地引入Jscex,因为我们轻易地将已有的异步操作“封装”为Jscex异步方法,例如<a href=“https://github.com/JeffreyZhao/nodeclub/commit/cb7082873452261a175c282c8c5ee6f2257a8f46”>在controllers/topic.js文件中</a>:</p>
/********** Jscex ************/
var Jscex = require("../libs/jscex").Jscex;
var Jscexify = Jscex.Async.Jscexify;
exports.get_topics_by_query_async = Jscexify.fromStandard(get_topics_by_query);
exports.get_count_by_query_async = Jscexify.fromStandard(get_count_by_query);
<p><a href=“https://github.com/JeffreyZhao/nodeclub/commit/a1b7635ea846828045b4e539887b20b0c6a472ff”>其他两个方法的封装</a>就不列举出来了。可以发现,我们完全不需要一次性把所有的依赖都“重新实现”,可以用到哪儿再封装哪儿,稍后再进行真正的Jscex化——有时候甚至完全无需基于Jscex再写一遍。Jscex适合编写异步操作之间相对复杂的交互,但对于原本就十分简单的异步操作来说,Jscex也并不会带来明显的附加优势。此时我们完全可以保留最普通的异步回调写法,这从任何角度来说都造成问题。</p>
<h1>实现首页</h1>
<p>还记得之前提出的首页逻辑吗?</p>
function (request, response) {
var tags = tag_ctrl.get_all_tags(); // 标签
var topics = topic_ctrl.get_topics_by_query(...); // 最新话题
var hot_topics = topic_ctrl.get_topics_by_query(...); // 热门话题
var stars = user_ctrl.get_topics_by_query(...); // 明星用户
var tops = user_ctrl.get_users_by_query(...); // 得分最高用户
var no_reply_topics = topic_ctrl.get_topics_by_query(...); // 无回复话题
var topic_count = topic_ctrl.get_count_by_query(...); // 话题总数
response.render("index", { ... }); // 输出HTML
}
<p><a href=“https://github.com/JeffreyZhao/nodeclub/commit/5959221771f3bfac27abc466de019cd394f80654#diff-0”>使用Jscex来实现这个逻辑</a>的话,与上述“伪代码”可谓完全一致,唯一的区别只是多了些前后附加的计算逻辑:</p>
var indexAsync = eval(Jscex.compile("async", function (req, res) {
var page = Number(req.query.page) || 1;
var limit = config.list_topic_count;
var data = {
current_page: page,
list_topic_count: limit
};
data.topics = $await(topic_ctrl.get_topics_by_query_async(...));
data.hot_topics = $await(topic_ctrl.get_topics_by_query_async(...));
data.stars = $await(user_ctrl.get_users_by_query_async(...));
data.tops = $await(user_ctrl.get_users_by_query_async(...));
data.no_reply_topics = $await(topic_ctrl.get_topics_by_query_async(...));
var all_topics_count = $await(topic_ctrl.get_count_by_query_async(...));
data.pages = Math.ceil(all_topics_count / limit);
data.tags = $await(tag_ctrl.get_all_tags_async());
// 计算最热标签
data.hot_tags = _.chain(data.tags)
.sortBy(function (t) { return -t.topic_count; })
.first(5);
// 计算最新标签
data.recent_tags = _.chain(data.tags)
.sortBy(function (t) { return -t.create_at.valueOf() })
.first(5);
res.render('index', data);
}));
exports.index = Unjscexify.toRequestHandler(indexAsync);
<p>最后一段代码,是使用一个<a href=“https://github.com/JeffreyZhao/nodeclub/commit/5959221771f3bfac27abc466de019cd394f80654#diff-1”>辅助方法</a>,将一个Jscex异步方法转化为一个普通的HTTP Request Handler:</p>
Jscex.Unjscexify = {
toRequestHandler: function (fn) {
return function (req, res, next) {
fn(req, res).addEventListener("failure", function () {
next(this.error);
}).start();
}
}
}
<p>这里我们将发生的任何错误都通过next向外传递,这是标准的处理方式,当然您也可以使用您自己的错误处理策略。您会发现,我们无需复杂的错误处理方式,在编写Jscex方法时,错误会像普通异常一样向外抛出,直到被统一捕获。</p>
<p>我这里还想提一下Node Club代码里的一些问题。Node Club在我看来是一个比较粗糙的项目,代码质量不太好,一方面是代码风格有些乱(例如使用Tab,符号周围空格等等),还有便是把一些简单的代码写得复杂,例如上面“计算最热标签”这样的逻辑,目前的实现是:</p>
tags.sort(function (tag_a, tag_b) {
if (tag_a.topic_count == tag_b.topic_count) return 0;
if (tag_a.topic_count > tag_b.topic_count) return -1;
if (tag_a.topic_count < tag_b.topic_count) return 1;
});
<p>其实这里完全可以一句话实现:</p>
tags.sort(function (tag_a, tag_b) {
return tag_b.topic_count - tag_a.topic_count;
});
<p>但我更喜欢使用<a href=“http://documentcloud.github.com/underscore/”>Underscore</a>提供的函数式编程方式,很重要的一点是它保持了输入数据的不变性。数组的sort方法是对本身进行排序,因此Node Club不得不创建一个数组拷贝。使用Underscore可以简化许多代码逻辑,就好比在实现相同功能时,C#代码比Java语言要简单许多。</p>
<h1>并发</h1>
<p>并发是好事,不过在上面的实现中,所有的操作是串行的。有人会说这么做起不到并发的效果,但其实我并不在意,因为我们目前的应用是一个服务器端程序,本身就是并发地承受客户端的请求。如果把处理一个请求看作一个事务的话,我们认为单个事务时串行的,但是已经有大量并发的事务。即便事务里的每个操作都并发起来,但单个事务还是要等到所有操作结束后才能完成(即用户看到页面)。由于系统的负载并没有降低,单个事务处理的总时长并没有减少。</p>
<p>您可以做个实验,硬盘上有10个1G大小的文件,您使用顺序的方式复制所有文件,和同时复制这10个文件,所花时间分别是多少?做过服务器压力测试的同学一定知道,我们加大并发量时,处理相同数量请求所需的总时长并不会减少,甚至随着并发量增加,单位时间内的请求处理能力会明显降低。因此,现代的服务器一般都会在并发量增大的情况下采取保护措施,例如对请求排队,或是返回Service Unavailable错误等等。</p>
<p>并发还有个问题,就是当一个操作失败时,很难“取消”其他操作,这会造成无谓的资源浪费。因此,如果我们盲目地将可以并发的操作都并发起来,对服务的健康并没有什么好处,除非可以确定这个并发操作的确可以同时进行(例如网络访问和磁盘读取),否则我并不倾向于(在一个服务器应用程序里)并发访问。不过作为一个演示,我便<a href=“https://github.com/JeffreyZhao/nodeclub/commit/39f75d74cce1bcd8fc9921aaf969c334e37336c2”>将所有操作都并发起来</a>吧:</p>
var indexAsync = eval(Jscex.compile("async", function (req, res) {
var page = Number(req.query.page) || 1;
var limit = config.list_topic_count;
var data = $await(Task.whenAll({
topics: topic_ctrl.get_topics_by_query_async({}, {
skip: (page - 1) * limit,
limit: limit,
sort: [['last_reply_at', 'desc']]
}),
hot_topics: topic_ctrl.get_topics_by_query_async({}, {
limit: 5,
sort: [['visit_count', 'desc']]
}),
stars: user_ctrl.get_users_by_query_async(
{ is_star: true },
{ limit: 5 }
),
tops: user_ctrl.get_users_by_query_async({}, {
limit: 10,
sort: [['score', 'desc']]
}),
no_reply_topics: topic_ctrl.get_topics_by_query_async(
{ reply_count: 0 },
{ limit: 5, sort: [['create_at', 'desc']] }
),
tags: tag_ctrl.get_all_tags_async(),
all_topics_count: topic_ctrl.get_count_by_query_async({})
}));
data.current_page = page;
data.list_topic_count = limit;
data.pages = Math.ceil(data.all_topics_count / limit);
// 计算最热标签
data.hot_tags = _.chain(data.tags)
.sortBy(function (t) { return -t.topic_count; })
.first(5);
// 计算最新标签
data.recent_tags = _.chain(data.tags)
.sortBy(function (t) { return -t.create_at.valueOf() })
.first(5);
res.render('index', data);
}))
<p>Jscex类库“不提倡”盲目并发,它的并发是“可选”的。如果您想要并发哪些操作,将其放在Task.whenAll即可。Task.whenAll是一个辅助方法,您可以输入一个保存Task的对象或是数组(甚至是对象和数组的嵌套),whenAll返回的Task对象会同时发起那些操作,并等待它们全部返回。返回的结果对象,其结构会和输入时完全一致,或是对象,或是数组(甚至对象和数组的嵌套)。在Jscex中,哪些操作串行,哪些操作并发都是由开发人员决定的,完全可以将其轻松地混合起来。例如,我们串行地执行一些前期处理,例如用户认证,然后再将后续的数个操作并发起来。</p>
<p>在Node Club中有个问题就是“盲目并发”,它是用EventProxy将所有的操作并发起来,而并非有选择的处理。EventProxy适合“盲目并发”的场景,但是对“有选择的并发”支持很差,我们很难选择部分操作串行执行。例如我之前的一个示例“<a href=“https://github.com/JeffreyZhao/jscex/blob/master/doc/async/samples/copy-dir-cn.md”>复制完整目录</a>”,<a href=“https://github.com/JeffreyZhao/talks/blob/master/nodeparty-20120108/copy-dir/copy-dir.js”>使用Jscex只要如普通编程那样直接实现逻辑</a>即可,而无需<a href=“https://github.com/JeffreyZhao/talks/blob/master/nodeparty-20120108/copy-dir/copy-dir-raw.js”>如传统编程那样使用各种回调</a>,但如果非要“事件驱动”,非要生搬硬套EventProxy,事件和回调交织在一起,<a href=“https://github.com/JeffreyZhao/talks/blob/master/nodeparty-20120108/copy-dir/copy-dir-eventproxy.js”>实现便会变得非常复杂</a>。</p>
<p>作为一个面向开发人员的工具,Jscex除了隐藏必要的复杂度之外,还要让目标程序“可控”,无论是串行、并发还是逻辑表达——Jscex使用JavaScript语法,保证了程序逻辑的灵活与可控,尽可能地避免出现<a href=“http://en.wikipedia.org/wiki/Leaky_abstraction”>Leaky Abstraction</a>。EventProxy的确提供了一种“完全并发”的抽象,但是对于需要“可控并发”,或是“串行执行”的逻辑和场景便显得无能为力了。</p>
<h1>相关文章</h1>
<ul> <li><a href=“http://blog.zhaojie.me/2012/02/jscexify-nodeclub-1-prepare-nodeclub-website.html”>使用Jscex改进Node Club(1):运行Node Club网站</a></li>
<li><a href=“http://blog.zhaojie.me/2012/02/jscexify-nodeclub-2-import-jscex.html”>使用Jscex改进Node Club(2):引入Jscex类库</a></li>
<li><a href=“http://blog.zhaojie.me/2012/02/jscexify-nodeclub-3-home-page-implementation.html”>使用Jscex改进Node Club(3):分析首页实现</a></li>
<li>使用Jscex改进Node Club(4):改写首页</li> </ul>
先顶一个,建议出个bench mark 看看,数据才是最有说服力的哈
async是可以选择性并行和串行的,eventproxy没用过,去看看
benchmark是一个词嘿嘿。
那个衣服拿来!
@jeffz 没有仔细看,对于评论也是有要求的。不过拿衣服对于你来说简直是手到擒来的事情。
想知道,Jscex 能和coffeescript 结合一起用吗?
nodeclub里随处可见的proxy.trigger()的确让代码不够优雅,另外一个就是楼主提出的错误处理问题,分散不易处理,EventProxy没有提供相应的处理机制。下面的代码,人家还等你去trigger呢,你直接就return了,太粗鲁了点。
if(err) return next(err);
proxy.trigger('topics',topics);
其实,async.js已经比较好的解决了这个问题。这里我不喜欢同步化,不伦不类,异步问题就用异步的解决方案,访问数据库时并行执行是有好处的,数据库不一定在同一机器上。async.js使用也简单,我花几分钟就把主页改过来了,跑了一下没问题,下面用async.js的代码,看起来顺多了,是个人喜好吗?另外,nodeclub里对mongodb的使用,不如用mysql了。好东西还看怎么用啊。
var async = require('../async');
exports.index = function(req,res,next){
var page = Number(req.query.page) || 1;
var limit = config.list_topic_count;
var render = function(tags,topics,...){
...
res.render('index',{...});
};
async.parallel({
tags: function(callback) {
tag_ctrl.get_all_tags(callback);
},
hot_topics: function(callback) {
opt = {limit:5, sort:[['visit_count','desc']]};
topic_ctrl.get_topics_by_query({},opt,callback);
},
...
},
function(err, results){
if (err) return next(err);
render(results.tags,...);
});
};
这一系列的文章都非常不错, 新手受益匪浅啊! 继续关注ing…
天然支持CoffeeScript。
显然是个人喜好,同步化有什么不伦不类的,好多语言早这么做了,用同步的方式表达异步逻辑就是异步问题的解决方法之一,非要用回调什么解决异步问题这个叫做禁锢思维。编程的目的在于易于表达逻辑,清晰表达,易于抽象等等,所以产生各种编程范式,没有说一定要怎样解决的。
还有你用async处理这里当然OK,如果稍微有点逻辑,例如循环啊,判断啊啥啥的。我不是提到Leaky Abstraction了吗,普通的异步抽象方式都比较Leaky,Jscex是遵循JS的语义和表达方式,灵活性是图灵完备的,所以比普通抽象要少Leaky得多。
还有我说了嘛,如果你能确定访问资源可以并行,那就并行,现在这种么就是盲目并行,我保证数据库跑在一台机器上且不能动态伸缩。还有其实MongoDB很好的啊,很方便,MySQL还要键表,还不能保存复杂结构,很麻烦。
哈哈,我正在把nodeclub改成async和mysql,还加了几个小功能.
- 复杂逻辑在async里一点问题都没有。nodeclub首页这个例子恰巧很简单。
- coding的时候很难保证以后数据库怎么部署的,再说也没必要。
- 纯属个人偏好,我的偏好比较大众化的。 关于mongodb, 我没表达清楚,我其实想说:nodeclub里对mongodb的使用方式,不如用mysql了,目前的这种使用方式,就是折腾自己,再说一句"不伦不类"吧。还是那句话:好东西还看怎么用啊。
@sumory 有点误会,我不是建议放弃mongodb。不过按目前这种存储结构设计,用mysql会好很多。目前的schema是为关系数据库设计的。
@newcoder 是的,model关系什么的都是单独保存的,"字段"也很齐
在nodejs里面搞同步化,我有一点提醒:nodejs中执行的函数必须是迅速返回的,包括回调函数也是如此。这是nodejs之所以称为异步框架的原因,也是nodejs对应用代码的要求。不遵循这个要求的应用是不可能在真实环境中跑起来的,当然做做research或者demo是可以的。
Jscex的神奇之处就是用同步的形式的代码,但是每一步$await都是异步操作,不是阻塞的。了解一下Jscex吧,不要被传统异步编程模式限制了思维。
@newcoder 多给点例子吧,我是不清楚怎么方便地使用async写while啊if什么的。Coding的时候当然要关注数据库怎么部署的,理想年代还未到来。MongoDB多方便啊,字段都不用定义的,想加字段随意。