nodejs模块学习之connect解析

前端之家收集整理的这篇文章主要介绍了nodejs模块学习之connect解析前端之家小编觉得挺不错的,现在分享给大家,也给大家做个参考。

nodejs 发展很快,从 npm 上面的包托管数量就可以看出来。不过从另一方面来看,也是反映了 nodejs 的基础不稳固,需要开发者创造大量的轮子来解决现实的问题。

知其然,并知其所以然这是程序员的天性。所以把常用的模块拿出来看看,看看高手怎么写的,学习其想法,让自己的技术能更近一步。

引言

express 是 nodejs 中最流行的 web 框架。express 中对 http 中的 request 和 response 的处理,还有以中间件为核心的处理流程,非常灵活,足以应对任何业务的需求。

而 connect 曾经是 express 3.x 之前的核心,而 express 4.x 已经把 connect 移除,在 express 中自己实现了 connect 的接口。可以说 connect 造就了 express 的灵活性。

因此,我很好奇,connect 是怎么写的。

争取把每一行代码都弄懂。

connect 解析

我们要先从 connect 的官方例子开始

var compression = require( 'compression' );

app.use(compression());

// store session state in browser cookie

var cookieSession = require( 'cookie-session' );

app.use(cookieSession({

keys: [ 'secret1','secret2' ] 

}));

// parse urlencoded request bodies into req.body

var bodyParser = require( 'body-parser' );

app.use(bodyParser.urlencoded({extended: false }));

// respond to all requests

app.use( function (req,res){

res.end( 'Hello from Connect!\n' );

});

//create node.js http server and listen on port

http.createServer(app).listen(3000);

从示例中可以看到一个典型的 connect 的使用:

app.use( function (req,res,next) {

// do something 

})

// http 服务器,使用

http.createServer(app).listen(3000);

先倒着看,从调用的地方更能看出来,模块怎么使用的。我们就先从 http.createServer(app) 来看看。

nodejs doc 的官方文档中可以知, createServer 函数的参数是一个回调函数,这个回调函数是用来响应 request 事件的。从这里看出,示例代码中 app 中函数签就是 (req,res) ,也就是说 app 的接口为 function (req,res) 。

但是从示例代码中,我们也可以看出 app 还有一个 use 方法。是不是觉得很奇怪,js 中函数实例上,还以带方法,这在 js 中就叫 函数对象,不仅能调用,还可以带实例变量。给个例子可以看得更清楚:

console.log( this );

}
app.statck = [];
return app;

}
var app = handle();
app() // ==> { [Function: app] handle: [Function],stack: [] }
app.apply({}) // ==>{ [Function: app] handle: [Function],stack: [] }

可以看出:函数中的实例函数中的 this 就是指当前的实例,不会因为你使用 apply 进行环境改变。

其他就跟对象没有什么区别。

再次回到示例代码,因该可以看懂了, connect 方法返回了一个函数,这个函数能直接调用,有 use 方法,用来响应 http 的 request 事件。

到此为此,示例代码就讲完了。 我们开始进入到 connect 模块的内部。

connect 只有一个导出方法。就是如下:

module.exports = createServer;

var proto = {};

function createServer() {

// 函数对象,这个对象能调用,能加属性

function app(req,next){ app.handle(req,next); }

merge(app,proto); // ===等于调用 Object.assign

merge(app,EventEmitter.prototype); // === 等于调用 Object.assign

app.route = '/' ;

app.stack = [];

return app;

}

代码中可以看出,createServer 函数把 app 函数返回了,app 函数有三个参数,多了一个 next (这个后面讲),app函数把 proto 的方法合并了。还有 EventEmitter 的方法也合并了,还增加了 route 和 stack 的属性

从前面代码来看,响应 request 的事件的函数,是 app.handle 方法。这个方法如下:

var index = 0;

var protohost = getProtohost(req.url) || '' ; //获得 http://www.baidu.com

var removed = '' ;

var slashAdded = false ;

var stack = this .stack;

// final function handler

var done = out || finalhandler(req,{

env: env,onerror: logerror 

}); // 接口 done(err);

// store the original URL

req.originalUrl = req.originalUrl || req.url;

