我是小十七_,今天和大家一起阅读 koa 的结构和所有源代码,koa 是带有异步中间件的一个 web 框架。如果你不知道 Koa 的中间件是如何工作的,你可以先看看这篇文章:【koa 源码阅读(一)】手把手教你搞懂 koa 中间件(洋葱模型)原理
我们将涵盖 Koa 中的所有文件,koa 源码仅包含四个文件(酷~):
文件 1: Application File (application.js)
这是 Koa 的入口文件。
我们一般这样初始化 koa 服务器:
const Koa = require('koa');
const app = new Koa();
app.listen(3000);
复制代码
new Koa()
实际上实例化了一个新的 Application 对象,这个是 application.js
中的构造函数:
module.exports = class Application extends Emitter {
constructor() {
super();
this.proxy = false;
this.middleware = [];
this.subdomainOffset = 2;
this.env = process.env.NODE_ENV || 'development';
this.context = Object.create(context); // 来自文件 2: context.js
this.request = Object.create(request); // 来自文件 3: request.js
this.response = Object.create(response); // 来自文件 4: response.js
if (util.inspect.custom) {
this[util.inspect.custom] = this.inspect;
}
}
复制代码
关于 Emitter
new Koa()
实例化了一个 Application
对象,它 extends 了 Emitter
。扩展 Emitter
类后,它将暴露一个 eventEmitterObject.on()
函数,这意味着我们可以像这样将事件附加到 Koa:
const app = new Koa();
app.on('event', (data) => {
console.log('an event occurred! ' + data); // an event occurred! 123
});
app.emit('event', 123);
复制代码
当 EventEmitter 对象发出事件时,附加到该特定事件的所有函数都被同步调用,被调用的侦听器返回的任何值都将被忽略并被丢弃。
Events | Node.js v12.4.0 Documentation
关于 Object.create()
我们也可以在构造函数中看到 Object.create()
,它只是创建一个新对象,使用一个现有的对象作为新创建对象的原型。它们的引用地址不同。
下面是一些例子:
const person = {
isHuman: false,
printIntroduction: function () {
console.log(`My name is ${this.name}. Am I human? ${this.isHuman}`);
}
};
const me = Object.create(person);
me.name = "Matthew"; // "name" 是 "me" 对象的一个属性, 但是不是 "person" 对象的
me.isHuman = true; // 继承的属性可以被重写
me.printIntroduction(); // "My name is Matthew. Am I human? true"
复制代码
启动服务器
说完 new Koa()
,我们可以看看 app.listen(3000)
。如果我们使用 app.listen(3000)
; 启动服务器,将执行以下代码:
listen(...args) {
debug('listen');
// Step 1: 调用 callback(), 创建一个 http 服务器
const server = http.createServer(this.callback());
// Step 5: http 服务器创建完成, 开始监听端口
return server.listen(...args);
}
callback() {
// Step 2: 准备中间件
const fn = compose(this.middleware);
if (!this.listenerCount('error'))
this.on('error', this.onerror);
const handleRequest = (req, res) => {
// Step 3: createContext, 我们会详细讨论它
const ctx = this.createContext(req, res);
// Step 4: handleRequest, 我们会详细讨论它
return this.handleRequest(ctx, fn);
};
return handleRequest;
}
复制代码
如果你想知道如何不使用 Koa 启动一个 http 服务器,这里是一个正常的方式,我们使用 http 这个包直接创建服务器:
const http = require('http');
http.createServer(function (req, res) {
res.writeHead(200, {'Content-Type': 'text/plain'});
res.write('Hello World!');
res.end();
}).listen(8080);
复制代码
关于 createContext(向代码添加了注释)
createContext(req, res) {
// 以 this.context 为原型创建了新的对象
const context = Object.create(this.context);
// 创建新的对象,确保 request 和 response 对象可以在 context 对象中访问
const request = context.request = Object.create(this.request);
const response = context.response = Object.create(this.response);
// 确保 context, request, response, app 对象之间可以相互访问
context.app = request.app = response.app = this;
context.req = request.req = response.req = req;
context.res = request.res = response.res = res;
request.ctx = response.ctx = context;
// 再次确保 response 对象可以在 request 对象内部访问
request.response = response;
response.request = request;
context.originalUrl = request.originalUrl = req.url;
context.state = {};
// 返回 context 对象,这是我们可以在中间件中使用的 ctx 对象
return context;
}
复制代码
关于 handleRequest(向代码添加了注释)
handleRequest(ctx, fnMiddleware) {
const res = ctx.res;
res.statusCode = 404;
const onerror = err => ctx.onerror(err);
// 当所有中间件都执行完成后, 调用 respond()
const handleResponse = () => respond(ctx);
// 如果来自 http 包 的 res 抛出了错误, 调用 onerror 函数
onFinished(res, onerror);
// 中间件部分已经在上一篇文章中讨论过了,我们不在这里讨论
return fnMiddleware(ctx).then(handleResponse).catch(onerror);
}
复制代码
respond(向代码添加了注释)
// 只是把 ctx.body 附加到 res, 这里没什么特别的
function respond(ctx) {
if (false === ctx.respond) return;
if (!ctx.writable) return;
const res = ctx.res;
let body = ctx.body;
const code = ctx.status;
// 忽略 body
if (statuses.empty[code]) {
// 去掉 headers
ctx.body = null;
return res.end();
}
if ('HEAD' == ctx.method) {
if (!res.headersSent && isJSON(body)) {
ctx.length = Buffer.byteLength(JSON.stringify(body));
}
return res.end();
}
// 如果 body 不存在, 返回
if (null == body) {
if (ctx.req.httpVersionMajor >= 2) {
body = String(code);
} else {
body = ctx.message || String(code);
}
if (!res.headersSent) {
ctx.type = 'text';
ctx.length = Buffer.byteLength(body);
}
return res.end(body);
}
// 如果 body 的类型是 buffer, 直接返回 body
if (Buffer.isBuffer(body)) return res.end(body);
if ('string' == typeof body) return res.end(body);
if (body instanceof Stream) return body.pipe(res);
// JSON 加密 body
body = JSON.stringify(body);
if (!res.headersSent) {
ctx.length = Buffer.byteLength(body);
}
res.end(body);
}
复制代码
文件 2: Context (context.js)
这个文件使用了一个叫做 delegate 的包来导出 context.js 中的方法,我写了一篇文章来了解这个包是如何工作的:
这是 context.js 文件的底部:
delegate(proto, 'response')
.method('attachment')
.method('redirect')
.....
delegate(proto, 'request')
.method('acceptsLanguages')
.method('acceptsEncodings')
.access('querystring')
复制代码
这意味着当您访问 ctx.querystring
时,它实际上是在访问 ctx.request.querystring
,并且在调用 createContext
时分配了 ctx.request
。
所以 delegate
主要让你通过在中间件中使用 ctx
轻松访问 response
和 request
中的方法(因为所有中间件都有 ctx
作为输入)。这是前一篇文章中提到的中间件示例:
// Here is the ctx
app.use(async (ctx, next) => {
console.log(3);
ctx.body = 'Hello World';
await next();
console.log(4);
});
复制代码
文件 3: Request (request.js)
这是 ctx.request
的原型。这个文件主要让你从 this.req
访问所有关于 http 请求的数据,比如 header、ip、host、url 等……这里是一些例子:
get(field) {
const req = this.req;
switch (field = field.toLowerCase()) {
case 'referer':
case 'referrer':
return req.headers.referrer || req.headers.referer || '';
default:
return req.headers[field] || '';
}
},
复制代码
文件 4: Response (response.js)
这是 ctx.response
的原型。这个文件主要让你访问 this.res
中的数据,比如 header 和 body,这里是部分源代码:
set(field, val) {
if (this.headerSent) return;
if (2 == arguments.length) {
if (Array.isArray(val)) val = val.map(v => typeof v === 'string' ? v : String(v));
else if (typeof val !== 'string') val = String(val);
this.res.setHeader(field, val);
} else {
for (const key in field) {
this.set(key, field[key]);
}
}
}
复制代码
感谢阅读,如果对你有帮助的话,欢迎点赞和讨论~