中间件

核心内容

  • 什么是中间件(1.x 和 2.x)
  • 理解回形针原理
  • 理解中间件原理,co + compose + convert源码解读
  • 如何自定义中间件?
  • 常用中间件

从最简单的http server开始

  1. var http = require('http');
  2. http.createServer(function(request,response){
  3. console.log(request);
  4. response.end('Hello world!');
  5. }).listen(8888);

这就是最简单的实现

如果要是根据不同url去处理呢?

  1. var http = require('http');
  2. http.createServer(function(req, res){
  3. console.log(req);
  4. if(req.url =='/'){
  5. res.end('Hello world!');
  6. }else if(req.url =='/2'){
  7. res.end('Hello world!2');
  8. }else{
  9. res.end('Hello world! other');
  10. }
  11. }).listen(8888);

如果成百上千、甚至更多http服务呢?这样写起来是不是太low了,肯定要封装一下,让它更简单。

Express框架的底层Connect就是这样的一个框架,Koa实际上也提供类似机制,下面我们看一下

我们用http和koa 1.x写一个同样功能的demo

  1. var http = require('http');
  2. var koa = require('koa');
  3. var app = koa();
  4. app.use(function *(next){
  5. if(this.url =='/'){
  6. this.body = 'Hello world!'
  7. }else if(this.url =='/2'){
  8. this.body = 'Hello world!2'
  9. }else{
  10. this.body = 'Hello world! other'
  11. }
  12. })
  13. var server = http.createServer(app.callback());
  14. server.listen(8888);

对比一下,koa的demo里

  1. http.createServer(app.callback())

和http示例里的

  1. http.createServer(function(req, res){
  2. console.log(req);
  3. if(req.url =='/'){
  4. res.end('Hello world!');
  5. }else if(req.url =='/2'){
  6. res.end('Hello world!2');
  7. }else{
  8. res.end('Hello world! other');
  9. }
  10. })

很明显,koa把http.createServer里的内容给封装到app.callback()里了。

那么app.callback()里到底有啥呢?

  • app是koa对象
  • app.use里使用generator函数
  • app.callback()把app.use里的内容转成function(req, res){...}

给出服务器启动流程图

Server Start

看到此处,你是否能够理解什么是中间件了呢?

什么是中间件?

中间件是Application提供请求处理的扩展机制,主要抽象HTTP协议里的request、response

如果把一个http处理过程比作是污水处理,中间件就像是一层层的过滤网。每个中间件在http处理过程中通过改写request或(和)response的数据、状态,实现了特定的功能。

http协议是无状态协议,所以http请求的过程可以这样理解,请求(request)过来,经过无数中间件拦截,直至响应(response)为止。

Koa 中间件

Koa目前主要分1.x版本和2.x版本,它们最主要的差异就在于中间件的写法,本小节会一一举例,并最后对比它们的异同,以便后面章节的源码分析。

日志中间件

Server Request Response

请求到达服务器后,依次经过各个中间件,直至被响应,所以整个流程

  • 请求到达log中间件,记录此时的时间
  • 放过,执行next
  • 执行到响应中间件,返回ctx.body
  • 回到log中间件,根据当前时间,打印请求耗时
  • 把响应写到浏览器里

1.x

  1. var koa = require('koa');
  2. var app = koa();
  3. // logger
  4. app.use(function *(next){
  5. var start = new Date;
  6. yield next;
  7. var ms = new Date - start;
  8. console.log('%s %s - %s', this.method, this.url, ms);
  9. });
  10. // response
  11. app.use(function *(){
  12. this.body = 'Hello World';
  13. });
  14. app.listen(3000);

2.x

Koa 2.x是一个现代的中间件框架,它支持3种不同类型函数的中间件:

  • common function 最常见的,也称modern middleware
  • generatorFunction 生成器函数,就是yield *那个
  • async function 最潮的es7 stage-3特性 async 函数,异步终极大杀器

common function

先看一下common function

  1. const Koa = require('koa');
  2. const app = new Koa();
  3. // logger
  4. app.use((ctx, next) => {
  5. const start = new Date();
  6. return next().then(() => {
  7. const ms = new Date() - start;
  8. console.log(`${ctx.method} ${ctx.url} - ${ms}ms`);
  9. });
  10. });
  11. // response
  12. app.use(ctx => {
  13. ctx.body = 'Hello Koa';
  14. });
  15. app.listen(3000);

中间件定义

  1. (ctx, next) => {
  2. }
  3. 等同于
  4. function (ctx, next){
  5. }

和1.x相比较

  • 这是普通的函数,不是generator函数。
  • 2.x中间件的参数是2个,而1.x是1个,主要是上下文在2.x里显示声明了。可以简单的理解1.x里的this就是2.x里的ctx,它们的api是一模一样的。
  • 2.x中间件内部,使用promise处理,而1.x是通过yield处理

它不限于Node.js版本,只要4.x以上都可以(Koa 2.x的中间件最终还是会转成generator),而且是支持Promise,所以整体来说,这种方式是最容易上手的,难度系数是3种中间件里最低的。如果有express经验,可以考虑从这种方式入手,学起来更简单。

generatorFunction

Koa设计初衷就是基于generator来构建更好的流程控制的web框架,所以在Koa 2.x里也支持generator,但和1.x稍有不同,先看代码,稍后会解释。

  1. const Koa = require('koa');
  2. const app = new Koa();
  3. // logger
  4. app.use(co.wrap(function *(ctx, next) {
  5. const start = new Date();
  6. yield next();
  7. const ms = new Date() - start;
  8. console.log(`${ctx.method} ${ctx.url} - ${ms}ms`);
  9. }));
  10. // response
  11. app.use(ctx => {
  12. ctx.body = 'Hello Koa';
  13. });
  14. app.listen(3000);

