中间件原理

你好奇为什么是1342么?

从generator说起

koa-getting-start/middleware/core/a.js

  1. function * a(){
  2. console.log('第1个中间件before 1')
  3. yield *b()
  4. console.log('第1个中间件after 2')
  5. }
  6. function * b(){
  7. console.log(' 业务逻辑处理')
  8. }
  9. function *hello() {
  10. yield *a()
  11. }
  12. var it1 = hello();
  13. console.log(it1.next()); // { value: 1, done: false }

用co简化一下代码

koa-getting-start/middleware/core/b.js

  1. var co = require('co')
  2. function * a(){
  3. console.log('第1个中间件before 1')
  4. yield *b()
  5. console.log('第1个中间件after 2')
  6. }
  7. function * b(){
  8. console.log(' 业务逻辑处理')
  9. }
  10. co(function* () {
  11. yield *a()
  12. })

看一下具体的1342

koa-getting-start/middleware/core/c.js

  1. var co = require('co')
  2. function * a(){
  3. console.log('第1个中间件before 1')
  4. yield *b()
  5. console.log('第1个中间件after 2')
  6. }
  7. function * b(){
  8. console.log(' 第2个中间件before 3')
  9. yield *c()
  10. console.log(' 第2个中间件after 4')
  11. }
  12. function * c(){
  13. console.log(' 业务逻辑处理')
  14. }
  15. co(function* () {
  16. yield *a()
  17. })

这看起来不太像中间件啊,next呢?

中间件写法

koa-getting-start/middleware/core/d.js

  1. var co = require('co');
  2. var compose = require('koa-compose')
  3. function * a(next){
  4. console.log('第1个中间件before 1')
  5. yield next
  6. console.log('第1个中间件after 2')
  7. }
  8. function * b(next){
  9. console.log(' 第2个中间件before 3')
  10. yield next;
  11. console.log(' 第2个中间件after 4')
  12. }
  13. function * c(next){
  14. console.log(' 业务逻辑处理')
  15. }
  16. var stack = [a, b, c];
  17. co(compose(stack))

总结

通过上面的3个demo,相信大家能够理解generator的yield转让处理权的。通过generator嵌套next,实现这种回溯功能。

探索中间件原理

之前讲了中间件各种用法,原理,以至于我们还挖出了1342问题,那么你好奇Koa里到底是怎么实现中间件机制的吗?

本小节将给出精简Koa中间件机制,以便读者更容易读懂Koa源码

v1

koa-getting-start/middleware/v1/app.js

  1. var co = require('co');
  2. var debug = require('debug')('v0')
  3. module.exports = {
  4. middleware :[],
  5. use: function (fn) {
  6. debug('use:' + fn);
  7. this.middleware.push(fn);
  8. return this;
  9. },
  10. callback: function (cb) {
  11. const fn = this.compose(this.middleware);
  12. debug('callback compose fn = ' + fn)
  13. co(fn).then(cb, function (err) {
  14. console.error(err.stack);
  15. });
  16. },
  17. compose: function (middleware) {
  18. debug('compose=' + middleware)
  19. return function *(next){
  20. if (!next) {
  21. next = function *(){}
  22. }
  23. var i = middleware.length;
  24. while (i--) {
  25. debug('compose middleware[' + i + ']=: ' + middleware[i])
  26. // next = co.wrap(middleware[i]).call(this);
  27. next = middleware[i].call(this, next);
  28. debug(next)
  29. }
  30. return yield *next;
  31. }
  32. }
  33. }

测试代码

  1. var app = require('./app')
  2. app.use(function *(next){
  3. console.log(1)
  4. yield next;
  5. console.log(2)
  6. })
  7. app.use(function *(next){
  8. console.log(3)
  9. yield next;
  10. console.log(4)
  11. })
  12. app.callback();
  13. // 1
  14. // 3
  15. // 4
  16. // 2

v1的代码里只依赖co,整体来说是非常精简和好理解的,下面我们一一解读

1)中间件是由数组保存的

  1. middleware :[],

2)app.use使用中间件

  1. use: function (fn) {
  2. debug('use:' + fn);
  3. this.middleware.push(fn);
  4. return this;
  5. },
  • 通过push方法给数组增加中间件
  • 返回this,是简单的链式写法,以便可以无限的app.use(fn).use(fn2)

3)核心入口

先看测试里代码

  1. app.callback();
  2. // 1
  3. // 3
  4. // 4
  5. // 2

