结构

package.json

这里发现koa对esm的支持是这样配置的,但是看源码目录下面并没有所谓的./dist, 那么看下是不是需要打包之后生成的,果然,在script中有一条:”build”: “gen-esm-wrapper . ./dist/koa.mjs”。gen-esm-wrapper 这个东西,是用来把commonjs包装称esm的。

  1. "exports": {
  2. ".": {
  3. "require": "./lib/application.js",
  4. "import": "./dist/koa.mjs"
  5. },
  6. "./": "./"
  7. },

文件结构

  • 核心代码
    核心代码是放在lib里面的,而且lib里面只有四个文件:application.js、context.js、request.js、response.js。其功用就显而易见了。
  • 测试
    测试代码自然是放在test里面了,测试代码量很大,值得注意的是,koa的测试命令是:
    1. "test": "egg-bin test test",
    2. "test-cov": "egg-bin cov test",

这个egg-bin是egg.js根据node-modules/common-bin自己扩展的命令行工具。其中测试部分使用的mocha
其它的没有特别的东西了,整体看起来,代码量是不大的,一千来行。
这里面的话中间件部分肯定是重点。

阅读代码

我们如何使用Koa?
下面是最基本的用法:

  1. const Koa = require('koa');
  2. const app = new Koa();
  3. // response
  4. app.use(ctx => {
  5. ctx.body = 'Hello Koa';
  6. });
  7. app.listen(3000);

简单来说,1. 实例化koa对象; 2. listen端口,启动服务器; 3. use不是可以不要,但是不要的话什么都做不了,这是注册中间件的;

koa对象主流程

  1. // Koa对外的输出
  2. module.exports = class Application extends Emitter {}

我们看到,当new Koa()的时候实际上是new了这个Application,这是EventEmitter的子类,那么首先断定他至少可以emit事件以及on事件。
接着看下构造函数:

  1. // 构造函数
  2. constructor(options) {
  3. super();
  4. // 1. 初始化设置部分
  5. options = options || {};
  6. this.proxy = options.proxy || false;
  7. this.subdomainOffset = options.subdomainOffset || 2;
  8. this.proxyIpHeader = options.proxyIpHeader || 'X-Forwarded-For';
  9. this.maxIpsCount = options.maxIpsCount || 0;
  10. this.env = options.env || process.env.NODE_ENV || 'development';
  11. if (options.keys) this.keys = options.keys;
  12. // 2. 核心对象初始化
  13. this.middleware = [];
  14. this.context = Object.create(context);
  15. this.request = Object.create(request);
  16. this.response = Object.create(response);
  17. // 3. 兼容,不详细追究
  18. // util.inspect.custom support for node 6+
  19. /* istanbul ignore else */
  20. if (util.inspect.custom) {
  21. this[util.inspect.custom] = this.inspect;
  22. }
  23. }

上面的构造函数中分成三个部分:1. 属性设置; 2. 核心对象初始化; 3. 兼容部分;最需要关系的就是第二部分;
看看它都做了什么事情?

  1. this.middleware = []; 中间件集合,是个数组;
  2. 构造了三个属性挂在this上,分别是context、request、response;

继续看下listen方法:

  1. listen(...args) {
  2. debug('listen');
  3. const server = http.createServer(this.callback());
  4. return server.listen(...args);
  5. }

看上去很简单,就是用http.createServer构造了一个服务,但是createServer中的参数,this.callback()是重中之重~
品品,首先返回肯定是一个函数这个没毛病, 就是handleRequest,具体代码:

  1. // 作为listen的参数
  2. // 自然是每当有请求进来,就会回调本方法的。
  3. callback() {
  4. // 1. 合并中间件
  5. const fn = compose(this.middleware);
  6. // 2. 容错,不管
  7. if (!this.listenerCount('error')) this.on('error', this.onerror);
  8. // 3. 构造handleReqest
  9. const handleRequest = (req, res) => {
  10. const ctx = this.createContext(req, res);
  11. return this.handleRequest(ctx, fn);
  12. };
  13. return handleRequest;
  14. }

我们看到,通过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;

  1. createContext(req, res) {
  2. const context = Object.create(this.context);
  3. const request = context.request = Object.create(this.request);
  4. const response = context.response = Object.create(this.response);
  5. context.app = request.app = response.app = this;
  6. context.req = request.req = response.req = req;
  7. context.res = request.res = response.res = res;
  8. request.ctx = response.ctx = context;
  9. request.response = response;
  10. response.request = request;
  11. context.originalUrl = request.originalUrl = req.url;
  12. context.state = {};
  13. return context;
  14. }

