中间件原理

你好奇为什么是1342么?

从generator说起

book-source/koa-core/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简化一下代码

book-source/koa-core/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

book-source/koa-core/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呢?

中间件写法

book-source/koa-core/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

book-source/koa-core/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. // 所以运行结果为:alert(4);
  11. // 注意:js 中的函数其实是对象,函数名是对 Function对象的引用。

那么

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

4.3) yield *

还记得

book-source/koa-core/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 1.x的,而你推荐使用的koa 2.x?不要着急,听我慢慢道来。

  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. }

测试

  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. })

回形针的实现 koa compose

  1. function compose (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. }

版本说明

  • koa 1.x用的是compose 2.4+
  • koa 2.x用的是compose 3.1+

常用中间件

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

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