源碼結(jié)構(gòu)
Koa的源碼中主要為lib目錄下的application.js, context.js, request.js與response.js文件
.
├── AUTHORS
├── CODE_OF_CONDUCT.md
├── History.md
├── LICENSE
├── Makefile
├── Readme.md
├── benchmarks
├── docs
├── lib
│ ├── application.js
│ ├── context.js
│ ├── request.js
│ └── response.js
├── package.json
└── test
application.js: 框架入口,導(dǎo)出Application類成玫,即使用時(shí)倒入的Koa類买羞。
context.js: context對(duì)象的原型熊杨,代理request與response對(duì)象因悲。
request.js: request對(duì)象的原型,提供請(qǐng)求相關(guān)的數(shù)據(jù)與操作。
response,js: response對(duì)象的原型,提供響應(yīng)相關(guān)的數(shù)據(jù)與操作冀偶。
Application
- proxy: 是否信任proxy header參數(shù),默認(rèn)為false
- middleware: 保存通過app.use(middleware)注冊(cè)的中間件
- subdomainOffset: 保存通過app.use(middleware)注冊(cè)的中間件
- env: 環(huán)境參數(shù)渔嚷,默認(rèn)為NODE_ENV或'development'
- context: context模塊进鸠,通過context.js創(chuàng)建
- request: request模塊,通過request.js創(chuàng)建
- response: response模塊形病,通過response.js創(chuàng)建
Application@listen
Koa通過app.listen(port)函數(shù)在某個(gè)端口啟動(dòng)服務(wù)客年。
listen函數(shù)通過http模塊開啟服務(wù):
/**
* shorthand for:
* http.createServer(app.callback()).listen(...)
*
* @param {Mixed} ...
* @return {Server}
* @api public
*/
listen(...args) {
debug('listen');
const server = http.createServer(this.callback());
return server.listen(...args);
}
實(shí)際上app.listen()為http.createServer(app.callback()).listen(...)的速記寫法。http.createServer()用于創(chuàng)建Web服務(wù)器漠吻,接受一個(gè)請(qǐng)求監(jiān)聽函數(shù)量瓜,并在得到請(qǐng)求時(shí)執(zhí)行。app.callback()用于處理請(qǐng)求途乃,合并中間件與創(chuàng)建請(qǐng)求上下文對(duì)象等等绍傲。
Application@use
Koa通過app.use()添加中間件,并將中間件存儲(chǔ)在app.middleware中欺劳。在執(zhí)行app.callback()時(shí)會(huì)將app,middleware中的中間件合并為一個(gè)函數(shù)唧取。
/**
* Use the given middleware 'fn',
*
* Old-style middleware will be converted.
*
* @param {Function} fn
* @return {Application} self
* @api public
*/
use(fn) {
if(typeof fn !== 'function') throw new TypeError('middleware must be a function!');
if(isGeneratorFunction(fn)) {
deprecate('Support for generators will be removed in v3. ' +
'See the documentation for examples of how to convert old middleware ' +
'https://github.com/koajs/koa/blob/master/docs/migration.md');
fn = convert(fn);
}
debug('use %s', fn._name || fn.name || '-');
this.middleware.push(fn);
return this;
}
Koa1.x版本使用Generator Function的方式寫中間件,而Koa2改用ES6 async/await划提。所以在use()函數(shù)中會(huì)判斷是否為舊風(fēng)格的中間件寫法枫弟,并對(duì)舊風(fēng)格寫法得中間件進(jìn)行轉(zhuǎn)換(使用koa-convert進(jìn)行轉(zhuǎn)換)。
可以注意到這里use()函數(shù)返回了this鹏往,這使得在添加中間件的時(shí)候能鏈?zhǔn)秸{(diào)用淡诗。
app
.use(function(ctx, next) {
// do some thing
})
.use(function(ctx, next) {
// do some thing
})
// ...
Application@callback
app.callback()負(fù)責(zé)合并中間件,創(chuàng)建請(qǐng)求上下文對(duì)象以及返回請(qǐng)求處理函數(shù)等伊履。
/**
* Return a request handler callback
* for node's native http server
*
* @return {Function}
* @api public
*/
callback() {
const fn = compose(this.middleware);
if (!this.listeners('error').length) this.on('error', this.onerror);
const handleRequest = (req, res) => {
res.statusCode = 404;
const ctx = this.createContext(req, res);
const onerror = err => ctx.onerror(err);
const handlerResponse = () => respond(ctx);
onFinished(res, onerror);
return fn(ctx).then(handleResponse.catch(onerror));
};
return handleRequest;
}
通過compose函數(shù)(koa-compose)合并app.middleware中的所有中間件韩容。查看關(guān)于koa-compose的分析。
app.callback()函數(shù)返回一個(gè)請(qǐng)求處理函數(shù)handleRequest唐瀑。該函數(shù)即為http.createServer接收的請(qǐng)求處理函數(shù)群凶,在得到請(qǐng)求時(shí)執(zhí)行。
handleRequest
handleRequest函數(shù)首先將響應(yīng)狀態(tài)設(shè)置為404哄辣,接著通過app.createContext()創(chuàng)建請(qǐng)求的上下文對(duì)象请梢。
onFinished(res, onerror)通過第三方庫(kù)on-finished監(jiān)聽http response,當(dāng)請(qǐng)求結(jié)束時(shí)執(zhí)行回調(diào)力穗,這里傳入的回調(diào)是context.onerror(err)毅弧,即當(dāng)錯(cuò)誤發(fā)生時(shí)才執(zhí)行。
最后返回fn(ctx).then(handleResponse).catch(onerror)当窗,即將所有中間件執(zhí)行(傳入請(qǐng)求上下文對(duì)象ctx)够坐,之后執(zhí)行響應(yīng)處理函數(shù)(app.respond(ctx)),當(dāng)拋出異常時(shí)同樣使用cintext,onerror(err)處理。
createContext
app.createContext()用來創(chuàng)建請(qǐng)求上下文對(duì)象元咙,并代理Koa的request和response模塊梯影。
/**
* Initialize a new context
*
* @api private
*/
createContext(req, res) {
const context = Object.create(this.context);
const request = context.request = Object.create(this.response);
const response = context.response = Object.create(this.response);
context.app = request.app = response.app = this;
context.req = request.req = response.req = req;
context.res = request.res = response.res = res;
request.response = response;
response.request = request;
context.originalUrl = request.originalUrl = req.url;
context.cookies = new Cookies(req, res, {
keys: this.keys,
secure: request.secure
});
request.ip = request.ips[0] || req.socket.remoteAddress || '';
context.accept = request.accept = accepts(req);
context.state = {};
return context;
}
這里對(duì)請(qǐng)求都對(duì)應(yīng)在上下文對(duì)象中添加對(duì)應(yīng)的cookies。
respond
app.respond(ctx)函數(shù)蛾坯,這就是app.createContext()函數(shù)中的handleResponse光酣,在所有中間件執(zhí)行完之后執(zhí)行。
在koa中可以通過設(shè)置ctx.respond = false來跳過這個(gè)函數(shù)脉课,但不推薦這樣子。另外财异,當(dāng)上下文對(duì)象不可寫時(shí)也會(huì)退出該函數(shù):
if (false === ctx.respond) return;
// ...
if (!ctx.writable) return;
當(dāng)返回的狀態(tài)碼表示沒有響應(yīng)主體時(shí)倘零,將響應(yīng)主體置空:
// ignore body
if (statues.empty[code]) {
// strip headers
ctx.body = null;
return res.end();
}
當(dāng)請(qǐng)求方法為HEAD時(shí),判斷響應(yīng)頭是否發(fā)送以及響應(yīng)主體是否為JSON格式戳寸,若滿足則設(shè)置響應(yīng)Content-Length:
if('HEAD' == ctx.method) {
if(!res.headersSent && isJSON(body)) {
ctx.length = Buffer.byteLength(JSON.stringify(body));
}
return res.end();
}
當(dāng)返回的狀態(tài)碼表示有響應(yīng)主體呈驶,但響應(yīng)主體為空時(shí),將響應(yīng)主體設(shè)置為響應(yīng)信息或狀態(tài)碼疫鹊。并當(dāng)響應(yīng)頭未發(fā)送時(shí)設(shè)置Content-Type與Content-Length:
if (null == body) {
body = ctx.message || String(code);
if (!res.headersSent) {
ctx.type = 'text';
ctx.length = Buffer.byteLength(body);
}
return res.end(body);
}
最后袖瞻,對(duì)不同的響應(yīng)主體進(jìn)行處理:
// response
if (Buffer.isBuffer(body)) return res.end(body);
if ('string' == typeof body) return res.end(body);
if(body instanceof Stream) return body.pipe(res);
// body: json
body = JSON.stringify(body);
if(!res.headersSent) {
ctx.length = Buffer.byteLength(body);
}
res.end(body);
Compose
在application.js中,callback()函數(shù)通過koa-compose組合所有的中間件拆吆,組合成單個(gè)函數(shù)聋迎。koa-compose的實(shí)現(xiàn)很簡(jiǎn)單:
function compose (middleware) {
if (!Array.isArray(middleware)) throw new TypeError('middleware stack must be an array!');
for(const fn of middleware) {
if (typeof fn !== 'function') throw new TypeError('middleware must be composed of functions!');
}
return function (context, next) {
// last called middleware #
let index = -1;
return dispatch(0);
function dispatch(i) {
if (i <= index) return Promise.reject(new Error('next() called multiple times'));
index = i;
let fn = middleware[i];
if (i === middleware.length) fn = next;
if (!fn) return Promise.resolve();
try {
return Promise.resolve(fn(context, function next() {
return dispatch(i + 1);
}));
} catch (err) {
return Promise.reject(err);
}
}
}
}
首先判斷傳入的中間件參數(shù)是否為數(shù)組,并檢查且該數(shù)組的元素是否為函數(shù)枣耀,然后返回了一個(gè)將中間件組合起來的函數(shù)霉晕。
重點(diǎn)關(guān)注返回的函數(shù)中的dispatch(i)函數(shù),這個(gè)函數(shù)將獲取第一個(gè)中間件捞奕,并在返回的Promise中執(zhí)行牺堰。當(dāng)中間件await next()時(shí)執(zhí)行下一個(gè)中間件,即:dispatch(i+1)颅围。
執(zhí)行流程可以簡(jiǎn)單看做:
async function middleware1() {
console.log('middleware1 begin');
await middleware2();
console.log('middleware1 end');
}
async function middleware2() {
console.log('middleware2 begin');
await middleware3();
console.log('middleware2 end');
}
async function middleware3() {
console.log('middleware3 begin');
console.log('middleware3 end');
}
middleware1();
// 執(zhí)行結(jié)果
middleware1 begin
middleware2 begin
middleware3 begin
middleware3 end
middleware2 end
middleware1 end
compose()函數(shù)通過Promise將這個(gè)過程串聯(lián)起來伟葫,從而返回單個(gè)中間件函數(shù)。
Context
Koa中的Context模塊封裝了request與response院促,代理了這兩個(gè)對(duì)象的方法與屬性筏养。其中使用了Tj寫的node-delegates庫(kù),用于代理context.request與context.response上的方法與屬性一疯。
/**
* Response delegation
*/
delegate(proto, 'response')
.method('attachment')
.method('redirect')
.method('remove')
.method('vary')
.method('set')
.method('append')
.method('flushHeaders')
.access('status')
.access('message')
.access('body')
.access('length')
.access('type')
.access('lastModified')
.access('etag')
.getter('headerSent')
.getter('writable')
// ...
context除了代理這兩個(gè)模塊外撼玄,還包含一個(gè)請(qǐng)求異常時(shí)的錯(cuò)誤處理函數(shù)。在application.js的callback()眾使用了這個(gè)函數(shù)墩邀。
const onerror = err => ctx.onerror(err);
const handleResponse = () => respond(ctx);
onFinished(res, onerror);
return fn(ctx).then(handleResponse).catch(onerror);
Context@onerror
context.onerror(err)首先對(duì)傳入的err變量進(jìn)行判斷掌猛,當(dāng)err為空時(shí)退出函數(shù),或者當(dāng)err不為空且不為Error類型時(shí)拋出異常。
if (null == err) return;
if (!(err instanceof Error)) err = new Error('non-error thrown: ${err}');
接著觸發(fā)app自身的error事件荔茬,將錯(cuò)誤拋給app废膘。
在此之前,設(shè)置headerSent變量表示響應(yīng)頭是否發(fā)送慕蔚,若響應(yīng)頭已發(fā)送丐黄,或者不可寫(即無法在響應(yīng)中添加錯(cuò)誤信息等),則退出該函數(shù)孔飒。
let headerSent = false;
if (this.headerSent || !this.writable) {
headerSent = err.headerSent = true;
}
// delegate
this.app.emit('error', err, this);
// nothing we can do here other
// than delegate to the app-level
// handler and log.
if (headerSent) {
return;
}
因?yàn)榘l(fā)生了錯(cuò)誤灌闺,所以必須將之前中間設(shè)置的響應(yīng)頭信息清空。
這里使用了Node提供的http.ServerResponse類上的getHeaderNames()與removeHeader()方法坏瞄。但getHeaderNames()這個(gè)函數(shù)在Node 7.7版本時(shí)加入的桂对,所以當(dāng)沒有提供該方法時(shí)需要使用_header來清空響應(yīng)頭。詳情可見:Node.js#10805鸠匀。
// first unset all headers
if (typeof res.getHeaderNames === 'function') {
res.getHeaderNames().forEach(name => res.removHeader(name));
} else {
res._headers = {}; // Node < 7.7
}
清空之前中間件設(shè)置的響應(yīng)頭之后蕉斜,將響應(yīng)頭設(shè)置為err.headers,并設(shè)置Content-Type與狀態(tài)碼缀棍。
當(dāng)錯(cuò)誤碼為ENOENT時(shí)宅此,意味著找不到該資源,將狀態(tài)碼設(shè)置為404爬范;當(dāng)沒有狀態(tài)碼或狀態(tài)碼錯(cuò)誤時(shí)默認(rèn)設(shè)置為500父腕。
// then set those specified
this.set(err.headers);
// force text/plain
this.type = 'text';
// ENOENT support
if ('ENOENT' == err.code) err.status = 404;
// default to 500
if('number' != typeof err.status || !statuses[err.status]) err.status = 500;
Request
Request模塊封裝了請(qǐng)求相關(guān)的屬性及方法。通過application中的createContext()方法坦敌,代理對(duì)應(yīng)的request對(duì)象:
const request = context.request = Object.create(this.request);
// ...
context.req = request.req = response.req = req;
// ...
request.response = response;
Response
Response模塊封裝了響應(yīng)相關(guān)的屬性以及方法侣诵。與request相同,通過createContext()方法代理對(duì)應(yīng)的response對(duì)象:
const response = context.response = Object.create(this.response);
// ...
context.res = request.res = response.res = res;
// ...
response.request = request;