Node.js的Connect框架的代码重写与改进

Node.js的Connect框架的代码重写与改进

Connect框架简介

Connect框架是建立在Node.js的基本http.server功能之上,帮助实现结构化的web服务器逻辑的框架。Connect框架建立在两个重要的设计模式之上。

1) 责任链模式

在处理web请求时常需要作分派处理。例如,ASP.NET MVC支持按照请求参数将处理分派至某个Controller类的某个Action方法,以及根据Action方法的返回结果类型分派不同的结果操作(如ViewResult、JsonResult)。根据请求参数分派至Action方法的过程与C#语言的方法重载(overload)语言机制有相似之处,因为选择方法不仅依靠方法名称,还将请求参数与方法声明的参数相匹配。根据结果类型分派执行不同操作则是标准的OO语言机制。Java的struts框架与ASP.NET MVC相似,用户请求按照路径被分派给Action类及相关的ActionForm类,它们代表请求数据的Java Bean(数据体)及对应操作,而Action执行后返回一个字符串,用来找到下一步的操作(ActionForward),通常是一个jsp页。与ASP.NET MVC依靠编写代码不同,struts的相关配置(如action-mappings)都用XML文件,这是Java平台的传统。它应当是为了方便设计工具,仅靠人眼阅读配置确实很困难。

作为题外话,Java平台的良好文档象是有意在作示范,例如:

Struts ActionForm类的文档快照

另一方面,一些Java文档也强烈暗示应当由社区补充完善文档的欠缺之处、并提供示例教程等。(工作中程序员也应当有充足的时间写文档。)下图中的文档指出充足的文档是与Java兼容的必要条件:

Struts action包的文档快照

基于Javascript脚本的Connect框架不能这么奢侈,它依赖于更简单的责任链模式。不过它简单清晰的概念却带来强大的功能。Node.js将Web服务器视为一个函数:function server (request, response) { ... };Connect则将所有服务的步骤都视为型为function action (request, response, next) { ... }的函数。这些步骤函数被简单地排列成一条责任链按顺序执行。每个步骤完成特定的处理任务,并且它可以判断是否应当由自己处理、以及处理后是否应结束整个服务;它通过调用next()将处理交给下一个步骤函数,如果不调用则服务结束。

Connect用简单的责任链也实现了分派机制,并且较前面所述的ASP.NET MVC与Java Struts的分派机制相比有更为灵活之处。Connect用相同机制实现按请求参数(request)的分派、以及按处理结果的分派(参见后继讨论)。不仅如此,Connect支持多级处理。作为一个典型例子,Express框架(基于Connect)允许根据请求的路径参数多次处理,如为同一用户id所作的不同操作,可以都首先经过相同的载入用户资料的步骤,再按操作类型分派给不同的后继步骤。(ASP.NET与Java也有Filter的概念,用于在Action执行前后作补充处理;但Filter概念并未很好地融入框架。)

Connect也用到组合(Composite)的设计模式。多个步骤函数可以组合成一个步骤,实际上作为Web服务器的主函数就是一个由子步骤合成的函数。不仅如此,Connect还允许将可以独立运行的Web服务器函数挂载到某一路径下,作为一个子步骤。

2) 单线程无阻塞异步模式

Node.js以单线程无阻塞异步执行为特色。Node.js下的Web服务器能够在单线程下运行,而同时响应多个用户的请求,不会因某个用户等待IO而挂起,这得益于在做耗时的IO操作时主动让出(yield)线程的设计模式。下例中的代码行读取远程url的数据,该代码行的执行立即结束,它所设置的handler则在数据或错误信息到达时被系统触发执行。

// 异步回调模式
http.get(url)
    .on('data', dataHandler)
    .on('error', errorHandler);

// 同步执行模式,仅为示例,getSync方法实际上不存在
var data = http.getSync(url);   // 可能抛出Error

使用回调函数来表达执行逻辑使编程较为麻烦,不过有了它不依赖于系统的帮助就能在单线程下完成多任务的执行。同步格式的代码实际上要清晰得多。如果系统支持线程上下文的切换,那么执行同步IO操作的线程自动进入休眠状态、等待被消息唤醒,而其他线程被调入执行,这就支持了同步编程而避免了回调编程的麻烦。

不过,正如我在此前的博客中指出,回调方式也有自己的优越之处。当生成某个结果同时需要多个耗时操作(如生成动态网页的不同部分)时,异步回调模式让系统选择执行顺序,而较同步代码效率更高。

// 同步执行模式,仅为示例
// 同步执行下,如果与bigTask2有关的数据能更快到达,系统不能优先处理它,同时等待bigTask1的数据;
// 实际上系统不能同时触发两个task。
var part1 = bigTask1(),
    part2 = bigTask2();
makePage(part1, part2);

