前言

注意:旧文章转成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。

但是这里需要注意:

  1. 在第一个中间件中调用了next();方法,假设我不调用呢?结果显然是只会输出:1,也就是说这里的next的意义在于手动调用下一个中间件
  2. 其实这里这样的代码会导致客户端的请求长时间处于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;
};

从上面的的中文注释应该可以看出个大概:

  1. route默认值是'/';
  2. fn除了是普通函数意外,还可以是express();返回的app函数,这时候就是挂载app了(这里我们暂不讨论)
  3. 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进行匹配,这里的匹配条件:

  1. 0 === path.toLowerCase().indexOf(layer.route.toLowerCase())
  2. 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源码解析之中间件的更多相关文章

  1. [原创]android开源项目源码解析(一)----CircleImageView的源码解析

    CircleImageView的代码很简洁,因此先将此工程作为源码解析系列的第一篇文章. 解析说明都在代码里了. /* * Copyright 2014 - 2015 Henning Dodenhof ...

  2. AspNetCore源码解析_1_CORS中间件

    概述 什么是跨域 在前后端分离开发方式中,跨域是我们经常会遇到的问题.所谓的跨域,就是处于安全考虑,A域名向B域名发出Ajax请求,浏览器会拒绝,抛出类似下图的错误. JSONP JSONP不是标准跨 ...

  3. AspNetCore3.1源码解析_2_Hsts中间件

    title: "AspNetCore3.1源码解析_2_Hsts中间件" date: 2020-03-16T12:40:46+08:00 draft: false --- 概述 在 ...

  4. 【原创】express3.4.8源码解析之Express结构图

    前记 最近为了能够更好的搭建博客,看了开源博客引擎ghost源代码,顺道更深入的去了解express这个出名的nodejs web framework. 所以接下来一段时间对expressjs做一个源 ...

  5. [原创]Laravel 的缓存源码解析

    目录 前言 使用 源码 Cache Facade CacheManager Repository Store 前言 Laravel 支持多种缓存系统, 并提供了统一的api接口. (Laravel 5 ...

  6. 【原创】express3.4.8源码解析之路由中间件

    前言 注意:旧文章转成markdown格式. 跟大家聊一个中间件,叫做路由中间件,它并非是connect中内置的中间件,而是在express中集成进去的. 显而易见,该中间件的用途就是 ------ ...

  7. 【原创】ui.router源码解析

    Angular系列文章之angular路由 路由(route),几乎所有的MVC(VM)框架都应该具有的特性,因为它是前端构建单页面应用(SPA)必不可少的组成部分. 那么,对于angular而言,它 ...

  8. 【原创】backbone1.1.0源码解析之View

    作为MVC框架,M(odel)  V(iew)  C(ontroler)之间的联系是必不可少的,今天要说的就是View(视图) 通常我们在写逻辑代码也好或者是在ui组件也好,都需要跟dom打交道,我们 ...

  9. 【原创】backbone1.1.0源码解析之Collection

    晚上躺在床上,继续完成对Backbone.Collection的源码解析. 首先讲讲它用来干嘛? Backbone.Collection的实例表示一个集合,是很多model组成的,如果用model比喻 ...

随机推荐

  1. Linux及安全——程序破解

    Linux及安全——程序破解 由于我的Ubuntu的vi有故障,所以用kaili做. 运行原程序 1.反汇编代码,查看 objdump -d login 2.修改代码 vi login 转换为16进制 ...

  2. Linux第14周学习笔记

    虚拟存储器 虚拟存储器是硬件异常.硬件地址翻译.主存.磁盘文件和内核软件的完美交互. 虚拟存储器的特点: 中心的 强大的 危险的 物理和虚拟寻址 计算机系统的主存被组织成一个由M个连续的字节大小的单元 ...

  3. WP&Win10仿微信消息框代码分享

    上次分享了幸运转盘的源码,感觉小伙伴们很喜欢:这次和大家分享下通信相关部分需要用到的类似微信的消息框代码,有需要的童鞋可以拿去用哟.自己尝试写的,可能有点low,勿喷呀! 希望以后有好的东西大家都分享 ...

  4. Material Design For Xamarin.Forms

    最近,升级 Xamarin.Forms 到最新版本后,发现Droid 项目下引入了以下几个依赖包: Xamarin.Android.Support.DesignXamarin.Android.Supp ...

  5. 记”Uri.IsWellFormedUriString”中的BUG

    场景 先上逻辑代码 1: /// <summary> 2: /// 图片真实地址 3: /// </summary> 4: public string FullImagePat ...

  6. maven integration with eclipse 3.0.4 does not work with NTLM proxy

    Recently downloaded m2e(maven integration with eclipse). The version is 3.0.4. My environment is beh ...

  7. css限制图片大小,避免页面撑爆

    /*==========限制图片大小======避免页面撑暴========*/img { max-width:100%;width:expression(width>669?"100 ...

  8. redis学习笔记——(4)

    一.概述: 在该系列的前几篇博客中,主要讲述的是与Redis数据类型相关的命令,如String.List.Set.Hashes和Sorted-Set.这些命令都具有一个共同点,即所有的操作都是针对与K ...

  9. AngularJS-MVC

    前言: 编程是一个很苦恼的工作,因为需要我们不断的去学习,不断的去专研,我本身就不是一个很喜欢学习的孩子,要不然从小到大也没有成绩好过,但是,我从来没有缺少过勤奋,还是让我们言归正传来说下 我们这段时 ...

  10. “耐撕”2016.04.13站立会议

    1. 时间 : 19:40--20:00  共计20分钟 2. 人员 : Z   郑蕊 * 组长 (博客:http://www.cnblogs.com/zhengrui0452/), P 濮成林(博客 ...