Hello World
Spiga

使用Jscex改进Node Club(2):引入Jscex类库

2012-02-20 21:57 by 老赵, 3443 visits

之前我们已经将Node Club在本地运行起来了,接着我们便来引入Jscex类库,为常用异步方法扩展出Jscex版本,并试着编写一些最简单的Jscex代码。

安装Jscex包

为项目中安装Jscex十分容易,因为Jscex已经发布在官方NPM源中,我们只需修改package.json文件即可:

{
    "name": "NodeClub"
  , "version": "0.0.1"
  , "main": "./app.js"
  , "private": true
  , "dependencies": {
      "express": "2.5.1",
      "ejs": "0.5.0",
      "eventproxy": "0.1.0",
      "mongoose": "2.4.1",
      "node-markdown": "0.1.0",
      "validator": "0.3.7",
      "nodemailer": "0.3.1",
      "jscex": "0.6.x",
      "jscex-jit": "0.6.x",
      "jscex-async": "0.6.x",
      "jscex-async-powerpack": "0.6.x"
  }
}

在此我们引入四个和Jscex相关的包,并指定为0.6.x版本,以便与NPM上的Jscex同步更新。修改之后,便可以使用npm install安装新增的Jscex包:

$ npm install
jscex-async@0.6.0 ./node_modules/jscex-async 
jscex-async-powerpack@0.6.0 ./node_modules/jscex-async-powerpack 
jscex@0.6.0 ./node_modules/jscex 
jscex-jit@0.6.0 ./node_modules/jscex-jit

有了NPM之后,每次为项目添加依赖库也只需编辑package.json再npm install就行了。如果需要将本地版本与NPM源保持同步更新,也只需一句npm update命令。

Node Club中的异步方法

在JavaScript有各种各样的异步方法,要配合Jscex使用的话,则必须使用能与Jscex适配的异步方法,简称Jscex异步方法。在Node Club项目中出现最多的异步方法便是使用mongoose访问MongoDB。mongoose不仅仅提供了MongoDB的访问能力,它还提供相当的ORM功能,可以让我们快速的定义模型,并与MongoDB数据库里的集合映射起来,例如:

var mongoose = require("mongoose"),
    Schema = mongoose.Schema,
    ObjectId = Schema.ObjectId;

var UserSchema = new Schema({
    name: String,
    password: String,
    createAt: Date
});

var User = mongoose.model('User', UserSchema);

此时User类型便已经和数据库建立了映射关系,例如我们可以操作数据:

function updatePassword(id, password, cb) {
    User.findOne({ _id: id }, function (error, user) {
        if (error) return cb(error);

        user.password = password;
        user.save(function (error) {
            if (error) return cb(error);

            cb(null);
        });
    });
}

以上代码的作用是更新一个用户的密码,我们先使用User.findOne获得用户对象,修改后再使用save方法存回数据库。这里用到的都是一种“标准”的异步方法模式,即使用回调函数获得(或返回)结果,其第一个参数为错误对象,如果不为空,则表示出错了。因此每次调用一个异步方法的时候,我们都需要在回调方法里判断是否出错,这也是异步编程的麻烦之一。

加载Jscex类库

如果要在项目里加载Jscex,我会建议使用一个简单的模块来存放与Jscex初始化相关的代码,例如libs/jscex.js

var Jscex = require("jscex");
require("jscex-jit").init(Jscex);
require("jscex-async").init(Jscex);
require("jscex-async-powerpack").init(Jscex);

var Jscexify = Jscex.Async.Jscexify;

var mongoose = require("mongoose");

var mp = mongoose.Model.prototype;
mp.saveAsync = Jscexify.fromStandard(mp.save);
mp.removeAsync = Jscexify.fromStandard(mp.remove);

var m = mongoose.Model;
m.findByIdAsync = Jscexify.fromStandard(m.findById);
m.findOneAsync = Jscexify.fromStandard(m.findOne);
m.findAsync = Jscexify.fromStandard(m.find);
m.countAsync = Jscexify.fromStandard(m.count);

exports.Jscex = Jscex;

前几行代码是标准的Jscex引入方式。而从mongoose相关的代码开始,便是在为其扩展Jscex异步方法。mongoose对扩展十分友好,它把内部的元数据暴露出来,让我们进行统一扩展。例如为Model扩展一些静态或是实例方法,便相当于为所有的模型及其它们的实例添加了方法。这种做法充分利用了JavaScript的灵活性,假如它把所有的元数据都隐藏起来,那我们没法如此简单直接地引入Jscex扩展了——当然总是有办法的,只是麻烦一些。

这里我强烈建议每个JavaScript类库或框架的开发者都能实现这种方式,给使用者充分的扩展途径。以Jscex自身为例,它的每个异步任务对象都是Jscex.Async.Task类型的实例,这样jscex-async-powerpack模块便可以轻易补充各种强大的辅助方法。

此外,由于mongoose遵守异步方法“标准模式”(即使用回调函数传回结果,其第一个参数为错误对象),因此只要一个fromStandard辅助函数便可全部应对。在以后的代码中,我们会大量使用这些扩展后的Jscex异步方法。

编写简单的Jscex异步函数

现在我们就直接写几行Jscex代码吧。例如在controllers/user.js文件里定义了以下几个方法:

function get_user_by_id(id, cb) {
    User.findOne({ _id: id }, function (err, user) {
        if (err) return cb(err, null);
        return cb(err, user);
    });
}

function get_user_by_name(name, cb) {
    User.findOne({ name: name }, function (err, user) {
        if (err) return cb(err, null);
        return cb(err, user);
    });
}

