Connect 源码解析

Posted wlbreath

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Connect 源码解析相关的知识,希望对你有一定的参考价值。

今天简单的解析一下nodejs的connect源码,毕竟是经典的框架,肯定是值得学习一下的。因为connect在之前的版本中是包含各种中间件的,之后都给拆分开了。所以本着偷懒的原则,我就只分析一下1.9.2版本connect,源码可以在 connect源码下载到。好,说了这么多,让我们开始吧。

这个是connect的目录结构,当然第一步肯定是从index.js文件开始看起了呀

index.js

module.exports = require('./lib/connect');

代码很简单就是作为整个connect项目的入口而已,我们继续看lib目录下面的connect文件

connect.js

var HTTPServer = require('./http').Server
  , HTTPSServer = require('./https').Server
  , fs = require('fs');

// node patches

require('./patch');

// expose createServer() as the module

exports = module.exports = createServer;

/**
 * Framework version.
 */

exports.version = '1.9.2';

/**
 * Initialize a new `connect.HTTPServer` with the middleware
 * passed to this function. When an object is passed _first_,
 * we assume these are the tls options, and return a `connect.HTTPSServer`.
 *
 * Examples:
 *
 * An example HTTP server, accepting several middleware.
 *
 *     var server = connect.createServer(
 *         connect.logger()
 *       , connect.static(__dirname + '/public')
 *     );
 *
 * An HTTPS server, utilizing the same middleware as above.
 *
 *     var server = connect.createServer(
 *          key: key, cert: cert 
 *       , connect.logger()
 *       , connect.static(__dirname + '/public')
 *     );
 *
 * Alternatively with connect 1.0 we may omit `createServer()`.
 *
 *     connect(
 *         connect.logger()
 *       , connect.static(__dirname + '/public')
 *     ).listen(3000);
 *
 * @param  Object|Function ...
 * @return Server
 * @api public
 */

function createServer() 
  if ('object' == typeof arguments[0]) 
    return new HTTPSServer(arguments[0], Array.prototype.slice.call(arguments, 1));
   else 
    return new HTTPServer(Array.prototype.slice.call(arguments));
  
;

// support connect.createServer()

exports.createServer = createServer;

// auto-load getters

exports.middleware = ;

/**
 * Auto-load bundled middleware with getters.
 */

fs.readdirSync(__dirname + '/middleware').forEach(function(filename)
  if (/\\.js$/.test(filename)) 
    var name = filename.substr(0, filename.lastIndexOf('.'));
    exports.middleware.__defineGetter__(name, function()
      return require('./middleware/' + name);
    );
  
);

// expose utils

exports.utils = require('./utils');

// expose getters as first-class exports

exports.utils.merge(exports, exports.middleware);

// expose constructors

exports.HTTPServer = HTTPServer;
exports.HTTPSServer = HTTPSServer;

通过63行、79行、92行和93行代码我们可以比较清楚的了解到,这个文件导出了一个函数createServer和三个对象,三个对象分别是中间件对象middleware,通过这个对象可以获取connect自带的中间件,其余两个是TJ大神自定义的http服务器和https服务器。

通过connect的api,我们可以知道那个createServer是connect的入口,那我们看一下呗:

function createServer() 
  if ('object' == typeof arguments[0]) 
    return new HTTPSServer(arguments[0], Array.prototype.slice.call(arguments, 1));
   else 
    return new HTTPServer(Array.prototype.slice.call(arguments));
  
;

嗯,好吧,也就是根据传入的参数来创建TJ大神自定的HTTPServer活着HTTPSServer对象而已,那我们就看看这两个对象呗,因为HTTPSServer是在HTTPServer的基础上扩展而来的,比较简单我们就不分析了,我们还是把主要的精力放在HTTPServer,那我们就看一下http.js文件呗

http.js

var http = require('http')
  , parse = require('url').parse
  , assert = require('assert')
  , utils = require('./utils');

// environment

var env = process.env.NODE_ENV || 'development';

/**
 * Initialize a new `Server` with the given `middleware`.
 *
 * Examples:
 *
 *     var server = connect.createServer(
 *         connect.favicon()
 *       , connect.logger()
 *       , connect.static(__dirname + '/public')
 *     );
 *
 * @params Array middleware 
 * @return Server
 * @api public
 */