知道到callback函数,打印出1342,即callback函数是入口,处理了所有中间件调用。

代码如下

  1. callback: function (cb) {
  2. const fn = this.compose(this.middleware);
  3. debug('callback compose fn = ' + fn)
  4. co(fn).then(cb, function (err) {
  5. console.error(err.stack);
  6. });
  7. },

解读

  • 通过this.compose把this.middleware转变一个名为fn的generator函数
  • 通过co来执行fn
  • co的返回值是Promise对象,所以后面的then接了2个参数,cb是成功的回调,后面的匿名函数是用于处理发生异常的。

这里的逻辑比较简单,就是co去执行fn获得最终结果。但问题是this.middleware是基于generator函数的数组,怎么把它转成generator呢?这其实就是this.compose做的事儿。

4)核心变化compose

  1. compose: function (middleware) {
  2. return function *(next){
  3. if (!next) {
  4. next = function *(){}
  5. }
  6. var i = middleware.length;
  7. while (i--) {
  8. next = middleware[i].call(this, next);
  9. }
  10. return yield *next;
  11. }
  12. }

其实就是compose([f1, f2, …, fn])转化为fn(…f2(f1(noop()))),最终的返回值是一个generator function。

4.0)高阶函数

高阶函数只是将函数作为参数或返回值的函数

4.1)i—

i—之后的i = i - 1,也就是,当前的next和

  • middleware[i]是前一个中间件。

4.2)call

先来看一下call

  1. function add(a,b){
  2. console.log(a+b)
  3. }
  4. function sub(a,b){
  5. console.log(a-b)
  6. }
  7. add.call(sub, 3, 1)
  8. // 这个例子中的意思就是用 add 来替换 sub,
  9. // add.call(sub,3,1) == add(3,1) ,
  10. // 所以运行结果为:console.log(4);
  11. // 注意:js 中的函数其实是对象,函数名是对 Function对象的引用。

那么

  1. next = middleware[i].call(this, next);

4.3) yield *

还记得

koa-getting-start/middleware/core/b.js

  1. var co = require('co')
  2. function * a(){
  3. console.log('第1个中间件before 1')
  4. yield *b()
  5. console.log('第1个中间件after 2')
  6. }
  7. function * b(){
  8. console.log(' 业务逻辑处理')
  9. }
  10. co(function* () {
  11. yield *a()
  12. })

这段代码相当于

  1. function * a(){
  2. console.log('第1个中间件before 1')
  3. // yield *b()
  4. console.log(' 业务逻辑处理')
  5. console.log('第1个中间件after 2')
  6. }

亦即

  1. a(b())

通过yield*递归执行generator,是非常有用的手段。

4.4) 总结

  • 如果next为空,next = function *(){}
  • i—之后,middleware[i]就是之前的中间件,即f(n-1)
  • 通过call,实际转成之前中间件为next
  • 通过while i—规约所有中间件为一个generator
  • 通过yield *递归执行generator

compose返回的是generator,那么它就需要一个generator执行器,也就是上文出现的co,与之搭配是非常合理的。

v2

koa-getting-start/middleware/v2/app.js

  1. var co = require('co');
  2. var debug = require('debug')('v1')
  3. const convert = require('koa-convert')
  4. module.exports = {
  5. middleware :[],
  6. use: function (fn) {
  7. this.middleware.push(fn);
  8. return this;
  9. },
  10. callback: function () {
  11. const fn = convert.compose(this.middleware);
  12. debug('callback compose fn = ' + fn)
  13. var ctx = {
  14. }
  15. fn(ctx).then(function(){
  16. })
  17. }
  18. }

测试

koa-getting-start/middleware/v2/test.js

  1. var app = require('./app')
  2. app.use((ctx, next) =>{
  3. console.log(1)
  4. // before
  5. return next().then(() => {
  6. // after
  7. console.log(2)
  8. })
  9. })
  10. app.use(function *(next){
  11. console.log(3)
  12. yield next;
  13. console.log(4)
  14. })
  15. // console.log(app.middleware)
  16. app.callback();
  17. // 1
  18. // 3
  19. // 4
  20. // 2

v2补充

