Skip to content

Instantly share code, notes, and snippets.

@luckydrq
Last active December 21, 2015 05:09
Show Gist options
  • Save luckydrq/6255046 to your computer and use it in GitHub Desktop.
Save luckydrq/6255046 to your computer and use it in GitHub Desktop.
Connect源码解析
var EventEmitter = require('events').EventEmitter
, proto = require('./proto')
, utils = require('./utils')
, path = require('path')
, basename = path.basename
, fs = require('fs');
//应用补丁,封装了一些方法
require('./patch');
exports = module.exports = createServer;
exports.version = '2.7.11';
exports.mime = require('./middleware/static').mime;
exports.proto = proto;
exports.middleware = {};
exports.utils = utils;
/**
* Connect模块
* 责任链模式(Chain of responsibility)
*
* example:
* var app = connect()
* .use(function(req, res, next){
* // do some stuff
* next()
* })
* .use(function(req, res, next){
* // do other stuff
* next()
* }
*
* http.createSever(app).listen(8080)
*
*
*/
function createServer() {
function app(req, res, next){ app.handle(req, res, next); }
//继承proto模块的接口: app.use、app.handle
utils.merge(app, proto);
//继承EventEmitter,支持事件驱动
utils.merge(app, EventEmitter.prototype);
app.route = '/';
//缓存中间件堆栈
app.stack = [];
//支持直接将中间件传入构造函数
for (var i = 0; i < arguments.length; ++i) {
app.use(arguments[i]);
}
return app;
};
/**
* 兼容旧版本,加入自身引用
* 旧版本用法:
* var app = require('connect').createServer()
* http.createServer(app).listen(8080)
*/
createServer.createServer = createServer;
/**
* 保存预设的中间件并暴露于exports及exports.middleware,供开发者调用
* 这些中间件从`./middleware`目录读取
* example:
* var connect = require('connect')
* var app = connect()
* app.use(connect.logger())
* //app.use(conncet.middleware.logger())
*/
fs.readdirSync(__dirname + '/middleware').forEach(function(filename){
if (!/\.js$/.test(filename)) return;
var name = basename(filename, '.js');
function load(){ return require('./middleware/' + name); }
//Getter: 以文件名为key, 暴露于Connect.middleware
exports.middleware.__defineGetter__(name, load);
//Getter: 以文件名为key, 暴露于Connect自身
exports.__defineGetter__(name, load);
});
/**
* 补丁模块,通过重载、增加一些方法和字段,以弥补node原生http模块功能性的不足
*/
var http = require('http')
, res = http.ServerResponse.prototype
, setHeader = res.setHeader
, _renderHeaders = res._renderHeaders
, writeHead = res.writeHead;
//只加载一次,通过_hasConnectPatch标识控制
if (!res._hasConnectPatch) {
/**
* http.ServerResponse.prototype
* 提供headSent标识,判断响应头是否已设置
* 在node 0.10.x版本,如果设置了header,在http.ServerResponse实例上会有_headerSent{Boolean}标识
*
* @return {Boolean}
* @api public
*/
res.__defineGetter__('headerSent', function(){
//_header是原生字段,即响应头字符串。设置header后会添加到http.ServerResponse实例中
return this._header;
});
/**
* 响应头字段的兼容处理
*
* @param {String} field
* @param {String} val
* @api public
*/
res.setHeader = function(field, val){
var key = field.toLowerCase()
, prev;
// `Set-Cookie`
if (this._headers && 'set-cookie' == key) {
if (prev = this.getHeader(field)) {
val = Array.isArray(prev)
? prev.concat(val)
: [prev, val];
}
// `charset`
} else if ('content-type' == key && this.charset) {
val += '; charset=' + this.charset;
}
return setHeader.call(this, field, val);
};
/**
* 代理 "header" event.
* 在v0.10.12里,该方法已不会调到。调用转到`http.OutgoingMessage#_renderHeaders`方法中
* 估计是老版本还不支持时搞的这个方法
*/
res._renderHeaders = function(){
if (!this._emittedHeader) this.emit('header');
this._emittedHeader = true;
return _renderHeaders.call(this);
};
res.writeHead = function(){
if (!this._emittedHeader) this.emit('header');
this._emittedHeader = true;
return writeHead.apply(this, arguments);
};
res._hasConnectPatch = true;
}
/**
* Connect核心接口模块
*/
var http = require('http')
, utils = require('./utils')
, debug = require('debug')('connect:dispatcher');
var app = module.exports = {};
//获取当前运行环境变量NODE_ENV
//http://cyj.me/f2e/deploying-express-app/
var env = process.env.NODE_ENV || 'development';
/**
* 公共接口
*
* 注册route及相应的路由处理程序(handler)
* 默认的route是'/',即app.use(fn)
*
* 只有当该route是当前请求Url的部分(或全部)时,相应的handler才会被调用。
* 注:url起始位置开始匹配,如是url中间的一部分则不会生效
*
* 例如,我们对'/admin'注册了一个中间件:
* 当Url为'/admin'或是'/admin/settings'时,该中间件会生效
* 当Url为'/other/admin'或是'/'时,该中间件则不会生效
* 如果有多个中间件生效,则按注册的顺序依次调用
*
*
* 此api是链式的,可以像以下这样写(事实上大多数情况下都是这么写的):
* connect()
* .use(connect.favicon())
* .use(connect.logger())
* .use(connect.static(__dirname + '/public'))
* .listen(3000);
*
* @param {String|Function|Server} route, callback or server
* @param {Function|Server} callback or server
* @return {Server} for chaining
* @api public
*/
app.use = function(route, fn){
//设置默认route为'/'
if ('string' != typeof route) {
fn = route;
route = '/';
}
//允许包裹另一个Connect app
//app.use(anotherApp)
//对任务流进行模块化封装
if ('function' == typeof fn.handle) {
var server = fn;
fn.route = route;
fn = function(req, res, next){
server.handle(req, res, next);
};
}
//如果fn是http.Server对象,则取该对象的第一个监听函数
if (fn instanceof http.Server) {
fn = fn.listeners('request')[0];
}
//去掉route末尾的斜杠
//如果route为'/',则变为''
if ('/' == route[route.length - 1]) {
route = route.slice(0, -1);
}
//缓存入栈
debug('use %s %s', route || '/', fn.name || 'anonymous');
this.stack.push({ route: route, handle: fn });
//返回app对象以支持链式操作
return this;
};
/**
* 私有接口
*
* 请求处理
* function(){
* function next(){
* ...
* }
* next()
* }
*
* 流程:
* 1、遍历middleware栈,根据route与请求Url进行匹配:
* a) 不匹配,则调用next()继续下一个匹配
* b) 匹配,则调用相应的handler,并将next作为回调传入
* 2、在handler处理完业务逻辑后需要调用next继续执行直至完成遍历
* 3、当某个handler里调用了res.end(),则停止遍历,请求结束(此时不应再调用next回调,否则会引发500错误)
*
*
*
*
* @api private
*/
app.handle = function(req, res, out) {
var stack = this.stack
, fqdn = ~req.url.indexOf('://')
, removed = ''
, slashAdded = false
, index = 0;
function next(err) {
var layer, path, status, c;
//去掉url的第一个斜杠`/`
if (slashAdded) {
req.url = req.url.substr(1);
slashAdded = false;
}
//重新拼接Url
//从后面可以看到,传入每个handler的req.url是被处理了
//Url里与路由相同的部分被移除
req.url = removed + req.url;
req.originalUrl = req.originalUrl || req.url;
removed = '';
// 取出当前的handler
layer = stack[index++];
// 处理全部完成,包括两种情况:
// 1、栈里已没有可用的handler
// 2、在某个handler里已经发送了响应(在patch.js里会详细分析),则停止后续处理。
if (!layer || res.headerSent) {
// app.use(anotherApp)
// sub app处理完后,调用父app的next继续处理
if (out) return out(err);
// 有未处理的error
if (err) {
// 内部出错默认返回500
if (res.statusCode < 400) res.statusCode = 500;
debug('default %s', res.statusCode);
// 如果在error里设置了err.status,则沿用
if (err.status) res.statusCode = err.status;
// 生产环境下用基本的出错提示语
var msg = 'production' == env
? http.STATUS_CODES[res.statusCode]
: err.stack || err.toString();
// 在非测试环境下,记录错误堆栈
if ('test' != env) console.error(err.stack || err.toString());
if (res.headerSent) return req.socket.destroy();
res.setHeader('Content-Type', 'text/plain');
res.setHeader('Content-Length', Buffer.byteLength(msg));
if ('HEAD' == req.method) return res.end();
res.end(msg);
} else {
//没有出错,默认返回404
debug('default 404');
res.statusCode = 404;
res.setHeader('Content-Type', 'text/plain');
if ('HEAD' == req.method) return res.end();
res.end('Cannot ' + utils.escape(req.method) + ' ' + utils.escape(req.originalUrl));
}
return;
}
try {
// 解析url路径,去除querystring。例如:'/a/b?q=1' => '/a/b'
path = utils.parseUrl(req).pathname;
if (undefined == path) path = '/';
// 如果不匹配route,则跳过进入下一轮递归
if (0 != path.toLowerCase().indexOf(layer.route.toLowerCase())) return next(err);
// 例如:route为'/a/b',则排除如'/a/bc'的url。但是,'/a/b.html'是匹配的。
c = path[layer.route.length];
if (c && '/' != c && '.' != c) return next(err);
// 把req.url中匹配route的部分移除
// 例如:原来的url是'/a/b/c',route是'/a',则保留'/b/c'。
removed = layer.route;
req.url = req.url.substr(removed.length);
// 确保req.url的剩余部分以'/'开头
if (!fqdn && '/' != req.url[0]) {
req.url = '/' + req.url;
slashAdded = true;
}
debug('%s %s : %s', layer.handle.name || 'anonymous', layer.route, req.originalUrl);
// 获取handler参数的个数
var arity = layer.handle.length;
if (err) {
// 当有error时,调用匹配的error-handler,其方法签名必须为fn(err,req,res,next);否则,调用next(err)将error传递给下一个递归
// http://expressjs.com/guide.html#error-handling
if (arity === 4) {
layer.handle(err, req, res, next);
} else {
next(err);
}
} else if (arity < 4) {
// 调用匹配的handler,方法签名为fn(req,res,next)
layer.handle(req, res, next);
} else {
// 方法签名不正确,跳过此handler
next();
}
} catch (e) {
next(e);
}
}
next();
};
/**
* 监听函数
* 与node的`http.Server#listen()`接受相同的参数
*
* HTTP and HTTPS:
*
* var connect = require('connect')
* , http = require('http')
* , https = require('https');
*
* var app = connect();
*
* http.createServer(app).listen(80);
* https.createServer(options, app).listen(443);
*
* @return {http.Server}
* @api public
*/
app.listen = function(){
var server = http.createServer(this);
return server.listen.apply(server, arguments);
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment