zoukankan      html  css  js  c++  java
  • 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自己创建服务器。

  • 相关阅读:
    CF598E Chocolate Bar 题解 动态规划
    CF864E Fire 题解 背包DP
    用 程序 解决 windows防火墙 的 弹窗 问题
    windbg 使用与技巧
    bat 下 字符串拆分 类似 split 可以使用 for /f delims
    vs2013 在按F5调试时,总是提示 “项目已经过期”的解决方案
    代理与反向代理
    关于维护用户状态的一致性
    视频的裁剪后缩放功能。
    通信协议的设计
  • 原文地址:https://www.cnblogs.com/aicanxxx/p/7132427.html
Copyright © 2011-2022 走看看