function get_user_by_loginname(name, cb) {
    User.findOne({ loginname: name }, function (err, user) {
        if (err) return cb(err, null);
        return cb(err, user);
    });
}

function get_users_by_ids(ids, cb) {
    User.find({ '_id': { '$in': ids } }, function (err, users) {
        if (err) return cb(err, null);
        return cb(err, users);
    });
}

function get_users_by_query(query, opt, cb) {
    User.find(query, [], opt, function (err, users) {
        if (err) return cb(err, null);
        return cb(err, users);
    });
}

以上几个方法都有相同的特征:它们都是简单的调用一个mongoose的异步方法,判断是否出错,并返回结果。正如之前提过的那样,编写异步代码的麻烦之一,便是在每次回调时都要判断是否出错,因此在实际项目中的异步代码都会比各种“演示”用的玩具代码更麻烦一些。不过我们可以使用Jscex来改写这些代码

var Jscex = require("../libs/jscex").Jscex;

var get_user_by_id_async = eval(Jscex.compile("async", function (id) {
    return $await(User.findOneAsync({ _id: id }));
}));

var get_user_by_name_async = eval(Jscex.compile("async", function (name) {
    return $await(User.findOneAsync({ name: name }));
}));

var get_user_by_loginname_async = eval(Jscex.compile("async", function (name) {
    return $await(User.findOneAsync({ loginname: name }));
}));

var get_users_by_ids_async = eval(Jscex.compile("async", function (ids) {
    return $await(User.findAsync({ '_id': { '$in': ids } }));
}));

var get_users_by_query_async = eval(Jscex.compile("async", function (query, opt) {
    return $await(User.findAsync(query, [], opt));
}));

在编写Jscex方法中,我们无需操作回调函数,只要在异步点上使用$await进行“等待”即可。我们也无需显式地处理错误,因为一旦出现错误便会抛出异常,异常如果没有被某个try…catch捕获到,则会顺着调用路径一路向上传递,直到被我们的代码或是系统捕获为止。Jscex将简单易用的传统编程模式与实践重新带回异步编程中,做到“同步编写,异步执行”的效果。这就是Jscex诞生的意义。

从下一篇文章开始,我们将逐步改造Node Club网站中现有的代码。

相关文章

Creative Commons License

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

Add your comment

7 条回复

  1. zero
    202.133.226.*
    链接

    zero 2012-02-21 09:35:35

    扩展mongoose时增加的几个函数如果放在一个对象里面会不会看起来更加舒服,而且可以减少污染。不知道有没有什么副作用?

    var mp = mongoose.Model.prototype;
    mp.Jscext.save = Jscexify.fromStandard(mp.save);
    mp.Jscext.remove= Jscexify.fromStandard(mp.remove);
    
    var m = mongoose.Model;
    m.Jscext.findById = Jscexify.fromStandard(m.findById);
    m.Jscext.findOne = Jscexify.fromStandard(m.findOne);
    m.Jscext.find = Jscexify.fromStandard(m.find);
    m.Jscext.count = Jscexify.fromStandard(m.count);
    

    最近对js异步长草

    还有就是我用不了code block?

  2. 老赵
    admin
    链接

    老赵 2012-02-21 09:58:03

    @zero

    其实我的目的就是用起来跟原来一样,原来怎么用某个方法,现在加个Async就可以了,怎么更舒服些?副作用基本想不出来,现在有什么污染?

    评论里的Code Block?了解下Markdown就可以了

  3. zero
    202.133.226.*
    链接

    zero 2012-02-21 10:46:12

    我本来想尽量少改动原来代码来实现异步,也想试试看几种不同的异步库,比较一下区别。saveAsync的确是一个很好的名字,但是具体如何实现这些,我想能统一处理,方便切换。

    试试看markdown

    var m = mongoose.Model;
    var aysncLib = m.Jscex;
    m.findOneAsync = asyncLib.findOne;
    
    ...
    
    var get_user_by_id = eval(Jscex.compile("async", function (id) {
        return $await(User.findOneAsync({ _id: id }));
    }));
    

    不知道是不是可行。我跑跑试试

  4. 老赵
    admin
    链接

    老赵 2012-02-21 10:53:18

    @zero

    你要从一个异步类库切换到另一个基本都要大量改写代码,这个基本没法避免,实现功能都不同。所以你“切换”以后最多那些异步方法的名字相同,不同的异步类库使用时代吗也不一样,又不能随时换来换去。

    还有Markdown不是你这么用的,网上搜下说明吧,比如这个

  5. 十一月的雨
    202.106.93.*
    链接

    十一月的雨 2012-02-23 14:48:47

    很好.很不错

  6. bruce
    121.15.171.*
    链接

    bruce 2012-07-03 10:06:26

    赵兄,能否帮我看下那里写错了?

    var getdeskitemsAsync=eval(Jscex.compile("async", function (req, res) {
        res.contentType('text/plain');
        res.end("ddd");
    }));
    
    Jscex.Unjscexify = {
        toRequestHandler: function (fn) {
            return function (req, res, next) {
                fn(req, res).addEventListener("failure", function () {
                    next(this.error);
                }).start();
            }
        }
    };
    
    exports.getdeskitems=Jscex.Unjscexify.toRequestHandler(getdeskitemsAsync);
    

    运行报异常:

    The "complete" method can only be called in "running" status.

  7. 老赵
    admin
    链接

    老赵 2012-07-03 10:42:21

    @bruce

    有问题发到用户组吧。我一会儿再想想,一下子没看出问题来。

发表回复

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

昵称:(必填)

邮箱:(必填,仅用于Gavatar

主页:(可选)

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

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

使用Live Messenger联系我