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  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 的设计方法叫做 中间件模式,对处理 流式模式,会有较好的效果。

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

(0)

相关推荐

  • 解决Node.js使用MySQL出现connect ECONNREFUSED 127.0.0.1:3306的问题

    前言 最近用 Node 写一个小玩意,需要用到 MySQL 数据库,现在用得最广泛的是 mysql 这个库.然后呢,现在 ORM 这么火,干脆也上 ORM 吧,正好我也不会可以学习一下,于是找到了 Sequelize.js 这个 ORM 库. 发现问题 看看 Sequelize 的文档,so easy,两分钟搞定~ import Sequelize from 'sequelize'; let sequelize = new Sequelize('database', 'username', 'p

  • NodeJS学习笔记之Connect中间件模块(二)

    一,开篇分析 大家好,今天这篇文章主要是对"Connect"中间件以及相关辅助中间件,做一个源码分析系列,我想上一篇文章大家也看了, 介绍了使用方式及用途,而这篇也是出于本人的兴趣,让读者对其有一个更深入的认识,如在分析阶段有什么不正确的地方,请大家多多指教, 好了!老规矩然我们进入正题.先来看一个例子,结合会用引入分析,如下: 复制代码 代码如下: var connect = require("./lib/connect") ;  var app = connec

  • NodeJS学习笔记之Connect中间件模块(一)

    希望大家能够坚持看下去我这系列的文章,这也是对我的最大的鼓励与支持,让我们共同进步,以文会友,相互帮助.好了直接进入今天的主题, 什么是"Connect",中间件又当如何理解,带着问题来看今天的文章. 如何理解 "中间件" ? 我的理解是这样的,中间件就是类似于一个过滤器的东西,在客户端和应用程序之间的一个处理请求和响应的的方法. 如果把一个http处理过程比作是污水处理,中间件就像是一层层的过滤网.每个中间件在http处理过程中通过改写request或(和)res

  • 从零开始学习Node.js系列教程之基于connect和express框架的多页面实现数学运算示例

    本文实例讲述了Node.js基于connect和express框架的多页面实现数学运算.分享给大家供大家参考,具体如下: 1.使用connect框架 .use方法用于绑定中间件到connect服务器,它会配置一系列在接到请求时调用的中间件模块,此例中我们要配置的中间件有favicon logger static router app.get/post/put        写法:app.requestName('path', function(req, res, next){}); app-co

  • NodeJS学习笔记之Connect中间件应用实例

    一,开篇分析 大家好哦,大熊君又来了,昨天因为有点个人的事没有写博客,今天又出来了一篇,这篇主要是写一个记事本的小应用,前面的文章, 我也介绍过"Connect"中间件的使用以及"Mongodb"的用法,今天就结合这两个中间件,写个实际的例子,不断完善和重构,已达到 充分学习的目的.好了,废话不说了,直接进入主题. 二,需求分析 (1),用户注册,登录功能(没有涉及很复杂的交互场景,注册时会有用户判断是否已存在). (2),用户登录成功,进入笔记管理系统的后台(笔记

  • Node.js connect ECONNREFUSED错误解决办法

    最近在准备Angularjs +node.js demo的时候在我的mac开发中 遇见此错误,如下: events.js:71 throw arguments[1]; // Unhandled 'error' event ^ Error: connect ECONNREFUSED at errnoException (net.js:770:11) at Object.afterConnect [as oncomplete] (net.js:761:19) 最后在stackoverflow找到解决

  • 分析Node.js connect ECONNREFUSED错误

    最近在准备Angularjs +node.js demo的时候在我的mac开发中 遇见此错误,如下: events.js:71 throw arguments[1]; // Unhandled 'error' event ^ Error: connect ECONNREFUSED at errnoException (net.js:770:11) at Object.afterConnect [as oncomplete] (net.js:761:19) 最后在stackoverflow找到解决

  • nodejs模块学习之connect解析

    nodejs 发展很快,从 npm 上面的包托管数量就可以看出来.不过从另一方面来看,也是反映了 nodejs 的基础不稳固,需要开发者创造大量的轮子来解决现实的问题. 知其然,并知其所以然这是程序员的天性.所以把常用的模块拿出来看看,看看高手怎么写的,学习其想法,让自己的技术能更近一步. 引言 express 是 nodejs 中最流行的 web 框架.express 中对 http 中的 request 和 response 的处理,还有以中间件为核心的处理流程,非常灵活,足以应对任何业务的

  • 实例分析nodejs模块xml2js解析xml过程中遇到的坑

    本文实例讲述了nodejs模块xml2js解析xml过程中遇到的坑.分享给大家供大家参考,具体如下: 在一个项目中,用到nodejs模块xml2js解析xml,xml的数据如下: <xml> <MsgId>6197906553041859764</MsgId> </xml> 用xml2js中的xml2js.parseString 方法解析,本来以为是一个json,但总是解析失败,把解析的结果log下后如下: { xml: { MsgId: [ '619790

  • python re模块findall()函数实例解析

    本文研究的是re模块findall()函数的相关内容,首先看看实例代码: >>> import re >>> s = "adfad asdfasdf asdfas asdfawef asd adsfas " >>> reObj1 = re.compile('((\w+)\s+\w+)') >>> reObj1.findall(s) [('adfad asdfasdf', 'adfad'), ('asdfas asd

  • node的process以及child_process模块学习笔记

    在死磕进程一个礼拜后,终于把晦涩难懂文档看明白了,准备把自己的理解分享给大家,也希望大家能指出一些意见 进程的概念 在Node.js中每个应用程序都是一个进程类的实例对象. 使用process对象代表应用程序,这是一个全局对象,可以通过它来获取Node.jsy应用程序以及运行该程序的用户.环境等各种信息的属性.方法和事件. 进程中几个重要的属性 stdin 标准输入可读流 stdout 标准输入可写流 stderr 标准错误输出流 argv 终端输入参数数组 env 操作系统环境信息 pid 应

  • 详解node child_process模块学习笔记

    NodeJs是一个单进程的语言,不能像Java那样可以创建多线程来并发执行.当然在大部分情况下,NodeJs是不需要并发执行的,因为它是事件驱动性永不阻塞.但单进程也有个问题就是不能充分利用CPU的多核机制,根据前人的经验,可以通过创建多个进程来充分利用CPU多核,并且Node通过了child_process模块来创建完成多进程的操作. child_process模块给予node任意创建子进程的能力,node官方文档对于child_proces模块给出了四种方法,映射到操作系统其实都是创建子进程

  • nodeJS模块简单用法示例

    本文实例讲述了nodeJS模块简单用法.分享给大家供大家参考,具体如下: 1.定义Student模块,Teacher模块 function add(student){ console.log('Add Student:'+student); } exports.add=add; function add(teacher){ console.log('Add Teacher:'+teacher); } exports.add=add; 2.定义kclass模块 //引入student模块 var

  • Python迭代器模块itertools使用原理解析

    这篇文章主要介绍了Python迭代器模块itertools使用原理解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 介绍 今天介绍一个很强大的模块,而且是python自带的,那就是itertools迭代器模块. 使用 使用起来很简单,先导入模块 import itertools 下面,我们通过一些例子边学边练 三个无限迭代器 先告诉大家 control + C 可以强制停止程序哦 1.count() num = itertools.count

  • Python openpyxl模块原理及用法解析

    这篇文章主要介绍了Python openpyxl模块原理及用法解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 此模块不是Python内置的模块需要安装,安装方法如下 pip install openpyxl 注意: 此模块只支持offce 2010,即是电子表格后缀是*.xlsx 1.openpyxl模块常用函数 import openpyxl wb = openpyxl.load_workbook('example.xlsx') ####

  • 详解阿里Node.js技术文档之process模块学习指南

    模块概览 process是node的全局模块,作用比较直观.可以通过它来获得node进程相关的信息,比如运行node程序时的命令行参数.或者设置进程相关信息,比如设置环境变量. 环境变量:process.env 使用频率很高,node服务运行时,时常会判断当前服务运行的环境,如下所示 if(process.env.NODE_ENV === 'production'){ console.log('生产环境'); }else{ console.log('非生产环境'); } 运行命令 NODE_EN

  • nodejs模块系统源码分析

    概述 Node.js的出现使得前端工程师可以跨端工作在服务器上,当然,一个新的运行环境的诞生亦会带来新的模块.功能.抑或是思想上的革新,本文将带领读者领略 Node.js(以下简称 Node) 的模块设计思想以及剖析部分核心源码实现. CommonJS 规范 Node 最初遵循 CommonJS 规范来实现自己的模块系统,同时做了一部分区别于规范的定制.CommonJS 规范是为了解决JavaScript的作用域问题而定义的模块形式,它可以使每个模块在它自身的命名空间中执行. 该规范强调模块必须

随机推荐