var Server = exports.Server = function HTTPServer(middleware) 
  this.stack = [];
  middleware.forEach(function(fn)
    this.use(fn);
  , this);
  http.Server.call(this, this.handle);
;

/**
 * Inherit from `http.Server.prototype`.
 */

Server.prototype.__proto__ = http.Server.prototype;

/**
 * Utilize the given middleware `handle` to the given `route`,
 * defaulting to _/_. This "route" is the mount-point for the
 * middleware, when given a value other than _/_ the middleware
 * is only effective when that segment is present in the request's
 * pathname.
 *
 * For example if we were to mount a function at _/admin_, it would
 * be invoked on _/admin_, and _/admin/settings_, however it would
 * not be invoked for _/_, or _/posts_.
 *
 * This is effectively the same as passing middleware to `connect.createServer()`,
 * however provides a progressive api.
 *
 * Examples:
 *
 *      var server = connect.createServer();
 *      server.use(connect.favicon());
 *      server.use(connect.logger());
 *      server.use(connect.static(__dirname + '/public'));
 *
 * If we wanted to prefix static files with _/public_, we could
 * "mount" the `static()` middleware:
 *
 *      server.use('/public', connect.static(__dirname + '/public'));
 *
 * This api is chainable, meaning the following is valid:
 *
 *      connect.createServer()
 *        .use(connect.favicon())
 *        .use(connect.logger())
 *        .use(connect.static(__dirname + '/public'))
 *        .listen(3000);
 *
 * @param String|Function route or handle
 * @param Function handle
 * @return Server
 * @api public
 */

Server.prototype.use = function(route, handle)
  this.route = '/';

  // default route to '/'
  if ('string' != typeof route) 
    handle = route;
    route = '/';
  

  // wrap sub-apps
  if ('function' == typeof handle.handle) 
    var server = handle;
    server.route = route;
    handle = function(req, res, next) 
      server.handle(req, res, next);
    ;
  

  // wrap vanilla http.Servers
  if (handle instanceof http.Server) 
    handle = handle.listeners('request')[0];
  

  // normalize route to not trail with slash
  if ('/' == route[route.length - 1]) 
    route = route.substr(0, route.length - 1);
  

  // add the middleware
  this.stack.push( route: route, handle: handle );

  // allow chaining
  return this;
;

/**
 * Handle server requests, punting them down
 * the middleware stack.
 *
 * @api private
 */

Server.prototype.handle = function(req, res, out) 
  var writeHead = res.writeHead
    , stack = this.stack
    , removed = ''
    , index = 0;

  function next(err) 
    var layer, path, c;
    req.url = removed + req.url;
    req.originalUrl = req.originalUrl || req.url;
    removed = '';

    layer = stack[index++];

    // all done
    if (!layer || res.headerSent) 
      // but wait! we have a parent
      if (out) return out(err);

      // error
      if (err) 
        var msg = 'production' == env
          ? 'Internal Server Error'
          : err.stack || err.toString();

        // output to stderr in a non-test env
        if ('test' != env) console.error(err.stack || err.toString());

        // unable to respond
        if (res.headerSent) return req.socket.destroy();

        res.statusCode = 500;
        res.setHeader('Content-Type', 'text/plain');
        if ('HEAD' == req.method) return res.end();
        res.end(msg);
       else 
        res.statusCode = 404;
        res.setHeader('Content-Type', 'text/plain');
        if ('HEAD' == req.method) return res.end();
        res.end('Cannot ' + req.method + ' ' + utils.escape(req.originalUrl));
      
      return;
    

    try 
      path = parse(req.url).pathname;
      if (undefined == path) path = '/';

      // skip this layer if the route doesn't match.
      if (0 != path.indexOf(layer.route)) return next(err);

      c = path[layer.route.length];
      if (c && '/' != c && '.' != c) return next(err);

      // Call the layer handler
      // Trim off the part of the url that matches the route
      removed = layer.route;
      req.url = req.url.substr(removed.length);

      // Ensure leading slash
      if ('/' != req.url[0]) req.url = '/' + req.url;

      var arity = layer.handle.length;
      if (err) 
        if (arity === 4) 
          layer.handle(err, req, res, next);
         else 
          next(err);
        
       else if (arity < 4) 
        layer.handle(req, res, next);
       else 
        next();
      
     catch (e) 
      if (e instanceof assert.AssertionError) 
        console.error(e.stack + '\\n');
        next(e);
       else 
        next(e);
      
    
  
  next();
