使用Jscex改进Node Club(3):分析首页实现

2012-03-04 00:57

使用Jscex改进Node Club(3):分析首页实现

by jeffz@live.com (老赵)

at 2012-03-03 16:57:04

original http://blog.zhaojie.me/2012/02/jscexify-nodeclub-3-home-page-implementation.html

上次我们已经将Jscex成功地引入项目,现在便可以正式开始关注Node Club的实现了。Node Club中存在大量基于回调的JavaScript代码,颇有无从下手的感觉。既然如此,我们便随便挑一个,从首页入手吧!

首页逻辑

我们先从首页的JavaScript代码开始。首页的目标其实很简单,加载几部分数据组成一个对象,再交给模板引擎生成HTML代码并输出。就目前来说,显示首页需要加载以下数据:

  • 标签
  • 最新话题
  • 热门话题
  • 明星用户
  • 得分最高的用户
  • 无回复的话题
  • 话题总数

对于传统Web开发技术来说,要做到这点着实容易,伪代码如下:

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
}

但是在Node.js这个大环境中,这些方法都是用回调函数来返回结果的,因此代码往往会写成:

function (request, response, next) {
    // 标签
    tag_ctrl.get_all_tags(function (err, tags) {
        if (err) return next(err);
        
        // 最新话题
        topic_ctrl.get_topics_by_query(..., function (err, topics) {
            if (err) return next(err);
            
            // 热门话题
            topic_ctrl.get_topics_by_query(..., function (err, hot_topics) {
                if (err) return next(err);
                
                // 明星用户
                topic_ctrl.get_topics_by_query(..., function (err, stars) {
                    if (err) return next(err);
                    
                    // 得分最高用户
                    user_ctrl.get_users_by_query(..., function (err, tops) {
                        if (err) return next(err);
                        
                        // 无回复话题
                        topic_ctrl.get_topics_by_query(..., function (err, no_reply_topics) {
                            if (err) return next(err);
                            
                            topic_ctrl.get_count_by_query(..., function (err, topic_count) {
                                if (err) return next(err);
                                
                                // 输出HTML
                                response.render("index", { ... });
                            });
                        });
                    }); 
                });
            });
        });
    });
}

很多人不喜欢这类层层嵌套的代码,但老实说,因为这里没有涉及逻辑判断,循环等令人头大的问题,所以在我看来其实这种串行的逻辑其实十分清晰(尽管不太漂亮),一眼就能看清楚在做什么。其实让我不喜欢的倒是这里不断出现的错误处理代码:

if (err) return next(err);

我一直把反复出现的错误处理代码作为传统异步编程中最麻烦(又不可遗漏)的方面之一。可惜在大量的示例代码中,这反而会被人忽略掉,造成其实“不怎么麻烦”的假象。我认为,作为一个优秀的异步类库,都应该在错误处理上花点功夫,避免开发人员在各个地方不断浪费青春。例如,在这方面各类Promise模型作的都不错。

首页实现

Node Club使用EventProxy类库来尝试解决大量异步函数的嵌套问题。在首页上主要使用了以下这种模式:

function (request, response, next) {

    // 定义最终的回调函数
    var render = function (tags, topics, hot_topics, stars, tops, no_reply_topics, pages){
        response.render('index', {...});
    };

    // 注册最终的回调函数
    var proxy = new EventProxy();
    proxy.assign('tags', 'topics', 'hot_topics', 'stars', 'tops', 'no_reply_topics', 'pages', render);
    
    tag_ctrl.get_all_tags(function (err, tags){
        if (err) return next(err);
        proxy.trigger('tags', tags);
    });

    topic_ctrl.get_topics_by_query(..., function (err, topics) {
        if (err) return next(err);
        proxy.trigger('topics', topics);
    });
    
    topic_ctrl.get_topics_by_query(..., function (err, hot_topics) {
        if (err) return next(err);
        proxy.trigger('hot_topics', hot_topics);
    });
    
    user_ctrl.get_users_by_query(..., function (err, users) {
        if (err) return next(err);
        proxy.trigger('stars', users);
    });
    
    user_ctrl.get_users_by_query(..., function (err, tops) {
        if (err) return next(err);
        proxy.trigger('tops', tops);
    });
    
    topic_ctrl.get_topics_by_query(..., function (err, no_reply_topics) {
        if (err) return next(err);
        proxy.trigger('no_reply_topics', no_reply_topics);
    });
    
    topic_ctrl.get_count_by_query(..., function (err, all_topics_count) {
        if (err) return next(err);
        var pages = Math.ceil(all_topics_count / limit);
        proxy.trigger('pages', pages);
    });
};

这种模式可以简单概括为:

  • 准备一个最终的回调方法,在获取所有结果后执行,并使用assign方法注册给EventProxy对象。
  • 发起各异步操作,并将结果使用trigger方法提交至EventProxy。

当得到所有结果后,EventProxy自然会执行最终的回调方法,即完成最终的任务。

实现分析

从表面上看来,似乎EventProxy避免了回调函数的层层嵌套,但它的做法只是将每个异步调用分离出来(当然,的确会清晰一些)。如果您把现在的代码与之前“传统”代码相比,会发现两者的差距似乎只是——Tab的数量,或者说是缩进数量。真实的工作,例如每步操作的错误处理代码,还必须完全保留。简单地说,使用EventProxy其实并没有节省什么工作。

而且,我们完全无需使用EventProxy,也可以轻松写出类似的代码:

function (request, response, next) {

    var data = { };
    var steps = ["tags", "topics", ...];

    var done = function (name, value) {
        data[name] = value;
        if (steps.remove(name).length > 0) return;

        response.render("index", {...});
    }

    tag_ctrl.get_all_tags(function (err, tags) {
        if (err) return next(err);
        done("tags", tags);
    });

    topic_ctrl.get_topics_by_query(..., function (err, topics) {
        if (err) return next(err);
        done("topics", topics);
    });

    ...
};

我们只要使用一个steps数组来准备所有的“数据”,每次完成后剔除一个,直到全部剔除为止即可。这么做还有个好处便是无需关注顺序,在使用EventProxy的时候,我们必须将注册时的使用的名称,和回调函数的参数保持顺序一致。试想,如果要增加一个步骤或是改变一些顺序,我们则必须加倍小心了。所以在我看来,在这里使用EventProxy并没有带来太多的益处,从简化编程的角度来说,效果十分有限。

要简化编程体验,还是得看Jscex的,下次我们便来改造Node Club的首页。

相关文章