CNode

connect源码分析--基础架构

精华
Cchangchang发布于14 年前最后回复9 年前19 回复20505 浏览0 收藏

connect是TJ tx给node.js社区贡献的一个热门的web基础框架。TJ的另一力作express框架便是在它基础之上构建的。与express不同,connect更加短小精悍,是一个偏向基础设施的框架。

正如名字所表达的一样,connect框架做的事情很简单,就是把一系列的组件连接到一起,然后让http的请求依次流过这些组件。这些被connect串联起来的组件被称为中间件(middlewire)。在connect中,http请求的处理流程被划分成一个个小片段,每一个小片段代表一项处理任务(如:请求body的解析,session的维护等),由一个中间件负责,前后片段之间靠request,response等对象传递中间数据。connect框架对这些处理细节并不关心,只知道将请求从一个中间件导向下一个中间件。connect的核心代码非常精简,加上注释,也就只有寥寥200来行代码。本文参考的connect版本为2.2.2(这个版本号好像有点特别-_-b)。

先上个connect官网提供的一个Hello world例子吧:)

var connect = require('connect')
  , http = require('http');
var app = connect()
  .use(connect.favicon())
  .use(connect.logger('dev'))
  .use(connect.static('public'))
  .use(connect.directory('public'))
  .use(connect.cookieParser('my secret here'))
  .use(connect.session())
  .use(function(req, res){
    res.end('Hello from Connect!\n');
  });
http.createServer(app).listen(3000);

很简单,很清晰的一个链式调用代码。风格与express的很像,不,应该是说express的风格跟它很像,总得有个先来后到,是吧。那么下面开始对connect的核心代码进行解析吧~

function createServer() {
  function app(req, res){ app.handle(req, res); }
  utils.merge(app, proto);
  utils.merge(app, EventEmitter.prototype);
  app.route = '/';
  app.stack = [];
  for (var i = 0; i < arguments.length; ++i) {
    app.use(arguments[i]);
  }
  return app;
};

这个方法里主要定义了一个名字叫app的函数,然后通过utils.merge方法,把proto(由lib/proto.js定义,后面会介绍)和EventEmitter的方法merge到了app函数上。在js中,function也是对象,这就相当与给app对象添加了proto和EventEmitter的特性。这个方式,有点像松本tx提到的Ruby中的mixin,是实现多继承的一种途径。与mixin所不同的是,这里的merge会把后面merge进来的属性覆盖掉原来已有的同名属性。

merge的结果,让我们拥有了一个继承了proto和EventEmitter的function对象,像app.handle, app.use等这些方法,其实都是从proto处继承而来的。这个function的函数体很简单,就是调用自己的handle方法来处理http请求。随后,这个function会被当作回调函数传递给node.js原生http模块的createServer方法,成为http请求处理的源头。

这里有个地方需要注意的,createServer函数返回的是一个叫app的function,期间并没有用new关键字,所以它不是由app作为构造函数而构建出来的一个纯粹的js对象,它依然还是一个function,所以它可以作为回调函数传递给http.createServer方法。

此外,createServer可以接受任意数量的中间件对象作为参数,在函数的最后会逐一加载这些传递进来的中间件。

lib/connect.js文件的最后,会遍历lib/middleware目录下的connect自带的中间件模块,并将这些模块挂到app和app.middleware之下,于是就有了例子里的connect.favicon, connect.logger等。

app.use(route, fn)

route是中间件所使用的请求url的pattern,默认为'/'。connect会按照加载顺序,逐一执行pattern与请求url匹配的中间件处理函数。第二个参数fn即中间件处理函数,有两种定义形式。 第一种形式为:

function(req, resp, next)

第二种形式为:

function(err, req, resp, next)

第一种是正常的处理函数,第二种是异常处理函数。 req, resp为http模块的request和response对象。next是触发后续流程的回调函数,带一个err参数。通过传递给next传递一个err参数,告诉框架当前中间件处理出现异常。如果err为空,则会按顺序执行后面正常处理函数,忽略异常处理函数;相反,如果err非空,则会按顺序执行后续的异常处理函数,而忽略正常处理函数。

在connect中,请求的处理流程是一个异步的过程,fn函数的返回并不代表处理流程的结束,所以在这里需要用next回调的形式通知框架执行后续的流程。但如果某一个中间件函数认为请求的流程到它那里已经处理完毕,无需再执行后面的流程,则可以直接返回,而不用再调用next回调函数了(包括正常和异常处理函数)。如果请求遍历完中间件列表后仍在调用next函数,connect则会认为这个请求没有中间件认领。这时,如果next的err参数非空,则会给页面返回500错误,表示server出现了内部错误;如果err为空,则返回404错误,即访问的资源不存在。