我们看到上面的代码可以简单总结为:一通设置,相互引用;我们最终的到了这样的上下文对象context:context上面挂了resquest和response,以及app,就是koa的实例。另外,request、response的ctx属性也引用context对象。
下面就是真正的核心了:

  1. handleRequest(ctx, fnMiddleware) {
  2. const res = ctx.res;
  3. res.statusCode = 404;
  4. const onerror = err => ctx.onerror(err);
  5. const handleResponse = () => respond(ctx);
  6. // 当res io流关闭的时候,会触发onerror
  7. // onFinished方法来自: const onFinished = require('on-finished');
  8. onFinished(res, onerror);
  9. return fnMiddleware(ctx) // 执行中间件
  10. .then(handleResponse) // 执行完了之后
  11. .catch(onerror);
  12. }

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:

  1. /**
  2. * Response helper.
  3. */
  4. function respond(ctx) {
  5. // 1. 规避错误
  6. if (false === ctx.respond) return;
  7. if (!ctx.writable) return;
  8. // 从ctx得到信息
  9. const res = ctx.res;
  10. let body = ctx.body;
  11. const code = ctx.status;
  12. // ignore body
  13. // const statuses = require('statuses'); 这个是http状态码工具集合
  14. // Returns true if a status code expects an empty body
  15. if (statuses.empty[code]) {
  16. // strip headers
  17. ctx.body = null;
  18. return res.end();
  19. }
  20. if ('HEAD' === ctx.method) {
  21. if (!res.headersSent && !ctx.response.has('Content-Length')) {
  22. const { length } = ctx.response;
  23. if (Number.isInteger(length)) ctx.length = length;
  24. }
  25. return res.end();
  26. }
  27. // status body
  28. if (null == body) {
  29. if (ctx.response._explicitNullBody) {
  30. ctx.response.remove('Content-Type');
  31. ctx.response.remove('Transfer-Encoding');
  32. return res.end();
  33. }
  34. if (ctx.req.httpVersionMajor >= 2) {
  35. body = String(code);
  36. } else {
  37. body = ctx.message || String(code);
  38. }
  39. if (!res.headersSent) {
  40. ctx.type = 'text';
  41. ctx.length = Buffer.byteLength(body);
  42. }
  43. return res.end(body);
  44. }
  45. // responses
  46. if (Buffer.isBuffer(body)) return res.end(body);
  47. if ('string' === typeof body) return res.end(body);
  48. if (body instanceof Stream) return body.pipe(res);
  49. // body: json
  50. body = JSON.stringify(body);
  51. if (!res.headersSent) {
  52. ctx.length = Buffer.byteLength(body);
  53. }
  54. res.end(body);
  55. }

我们看到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方法,这个简单:

  1. use(fn) {
  2. if (typeof fn !== 'function') throw new TypeError('middleware must be a function!');
  3. if (isGeneratorFunction(fn)) {
  4. deprecate('Support for generators will be removed in v3. ' +
  5. 'See the documentation for examples of how to convert old middleware ' +
  6. 'https://github.com/koajs/koa/blob/master/docs/migration.md');
  7. fn = convert(fn);
  8. }
  9. debug('use %s', fn._name || fn.name || '-');
  10. this.middleware.push(fn); // 就是push到中间件集合中
  11. return this;
  12. }

那么,主流程简单总结起来就是:

  1. 构造koa实例中,构造了middleware集合;
  2. koa实例每次use一下,就把use的参数推到中间件集合中;
  3. 在listen方法中,先根据req,res构造出本次请求的context对象,然后把中间件整合起来(compose)执行,执行完了之后,要不执行onerror,要不再最后兜底下(用户可能会把一些属性设置错了),请求结束;

koa-compose

koa-compose肯定是一个函数式编程的compose思想,但是具体,是怎么把这些中间件整合起来的呢?
官方文档很简单:

compose([a, b, c, …]) Compose the given middleware and return middleware.

