Node.js之connect中间件的源码解析

对于connect中间件进行源码解析,源码地址,总共不超过300行。

connect中间件

  connect是一个基于HTTP服务器的工具集,它提供了一种新的组织代码的方式来与请求和响应对象进行交互,称为中间件(书上原话)。通俗的来说,http创建服务器接收请求时,所有的响应都要写在一个回调函数里面,对于不同的请求路径,所返回的响应信息都是通过if和else来区分,所有的逻辑都是在一个函数中,当逻辑复杂起来会有各种回调,极容易出现问题,故有了让问题简单起来的connect中间件的产生,connect把所有的请求信息都拆分开,形成多个中间件,http请求就相当于是水流一样流过中间件,当路径相同时,就会响应该请求,否则就继续往下流,直到结束。中间件就是函数组成的
  下面代码就是原生的Node.js写的响应请求,这是比较简单的,但是当服务器要处理无数个请求时,这样写会很容易出错。

    var http=require('http');
    var server=http.createServer(function (req,res) {
          if('/'==req.url){
              res.writeHead(200,{'Content-Type':'text/html'});
              res.write('...');
              res.end();
          }else if('/url'==req.url&&req.method=='POST'){
              var reqBody='';
              req.on('data',function (data) {
              reqBody += data;
          });
          req.on('end',function () {//用于数据接收完成后再获取
              res.writeHead(200,{'Content-Type':'text/html'});
              res.write('...');
              res.end();
          })
          }else{
              res.writeHead(404);
              res.write('Not Found');
              res.end();
          }
      }).listen(3000,function () {
          console.log('server is listening 3000');
      });

  下面代码是使用connect中间件写的http响应请求的代码,用app.use将代码拆分成更小的单元,具有更强的表达能力了。

    var connect=require('connect');
    var app=connect();
    app.use(function(req,res,next){
        if('/'==req.url){
            res.writeHead(200,{'Content-Type':'text/html'});
            res.write('...');
            res.end();
        }else{
            next();
        }
    });
    app.use(function(req,res,next){
        if('/'==req.url){if('/url'==req.url&&req.method=='POST')
            res.writeHead(200,{'Content-Type':'text/html'});
            res.write('...');
            res.end();
        }else{
            next();
        }
    });
    app.use(function(req,res,next){
        res.writeHead(404);
        res.write('Not Found');
        res.end();
    });
    app.listen(3000);

connect中间件源码解析

createServer函数

  中间件其实就是函数,中间件有点类似JavaScript事件循环的一个概念,将所有的中间件函数都存在一个栈中,请求到达时然后按顺序调用。
  先看一下开头部分已引入的模块,debug模块是用来开发调试的,EventEmitter是事件模块,finalhandler模块是让函数作为最后一个响应request,http模块是控制客户端请求与服务端响应的模块,merge是将属性从源对象合并到目标对象,parseUrl是解析给定请求对象的URL(查看req.url属性)并返回结果。

    var debug = require('debug')('connect:dispatcher');
    var EventEmitter = require('events').EventEmitter;
    var finalhandler = require('finalhandler');
    var http = require('http');
    var merge = require('utils-merge');
    var parseUrl = require('parseurl');

  由module.exports = createServer可以看出connect模块的出口是createServer,那前面代码中var app=connect()就可以看成var app=createServer(),那createServer函数到底是怎么定义的:

    var env = process.env.NODE_ENV || 'development';
    var proto = {};

    /* istanbul ignore next */
    var defer = typeof setImmediate === 'function'? setImmediate:function(fn){ process.nextTick(fn.bind.apply(fn, arguments)) }

    function createServer() {
      function app(req, res, next){ app.handle(req, res, next); }
      merge(app, proto);
      merge(app, EventEmitter.prototype);
      app.route = '/';
      app.stack = [];
      return app;
    }

  这里首先定义了env确定代码是在什么环境下运行,若没有定义则为开发环境,proto是一个空对象,defer定义了setImmediate函数,相当于一个插队的函数。后面就是createServer函数的定义了,它返回的是一个app,而app是定义的一个函数,参数为req,res,next三个参数,并且app函数中调用的是app.handle(req,res,next),可是app没有handle方法,代码往下走发现app合并了proto和EventEmitter.prototype的属性和方法,app的handle方法是来自proto对象的。app创建了两个属性app.route以及app.stack,app.route是一个路径地址,app.stack是一个存放中间件函数的堆栈。

connect中间件的方法有:app.use、app.handle、app.listen,是合并了proto对象得来的方法。

app.use

  在上面的代码中,app.use()是用来添加中间件的,而这个方法是来自于proto.use()方法。

proto.use(route,fn)

  route是请求的路径,可以写也可以不写,如果route不是string,则默认第一个参数是fn,路径默认为‘/’;

  fn是中间件函数,可以是传入3个或者4个参数,fn(req,res,next)或fn(err,req,res,next),fn(req,res,next)是正常处理函数,fn(err,req,res,next)是异常处理函数。