Node.js的单线程模式要求用户代码不能过长时间地占用CPU(如耗时的同步IO操作),否则会使系统不能响应其他用户的请求。即时有了线程切换,由用户代码自行让出线程(而不是强制切换)仍然是操作系统的选择之一。苹果公司直至1999年发布的Mac OS 9仍然是非占先(non-preemptive multitasking)、无进程内存保护的操作系统,直到那时出错的软件仍会挂起整个系统;此后苹果机的CPU改为使用与PC兼容的Intel芯片、操作系统改为使用Unix标准的Darwin内核的Mac OS X。操作系统不应仅被理解为用户操作界面,操作界面是可以模拟的。独特的CPU架构与非占先、无内存保护的操作系统,可能会有自己的软件生态圈,如产生独特的软件相互合作的模式。应当相信作为首个有图形界面的个人电脑Apple,它的原生系统作为领跑者被社区继承,只是不足与外人道尔。

我重写的Connect框架中步骤函数被命名为NextStep。NeXTSTEP图形操作系统发布与80年代末,它是Mac OS X的前身,也是首个Web浏览器的诞生地。

Connect框架也是单线程无阻塞异步模式的范例。Connect的步骤函数不是用return返回传递给后继步骤的数据,而是使用next()调用,而next()调用可以被嵌套在回调中。

connect()
    .use(function (req, res, next) {
        filesys.readFile(fname, function (err, data) {
            // 无法用return。
            next(err || data);          // 用法与Connect框架略有不同,说明见后。
        });
    })

对Connect框架的代码重写与改进

我重写的Connect框架与标准版本有以下的不同(不兼容)之处:

  1. 标准版本的带参数的next(err)调用只用来传递错误、而不是数据,不带参数的next()调用表示无错误的后继执行。标准版本不支持显式的数据传递。

    我的实现中单参数的next(obj)可被用来传递数据或错误,是否是错误由obj是否是Error类型决定。而双参数的next(obj, isError)则可显式指定参数是数据还是错误。

    后继步骤函数执行时,数据或错误可用req.datareq.err获取。

  2. 标准版本中,添加普通步骤函数及错误处理步骤函数都调用唯一的方法use(handler)handler是否是错误处理步骤函数由其参数个数(带有额外的err参数)决定。另外,use(route, handler)也用于将步骤函数挂载到指定路径下。

    我的实现中为这些目的引入了不同方法,如mount(route, handler)用于挂载、model(handler)error(handler)用于设置普通与错误处理步骤函数。双参数的model(modelClass, handler)error(errorClass, handler)则用来根据数据或错误的类型匹配处理函数。我的版本中错误处理步骤函数与普通步骤函数有数量相同的参数个数,这是因为如果为错误处理函数给出err参数,就应该也为普通函数给出data参数。

    如果设置了compatibleMode选项,原版中用use()根据参数个数设置错误处理步骤函数、以及用use()作挂载仍可使用。

我重写Connect框架的动机另一个动机,是目前流行的扩展Connect框架的Express框架引入了过分复杂的View Engine的概念,而未充分使用Connect的基本的责任链设计模式。实际上,很容易直接使用Connect框架同时支持不同的视图生成模板。配置复杂的框架是令人生厌的,尤其在缺少文档需要尝试不同设置的效果的时候;我认为这种知识是没有智慧的。使用较为清晰简单的工具集、在需要时手写代码定制出想要的功能,这更能让人发挥出才能。Node.js本身及Connect框架是较好的用少量代码实现Web服务器的示例;而写出高质量的Javascript代码也有一定难度,既是“个人项目”、也是“集体项目”。

Connect框架本身很小,原版的主要模块也只有300行代码,但代码较不可读。而代码质量比之更差的有的是,如Express框架下用来发送静态文件的serve-static模块、及被它用到的send模块。Node.js本身的基础模块代码也缺少可读性。我认为开源代码与文档必须清晰准确,否则无法使用,摸索使用改动未真正理解的软件是被用作黑客试验材料(有些开源代码象是被故意乱码,或是由更高级的工具生成)。另一方面,开源代码应当成为程序员学习的范例,清晰的代码也是程序员贡献改进意见的基础。实际上,质量很差的开源代码很多,如Java JDK的源代码,它们都亟待社区的重新编写。社区的这些投入会带来有价值的工作机会,并积累知识与智慧。我也在此前的博客中议论,社区有隐私权,工作的成绩没有必要对外公开,而应当在水平相当的社区之间自愿交流。

本文附带了一个演示我写的Connect框架功能的小示例网站程序,其中包括简单的读取静态文件的功能、以及获取远程天气预报数据的功能(仅为演示)。此外,由于代码不长,我写的Connect框架源码也粘贴于下。说明:

  • 由于可能将代码发布到github,代码与示例使用英文注释。
  • 注意到示例中远程联接timeout会抛出异常终止服务器,尽管代码中已经添加了错误处理。Node.js的net模块在延时回调中抛出异常(!),代码也不易理解,因此难以跟踪判断错误位置。
/*!
 * connect
 * Copyright(c) 2010 Sencha Inc.
 * Copyright(c) 2011 TJ Holowaychuk
 * Copyright(c) 2015 Douglas Christopher Wilson
 * Copyright(c) 2015 Ke Hang
 * MIT Licensed
 */