https://github.com/koajs/convert/blob/master/test.js

  1. describe('convert.compose()', () => {
  2. it('should work', () => {
  3. let call = []
  4. let context = {}
  5. let _context
  6. let mw = convert.compose([
  7. function * name (next) {
  8. call.push(1)
  9. yield next
  10. call.push(11)
  11. },
  12. (ctx, next) => {
  13. call.push(2)
  14. return next().then(() => {
  15. call.push(10)
  16. })
  17. },
  18. function * (next) {
  19. call.push(3)
  20. yield* next
  21. call.push(9)
  22. },
  23. co.wrap(function * (ctx, next) {
  24. call.push(4)
  25. yield next()
  26. call.push(8)
  27. }),
  28. function * (next) {
  29. try {
  30. call.push(5)
  31. yield next
  32. } catch (e) {
  33. call.push(7)
  34. }
  35. },
  36. (ctx, next) => {
  37. _context = ctx
  38. call.push(6)
  39. throw new Error()
  40. }
  41. ])
  42. return mw(context).then(() => {
  43. assert.equal(context, _context)
  44. assert.deepEqual(call, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11])
  45. })
  46. })

v3

koa-getting-start/middleware/v3/app.js

  1. const co = require('co');
  2. const debug = require('debug')('v1')
  3. const compose = require('koa-compose')
  4. module.exports = {
  5. middleware :[],
  6. use: function (fn) {
  7. this.middleware.push(fn);
  8. return this;
  9. },
  10. callback: function () {
  11. const fn = compose(this.middleware);
  12. debug('callback compose fn = ' + fn)
  13. var ctx = {
  14. }
  15. fn(ctx).then(function(){
  16. })
  17. }
  18. }

koa-getting-start/middleware/v3/test.js

  1. 'use strict'
  2. const co = require('co');
  3. const debug = require('debug')('v1')
  4. module.exports = {
  5. middleware :[],
  6. use: function (fn) {
  7. this.middleware.push(fn);
  8. return this;
  9. },
  10. compose: function (middleware) {
  11. return function (context, next) {
  12. // last called middleware #
  13. let index = -1
  14. return dispatch(0)
  15. function dispatch (i) {
  16. if (i <= index) return Promise.reject(new Error('next() called multiple times'))
  17. index = i
  18. const fn = middleware[i] || next
  19. if (!fn) return Promise.resolve()
  20. try {
  21. return Promise.resolve(fn(context, function next () {
  22. return dispatch(i + 1)
  23. }))
  24. } catch (err) {
  25. return Promise.reject(err)
  26. }
  27. }
  28. }
  29. },
  30. callback: function () {
  31. const fn = this.compose(this.middleware);
  32. debug('callback compose fn = ' + fn)
  33. var ctx = {
  34. }
  35. fn(ctx).then(function(){
  36. })
  37. }
  38. }

如果此时把其中的一个中间件变成koa 1.x支持的v3/test2.js

  1. var app = require('./app')
  2. var co = require('co')
  3. app.use((ctx, next) => {
  4. console.log(1)
  5. // before
  6. return next().then(() => {
  7. // after
  8. console.log(2)
  9. })
  10. })
  11. app.use(function *(next) {
  12. console.log(3)
  13. yield next;
  14. console.log(4)
  15. })
  16. // app.use(function *(next) {
  17. // console.log(3)
  18. // yield next;
  19. // console.log(4)
  20. // })
  21. // console.log(app.middleware)
  22. app.callback();
  23. //1
  24. //2

回形针的实现 koa compose

  1. compose: function (middleware) {
  2. return function (context, next) {
  3. // last called middleware #
  4. let index = -1
  5. return dispatch(0)
  6. function dispatch (i) {
  7. if (i <= index) return Promise.reject(new Error('next() called multiple times'))
  8. index = i
  9. const fn = middleware[i] || next
  10. if (!fn) return Promise.resolve()
  11. try {
  12. return Promise.resolve(fn(context, function next () {
  13. return dispatch(i + 1)
  14. }))
  15. } catch (err) {
  16. return Promise.reject(err)
  17. }
  18. }
  19. }
  20. }

其实就是compose([f1, f2, …, fn])转化为f1(…f(n-1)(fn(noop()))),最终的返回值是一个function (context, next) {}。

1)最终的返回值是function (context, next) {},递归函数。

2)递归,从dispatch(0)开始,

i = 0

  1. fn == middleware[i]

然后

  1. fn(context, function next () {
  2. return dispatch(i + 1)
  3. })

此处是执行第一个中间件,如果成功的化,就继续执行第二个中间件。其他依次类推,直至执行完所有的中间件。

结合这个中间件

  1. app.use((ctx, next) => {
  2. console.log(1)
  3. // before
  4. return next().then(() => {
  5. // after
  6. console.log(2)
  7. })
  8. })

