【原创】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比喻 ...
随机推荐
- LeetCode:Single Number II
题目地址:here 题目大意:一个整数数组中,只有一个数出现一次,其余数都出现3次,在O(n)时间,O(1)空间内找到这个出现一次的数 对于”只有一个数出现一次,其余数出现2次“的情况,很简单,只要把 ...
- Java学习笔记(二十)——Java 散列表_算法内容
[前面的话] 周末,本来打算找人去玩,结果没找到,所以我只好有学习了. 为什么会学习散列表,因为要使用HashMap?因为在做项目的时候,在服务器和客户端需要传输DTO,而传输的属性是动态增加的,所以 ...
- javascript继承(八)-封装
这个系列主要探讨的是javascript面向对象的编程,前面已经着重介绍了一下js的继承,下面想简单的说一下js如何实现封装的特性. 我们知道面向对象的语言实现封装是把成员变量和方法用一个类包围起来, ...
- 国内公共DNS
DNS(Domain Name System,域名系统),因特网上作为域名和IP地址相互映射的一个分布式数据库,能够使用户更方便的访问互联网,而不用去记住能够被机器直接读取的IP数串.通过主机名,最终 ...
- Xdebug开源PHP程序调试器
Xdebug是一个开放源代码的PHP程序调试器(即一个Debug工具),可以用来跟踪,调试和分析PHP程序的运行状况. 本文为大家讲解的是在linux下xdebug的安装和配置方法,感兴趣的同学参考下 ...
- java web名词解释
来源于:http://www.cnblogs.com/yxnchinahlj/archive/2012/02/24/2366110.html PO(persistant object) 持久对象 在o ...
- Lucene 4.7 --实现搜索
先看一段代码 IndexSearcher searcher = new IndexSearcher(DirectoryReader.open(FSDirectory.open(new File(&qu ...
- 获取和设置tinyMCE 4编辑器的内容
对于tinymce编辑器是无法通过js进行内容的读写的,必须使用编辑器自身的方法才行,下面是一些方法,希望能对用到的朋友有所帮助: 1.如果当前页面只有一个编辑器: 获取内容:tinyMCE.acti ...
- Java获取各种常用时间方法大全
Java获取各种常用时间方法大全 package cc.javaweb.test; Java中文网,Java获取各种时间大全 import java.text.DateFormat; import j ...
- 38.Android之ListView简单学习(一)
android中ListView用的很普遍,今天来学习下,本篇主要以本地数据加载到listview,后面会学习从网络获取数据添加到listview. 首先改下布局文件: <?xml versio ...