'use strict';

var http = require('http');

/**
 * Makes a new function instance from a base function.
 *
 * The new function uses the base function as its proto, thus inherits all
 * member fields of it. When called, the new function forwards the call to the
 * base function, and uses itself as context.
 */
function inheritFunction (baseFn) {
    inherited.__proto__ = baseFn;
    return inherited;

    function inherited () {
        return baseFn.apply(inherited, arguments);
    }
}

module.exports = createNextStep;

// additional exports
createNextStep.__proto__ = {
    __proto__:      Function.prototype,

    NextStep:       NextStep
};
function createNextStep () {
    var nextStep = inheritFunction(NextStep);
    nextStep._init.apply(nextStep, arguments);
    return nextStep;
}

NextStep.__proto__ = {
    __proto__:      Function.prototype,

    _init:          function (handler, options) {
                        if (!options && typeof handler == 'object')
                            options = handler, handler = null;

                        this._stack = [];
                        this.options = options || {};
                        this.options.__proto__ = this.defaultOptions;

                        if (handler) this.use(handler);
                    },

    use:            use,
    mount:          mount,
    model:          model,
    error:          error,
    listen:         listen,

    Link:           Link,                       // exported for user extension
    defaultOptions: {
                        compatibleMode: false,  // use(errorHandler); use(route, handler)
                        port: 3000
                    },

    // Instance fields, for documentation purpose only.
    options:        void {},
    data:           void {},
    err:            void {},
    _stack:         void [Link]
};

/**
 * The main function that handles http requests.
 */
function NextStep (req, res, done) {
    var self = this, i = 0;
    next();

    function next (data, isError) {
        if ((isError !== undefined) ? isError : (data instanceof Error))
            req.err = data, req.data = undefined;
        else
            req.data = data, req.err = undefined;

        while (true) {
            if (i == self._stack.length) {
                if (done == null)
                    throw new Error('connect: NextStep: unfulfilled next() call!');
                else
                    return done();
            }
            var link = self._stack[i++];
            if (link.test.call(self, req, res)) break;
        }

        // no fall-through for MountLink
        link.handler.call(self, req, res, link instanceof MountLink ? null : next);
    }
}

function listen (port) {
    var httpServer = http.createServer(this);
    httpServer.listen(port || this.options.port);
    return this;
}

function use (route, handler) {
    if (!handler) handler = route, route = null;

    if (this.options.compatibleMode && route)
        this.mount(route, handler);
    else if (this.options.compatibleMode && !route && handler.arity == 4)
        this.error(compatibleErrorHandler);
    else if (!route)
        this._stack.push(new UseLink(handler));
    else
        throw new Error("connect: NextStep.use(): Invalid argument!");

    return this;

    function compatibleErrorHandler (req, res, next) {
        return handler.call(this, req.err, req, res, next);
    }
}

function mount (route, handler) {
    if (route == '/') throw Error('connect: NextStep.mount(): Mount on root is not allowed!');
    this._stack.push(new MountLink(route, handler));
    return this;
}

function model (modelClass, handler) {
    if (!handler) handler = modelClass, modelClass = null;
    this._stack.push(new ModelLink(modelClass, handler));
    return this;
}

function error (errorClass, handler) {
    if (!handler) handler = errorClass, errorClass = null;
    this._stack.push(new ErrorLink(errorClass, handler));
    return this;
}

function Link (test, handler, isError) {
    this.test       = doTest;
    this.handler    = (handler instanceof http.Server) ? httpServerHandler : handler;

    /**
     * Tests whether there is an error first.
     */
    function doTest (req, res) {
        if (Boolean(req.err) != Boolean(isError)) return false;
        if (test) return test.call(this, req, res);
        return true;
    }

    /**
     * Helps to use an http.Server instance.
     */
    function httpServerHandler (req, res) {
        handler.emit('request', req, res);
    }
}

function UseLink (handler) {
    Link.call(this, null, handler);
}

function ModelLink (modelClass, handler) {
    Link.call(this, modelClass && modelClassTest, handler);
    function modelClassTest (req) { return req.data instanceof modelClass; }
}

function ErrorLink (errorClass, handler) {
    Link.call(this, errorClass && errorClassTest, handler, true);
    function errorClassTest (req) { return req.err instanceof errorClass; }
}

function MountLink(route, handler) {
    var s = route
            .replace(/^\/|\/$/g, '')
            .replace(/([\/.$()\[\]{}])/g, '\\$1'),
        re = new RegExp('^(.*://[^/]+/|/)(' + s + ')([?/]|$)(.*)', 'i');

    Link.call(this, mountTest, handler);

    // req.originalUrl and req.url are set for the mounted handler
    function mountTest (req) {
        var mm = req.url.match(re);
        if (mm) {
            req.originalUrl = req.originalUrl || req.url
            req.url = mm[1] + (mm[3] == '?' ? '?' : '') + mm[4];
            return true;
        }
        return false;
    }
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值