Hello World
Spiga

使用Jscex改进Node Club(4):改写首页

2012-03-10 14:11 by 老赵, 2436 visits

上次我们分析了Node Club的首页实现,了解了它的功能以及目前的实现方式。不过在我看来,如今使用EventProxy来辅助页面开发并没有解决部分异步编程中的主要问题。甚至可以说,就目前的EventProxy的使用方式而言,即便不借助任何类库,单纯基于JavaScript也可以得到有过之而无不及的编程体验。这次我们便来使用尝试使用Jscex来改进首页的逻辑。

最后准备

还记得首页的逻辑吗?其中可是用到了许多异步方法:

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) { ... });

如果要用Jscex来实现首页,这些方法都必须变成Jscex异步方法。之前有朋友说,在一个现成的项目中使用Jscex代价太高,因为每个函数都必须Jscex化。但事实上,我们完全可以在一个现有的项目逐步地引入Jscex,因为我们轻易地将已有的异步操作“封装”为Jscex异步方法,例如在controllers/topic.js文件中

/********** 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);

其他两个方法的封装就不列举出来了。可以发现,我们完全不需要一次性把所有的依赖都“重新实现”,可以用到哪儿再封装哪儿,稍后再进行真正的Jscex化——有时候甚至完全无需基于Jscex再写一遍。Jscex适合编写异步操作之间相对复杂的交互,但对于原本就十分简单的异步操作来说,Jscex也并不会带来明显的附加优势。此时我们完全可以保留最普通的异步回调写法,这从任何角度来说都造成问题。

实现首页

还记得之前提出的首页逻辑吗?

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
}

使用Jscex来实现这个逻辑的话,与上述“伪代码”可谓完全一致,唯一的区别只是多了些前后附加的计算逻辑:

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);

最后一段代码,是使用一个辅助方法,将一个Jscex异步方法转化为一个普通的HTTP Request Handler:

Jscex.Unjscexify = {
    toRequestHandler: function (fn) {
        return function (req, res, next) {
            fn(req, res).addEventListener("failure", function () {
                next(this.error);
            }).start();
        }
    }
}

这里我们将发生的任何错误都通过next向外传递,这是标准的处理方式,当然您也可以使用您自己的错误处理策略。您会发现,我们无需复杂的错误处理方式,在编写Jscex方法时,错误会像普通异常一样向外抛出,直到被统一捕获。

我这里还想提一下Node Club代码里的一些问题。Node Club在我看来是一个比较粗糙的项目,代码质量不太好,一方面是代码风格有些乱(例如使用Tab,符号周围空格等等),还有便是把一些简单的代码写得复杂,例如上面“计算最热标签”这样的逻辑,目前的实现是:

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;
});

其实这里完全可以一句话实现:

tags.sort(function (tag_a, tag_b) {
    return tag_b.topic_count - tag_a.topic_count;
});

但我更喜欢使用Underscore提供的函数式编程方式,很重要的一点是它保持了输入数据的不变性。数组的sort方法是对本身进行排序,因此Node Club不得不创建一个数组拷贝。使用Underscore可以简化许多代码逻辑,就好比在实现相同功能时,C#代码比Java语言要简单许多。

并发

并发是好事,不过在上面的实现中,所有的操作是串行的。有人会说这么做起不到并发的效果,但其实我并不在意,因为我们目前的应用是一个服务器端程序,本身就是并发地承受客户端的请求。如果把处理一个请求看作一个事务的话,我们认为单个事务时串行的,但是已经有大量并发的事务。即便事务里的每个操作都并发起来,但单个事务还是要等到所有操作结束后才能完成(即用户看到页面)。由于系统的负载并没有降低,单个事务处理的总时长并没有减少。

您可以做个实验,硬盘上有10个1G大小的文件,您使用顺序的方式复制所有文件,和同时复制这10个文件,所花时间分别是多少?做过服务器压力测试的同学一定知道,我们加大并发量时,处理相同数量请求所需的总时长并不会减少,甚至随着并发量增加,单位时间内的请求处理能力会明显降低。因此,现代的服务器一般都会在并发量增大的情况下采取保护措施,例如对请求排队,或是返回Service Unavailable错误等等。

并发还有个问题,就是当一个操作失败时,很难“取消”其他操作,这会造成无谓的资源浪费。因此,如果我们盲目地将可以并发的操作都并发起来,对服务的健康并没有什么好处,除非可以确定这个并发操作的确可以同时进行(例如网络访问和磁盘读取),否则我并不倾向于(在一个服务器应用程序里)并发访问。不过作为一个演示,我便将所有操作都并发起来吧:

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);
}))

Jscex类库“不提倡”盲目并发,它的并发是“可选”的。如果您想要并发哪些操作,将其放在Task.whenAll即可。Task.whenAll是一个辅助方法,您可以输入一个保存Task的对象或是数组(甚至是对象和数组的嵌套),whenAll返回的Task对象会同时发起那些操作,并等待它们全部返回。返回的结果对象,其结构会和输入时完全一致,或是对象,或是数组(甚至对象和数组的嵌套)。在Jscex中,哪些操作串行,哪些操作并发都是由开发人员决定的,完全可以将其轻松地混合起来。例如,我们串行地执行一些前期处理,例如用户认证,然后再将后续的数个操作并发起来。

在Node Club中有个问题就是“盲目并发”,它是用EventProxy将所有的操作并发起来,而并非有选择的处理。EventProxy适合“盲目并发”的场景,但是对“有选择的并发”支持很差,我们很难选择部分操作串行执行。例如我之前的一个示例“复制完整目录”,使用Jscex只要如普通编程那样直接实现逻辑即可,而无需如传统编程那样使用各种回调,但如果非要“事件驱动”,非要生搬硬套EventProxy,事件和回调交织在一起,实现便会变得非常复杂

作为一个面向开发人员的工具,Jscex除了隐藏必要的复杂度之外,还要让目标程序“可控”,无论是串行、并发还是逻辑表达——Jscex使用JavaScript语法,保证了程序逻辑的灵活与可控,尽可能地避免出现Leaky Abstraction。EventProxy的确提供了一种“完全并发”的抽象,但是对于需要“可控并发”,或是“串行执行”的逻辑和场景便显得无能为力了。

相关文章

Creative Commons License

本文基于署名 2.5 中国大陆许可协议发布,欢迎转载,演绎或用于商业目的,但是必须保留本文的署名赵劼(包含链接),具体操作方式可参考此处。如您有任何疑问或者授权方面的协商,请给我留言

Add your comment

7 条回复

  1. mathgl
    42.2.25.*
    链接

    mathgl 2012-03-11 19:44:19

    考虑在下一个项目中尝试使用 Jscex...

    yield方式的异步很直观.

  2. 老赵
    admin
    链接

    老赵 2012-03-12 10:00:13

    @mathgl

    感动……

  3. 噜哥
    113.108.195.*
    链接

    噜哥 2012-03-12 14:29:38

    看了你的文章:VirtualBox下安装Mac,我按照教程完成后,发现Mac的USB用不了啊,把iPad连上去,VirtualBox里边的MAC没有搜到设备... 我想在虚拟机上做iOS开发的,如果连不了USB,很麻烦啊,悲剧,求解决办法。。。

  4. 老赵
    admin
    链接

    老赵 2012-03-12 15:34:50

    @噜哥

    买Mac。

  5. 古迹
    119.122.235.*
    链接

    古迹 2012-03-30 00:12:10

    看了你的,技巧:使用User Control做HTML生成 http://www.cnblogs.com/JeffreyZhao/archive/2007/12/30/usercontrolasan_template.html

    protected override void OnPreRender(EventArgs e) { base.OnPreRender(e);

    this.rptComments.DataSource = Comment.GetComments(this.PageSize,
            this.PageIndex, out this.m_totalCount);
        this.DataBind();
    }
    

    OnPreRender,还有base.OnPreRender(e);

    这个事件作用是什么,能解释下吗

    有些不明白的是,绑定Repeater为什么放在这个事件里,而不是放到用户控件的 pageload中,就如通常的页面那样,在用户控件中 protected void PageLoad(object sender, EventArgs e) { if (!Page.IsPostBack) { this.rptComments.DataSource = Comment.GetComments(this.PageSize, this.PageIndex, out this.m_totalCount); this.DataBind();

    }
    }
    
  6. 链接

    w_soft 2012-05-29 15:36:52

    老赵你好, 我试了一下,Unjscexify没有定义,现在是怎么用的呢?

  7. 老赵
    admin
    链接

    老赵 2012-05-29 17:12:13

    @w_soft

    你看的是哪里?

发表回复

登录 / 登录并记住我 ,登陆后便可删除或修改已发表的评论 (请注意保留评论内容)

昵称:(必填)

邮箱:(必填,仅用于Gavatar

主页:(可选)

评论内容(大于5个字符):

  1. Your Name yyyy-MM-dd HH:mm:ss

使用Live Messenger联系我