【原创】express3.4.8源码解析之中间件
前言
注意:旧文章转成markdown格式。
中间件(middleware)的概念来自于TJ的connect库,express就是建立在connect之上。
就如同connect的意思是 连接 一样,connect通过客户端过来的http请求通过将一系列注册的中间件连接起来,而这些中间件则会按照注册的先后顺序依次来处理这个http请求,在每个中间件处理请求的过程中,得出的数据都可以传递到下一个中间件,当然我们可以有选择地决定是否继续执行后面的一些的中间件,也可以直接返回响应给客户端,这就是所谓的流式处理。
简单的例子
var express = require('express');
var app = express();
app.set('port', 3000);
app.use(function (req, res, next) {
	console.log(1);
	next();
});
app.use(function (req, res, next) {
	console.log(2);
});
app.listen(app.get('port'), function () {
	console.log('server listening...');
});
执行上面这段代码,并打开浏览器输入127.0.0.1:3000,再回头看看控制台:

很明显的我们看到控制台输出: 1 2
解释一下上述代码:
首先我们创建了app,然后两处调用了app.use(..);,最后开启server监听。
我们调用了app.use(..);,其实就是注册了中间件,这里显然是有了两个中间件,所以当http请求过来的时候,按常理说会依次触发这两个中间件,所以输出了1和2。
但是这里需要注意:
- 在第一个中间件中调用了
next();方法,假设我不调用呢?结果显然是只会输出:1,也就是说这里的next的意义在于手动调用下一个中间件。 - 其实这里这样的代码会导致客户端的请求长时间处于pending状态,以致于最后超时,原因是:服务端只是执行完了代码并没有给客户端一个响应,所以我们可以在第二个中间件里面加上一段响应代码,如:
res.send('hello world');。 
应用的例子
var express = require('express');
var routes = require('./routes');
var user = require('./routes/user');
var http = require('http');
var path = require('path');
var app = express();
...
app.use(express.favicon());
app.use(express.logger('dev'));
app.use(express.json());
app.use(express.urlencoded());
app.use(express.methodOverride());
app.use(app.router);
app.use(express.static(path.join(__dirname, 'public')));
...
http.createServer(app).listen(app.get('port'), function(){
  console.log('Express server listening on port ' + app.get('port'));
});
上面是用express-cli生成的app.js文件,这里其实就是一个实际的应用场景。
上面注册了很多中间件,都是connect中内置的,如:
express.favicon() --- 网站icon图标
express.logger('dev') --- 日志统计
app.router --- 路由中间件
express.static(path.join(__dirname, 'public') --- 静态文件路由
...
上述的函数返回值或者变量都是一个(中间件接口)函数,形式就是:function(req, res, next) {...},用来等待处理进来的http请求,并在内部调用next();来调用下一个中间件。
其实整个流程就归纳为下面这张图(盗用朴大的):

另外还要注意的是:
我们在一个中间件中通过调用next();来调用下一个中间件,其实从程序的角度是next函数自身的不断地迭代过程,express就是通过这样的方式进行流式处理的,这一点本人认为是这个框架最值得学习的地方。
源码学习
这里我主要讲解以下两个函数:
app.use([path], function) --- 注册中间件
app.handle(req, res, out) --- 处理中间件
app.use
上面的例子可以看出,是通过app.use(..);注册了中间件,当时只是传递了一个函数作为参数,其实是还有另一个一个参数path(不传时,path被设为'/')。
path是用来干什么的呢?很明显是过滤中间件的。
举个例子:
app.use('/list', funciton (req, res, next) {
	// 访问列表
	...
});
app.use('/edit', function (req, res, next) {
	// 访问编辑页
	// 检查用户的合法性
	if (check()) {
		next();
	} else {
		next(new Error('不合法'))
	}
});
很明显,从上面的代码和注释可以看出当一个用户通过不同的url访问不同页面时,根据path就可以进行不同处理,比如:你要编辑帖子,当然需要check以下用户身份,而看帖子就不需要。
所以总结下path的作用在于过滤中间件,不同的url请求通过匹配path,可能会由不同的中间件处理。
我们来分析下源代码:
app.use = function(route, fn){
  var app;
  // default route to '/'
  // route不设时,默认值是'/'
  if ('string' != typeof route) fn = route, route = '/';
  // express app
  // 如果fn为express返回的app
  if (fn.handle && fn.set) app = fn;
  // restore .app property on req and res
  // 设置挂载的app
  if (app) {
    app.route = route;
    // 挂载的app被当作中间件处理
    fn = function(req, res, next) {
      var orig = req.app;
      // 调用挂载app的handle,处理自己的中间件
      // 这里传递的回调,用于挂载app执行完后的还原操作
      // 还原当前app的req和res
      // 并且执行下一个中间件
      app.handle(req, res, function(err){
        req.__proto__ = orig.request;
        res.__proto__ = orig.response;
        next(err);
      });
    };
  }
  // 实质还是调用connect的use方法
  connect.proto.use.call(this, route, fn);
  // mounted an app
  // 如果是挂载app
  // 设置的parent为当前app
  // 并触发挂载app的mount的事件,来完成一些继承
  if (app) {
    app.parent = this;
    app.emit('mount', this);
  }
  return this;
};
从上面的的中文注释应该可以看出个大概:
- route默认值是'/';
 - fn除了是普通函数意外,还可以是
express();返回的app函数,这时候就是挂载app了(这里我们暂不讨论) app.use的本质还是调用connect.proto.use
因此我们又跑到connect中use方法定义的地方:
app.use = function(route, fn){
  if ('string' != typeof route) {
    fn = route;
    route = '/';
  }
 ...
  // strip trailing slash
  // 去除尾部可能出现的'/'
  if ('/' == route[route.length - 1]) {
    route = route.slice(0, -1);
  }
  // add the middleware
  // 向stack中添加该中间件
  debug('use %s %s', route || '/', fn.name || 'anonymous');
  this.stack.push({ route: route, handle: fn });
  return this;
};
我们发现我们的path和fn最后会被作为一个对象{ route: route, handle: fn }push到this.stack中,这样就算是注册了一个中间件,所以很容易联想到接下来的app.hanlde()就是一个一个从stack里面取出匹配的中间件,然后执行中间件,ok,带着这样的想法我们来看看app.handle的实现。
app.handle
首先我们必须知道当请求过来时,app.handle何时被调用?
如果你仔细研究过我上一篇的结构图,我猜你肯定知道express()返回的app是一个函数,再通过上面的第二个例子,你应该知道创建的server在监听到请求时会开始最先调用app,并被传递req,res这些对象作为参数。
ok,我在connect.js中找到了这样的一个函数定义的地方:
function createServer() {
  function app(req, res, next){ app.handle(req, res, next); }
  utils.merge(app, proto);
  utils.merge(app, EventEmitter.prototype);
  app.route = '/';
  app.stack = [];
  for (var i = 0; i < arguments.length; ++i) {
    app.use(arguments[i]);
  }
  return app;
};
从上面代码我们看到,当请求来时,app函数便会自动调用app.handle(...),并传递res,req作为参数,于是中间件的执行遍开始了~~
我慢慢地找到了它的源码:
// 处理中间件的入口,也是整个app处理请求的入口
app.handle = function(req, res, out) {
  var stack = this.stack
    , search = 1 + req.url.indexOf('?')
    , pathlength = search ? search - 1 : req.url.length
    , fqdn = 1 + req.url.substr(0, pathlength).indexOf('://')
    , protohost = fqdn ? req.url.substr(0, req.url.indexOf('/', 2 + fqdn)) : ''
    , removed = ''
    , slashAdded = false
    , index = 0;
  // 处理(下一个)中间件
  // 会被作为中间件接口函数的形参next传入,便于调用下一个中间件
  // 所以这个next函数是被迭代调用的,也就意味着上面这些变量会被共享使用
  // 随时都在变化着,需要进行必要的重置和还原
  function next(err) {
    var layer, path, c;
    // 还原操作
    // 去除上一个中间件可能添加的slash
    if (slashAdded) {
      req.url = req.url.substr(1);
      slashAdded = false;
    }
    // 还原操作,补回上一个中间件因为匹配成功删除的route部分
    req.url = protohost + removed + req.url.substr(protohost.length);
    // 保存原始的url
    // 因为后面的匹配操作可能会修改req.url
    req.originalUrl = req.originalUrl || req.url;
    removed = '';
    // next callback
    // 去除中间件
    layer = stack[index++];
    // all done
    // 如果中间件已经遍历完
    // 或者已经响应了客户端,即执行了res.end(..)等操作
    if (!layer || res.headerSent) {
      // delegate to parent
      // 用于挂载express app时,是挂载的app结束出口
      if (out) return out(err);
      // unhandled error
      // 如果某一个中间件报错了(即next(new Error(...)));
      // 那么处理错误,返回客户端应有的响应
      if (err) {
        // default to 500
        // 错误默认返回500
        if (res.statusCode < 400) res.statusCode = 500;
        debug('default %s', res.statusCode);
        // respect err.status
        // 使用给定err.status作为状态码
        if (err.status) res.statusCode = err.status;
        // production gets a basic error message
        // 错误信息
        var msg = 'production' == env
          ? http.STATUS_CODES[res.statusCode]
          : err.stack || err.toString();
        msg = utils.escape(msg);
        // log to stderr in a non-test env
        if ('test' != env) console.error(err.stack || err.toString());
        // 如果请求已经发出,直接destory
        if (res.headerSent) return req.socket.destroy();
        // 否则,返回响应客户端信息
        res.setHeader('Content-Type', 'text/html');
        res.setHeader('Content-Length', Buffer.byteLength(msg));
        if ('HEAD' == req.method) return res.end();
        res.end(msg);
      // 返回404,即该请求没有找到任何响应
      // 其实这里有可能存在hearSent的情况
      // 那么下面的res.setHeader 这个会报出异常,最后还是回到上一个判断
      } else {
        debug('default 404');
        res.statusCode = 404;
        res.setHeader('Content-Type', 'text/html');
        if ('HEAD' == req.method) return res.end();
        res.end('Cannot ' + utils.escape(req.method) + ' ' + utils.escape(req.originalUrl) + '\n');
      }
      return;
    }
    try {
      // 解析路径
      // 注意这里是pathname不是path
      path = utils.parseUrl(req).pathname;
      if (undefined == path) path = '/';
      // skip this layer if the route doesn't match.
      // 路由不匹配,直接执行到下一个中间件
      // 这里的匹配用的是startWith
      if (0 != path.toLowerCase().indexOf(layer.route.toLowerCase())) 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
      // 将req.url去除匹配到的部分,并保证传递给callback
      // 所以接下来的中间件再执行时会恢复这个req.url的(代码在上面)
      removed = layer.route;
      req.url = protohost + req.url.substr(protohost.length + removed.length);
      // Ensure leading slash
      // 如果url不是以'/'开头,那么加上
      if (!fqdn && '/' != req.url[0]) {
        req.url = '/' + req.url;
        slashAdded = true;
      }
      debug('%s %s : %s', layer.handle.name || 'anonymous', layer.route, req.originalUrl);
      // 通过中间件callback的参数个数
      // 用来判断是处理错误的callback还是处理正常逻辑的callback
      var arity = layer.handle.length;
      // 如果出错
      // 参数个数为4,那么传递err并执行错误处理函数
      // 否则,执行下一个中间件
      if (err) {
        if (arity === 4) {
          layer.handle(err, req, res, next);
        } else {
          next(err);
        }
      // 否则没有错误
      // 参数 < 4,执行callback处理正常逻辑
      // 否则,执行下一个中间件
      } else if (arity < 4) {
        layer.handle(req, res, next);
      } else {
        next();
      }
    // 捕获中间件的异常,并传递下去
    // 其实如果中间件本身也可以自己先捕获异常
    } catch (e) {
      next(e);
    }
  }
  // 执行第一个中间件
  next();
};
上述的代码貌似有那么点长,不过有中文注释会好点吧_。我们把注意力全部集中到function next () {...}这个函数,因为正是它的迭代才使得中间件得以被流式一样地一个一个有选择地被处理,而且随时可以终止这个处理流。
ok,我们逐步来分析:
首先我们从this.stack里取出一个中间件{route:route, handle: fn},
接下来的判断if (!layer || res.headerSent) {..}分别表示中间件已经遍历完毕和服务端已经响应了客户端这两种情况。
我们重点看中间件执行的过程,通过匹配将当前url的path与中间件的route进行匹配,这里的匹配条件:
- 0 === path.toLowerCase().indexOf(layer.route.toLowerCase())
 - c = path[layer.route.length]; (c && '/' != c && '.' != c)
 
第一个条件是startWidth的形式
第二个条件是对第一个条件的补充,比如像这样:
route : /edit
url : /edit/332 匹配
url : /editXXX/332 不匹配
url : /edit.json 匹配
接下来,如果匹配不了当前中间件,调用return next(err);执行下一个中间件;否则的话,即将开始执行中间件的handle。
在执行handle时,进行了一定的判断:
if (err) {..} 这里的err是从上一个中间件传过来
- 如果存在错误,
- 且中间件的handle的参数个数为4(表示错误处理函数),那么将执行该handle,
 - 否则认为该中间件没能力处理该错误,那么直接
next(err);交给下一个中间件处理; 
 - 那如果不存在错误,那么就是正常的逻辑处理
- 且中间件的handle的参数个数小于4(表示正常逻辑处理函数),那么执行该handle,
 - 否则认为该中间件没有处理该逻辑的函数,同样是
next();,交给下一个中间件处理。 
 
当handle被调用时,req, res, next都被传递给handle,他们都有着自己的意义:
- req --- 可以往上面赋值从而达到中间件共享数据
 - res --- 保证任何中间件都可以随时向客户端响应,从而阻止接下来的中间件的执行
 - next --- 可以通过next手动地选择是否执行下一个匹配地中间件
 
最后
差不多,以上就是我对中间件的理解了。
又周五了,大家周末快乐_!
【原创】express3.4.8源码解析之中间件的更多相关文章
- [原创]android开源项目源码解析(一)----CircleImageView的源码解析
		
CircleImageView的代码很简洁,因此先将此工程作为源码解析系列的第一篇文章. 解析说明都在代码里了. /* * Copyright 2014 - 2015 Henning Dodenhof ...
 - AspNetCore源码解析_1_CORS中间件
		
概述 什么是跨域 在前后端分离开发方式中,跨域是我们经常会遇到的问题.所谓的跨域,就是处于安全考虑,A域名向B域名发出Ajax请求,浏览器会拒绝,抛出类似下图的错误. JSONP JSONP不是标准跨 ...
 - AspNetCore3.1源码解析_2_Hsts中间件
		
title: "AspNetCore3.1源码解析_2_Hsts中间件" date: 2020-03-16T12:40:46+08:00 draft: false --- 概述 在 ...
 - 【原创】express3.4.8源码解析之Express结构图
		
前记 最近为了能够更好的搭建博客,看了开源博客引擎ghost源代码,顺道更深入的去了解express这个出名的nodejs web framework. 所以接下来一段时间对expressjs做一个源 ...
 - [原创]Laravel 的缓存源码解析
		
目录 前言 使用 源码 Cache Facade CacheManager Repository Store 前言 Laravel 支持多种缓存系统, 并提供了统一的api接口. (Laravel 5 ...
 - 【原创】express3.4.8源码解析之路由中间件
		
前言 注意:旧文章转成markdown格式. 跟大家聊一个中间件,叫做路由中间件,它并非是connect中内置的中间件,而是在express中集成进去的. 显而易见,该中间件的用途就是 ------ ...
 - 【原创】ui.router源码解析
		
Angular系列文章之angular路由 路由(route),几乎所有的MVC(VM)框架都应该具有的特性,因为它是前端构建单页面应用(SPA)必不可少的组成部分. 那么,对于angular而言,它 ...
 - 【原创】backbone1.1.0源码解析之View
		
作为MVC框架,M(odel) V(iew) C(ontroler)之间的联系是必不可少的,今天要说的就是View(视图) 通常我们在写逻辑代码也好或者是在ui组件也好,都需要跟dom打交道,我们 ...
 - 【原创】backbone1.1.0源码解析之Collection
		
晚上躺在床上,继续完成对Backbone.Collection的源码解析. 首先讲讲它用来干嘛? Backbone.Collection的实例表示一个集合,是很多model组成的,如果用model比喻 ...
 
随机推荐
- EF实体框架之CodeFirst三
			
前两篇博客学习了数据库映射和表映射,今天学习下数据库初始化.种子数据.EF执行sql以及执行存储过程这几个知识. 一.数据库初始化策略 数据库初始化有4种策略 策略一:数据库不存在时重新创建数据库 D ...
 - Java实现文件的加密与解密
			
最近在做一个项目,需要将资源文件(包括图片.动画等类型)进行简单的加密后再上传至云上的服务器,而在应用程序中对该资源使用前先将读取到的文件数据进行解密以得到真正的文件信息.此策略的原因与好处是将准备好 ...
 - Bootstrap系列 -- 36. 向上弹起的下拉菜单
			
有些菜单是需要向上弹出的,比如说你的菜单在页面最底部,而这个菜单正好有一个下拉菜单,为了让用户有更好的体验,不得不让下拉菜单向上弹出.在Bootstrap框架中专门为这种效果提代了一个类名“dropu ...
 - SQL温故系列两篇(一)
			
1.不允许保存更改.您所做的更改要求删除并重新创建以下表 关于SQL2008 “不允许保存更改.您所做的更改要求删除并重新创建以下表. 打开SQL SERVER 2008 工具-->选项--&g ...
 - SequoiaDB 系列之四   :架构简析
			
在本系列的第一篇中,简述了SequoiaDB的安装,以及一个(伪)集群的部署 第二篇和第三篇对SequoiaDB的集群,做了简单地操作. 在本篇中,将对SequoiaDB的架构进行简单的分析. 因为自 ...
 - Oracle创建表格报ORA-00906:缺失左括号错误解决办法
			
来源于:http://www.linuxidc.com/Linux/2013-06/85297.htm 解决办法: create table myTable(id number(5,2),name v ...
 - hdu3374 KMP+最大最小表示法
			
这题要求的是字符串左移时字典序最小和最大的第几次出现,并求出现次数.考虑一会可以发现,出现次数和循环节是有关系的. 出现了几次,就是循环了几次,如果循环节是他本身,也就是无循环,那这个字符串不管怎么移 ...
 - 【kAri OJ 616】Asce的树
			
时间限制 1000 ms 内存限制 65536 KB 题目描述 作为一个东北大老爷们,大A熊以力气大著称,现在有一颗半径为r的树,剖面图如黑色的圆,大A熊决定搬几个半径为R的圆柱形桶将其围住,剖面图如 ...
 - 44.Android之Shape设置虚线、圆角和渐变学习
			
Shape在Android中设定各种形状,今天记录下,由于比较简单直接贴代码. Shape子属性简单说明一下: gradient -- 对应颜色渐变. startcolor.endcolor就不多说 ...
 - BZOJ2456 mode
			
Description 给你一个n个数的数列,其中某个数出现了超过n div 2次即众数,请你找出那个数. Input 第1行一个正整数n. 第2行n个正整数用空格隔开. Output 一行一个正整数 ...