结构
package.json
这里发现koa对esm的支持是这样配置的,但是看源码目录下面并没有所谓的./dist, 那么看下是不是需要打包之后生成的,果然,在script中有一条:”build”: “gen-esm-wrapper . ./dist/koa.mjs”。gen-esm-wrapper 这个东西,是用来把commonjs包装称esm的。
"exports": {
".": {
"require": "./lib/application.js",
"import": "./dist/koa.mjs"
},
"./": "./"
},
文件结构
- 核心代码
核心代码是放在lib里面的,而且lib里面只有四个文件:application.js、context.js、request.js、response.js。其功用就显而易见了。 - 测试
测试代码自然是放在test里面了,测试代码量很大,值得注意的是,koa的测试命令是:"test": "egg-bin test test",
"test-cov": "egg-bin cov test",
这个egg-bin是egg.js根据node-modules/common-bin自己扩展的命令行工具。其中测试部分使用的mocha。
其它的没有特别的东西了,整体看起来,代码量是不大的,一千来行。
这里面的话中间件部分肯定是重点。
阅读代码
我们如何使用Koa?
下面是最基本的用法:
const Koa = require('koa');
const app = new Koa();
// response
app.use(ctx => {
ctx.body = 'Hello Koa';
});
app.listen(3000);
简单来说,1. 实例化koa对象; 2. listen端口,启动服务器; 3. use不是可以不要,但是不要的话什么都做不了,这是注册中间件的;
koa对象主流程
// Koa对外的输出
module.exports = class Application extends Emitter {}
我们看到,当new Koa()的时候实际上是new了这个Application,这是EventEmitter的子类,那么首先断定他至少可以emit事件以及on事件。
接着看下构造函数:
// 构造函数
constructor(options) {
super();
// 1. 初始化设置部分
options = options || {};
this.proxy = options.proxy || false;
this.subdomainOffset = options.subdomainOffset || 2;
this.proxyIpHeader = options.proxyIpHeader || 'X-Forwarded-For';
this.maxIpsCount = options.maxIpsCount || 0;
this.env = options.env || process.env.NODE_ENV || 'development';
if (options.keys) this.keys = options.keys;
// 2. 核心对象初始化
this.middleware = [];
this.context = Object.create(context);
this.request = Object.create(request);
this.response = Object.create(response);
// 3. 兼容,不详细追究
// util.inspect.custom support for node 6+
/* istanbul ignore else */
if (util.inspect.custom) {
this[util.inspect.custom] = this.inspect;
}
}
上面的构造函数中分成三个部分:1. 属性设置; 2. 核心对象初始化; 3. 兼容部分;最需要关系的就是第二部分;
看看它都做了什么事情?
- this.middleware = []; 中间件集合,是个数组;
- 构造了三个属性挂在this上,分别是context、request、response;
继续看下listen方法:
listen(...args) {
debug('listen');
const server = http.createServer(this.callback());
return server.listen(...args);
}
看上去很简单,就是用http.createServer构造了一个服务,但是createServer中的参数,this.callback()是重中之重~
品品,首先返回肯定是一个函数这个没毛病, 就是handleRequest,具体代码:
// 作为listen的参数
// 自然是每当有请求进来,就会回调本方法的。
callback() {
// 1. 合并中间件
const fn = compose(this.middleware);
// 2. 容错,不管
if (!this.listenerCount('error')) this.on('error', this.onerror);
// 3. 构造handleReqest
const handleRequest = (req, res) => {
const ctx = this.createContext(req, res);
return this.handleRequest(ctx, fn);
};
return handleRequest;
}
我们看到,通过compose将this.middleware中的中间件合成同一个函数了,compose这里用的是:const compose = require(‘koa-compose’);但不用看代码咱们就都能知道这compose就是函数式编程中的compose,概念都是一样的,最多有些特定的实现细节而已。
在这个this.callback中最重要的是返回了handleRequest,当然,这个handleRequest符合listen中参数的要求,req,res。
server.listen(cb)中这个cb是每当新请求到达时候都执行的,所以,我们看到handleRequest第一步自然是根据当下的req和res构造了context对象,然后它返回的是this.handleRequest(ctx, fn)的执行结果,我们先看createContext,再看this.handleRequest;
createContext(req, res) {
const context = Object.create(this.context);
const request = context.request = Object.create(this.request);
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.ctx = response.ctx = context;
request.response = response;
response.request = request;
context.originalUrl = request.originalUrl = req.url;
context.state = {};
return context;
}
我们看到上面的代码可以简单总结为:一通设置,相互引用;我们最终的到了这样的上下文对象context:context上面挂了resquest和response,以及app,就是koa的实例。另外,request、response的ctx属性也引用context对象。
下面就是真正的核心了:
handleRequest(ctx, fnMiddleware) {
const res = ctx.res;
res.statusCode = 404;
const onerror = err => ctx.onerror(err);
const handleResponse = () => respond(ctx);
// 当res io流关闭的时候,会触发onerror
// onFinished方法来自: const onFinished = require('on-finished');
onFinished(res, onerror);
return fnMiddleware(ctx) // 执行中间件
.then(handleResponse) // 执行完了之后
.catch(onerror);
}
onFinished方法来自: const onFinished = require(‘on-finished’);on-finished, 这个玩意是这样:Execute a callback when a HTTP request closes, finishes, or errors.就是当http请求关闭之后,执行回调。这里就执行的是onerror,而onerror执行的是ctx.onerror(err),当然如果没有error的话,参数err应该是不存在的。
ctx.onerror中第一句就是 if (null == err) return;
下面的return返回的是fnMiddleware(ctx).then(handleResponse).catch(onerror);
fnMiddleware是刚才经过compose的函数,这里我们注意,这里的then的调用时机,是fnMiddleware全部调用完成之后。
当fnMiddleware函数调用结束后,我们的业务在不出错的情况下应该都结束了。所以,这里的then中的handleResponse应该是业务后的处理。它调用了工具函数respond:
/**
* Response helper.
*/
function respond(ctx) {
// 1. 规避错误
if (false === ctx.respond) return;
if (!ctx.writable) return;
// 从ctx得到信息
const res = ctx.res;
let body = ctx.body;
const code = ctx.status;
// ignore body
// const statuses = require('statuses'); 这个是http状态码工具集合
// Returns true if a status code expects an empty body
if (statuses.empty[code]) {
// strip headers
ctx.body = null;
return res.end();
}
if ('HEAD' === ctx.method) {
if (!res.headersSent && !ctx.response.has('Content-Length')) {
const { length } = ctx.response;
if (Number.isInteger(length)) ctx.length = length;
}
return res.end();
}
// status body
if (null == body) {
if (ctx.response._explicitNullBody) {
ctx.response.remove('Content-Type');
ctx.response.remove('Transfer-Encoding');
return res.end();
}
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
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);
}
我们看到respond中,就是在分情况讨论合理的body值应该是什么样,相当于做兜底。比如说:
- statuses.empty[code]:statuses是专门处理HTTP状态码的工具,比如这个empty:Returns true if a status code expects an empty body.当状态码含义是空body的时候返回true。代码中确实也是这么做的。
- ‘HEAD’ === ctx.method的时候,如果返回头里面没有content-length,把ctx.length = length;
- 后面又分别讨论了body是空,response是Buffer,是string,是流,以及body是json的情况, 对这些情况分别做了兜底的设置;
至此,listen的流程看完了,主流程中还有一个use方法,这个简单:
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); // 就是push到中间件集合中
return this;
}
那么,主流程简单总结起来就是:
- 构造koa实例中,构造了middleware集合;
- koa实例每次use一下,就把use的参数推到中间件集合中;
- 在listen方法中,先根据req,res构造出本次请求的context对象,然后把中间件整合起来(compose)执行,执行完了之后,要不执行onerror,要不再最后兜底下(用户可能会把一些属性设置错了),请求结束;
koa-compose
koa-compose肯定是一个函数式编程的compose思想,但是具体,是怎么把这些中间件整合起来的呢?
官方文档很简单:
compose([a, b, c, …]) Compose the given middleware and return middleware.
代码下下来,一探究竟,没错下面就是全部代码了:
'use strict'
/**
* Expose compositor.
*/
module.exports = compose
/**
* Compose `middleware` returning
* a fully valid middleware comprised
* of all those which are passed.
*
* @param {Array} middleware
* @return {Function}
* @api public
*/
function compose (middleware) {
// 1. 容错
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!')
}
/**
* @param {Object} context
* @return {Promise}
* @api public
*/
// 2. 这段代码需要细细品
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, dispatch.bind(null, i + 1)));
} catch (err) {
return Promise.reject(err)
}
}
}
}
忽略掉上面容错的部分,直接看return部分。
首先,return的是一个函数,而这个函数返回的是dispatch(0)的调用结果;返回的是一个函数就对了,因为我们看koa的代码中最终是这样用的:
// const fnMiddleware = compose(this.middleware);
fnMiddleware(ctx).then(handleResponse).catch(onerror);
下面着重看下dispatch:
dispatch是从0开始的,假如我们现在又两个中间件,this.middleware = [m1, m2];
当我们调用fnMiddleware(ctx)的时候,
a. 进入代码中dispatch,i = 0, fn => m1, 此时return的是
Promise.resolve(m1(context, dispatch(1)));
b. 我们再看下dispatch(i = 1):
fn => m2, 这时候return的是Promise.resolve(m2(context, dispatch(2)));
c. 我们再看dispatch(i = 2):
此时命中条件:if (i === middleware.length) fn = next,fn => next,而这个next是啥?是fnMiddleware(ctx)的第二个参数,很显然我们没有传递,所以fn === undefined,所以这时候return Promise.resolve();
所以我们是不是可以把compose简化下?
// this.middleware = [m1, m2]
function compose (middleware) {
// return
return function (context, next) {
return Promise.resolve(m1(context, function m1_next () {
return Promise.resolve(m2(context, function m2_next() {
return Promise.resolve();
}))
})
);
}
}
这个形式还是一个调用的嵌套,只不过是基于promise的。所以叫compose没毛病。
另外我们看下一个老生常谈的问题,洋葱圈模型。
如果我的m1、m2是这么写的;
async function m1(ctx, next) {
console.log('do in m1 before~')
await next();
console.log('do in m1 after~')
}
async function m2(ctx, next) {
console.log('do in m2 before~')
await next();
console.log('do in m2 after~')
}
app.use(m1);
app.use(m2);
按照上述compose嵌套的逻辑拆解下来,自然是:
do in m1 before~
do in m2 before~
do in m2 after~
do in m1 after~
传统功夫,点到为止~
context对象的代理
我们刚才看到,在createContext构造了上下文对象。其中下面的代码中,我们看到,context.request,context.response都被赋值了:
createContext(req, res) {
const context = Object.create(this.context);
const request = context.request = Object.create(this.request);
const response = context.response = Object.create(this.response);
// ...
return context;
}
所以我们比如想拿到请求头,自然是可以context.request.header,因为header是定义在request上的属性。但是,koa还允许我们这样拿:context.header。嗯?这是咋弄的?
原因就是它用了委托,context把一些方法或者属性委托给request或者response了。
比如,在context.js中就用大量的代码如下:
delegate(proto, 'response')
.method('attachment')
.method('redirect')
.method('remove')
.method('vary')
.method('has')
.method('set')
.method('append')
.method('flushHeaders')
.access('status')
.access('message')
.access('body')
.access('length')
.access('type')
.access('lastModified')
.access('etag')
.getter('headerSent')
.getter('writable');
我曾经纠结于一点就是,这个response是一个字符串,咋就委托了,后来看了看delegates的代码:
function Delegator(proto, target) {
if (!(this instanceof Delegator)) return new Delegator(proto, target);
this.proto = proto;
this.target = target;
this.methods = [];
this.getters = [];
this.setters = [];
this.fluents = [];
}
// .....
Delegator.prototype.method = function(name){
var proto = this.proto;
var target = this.target;
this.methods.push(name);
proto[name] = function(){
return this[target][name].apply(this[target], arguments);
};
return this;
};
这个应该这么玩的,写一个简单的demo:
const yxnne = {
do() {
console.log('good good study~');
return 'giao~~~'
}
}
const csg = {};
csg.yxnne = yxnne;
// 将csg.do委托给yxnne处理
delegate(csg, 'yxnne')
.method('do');
console.log('csg do:', csg.do());
最终输出:
good good study~
csg do giao~~~