依照中间件函数的定义,我们也可以编写自己的中间件函数,然后通过use方法加入到connect的处理流程中。

在这里,我们也可以看出来,各个middleware之间其实并没有直接的依赖。request和response就成为它们在connect中传递信息的唯一桥梁。前面的中间件处理完毕后,把结果附到request或response之上,后面的中间件便可以从中获取到这些数据。所以,中间件的加载顺序在connect中就显得格外重要,必须将被依赖的中间件放在依赖它的模块之前。比如说,解析cookie的中间件应该放在处理session的中间件之前,因为一般session id是通过cookie来传递的。

前面也提到过,http请求的处理源头就是app.handler方法。这个方法也比较简单,主要定义了一个next的函数,也就是上一节提到的next回调函数。next的调用其实是一个尾递归过程。每一次调用next,都会从列表中取出一个中间件,进行pattern匹配检查。这里的pattern匹配,其实是简单的startWith检查。也就是说,请求的url是以pattern开头,并且紧接着下一个字符是/或.的话,才认为是pattern匹配上了,然后才会将请求流入当前这个中间件的处理函数中。pattern匹配的代码如下:

path = utils.parseUrl(req).pathname;
if (undefined == path) path = '/';
// skip this layer if the route doesn't match.
if (0 != path.indexOf(layer.route)) return next(err);
c = path[layer.route.length];
if (c && '/' != c && '.' != c) return next(err);

其中,layter是保存中间件信息的数据结构,结构为:{route: route, handle: fn}。 在handler方法中,我们也可以看到,上一节提到的两类中间件函数的处理判断,代码如下:

var arity = layer.handle.length;
if (err) {
  if (arity === 4) {
    layer.handle(err, req, res, next);
  } else {
    next(err);
  }
} else if (arity < 4) {
  layer.handle(req, res, next);
} else {
  next();
}

可以看出来,connect是根据中间件处理函数的参数列表长度来区分中间件种类的。

查看回复

回复 (19)

L
liushaobo05#1·9 年前

很不错的文章,看了一遍又一遍

P
pittmak#2·14 年前

想了很久要不要回复,怕我的回复贬低了这篇高质量文章,因为没有人回复。 不想说什么,总之以后需要这样的文章,让我们能养成阅读源代码的习惯。

D
dead-horse#4·14 年前

因为router被迁移导致一直在用1.8.5的飘过...

D
dotlife#5·14 年前
引用 dead-horse因为router被迁移导致一直在用1.8.5的飘过...

其实我也遇到相同问题,不过我迁移了,迁移成本很低的。

C
cmingxu#6·14 年前

分析透彻。 connect是 Ruby Rack的克隆版, 最早python也有类似框架。

H
hellowhat#7·14 年前

非常好的文章,对原理讲解的很透彻。

这么好的文章应该让更多人都能看到,要不是通过google搜索,还找不到这儿呢。

建议CNode像ruby-china那样,开一个wiki栏目,把整个社区的知识点系统整理下,方便像我这样的初学者。 再感谢作者分享;)

P
py8765#8·14 年前

看后收获颇多

L
lzhypt#9·14 年前

我想问下楼主,如何用app.use()来实现express中的get、post、put、delete的功能,有没有实例来说明下??还有就是如何用app.use(route,function(){})来直接显示出想要显示的public下面的网页文件???求教

X
xieccy#10·14 年前

给力的文章, 好久没看到分析的这么全的了

J
java0racle#11·13 年前

你自己写的。。。。

M
mq1980#12·13 年前

connect的用法: var api = connect() .use(function(req,res,next){ control.log('api function'); next(); }) .use(function(req,res.next){ foo(); }) .use(function(err,req,res.next){ control.log(err); res.setStatusCode=404; res.wriete('Info Not found.'); res.end(); );

var app = connect() .use(function(req,res,next){ control.log('app function'); next(); }) .use('/api',api) .use(function(err,req.res.next){ control.log('Server error'); });

根据connect的中间件链式的处理,对connect的请求都会在第一个中间件处做处理,当有对api的处理时会到api的处理链中进行,这种用法可以为不同的中间件设置不同的处理流程

《Nodejsinaction》中对Connect有专门的两章进行讲解

L
luckydrq#13·13 年前

中间件之间是以异步的形式来进行衔接的

用“异步”这个词有些不确切,机制上是同步的链式调用

C
c060604#16·12 年前

灰常好 感谢楼主分享

C
CommanderXL#17·11 年前

非常好~

A
Acceptedlc#18·11 年前

顶一个 自豪地采用 CNodeJS ionic

Z
zhangjx#19·10 年前

mark, 不错

参与回复
登录后即可参与回复。登录