nodejs模块学习之connect解析


Posted in NodeJs onJuly 05, 2017

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 相关文章推荐
NodeJS Express框架中处理404页面一个方式
May 28 NodeJs
nodejs教程之入门
Nov 21 NodeJs
Nodejs学习笔记之Stream模块
Jan 13 NodeJs
nodejs实现HTTPS发起POST请求
Apr 23 NodeJs
Nodejs中session的简单使用及通过session实现身份验证的方法
Feb 04 NodeJs
在windows上用nodejs搭建静态文件服务器的简单方法
Aug 11 NodeJs
async/await与promise(nodejs中的异步操作问题)
Mar 03 NodeJs
nodejs使用express创建一个简单web应用
Mar 31 NodeJs
nodejs socket实现的服务端和客户端功能示例
Jun 02 NodeJs
Nodejs 和Session 原理及实战技巧小结
Aug 25 NodeJs
nodejs实现OAuth2.0授权服务认证
Dec 27 NodeJs
关于Mac下安装nodejs、npm和cnpm的教程
Apr 11 NodeJs
详解HTTPS 的原理和 NodeJS 的实现
Jul 04 #NodeJs
nodejs 子进程正确的打开方式
Jul 03 #NodeJs
Express+Nodejs 下的登录拦截实现代码
Jul 01 #NodeJs
NodeJS实现图片上传代码(Express)
Jun 30 #NodeJs
详解nodejs实现本地上传图片并预览功能(express4.0+)
Jun 28 #NodeJs
NodeJS链接MySql数据库的操作方法
Jun 27 #NodeJs
NodeJS自定义模块写法(详解)
Jun 27 #NodeJs
You might like
迅雷下载《中学科技》怀旧期刊下载
2021/02/27 无线电
通过文字传递创建的图形按钮
2006/10/09 PHP
php导出CSV抽象类实例
2014/09/24 PHP
php程序员应具有的7种能力小结
2014/11/27 PHP
浅析Yii2缓存的使用
2016/05/10 PHP
php中通过eval实现字符串格式的计算公式
2017/03/18 PHP
PHP笛卡尔积实现算法示例
2018/07/30 PHP
php 实现银联商务H5支付的示例代码
2019/10/12 PHP
PHP 计算至少是其他数字两倍的最大数的实现代码
2020/05/26 PHP
tbody元素支持嵌套的注意方法
2007/03/24 Javascript
不能再简单的无闪刷新验证码原理很简单
2007/11/05 Javascript
javascript之querySelector和querySelectorAll使用介绍
2011/12/20 Javascript
Javascript图像处理—为矩阵添加常用方法
2012/12/27 Javascript
javascript将异步校验表单改写为同步表单
2015/01/27 Javascript
基于Arcgis for javascript实现百度地图ABCD marker的效果
2015/09/12 Javascript
需要牢记的JavaScript基础知识
2016/09/25 Javascript
使用JavaScript实现表格编辑器(实例讲解)
2017/08/02 Javascript
JS实现的DOM插入节点操作示例
2018/04/04 Javascript
Vue中的组件及路由使用实例代码详解
2019/05/22 Javascript
原生JS实现微信通讯录
2020/06/18 Javascript
webpack5 联邦模块介绍详解
2020/07/08 Javascript
python实现获取序列中最小的几个元素
2014/09/25 Python
在Python的Flask中使用WTForms表单框架的基础教程
2016/06/07 Python
django 通过ajax完成邮箱用户注册、激活账号的方法
2018/04/17 Python
关于Python的一些学习总结
2018/05/25 Python
Python常见MongoDB数据库操作实例总结
2018/07/24 Python
python3 flask实现文件上传功能
2020/03/20 Python
pandas通过索引进行排序的示例
2018/11/16 Python
Python理解递归的方法总结
2019/01/28 Python
python实现在函数图像上添加文字和标注的方法
2019/07/08 Python
马来西亚在线药房:RoyalePharma
2019/12/01 全球购物
双十佳事迹材料
2014/01/29 职场文书
《充气雨衣》教学反思
2014/04/07 职场文书
培训师岗位职责
2015/02/14 职场文书
超市员工辞职信范文
2015/05/12 职场文书
pytorch 梯度NAN异常值的解决方案
2021/06/05 Python