代码下下来,一探究竟,没错下面就是全部代码了:

  1. 'use strict'
  2. /**
  3. * Expose compositor.
  4. */
  5. module.exports = compose
  6. /**
  7. * Compose `middleware` returning
  8. * a fully valid middleware comprised
  9. * of all those which are passed.
  10. *
  11. * @param {Array} middleware
  12. * @return {Function}
  13. * @api public
  14. */
  15. function compose (middleware) {
  16. // 1. 容错
  17. if (!Array.isArray(middleware)) throw new TypeError('Middleware stack must be an array!')
  18. for (const fn of middleware) {
  19. if (typeof fn !== 'function') throw new TypeError('Middleware must be composed of functions!')
  20. }
  21. /**
  22. * @param {Object} context
  23. * @return {Promise}
  24. * @api public
  25. */
  26. // 2. 这段代码需要细细品
  27. return function (context, next) {
  28. // last called middleware #
  29. let index = -1
  30. return dispatch(0)
  31. function dispatch (i) {
  32. if (i <= index) return Promise.reject(new Error('next() called multiple times'))
  33. index = i
  34. let fn = middleware[i]
  35. if (i === middleware.length) fn = next
  36. if (!fn) return Promise.resolve()
  37. try {
  38. return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
  39. } catch (err) {
  40. return Promise.reject(err)
  41. }
  42. }
  43. }
  44. }

忽略掉上面容错的部分,直接看return部分。
首先,return的是一个函数,而这个函数返回的是dispatch(0)的调用结果;返回的是一个函数就对了,因为我们看koa的代码中最终是这样用的:

  1. // const fnMiddleware = compose(this.middleware);
  2. 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简化下?

  1. // this.middleware = [m1, m2]
  2. function compose (middleware) {
  3. // return
  4. return function (context, next) {
  5. return Promise.resolve(m1(context, function m1_next () {
  6. return Promise.resolve(m2(context, function m2_next() {
  7. return Promise.resolve();
  8. }))
  9. })
  10. );
  11. }
  12. }

这个形式还是一个调用的嵌套,只不过是基于promise的。所以叫compose没毛病。
另外我们看下一个老生常谈的问题,洋葱圈模型。
如果我的m1、m2是这么写的;

  1. async function m1(ctx, next) {
  2. console.log('do in m1 before~')
  3. await next();
  4. console.log('do in m1 after~')
  5. }
  6. async function m2(ctx, next) {
  7. console.log('do in m2 before~')
  8. await next();
  9. console.log('do in m2 after~')
  10. }
  11. app.use(m1);
  12. app.use(m2);

按照上述compose嵌套的逻辑拆解下来,自然是:

  1. do in m1 before~
  2. do in m2 before~
  3. do in m2 after~
  4. do in m1 after~

传统功夫,点到为止~

context对象的代理

我们刚才看到,在createContext构造了上下文对象。其中下面的代码中,我们看到,context.request,context.response都被赋值了:

  1. createContext(req, res) {
  2. const context = Object.create(this.context);
  3. const request = context.request = Object.create(this.request);
  4. const response = context.response = Object.create(this.response);
  5. // ...
  6. return context;
  7. }

所以我们比如想拿到请求头,自然是可以context.request.header,因为header是定义在request上的属性。但是,koa还允许我们这样拿:context.header。嗯?这是咋弄的?
原因就是它用了委托,context把一些方法或者属性委托给request或者response了。
比如,在context.js中就用大量的代码如下:

  1. delegate(proto, 'response')
  2. .method('attachment')
  3. .method('redirect')
  4. .method('remove')
  5. .method('vary')
  6. .method('has')
  7. .method('set')
  8. .method('append')
  9. .method('flushHeaders')
  10. .access('status')
  11. .access('message')
  12. .access('body')
  13. .access('length')
  14. .access('type')
  15. .access('lastModified')
  16. .access('etag')
  17. .getter('headerSent')
  18. .getter('writable');

我曾经纠结于一点就是,这个response是一个字符串,咋就委托了,后来看了看delegates的代码:

  1. function Delegator(proto, target) {
  2. if (!(this instanceof Delegator)) return new Delegator(proto, target);
  3. this.proto = proto;
  4. this.target = target;
  5. this.methods = [];
  6. this.getters = [];
  7. this.setters = [];
  8. this.fluents = [];
  9. }
  10. // .....
  11. Delegator.prototype.method = function(name){
  12. var proto = this.proto;
  13. var target = this.target;
  14. this.methods.push(name);
  15. proto[name] = function(){
  16. return this[target][name].apply(this[target], arguments);
  17. };
  18. return this;
  19. };

这个应该这么玩的,写一个简单的demo:

  1. const yxnne = {
  2. do() {
  3. console.log('good good study~');
  4. return 'giao~~~'
  5. }
  6. }
  7. const csg = {};
  8. csg.yxnne = yxnne;
  9. // 将csg.do委托给yxnne处理
  10. delegate(csg, 'yxnne')
  11. .method('do');
  12. console.log('csg do:', csg.do());

最终输出:

  1. good good study~
  2. csg do giao~~~