实际上,

  1. var value = fn(context, function next () {
  2. return dispatch(i + 1)
  3. })
  4. return Promise.resolve(value)

我们知道Promise.resolve()返回的是Promise对象,它可以thenable的。

举个多个中间件的例子

  1. app.use((ctx, next) => {
  2. console.log(1)
  3. // before
  4. return next().then(() => {
  5. // after
  6. console.log(2)
  7. })
  8. })
  9. app.use((ctx, next) => {
  10. console.log(3)
  11. // before
  12. return next().then(() => {
  13. // after
  14. console.log(4)
  15. })
  16. })

实际上

  1. var value = fn(context, function next () {
  2. var dispatch_1_value = fn(context, function next () {
  3. return dispatch(2)
  4. })
  5. return Promise.resolve(dispatch_1_value)
  6. })
  7. return Promise.resolve(value)

如果dispatch_1_value有then,那么肯定有些执行,即打印出4,然后执行

  1. return Promise.resolve(value)

此时,还有个then,即打印出2.

大致可以这样理解

  1. function(ctx, next){
  2. console.log(1)
  3. reutrn Promise.resolve(
  4. console.log('3')
  5. return Promise.resolve(
  6. )
  7. .then(function(){
  8. console.log('4')
  9. })
  10. )
  11. .then(function(){
  12. console.log(2)
  13. })
  14. }

至此,1342有完美的解决了。

v3 app2

此时需要修改app为app2

  1. 'use strict'
  2. const co = require('co');
  3. const debug = require('debug')('v1')
  4. const convert = require('koa-convert')
  5. const isGeneratorFunction = require('is-generator-function');
  6. module.exports = {
  7. middleware :[],
  8. use: function (fn) {
  9. if (isGeneratorFunction(fn)) {
  10. console.log('Support for generators will been removed in v3. ' +
  11. 'See the documentation for examples of how to convert old middleware ' +
  12. 'https://github.com/koajs/koa/tree/v2.x#old-signature-middleware-v1x');
  13. fn = convert(fn);
  14. }
  15. this.middleware.push(fn);
  16. return this;
  17. },
  18. compose: function (middleware) {
  19. return function (context, next) {
  20. // last called middleware #
  21. let index = -1
  22. return dispatch(0)
  23. function dispatch (i) {
  24. if (i <= index) return Promise.reject(new Error('next() called multiple times'))
  25. index = i
  26. const fn = middleware[i] || next
  27. if (!fn) return Promise.resolve()
  28. try {
  29. return Promise.resolve(fn(context, function next () {
  30. return dispatch(i + 1)
  31. }))
  32. } catch (err) {
  33. return Promise.reject(err)
  34. }
  35. }
  36. }
  37. },
  38. callback: function () {
  39. const fn = this.compose(this.middleware);
  40. debug('callback compose fn = ' + fn)
  41. var ctx = {
  42. }
  43. fn(ctx).then(function(){
  44. })
  45. }
  46. }

主要变化是

  1. use: function (fn) {
  2. if (isGeneratorFunction(fn)) {
  3. console.log('Support for generators will been removed in v3. ' +
  4. 'See the documentation for examples of how to convert old middleware ' +
  5. 'https://github.com/koajs/koa/tree/v2.x#old-signature-middleware-v1x');
  6. fn = convert(fn);
  7. }
  8. this.middleware.push(fn);
  9. return this;
  10. },

中的

  1. if (isGeneratorFunction(fn)) {
  2. console.log('Support for generators will been removed in v3. ' +
  3. 'See the documentation for examples of how to convert old middleware ' +
  4. 'https://github.com/koajs/koa/tree/v2.x#old-signature-middleware-v1x');
  5. fn = convert(fn);
  6. }

如果fn是GeneratorFunction(koa 1.x中间件),需要通过convert转换一下,转成类似的

  1. const converted = function (ctx, next) {
  2. return co.call(ctx, mw.call(ctx, createGenerator(next)))
  3. }

这样就保证了,所有的compose里的

从koa 1.x到koa 2.x的演变

有人问,为什么你的例子都是koa 1.x的,而你推荐使用的koa 2.x?不要着急,听我慢慢道来。

koa 1.x

其实就是compose([f1, f2, …, fn])转化为fn(…f2(f1(noop()))),最终的返回值是一个generator function。

koa 2.x

其实就是compose([f1, f2, …, fn])转化为f1(…f(n-1)(fn(noop()))),最终的返回值是一个function (context, next) {}。

