Last active
December 21, 2015 05:09
-
-
Save luckydrq/6255046 to your computer and use it in GitHub Desktop.
Connect源码解析
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | |
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* 补丁模块,通过重载、增加一些方法和字段,以弥补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; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* 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