next()

  next函数用来做流控制,即用来触发下一个中间件的回调函数,调用next()后,程序会继续从app.stack堆栈中调用下一个中间件的回调函数,一直匹配合适的路由,若如果没有调用next(),则会在该中间件回调函数处理完之后停止。所以响应内容和next()都是在if和else中,否则当服务器已经响应请求之后还在调用next(),之后的中间件还会继续匹配路由,匹配到的路由的响应信息会被忽略。

  function next(err) {
     …
  }

  从上面代码知道,next()接收一个参数err,当err不为空时,err会被传递到next(err)中,进入异常处理函数,如果err为空,则会进入正常处理请求的函数。当所有的中间件函数都被调用之后仍旧没有匹配到路由,则会出现错误,如果next的err参数非空,则会给页面返回500错误,表示server出现了内部错误;如果err为空,则返回404错误,即访问的资源不存在。

proto.use()源码

  use()是用来添加中间件的,添加的方法是this.stack.push({ route: path, handle: handle }),传递了一个对象,对象包含两个属性route和handle,它们的值是path和handle,从下面proto.use的源码可以看出,新定义handle变量就是use要添加的中间件,path是请求的路径,handle和path是要存储到app.stack中的。如果path不是string,则默认第一个参数是中间件fn,路径默认为‘/’。代码分了几种情况讨论fn的情况,当fn=connect()时,即fn也为一个中间件时,那么堆栈中存储的handle为这个子中间件的fn.handle()方法,。当fn是http.Server类的实例时,那么request事件的第一个监听器为新的handle的值。

    proto.use = function use(route, fn) {
        var handle = fn;
        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) {
                server.handle(req, res, next);
            };
        }
    
        // wrap vanilla http.Servers
        if (handle instanceof http.Server) {
            handle = handle.listeners('request')[0];
        }
    
        // 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;
    };

call函数

  前面说app函数中调用了handle方法,handle是proto对象的一个方法,并合并到了app上。由于proto.handle方法中有用到call函数,这里就先介绍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);
    }

  不需要了解每一句代码的意思,只要懂这个函数的作用就可以了,当发生错误且传递了4个参数时就会调用handle(err,req, res, next)这个函数,当没有发生错误且传递的参数小于4个时就会调用handle(req, res, next),这里的handle函数是传进来的一个函数。

app.handle

  app.handle方法来自于proto.handle。proto.handle方法最主要的作用就是定义了next函数,是用来解决http请求的,layer=stack[index++]表示layer保存了当前的请求路径和中间件函数,用来匹配http请求的route和中间件里layer.route属性的值是否匹配,如果不匹配则会返回next(err)重新调用next函数,此时layer的值是下一个中间件的相关信息,这样就会一直循环下去,直到堆栈已经没有值或者route的值匹配成功为止。如果匹配成功,则会调用call(layer.handle, route, err, req, res, next),这个layer.handle就是中间件函数,call的作用就是调用layer.handle(err,req, res, next)或layer.handle(req, res, next)。

  下面代码是proto.handle的代码,主要就是获取请求的url并和存储的layer.route进行比较,这里就不说了,可以仔细看一下代码。

    proto.handle = function handle(req, res, out) {
        var index = 0;
        var protohost = getProtohost(req.url) || '';
        var removed = '';
        var slashAdded = false;
        var stack = this.stack;
    
        // final function handler
        var done = out || finalhandler(req, res, {
                env: env,
                onerror: logerror
            });
    
        // 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);
                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();
    };

app.listen

  app.listen方法来自于proto.listen。proto.listen的代码特别简单,就是调用了http模块的createServer()和listen()方法,那么app.listen(3000)的作用就是创建了一个服务器并监听端口3000。

    proto.listen = function listen() {
      var server = http.createServer(this);
      return server.listen.apply(server, arguments);
    };

总结

  1、从代码可以发现,所有的中间件函数传递的都是req与res,req与res分别是http服务器端的请求信息和响应信息,中间件只对req和res进行操作,故将请求信息比喻成水流流过这些中间件,遇到响应信息,这道水流就停止了,否则会一直流下去。
  2、app=connect()相当于app=createServer(),返回的是一个函数,故app本身就是一个函数,可以app(req,res,next),因为app的函数中调用了app.handle()函数。
  3、app.handle(req,res,next)用来处理http的请求信息,一般不是自行调用,而是添加中间件时app.use调用其中的next函数。
  4、app.use(route,fn)用来添加中间件,也可以是app.use(fn),此时route默认为‘/’。
  5、app.listen(port)就是创建服务器并监听端口,这一行代码是必须要调用的,当然也可以使用http自己创建服务器。

posted @ 2017-07-09 22:46  艾新觉罗  阅读(917)  评论(0编辑  收藏  举报