如果是function *(next){}也会被转换成成function (context, next) {},这就是convert做的向后1.x兼容。

  • function (context, next) {} 是commonfunction,因为最终返回值也是它,所以称为common也不过分
  • co.wrap(function *(ctx, next){})其实是commonfunction一样的,co的wrap就是执行有参数的generator,而参数也是ctx和next,所以也是天生支持的。

那么剩下的就是async函数了。

  1. // 定义
  2. async function middleware(context, next) {
  3. await next()
  4. }
  5. // 调用
  6. middleware(context, next);

从这段可知,async函数定义和普通函数定义执行都是一样的(除了需要es7-stage-3特性编译),所以无缝集成,唯一要说明的就是next,上文讲next实际是Promise对象,在想await关键字,是不是有点恍然大悟了?

绝配啊。

koa源码说明

核心源码

  • koa 1.x用的是compose 2.4+,注意无convert
  • koa 2.x用的是compose 3.1+,使用convert把generatorfunction转成commonfunction,以便后续的compose。

为啥2.x要多出个ctx?

@jonathanong

这样变化的最主要的原因是,在你写koa apps 时使用async箭头函数的时候:

  1. app.use(async (ctx, next) => {
  2. await next()
  3. })

这种情况下,使用this是万万不可能的。

因为 Arrow Function是 Lexical scoping(定义时绑定), this指向定义Arrow Function时外围, 而不是运行时的对象。

引用koa 1.x和2.x的差异根源

1.x

  1. var koa = require('koa');
  2. var app = koa();

2.x

  1. const Koa = require('koa');
  2. const app = new Koa();

源码

1.x

  1. /**
  2. * Application prototype.
  3. */
  4. var app = Application.prototype;
  5. /**
  6. * Expose `Application`.
  7. */
  8. module.exports = Application;
  9. /**
  10. * Initialize a new `Application`.
  11. *
  12. * @api public
  13. */
  14. function Application() {
  15. if (!(this instanceof Application)) return new Application;
  16. this.env = process.env.NODE_ENV || 'development';
  17. this.subdomainOffset = 2;
  18. this.middleware = [];
  19. this.proxy = false;
  20. this.context = Object.create(context);
  21. this.request = Object.create(request);
  22. this.response = Object.create(response);
  23. }

2.x

  1. /**
  2. * Expose `Application` class.
  3. * Inherits from `Emitter.prototype`.
  4. */
  5. module.exports = class Application extends Emitter {
  6. /**
  7. * Initialize a new `Application`.
  8. *
  9. * @api public
  10. */
  11. constructor() {
  12. super();
  13. this.proxy = false;
  14. this.middleware = [];
  15. this.subdomainOffset = 2;
  16. this.env = process.env.NODE_ENV || 'development';
  17. this.context = Object.create(context);
  18. this.request = Object.create(request);
  19. this.response = Object.create(response);
  20. }
  21. }

很明显,1.x是函数,而2.x是类,需要new来实例化。

整个Koa2.x里只有application做了类化,其他的还是保持之前的风格,大概还没有到必须修改的时候吧。

常用中间件

根据中间件在整个http处理流程的位置,将中间件大致分为3类:

  1. Pre-Request 通常用来改写request的原始数据
  2. Request/Response 大部分中间件都在这里,功能各异
  3. Post-Response 全局异常处理,改写response数据等

练习koa用法,集成以下中间件

  1. "koa": "^2.0.0",
  2. "koa-compress": "^2.0.0",
  3. "koa-conditional-get": "^2.0.0",
  4. "koa-etag": "^3.0.0",
  5. "koa-favicon": "^2.0.0",
  6. "koa-static": "^3.0.0",

app.js

  1. var serve = require('koa-static');
  2. var Koa = require('koa');
  3. var app = new Koa();
  4. var favicon = require('koa-favicon');
  5. var compress = require('koa-compress')
  6. var conditional = require('koa-conditional-get');
  7. var etag = require('koa-etag');
  8. app.use(compress({
  9. filter: function (content_type) {
  10. return /text/i.test(content_type)
  11. },
  12. threshold: 2048,
  13. flush: require('zlib').Z_SYNC_FLUSH
  14. }))
  15. app.use(favicon(__dirname + '/public/favicon.ico'));
  16. // etag works together with conditional-get
  17. app.use(conditional());
  18. app.use(etag());
  19. // or use absolute paths
  20. app.use(serve(__dirname + '/dist'));
  21. app.listen(9090);
  22. console.log('listening on port 9090');