;

HTTPServer

通过查看源码,我们可以发现26~38行是TJ大神自定义的HTTPServer。

var Server = exports.Server = function HTTPServer(middleware) 
  this.stack = [];
  middleware.forEach(function(fn)
    this.use(fn);
  , this);
  http.Server.call(this, this.handle);
;

/**
 * Inherit from `http.Server.prototype`.
 */

Server.prototype.__proto__ = http.Server.prototype;

这几句代码值得我们好好的推敲一下的,2行定义了一个stack,这个是用来保存路由和中间件的具体的格式为 route: route, handle: handle ,一般我们通过connect的use函数就是把相应的路由和中间件加入到stack中。3~5行比较简单就是把传过来的中间件加入到stack中而已,对于use函数我们会在具体解释的。第六行代码是用来继承node原生的http.Server(也是http服务器)的属性,那么那个第二个参数this.handle传入有什么用呢?我们查一下node的文档吧,好吧,通过文档没有查到,那怎么办呀,只有查看源码了,下面是http.Server的构造函数(源代码在node源码lib目录下_http_server.js文件中):

node http.Server构造函数

function Server(requestListener) 
  if (!(this instanceof Server)) return new Server(requestListener);
  net.Server.call(this,  allowHalfOpen: true );

  if (requestListener) 
    this.addListener('request', requestListener);
  

  /* eslint-disable max-len */
  // Similar option to this. Too lazy to write my own docs.
  // http://www.squid-cache.org/Doc/config/half_closed_clients/
  // http://wiki.squid-cache.org/SquidFaq/InnerWorkings#What_is_a_half-closed_filedescriptor.3F
  /* eslint-enable max-len */
  this.httpAllowHalfOpen = false;

  this.addListener('connection', connectionListener);

  this.addListener('clientError', function(err, conn) 
    conn.destroy(err);
  );

  this.timeout = 2 * 60 * 1000;

  this._pendingResponseData = 0;

util.inherits(Server, net.Server);

通过第六行的代码我们可以知道,哦原来就是添加request事件的监听器呀。也就是意味着每次有http请求的时候就会调用这个this.hanle函数,这个函数我们会在下面解释的。让我们先看一下use函数吧。

use函数

Server.prototype.use = function(route, handle)
  this.route = '/';

  // default route to '/'
  if ('string' != typeof route) 
    handle = route;
    route = '/';
  

  // wrap sub-apps
  if ('function' == typeof handle.handle) 
    var server = handle;
    server.route = route;
    handle = function(req, res, next) 
      server.handle(req, res, next);
    ;
  

  // wrap vanilla http.Servers
  if (handle instanceof http.Server) 
    handle = handle.listeners('request')[0];
  

  // normalize route to not trail with slash
  if ('/' == route[route.length - 1]) 
    route = route.substr(0, route.length - 1);
  

  // add the middleware
  this.stack.push( route: route, handle: handle );

  // allow chaining
  return this;
;

use 函数真心简单就是讲中间件加入到stack属性中去,对于没有指定的路由的就是默认路由/。

handle函数