function next(err) {

if  (slashAdded) { 

 req.url = req.url.substr(1);  // 除掉 / 之后的字符串 

 slashAdded =  false ;  // 已经拿掉 

} 



if  (removed.length !== 0) { 

 req.url = protohost + removed + req.url.substr(protohost.length); 

 removed =  '' ; 

} 



// next callback 

var  layer = stack[index++]; 



// all done 

if  (!layer) { 

 defer(done,err);  // 没有中间件,<a href="/tag/diaoyong/" target="_blank" class="keywords">调用</a> finalhandler 进行处理,如果 err 有值,就返回 <a href="/tag/404/" target="_blank" class="keywords">404</a> 进行处理 

 return ; 

} 



// route data 

var  path = parseUrl(req).pathname ||  '/' ; 

var  route = layer.route; 



// skip this layer if the route doesn't match 

if  (path.toLowerCase().substr(0,route.length) !== route.toLowerCase()) { 

 return  next(err);  // 执行下一个 

} 



// skip if route match does not border "/",".",or end 

var  c = path[route.length]; 

if  (c !== undefined && '/ ' !== c && ' . ' !== c) { 

 return next(err); // 执行下一个 

} 



// trim off the part of the url that matches the route 

if (route.length !== 0 && route !== ' / ') { 

 removed = route; 

 req.url = protohost + req.url.substr(protohost.length + removed.length); 



 // ensure leading slash 

 if (!protohost && req.url[0] !== ' / ') { 

  req.url = ' /' + req.url; 

  slashAdded =  true ; 

 } 

} 



// call the layer handle 

call(layer.handle,route,err,req,next); 

}

next();

};

代码中有相应的注释,可以看出,next 方法就是一个递归调用,不断的对比 route 是否匹配,如果匹配则调用 handle,如果不匹配,则调用下一个 handle.

call 函数代码如下:

var arity = handle.length;

var error = err;

var hasError = Boolean(err);

debug( '%s %s : %s',handle.name || '',req.originalUrl);

try {

if  (hasError && arity === 4) { 

 // error-handling middleware 

 handle(err,next); 

 return ; 

}  else  if  (!hasError && arity < 4) { 

 // request-handling middleware 

 handle(req,next); 

 return ; 

} 

} catch (e) {

// replace the error 

error = e; 

}

// continue

next(error);

}

可以看出一个重点:对错误处理,connect 的要求 是函数必须是 四个参数,而 express 也是如此。如果有错误, 中间件没有一个参数的个数是 4, 就会错误一直传下去,直到后面的 defer(done,err); 进行处理。

还有 app.use 添加中间件:

var handle = fn; // fn 只是一个函数的话 三种接口 // 1. err,next 2. req,3,next

var path = route;

// default route to '/'

if ( typeof route !== 'string' ) {

handle = route; 

path =  '/' ; 

}

// wrap sub-apps

if ( typeof handle.handle === 'function' ) { // 自定义中的函数对象

var  server = handle; 

server.route = path; 

handle =  function  (req,next) {  // req,next 中间件 

 server.handle(req,next); 

}; 

}

// wrap vanilla http.Servers

if (handle instanceof http.Server) {

handle = handle.listeners( 'request' )[0];  // (req,res) // 最后的<a href="/tag/hanshu/" target="_blank" class="keywords">函数</a> 

}

// strip trailing slash

if (path[path.length - 1] === '/' ) {

path = path.slice(0,-1); 

}

// add the middleware

debug( 'use %s %s',path || '/',handle.name || 'anonymous' );

this .stack.push({ route: path,handle: handle });

return this ;

};

代码中,可以看出,use 方法添加中间件到 this.stack 中,其中 fn 中间件的形式有两种: function (req,next) 和 handle.handle(req,next) 这两种都可以。还有对 fn 情况进行特殊处理。

总的处理流程就是这样,用 use 方法添加中间件,用 next 编历中间件,用 finalHandle 进行最后的处理工作。

代码中还有一个函数非常奇怪:

var defer = typeof setImmediate === 'function'

? setImmediate

: function (fn){ process.nextTick(fn.bind.apply(fn,arguments)) }

defer 函数中的 fn.bind.apply(fn,arguments) ,这个方法主要解决了,一个问题,不定参的情况下,第一个参数函数,怎样拿到的问题,为什么这样说呢?如果中我们要达到以上的效果,需要多多少行代码

var cb = Array.from(arguments)[0]; var args = Array.from(arguments).splice(1); process.nextTick( function () { cb.apply( null,args); })

}

这还是 connect 兼容以前的 es5 之类的方法。如果在 es6 下面,方法可以再次简化

总结

connect 做为 http 中间件模块,很好地解决对 http 请求的插件化处理的需求,把中间件组织成请求上的一个处理器,挨个调用中间件对 http 请求进行处理。

其中 connect 的递归调用,和对 js 的函数对象的使用,让值得学习,如果让我来写,就第一个调个的地方,就想不到使用 函数对象 来进行处理。

而且 next 的设计如此精妙,整个框架的使用和概念上,对程序员基本上没有认知负担,这才是最重要的地方。这也是为什么 express 框架最受欢迎。koa 相比之下,多几个概念,还使用了不常用的 yield 方法

connect 的设计理念可以用在,类似 http 请求模式上, 如 rpc,tcp 处理等。

我把 connect 的设计方法叫做 中间件模式,对处理 流式模式,会有较好的效果

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持编程之家。

猜你在找的Node.js相关文章