微信公众号搜"智元新知"关注
微信扫一扫可直接关注哦!

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 的官方例子开始

rush:js;"> var connect = require( 'connect' ); var http = require( 'http' ); var app = connect(); // gzip/deflate outgoing responses

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 的使用:

rush:js;"> var app = 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 只有一个导出方法。就是如下:

rush:js;"> var merge = require( 'utils-merge' );

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 方法。这个方法如下:

rush:js;"> proto.handle = function handle(req,out) {

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="https://www.jb51.cc/tag/diaoyong/" target="_blank" class="keywords">调用</a> finalhandler 进行处理,如果 err 有值,就返回 404 进行处理 

 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);  // 执行下<a href="https://www.jb51.cc/tag/yige/" target="_blank" class="keywords">一个</a> 

} 



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

var  c = path[route.length]; 

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

 return next(err); // 执行下<a href="https://www.jb51.cc/tag/yige/" target="_blank" class="keywords">一个</a> 

} 



// 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 函数代码如下:

rush:js;"> function call(handle,next) {

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 添加中间件:

rush:js;"> proto.use = function use(route,fn) {

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="https://www.jb51.cc/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 进行最后的处理工作。

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

rush:js;"> /* istanbul ignore next */

var defer = typeof setImmediate === 'function'

? setImmediate

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

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

rush:js;"> function () {
var  cb = Array.from(arguments)[0]; 

var  args = Array.from(arguments).splice(1); 

process.nextTick( function () { 

  cb.apply( null,args); 

}) 

}

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

rush:js;"> function (..args){ process.nextTick(fn.bind(...args)) }

总结

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

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

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

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

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

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

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