nodejs模块学习: connect解析
nodejs 实践:express 最佳实践(五) 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 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 的使用:
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 中就叫 函数对象,不仅能调用,还可以带实例变量。给个例子可以看得更清楚:
function handle () {
  function app(req, res, next) { app.handle(req, res, next)}
  app.handle = function (req, res, next) {
    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 只有一个导出方法。就是如下:
var merge = require('utils-merge');
module.exports = createServer;
var proto = {};
function createServer() {
  // 函数对象,这个对象能调用,能加属性
  function app(req, res, next){ app.handle(req, res, 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 方法。这个方法如下:
proto.handle = function handle(req, res, 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, res, {
    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); // 没有中间件,调用 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); // 执行下一个
    }
    // 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, res, next);
  }
  next();
};
代码中有相应的注释,可以看出,next 方法就是一个递归调用,不断的对比 route 是否匹配,如果匹配则调用 handle, 如果不匹配,则调用下一个 handle.
call 函数的代码如下:
function call(handle, route, err, req, res, next) {
  var arity = handle.length;
  var error = err;
  var hasError = Boolean(err);
  debug('%s %s : %s', handle.name || '<anonymous>', route, req.originalUrl);
  try {
    if (hasError && arity === 4) {
      // error-handling middleware
      handle(err, req, res, next);
      return;
    } else if (!hasError && arity < 4) {
      // request-handling middleware
      handle(req, res, next);
      return;
    }
  } catch (e) {
    // replace the error
    error = e;
  }
  // continue
  next(error);
}
可以看出一个重点:对错误处理,connect 的要求 是函数必须是 四个参数,而 express 也是如此。如果有错误, 中间件没有一个参数的个数是 4, 就会错误一直传下去,直到后面的 defer(done, err);  进行处理。
还有 app.use 添加中间件:
proto.use = function use(route, fn) {
  var handle = fn; // fn 只是一个函数的话 三种接口 // 1. err, req, res, next 2. req, res, 3, req, res, 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, res, next) {  // req, res, next 中间件
      server.handle(req, res, next);
    };
  }
  // wrap vanilla http.Servers
  if (handle instanceof http.Server) {
    handle = handle.listeners('request')[0]; // (req, res) // 最后的函数
  }
  // 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, res, next) 和 handle.handle(req, res, next) 这两种都可以。还有对 fn 情况进行特殊处理。
总的处理流程就是这样,用 use 方法添加中间件,用 next 编历中间件,用 finalHandle 进行最后的处理工作。
在代码中还有一个函数非常奇怪:
/* istanbul ignore next */
var defer = typeof setImmediate === 'function'
  ? setImmediate
  : function(fn){ process.nextTick(fn.bind.apply(fn, arguments)) }
defer 函数中的 fn.bind.apply(fn, arguments),这个方法主要解决了,一个问题,不定参的情况下,第一个参数函数,怎样拿到的问题,为什么这样说呢?如果中我们要达到以上的效果,需要多多少行代码?
function () {
    var cb = Array.from(arguments)[0];
    var args = Array.from(arguments).splice(1);
    process.nextTick(function() {
        cb.apply(null,args);
    })
}
这还是 connect 兼容以前的 es5 之类的方法。如果在 es6 下面,方法可以再次简化
function(..args){ process.nextTick(fn.bind(...args)) }
总结
connect 做为 http 中间件模块,很好地解决对 http 请求的插件化处理的需求,把中间件组织成请求上的一个处理器,挨个调用中间件对 http 请求进行处理。
其中 connect 的递归调用,和对 js 的函数对象的使用,让值得学习,如果让我来写,就第一个调个的地方,就想不到使用 函数对象 来进行处理。
而且 next 的设计如此精妙,整个框架的使用和概念上,对程序员基本上没有认知负担,这才是最重要的地方。这也是为什么 express 框架最受欢迎。koa 相比之下,多几个概念,还使用了不常用的 yield 方法。
connect 的设计理念可以用在,类似 http 请求模式上, 如 rpc, tcp 处理等。
我把 connect 的设计方法叫做 中间件模式,对处理 流式模式,会有较好的效果。
nodejs模块学习: connect解析的更多相关文章
- # nodejs模块学习: express 解析
		
# nodejs模块学习: express 解析 nodejs 发展很快,从 npm 上面的包托管数量就可以看出来.不过从另一方面来看,也是反映了 nodejs 的基础不稳固,需要开发者创造大量的轮子 ...
 - nodejs模块学习: webpack
		
nodejs模块学习: webpack nodejs 发展很快,从 npm 上面的包托管数量就可以看出来.不过从另一方面来看,也是反映了 nodejs 的基础不稳固,需要开发者创造大量的轮子来解决现实 ...
 - nodejs模块学习: connect2解析
		
nodejs模块学习: connect2 解析 nodejs 发展很快,从 npm 上面的包托管数量就可以看出来.不过从另一方面来看,也是反映了 nodejs 的基础不稳固,需要开发者创造大量的轮子来 ...
 - nodejs模块学习: express-session 解析
		
nodejs模块学习: express-session 解析 nodejs 发展很快,从 npm 上面的包托管数量就可以看出来.不过从另一方面来看,也是反映了 nodejs 的基础不稳固,需要开发者创 ...
 - python模块学习---HTMLParser(解析HTML文档元素)
		
HTMLParser是Python自带的模块,使用简单,能够很容易的实现HTML文件的分析. 本文主要简单讲一下HTMLParser的用法. 使用时需要定义一个从类HTMLParser继承的类,重定义 ...
 - nodejs 实践:express 最佳实践(五) connect解析
		
nodejs 实践:express 最佳实践(五) connect解析 nodejs 发展很快,从 npm 上面的包托管数量就可以看出来.不过从另一方面来看,也是反映了 nodejs 的基础不稳固,需 ...
 - nodeJS学习(9)--- nodeJS模块:exports vs module.exports
		
模块简介: 通过Node.js的官方API可以看到Node.js本身提供了很多核心模块 http://nodejs.org/api/ 这些核心模块被编译成二进制文件,可以 require('模块名') ...
 - nodejs模块xml2js解析xml的坑
		
在一个项目中,用到nodejs模块xml2js解析xml,xml的数据如下: <xml> <MsgId>6197906553041859764</MsgId> &l ...
 - Day5 - Python基础5 常用模块学习
		
Python 之路 Day5 - 常用模块学习 本节大纲: 模块介绍 time &datetime模块 random os sys shutil json & picle shel ...
 
随机推荐
- Hadoop2.6.1中的Reducer实现
			
正在考虑怎么方便上传图片 1.Partitioner其是一个抽象类,只有一个抽象方法.其作用是对Reducer产生的中间结果进行分片,以方便将同一分组的数据交给同一个Reducer处理 2.类的继承结 ...
 - Linux C 程序的开发环境
			
1.开发环境的构成 编辑器 vim,vi 编译器 gcc 调试器 gdb 函数库glibc 系统头文件glibc_header 2.gcc编译器 功能强大.性能优越的多平台编译器,gcc可以将c.c+ ...
 - Bitwise And Queries
			
Bitwise And Queries Time limit: 1500 msMemory limit: 128 MB You are given QQ queries of the form a\ ...
 - java虚拟机学习-JVM调优总结-垃圾回收面临的问题(8)
			
如何区分垃圾 上面说到的“引用计数”法,通过统计控制生成对象和删除对象时的引用数来判断.垃圾回收程序收集计数为0的对象即可.但是这种方法无法解决循环引用.所以,后来实现的垃圾判断算法中,都是从程序运行 ...
 - 关于IOS sourcetree 注册    2017最新hosts
			
今天用sourcetree git管理工具的时候,第一次打开发现需要注册. 在网上搜索了一下教程,发现现在新版本没有 (我同意协议)这个条款,这就尴尬.我以前没有sourcetree的账号. 试了 ...
 - PF2.1版本总结,在设计过程中遇到的问题以及技术分享
			
在距离上一次的版本发布已经过去4个月的时间,因为个人的能力以及时间有限,所以这次的版本会推迟这么久.可是无论怎样,PF2.1带着自身的完善总算不负所望推出.在这次的版本调整中让我深有体会到了程序设计中 ...
 - 1089 Intervals(中文)
			
开始前先讲几句废话:这个题我开始也没看懂,后来借助百度翻译,明白了大概是什么意思. 试题描述 输入一个n,然后输入n组数据,每个数据有两个数,代表这个闭区间是从几到几.然后看,如果任意两个闭区间有相重 ...
 - Java静态代理与动态代理模式的实现
			
前言: 在现实生活中,考虑以下的场景:小王打算要去租房,他相中了一个房子,准备去找房东洽谈相关事宜.但是房东他很忙,平时上班没时间,总找不到时间去找他,他也没办法.后来,房东想了一个办法,他找到 ...
 - iOS地理围栏技术的应用
			
遇到一个需求,要求监测若干区域,设备进入这些区域则要上传数据,且可以后台监测,甚至app被杀死也要监测.发现oc的地理围栏技术完美匹配这个需求,任务做完了,把遇到的坑记录下来,也许能帮到你呢. 要做这 ...
 - Go从入门到精通(一)go语言初始
			
一.第一个go程序 package main import ( "fmt" ) func main(){ fmt.Println("hello world") ...