对比1.x

  • 2.x中间件generatorFunction被co.wrap包裹了,被转换成function *(){}
  • 2.x中间件generatorFunction的参数多了ctx,和上面common function说的一样,上下文ctx被显示声明了,统一写法
  • 2.x中间件generatorFunction的跳到下一个中间件是yield next();,而不是1.x用的yield next;

它和上面common function一样,不限于Node.js版本,只要4.x以上都可以(Koa 2.x的中间件最终还是会转成generator),2.x中间件generatorFunction这种方式是唯一和Koa 1.x最像的方式,可以说是从1.x迁移到2.x的最佳手段。

同样是generator的好处是,可以在generator里进行yieldable操作,对于熟悉1.x的读者来说,是非常好的。

还得爽爽的yieldable么?

async函数

Koa 之所以从1.x升级到2.x,可以说async函数居功不小。async函数是es7里stage-3里的特性,可以非常好的解决异步问题,相当于更高级的generator(注:babel或regenerator就是用generator实现的async)。

async函数的好处

  • 无需执行器
  • await直接异步Promise方法

下面给出async函数中间件用法

  1. const Koa = require('koa');
  2. const app = new Koa();
  3. // logger
  4. app.use(async (ctx, next) => {
  5. const start = new Date();
  6. await next();
  7. const ms = new Date() - start;
  8. console.log(`${ctx.method} ${ctx.url} - ${ms}ms`);
  9. });
  10. // response
  11. app.use(ctx => {
  12. ctx.body = 'Hello Koa';
  13. });
  14. app.listen(3000);

对比一下

  • async函数除了多一个async关键字外,和普通函数无异,同样支持箭头函数(generator是不支持箭头函数写法的)。
  • 与async函数搭配的await通过对接promise方法,没有yield的Yieldable强大,但也足够用的
  • 2.x中间件async函数跳到下一个中间件是await next();,和commonfunction里的return next()类似

总结一下,从形式讲,async函数无疑是所有中间件中最耀眼的那个,语言清楚,简洁,结合await关键字,可以非常好的正好Promise资源。它虽好,可是执行的时候却很麻烦,目前Node.js里还没有原生支持async函数,所以只能借助于babel这样编译器工具来完成代码转换。

Koa 2.x迟迟没有发布2.0正式版原因就是Node.js没有原生支持async函数,但我们都非常期待使用这样好的特性。目前Chrome已经实现了async函数,所以Node.js实现async函数也会非常快的,预计是10月份之前,只要性能不是太差,大家都切过去吧。

为啥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

  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做了类化,其他的还是保持之前的风格,大概还没有到必须修改的时候吧。

实例与原理

中间件简介 - 图3

看一下解析流程

中间件简介 - 图4

中间件定义

中间件定义

  1. // m1
  2. app.use((ctx, next) => {
  3. console.log('第1个中间件before 1')
  4. return next().then(() => {
  5. // after
  6. console.log('第1个中间件after 2')
  7. })
  8. });

完整代码

  1. const Koa = require('koa');
  2. const app = new Koa();
  3. var co = require('co')
  4. // m1
  5. app.use((ctx, next) => {
  6. console.log('第1个中间件before 1')
  7. return next().then(() => {
  8. // after
  9. console.log('第1个中间件after 2')
  10. })
  11. });
  12. // response
  13. app.use(ctx => {
  14. console.log('业务逻辑处理')
  15. return ctx.body = {
  16. data: {},
  17. status: {
  18. code : 0,
  19. msg :'sucess'
  20. }
  21. }
  22. });
  23. app.listen(3000);

执行

  1. $ nodemon koa-core/middleware/app.js
  2. [nodemon] 1.9.1
  3. [nodemon] to restart at any time, enter `rs`
  4. [nodemon] watching: *.*
  5. [nodemon] starting `node koa-core/middleware/app.js`
  6. 1个中间件before 1
  7. 业务逻辑处理
  8. 1个中间件after 2

我们可以看看

  • 1 是中间件具体处理的前置位置
  • 2 是中间件,before中间件依次向下处理,业务逻辑处理后,after中间件向上执行到回形针底部,再次返回到该中间件,2

多个中间件例子

定义中间件

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

这里定义m1和m2中间件,

具体代码

  1. const Koa = require('koa');
  2. const app = new Koa();
  3. // m1
  4. app.use((ctx, next) => {
  5. console.log('第1个中间件before 1')
  6. return next().then(() => {
  7. // after
  8. console.log('第1个中间件after 2')
  9. })
  10. });
  11. // m2
  12. app.use((ctx, next) => {
  13. console.log('第2个中间件before 3')
  14. return next().then(() => {
  15. // after
  16. console.log('第2个中间件after 4')
  17. })
  18. });
  19. // response
  20. app.use(ctx => {
  21. return ctx.body = {
  22. data: {},
  23. status: {
  24. code : 0,
  25. msg :'sucess'
  26. }
  27. }
  28. });
  29. app.listen(3000);

执行结果

  1. $ nodemon koa-core/middleware/app.js
  2. 1个中间件before 1
  3. 2个中间件before 3
  4. 业务逻辑处理
  5. 2个中间件after 4
  6. 1个中间件after 2

说明

  • before按照中间件顺序依次向下处理
  • 业务逻辑处理
  • after按照中间件想法顺序向上执行

如果中间件里调用中间件呢?