Server.prototype.handle = function(req, res, out) 
  var writeHead = res.writeHead
    , stack = this.stack
    , removed = ''
    , index = 0;

  function next(err) 
    var layer, path, c;
    req.url = removed + req.url;
    req.originalUrl = req.originalUrl || req.url;
    removed = '';

    layer = stack[index++];

    // all done
    if (!layer || res.headerSent) 
      // but wait! we have a parent
      if (out) return out(err);

      // error
      if (err) 
        var msg = 'production' == env
          ? 'Internal Server Error'
          : err.stack || err.toString();

        // output to stderr in a non-test env
        if ('test' != env) console.error(err.stack || err.toString());

        // unable to respond
        if (res.headerSent) return req.socket.destroy();

        res.statusCode = 500;
        res.setHeader('Content-Type', 'text/plain');
        if ('HEAD' == req.method) return res.end();
        res.end(msg);
       else 
        res.statusCode = 404;
        res.setHeader('Content-Type', 'text/plain');
        if ('HEAD' == req.method) return res.end();
        res.end('Cannot ' + req.method + ' ' + utils.escape(req.originalUrl));
      
      return;
    

    try 
      path = parse(req.url).pathname;
      if (undefined == path) path = '/';

      // skip this layer if the route doesn't match.
      if (0 != path.indexOf(layer.route)) return next(err);

      c = path[layer.route.length];
      if (c && '/' != c && '.' != c) return next(err);

      // Call the layer handler
      // Trim off the part of the url that matches the route
      removed = layer.route;
      req.url = req.url.substr(removed.length);

      // Ensure leading slash
      if ('/' != req.url[0]) req.url = '/' + req.url;

      var arity = layer.handle.length;
      if (err) 
        if (arity === 4) 
          layer.handle(err, req, res, next);
         else 
          next(err);
        
       else if (arity < 4) 
        layer.handle(req, res, next);
       else 
        next();
      
     catch (e) 
      if (e instanceof assert.AssertionError) 
        console.error(e.stack + '\\n');
        next(e);
       else 
        next(e);
      
    
  
  next();
;

这个函数也是http.js文件中的最后一个函数,我们来粗略的看一下,毕竟我们是阅读别人的代码,重要的是理解好代码的思路。

通过查看函数我们可以清楚的了解到,函数中定义了一个next函数,然后在handle函数的最后调用了它,我们就自己的看下这个函数是干什么的。

第13行代码获取中间件。

第16行判断是不是没有中间件要执行了,如果没有了的话,在21行判断在整个请求处理过程中是否有错误发生,如果有错误发生了的话,则将错误的信息返回给给请求者。如果还有中间件的话,那么就执行45行及其后面的代码。

第46~61行代码就是分析请求的url判断,判断是否合法,并且和13行代码中获取的中间件的路由是否匹配,如果不匹配就递归执行next来分析下一个中间件。如果匹配成功的话就执行63行及其后面的代码。

63行代码使用来获得函数在定义的时候指定了几个参数(因为es6中可以给参数指定默认值,函数的length属性是不包含那个参数的)

65~69行代码是在请求处理过程工作如果有错误的情况下执行的,判断的当前的中间件事否是处理错误的中间件(函数定义的时候含有4个参数),如果是则执行,反之调用next来分析下一个中间件。

如果请求处理过程中没有错误,中间件是正常的中间件,并且定义时指定的参数小于4则执行该中间件。

如果中间件定义的时候指定的参数大于4的话,我们就直接忽略,调用next函数。

到这里我么把connect的答题构架已经分析完了。在connect.js中还有一个地方我觉得是值得我们学习的,那就是middleware的定义。

connect.js导出的middleware对象

exports.middleware = ;

/**
 * Auto-load bundled middleware with getters.
 */

fs.readdirSync(__dirname + '/middleware').forEach(function(filename)
  if (/\\.js$/.test(filename)) 
    var name = filename.substr(0, filename.lastIndexOf('.'));
    exports.middleware.__defineGetter__(name, function()
      return require('./middleware/' + name);
    );
  
);

第10行代码中的_ _ defineGetter _ 使用来添加get 属性的,具体的可以看下面的文档Object.prototype._defineGetter_。这里值得注意的时候,中间件的加载采用的是惰性加载,只有在调用的对应的中间件才会require。

总结

看了TJ大神的代码,感觉自己还是好弱,每天进步一点吧,对于各位看到最后的同学,如果发现哪里有不对的地方请指出。

以上是关于Connect 源码解析的主要内容,如果未能解决你的问题,请参考以下文章

CAS集成源码解析

koa-connect源码解析

Memtiter-benchmark源码解析1client类功能解析

Memtiter-benchmark源码解析1client类功能解析

源码编译安装Apache,MySQL,PHP,LAMP构架

前后端分离构架特点