代码结构:
整个模块以applacation文件为主,其余context、request、response三个文件返回三个对象供applacation文件调用
application.js:
- Class Applacation
- constructor()
- listen()
- callback()
- handleRequest()
- createContext()
- inspect()
- onerror()
- toJson()
- use()
- respond()
以app.listen函数为线索:
const Koa = require("koa")
const app = new Koa()
app.listen(3000)
这段代码在Koa中运行情况如下:
-
new Koa()
是创建一个新的application类的实例,这就会首先执行application类的constructor方法,执行完就使得app实例获得了一些属性和方法,如listen。 -
app.listen
内部执行情况:
//application.js
listen(...args) {
debug('listen');
const server = http.createServer(this.callback());
return server.listen(...args);
}
callback() {
//从这里将中间件数组打包为一个包含所有中间件的函数
const fn = compose(this.middleware);
// 检查error事件的listener个数,如果为0则添加一个error的listener:this.onerror
if (!this.listenerCount('error')) this.on('error', this.onerror);
// 返回一个函数去处理req和res,因为这个函数要放到http.createServer里面作为回调函数去处理请求
const handleRequest = (req, res) => {
const ctx = this.createContext(req, res);//得到一个新的context,该context已经完成了一些基本属性的创建
return this.handleRequest(ctx, fn);
};
return handleRequest; //也就是说每次请求进来都执行的是这个handleRequest函数
}
子函数:
//application.js
handleRequest(ctx, fnMiddleware) {
console.log('handleRequest');
const res = ctx.res;
res.statusCode = 404; // 设置返回状态码为404,即404是默认的
const onerror = err => ctx.onerror(err);
const handleResponse = () => respond(ctx);// 定义handleResponse函数在其中调用respond函数,作用为绑定respond调用时的参数ctx
onFinished(res, onerror);
return fnMiddleware(ctx).then(handleResponse).catch(onerror); // 在这里先执行middleware然后执行handleResponse
}
function respond(ctx) {
// allow bypassing koa 当ctx.respond或者ctx.writable是false的时候koa不处理response
if (false === ctx.respond) return;
if (!ctx.writable) return;
const res = ctx.res;
let body = ctx.body;
const code = ctx.status;
// ignore body 通过statuses.empty[code]来判断该状态码是否有返回主体,如3xx,4xx,5xx就没有返回主体
if (statuses.empty[code]) {
// strip headers
ctx.body = null;
return res.end();
}
//当请求方法是HEAD的时候,不需要返回实体内容
if ('HEAD' == ctx.method) {
if (!res.headersSent && isJSON(body)) { //res.headersSent是http模块的api,用于检查是否已经发送过header,如果已经发送就不再发送
ctx.length = Buffer.byteLength(JSON.stringify(body)); // 设置response的length
}
return res.end();
}
// status body 当body是null的时候
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);
}
// responses 真正的往客户端响应,当body的类型是buffer或者string的时候就直接返回,是stream的时候使用pipe
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);
}
以上就是一个app.listen的流程,接下来还有一个重点就是app.use
以app.use函数为线索
//application.js
use(fn) {
if (typeof fn !== 'function') throw new TypeError('middleware must be a function!');
if (isGeneratorFunction(fn)) { // 如果是调用了generator函数,则发出弃用警告,警告内容如下
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); // 将generator函数转化为promise
}
debug('use %s', fn._name || fn.name || '-');
this.middleware.push(fn);
return this;
}
//这个函数最主要的操作就是this.middleware.push(fn),this.middleware究竟是什么就要看constructor构造函数了
constructor() {
super();
this.proxy = false;
this.middleware = []; // 揭示了中间件的本质实际上是一个函数调用,在内部仅仅调用了第一个函数,只有在每个函数中调用next才会调用下一个中间件
this.subdomainOffset = 2;
this.env = process.env.NODE_ENV || 'development';
this.context = Object.create(context); // ctx就是根据app.context来创建的,因此,如果想要拓展ctx的内容,就需要修改app.context
this.request = Object.create(request);
this.response = Object.create(response);
if (util.inspect.custom) { // 如果用户自定义了util.inspect那就用自己的inspect代替他的
this[util.inspect.custom] = this.inspect;
}
}
// 所以我们就可以发现app.use其实就是再往middleware数组中push了一个函数。
在上述callback函数中我们可以发现 const fn = compose(this.middleware)
这行代码。通过compose可以将middleware这个函数数组转换为一个包含了数组中所有函数的Promise函数,然后通过this.handleRequest(ctx, fn)
这行代码将返回的Promise函数传给下一个函数,在handleRequest中有fnMiddleware(ctx).then(handleResponse).catch(onerror)
,将ctx传给返回的Promise函数就完成了对第一个中间件的调用,如果想调用下一个中间件就必须再上一个中间件中调用next函数,next函数实际上就是一个绑定了调用顺序的Promise函数。
关于compose的具体内容请见https://blog.csdn.net/qq_39807732/article/details/91129409
以ctx.body=为线索
首先我们要找到body在context中的定义从哪来的:
//application.js
createContext(req, res) {
const context = Object.create(this.context);// 创建context继承自context
const request = context.request = Object.create(this.request);// 创建context.request继承自request
const response = context.response = Object.create(this.response);// 创建context.response继承自reponse
context.app = request.app = response.app = this; // 创建context.app指向this
context.req = request.req = response.req = req; // context.req指向http.createServer传入的req
context.res = request.res = response.res = res; // context.res指向http.createServer传入的res
request.ctx = response.ctx = context; // context.ctx指向context
request.response = response;
response.request = request;
context.originalUrl = request.originalUrl = req.url; // context.originalUrl指向req.url
context.state = {}; // context.state创建为空对象
return context;
}
回顾上面的callback函数,其中const ctx = this.createContext(req, res);
说明了ctx就是从createContext这个函数创建而来的,但这个函数中并没有定义body属性,但是可以发现context是从context.js文件暴露出来的对象继承来的,所以body属性应当定义在context.js中
//application.js
delegate(proto, 'response')
.method('attachment')
.method('redirect')
.method('remove')
.method('vary')
.method('set')
.method('append')
.method('flushHeaders')
.access('status')
.access('message')
.access('body') // body属性从这里来
.access('length')
.access('type')
.access('lastModified')
.access('etag')
.getter('headerSent')
.getter('writable');
这个delegate模块的作用就是实现了设计模式当中的委托模式(Delegation Pattern),道理很简单,就是将proto对象的response属性的属性设置到proto上。关于delegate模块具体可以参考这里。
我们知道context的response属性就是根据response.js所暴露出来对象创建的,所以说body属性定义在了response.js中。
//esponse.js
get body() {
return this._body;
},
set body(val) {
const original = this._body;
this._body = val;
// no content
if (null == val) {
if (!statuses.empty[this.status]) this.status = 204;
this.remove('Content-Type');
this.remove('Content-Length');
this.remove('Transfer-Encoding');
return;
}
// set the status
if (!this._explicitStatus) this.status = 200;
// set the content-type only if not yet set
const setType = !this.header['content-type'];
// string
if ('string' == typeof val) {
if (setType) this.type = /^\s*</.test(val) ? 'html' : 'text';
this.length = Buffer.byteLength(val);
return;
}
// buffer
if (Buffer.isBuffer(val)) {
if (setType) this.type = 'bin';
this.length = val.length;
return;
}
// stream
if ('function' == typeof val.pipe) {
onFinish(this.res, destroy.bind(null, val));
ensureErrorHandler(val, err => this.ctx.onerror(err));
// overwriting
if (null != original && original != val) this.remove('Content-Length');
if (setType) this.type = 'bin';
return;
}
// json
this.remove('Content-Length');
this.type = 'json';
},
可以看出body属性是通过get/set设置的,其内部将body真正的值设置在了_body上,而_body是在set body的时候添加到this上的。我们可以看到set body其实就是完成了对this._body的赋值,以及对传入的val的类型验证,通过val的类型去设置headers。注意:这里并没有进行res.end.
所以当我们使用ctx.body=
的时候其实就是一个普通的赋值操作。那究竟是在哪里返回的连接呢。请注意application.js中的listen流程。
在listen流程中我们可以发现:当一个http请求到来时先执行了所有的中间件,最后再执行respond函数,而res.end也是在此时进行的。
然后我们思考:当我们在中间件中自己调用了ctx.res.end会怎么样呢?我们可以发现在respond函数中,每次res.end之前都会检查headersSent, 如果还没有返回链接才回调用res.end
最后
最近读了一些npm模块的源码,有一些阅读源码的小感受:
在阅读源码的时候,最好是结合自己常用的API,以某个API为线索寻找该API的实现流程,在完成几条线索之后整个模块的实现思路就会清晰起来。这样的方式也更符合一般人的思维流程和知识的结构化。