《何为 connect 中间件》

  1. 理解中间件的概念
  2. 了解 Connect 的实现

课程内容

  1. 原生 httpServer 遇到的问题
  2. 中间件思想
  3. Connect 实现
  4. Express 简介

HTTP

Nodejs 的经典 httpServer 代码

里面的函数 就是所有http请求的响应函数,即所有的请求都经过这个函数的处理,是所有请求的入口函数。

通过 requestHandler 函数我们能写一些简单的 http 逻辑,比如上面的例子,所有请求都返回 hello visitor!

然而,我们的业务逻辑不可能这么简单。例如:需要实现一个接口,要做的是当请求过来时,先判断来源的请求是否包含请求体,然后判断请求体中的id是不是在数据库中存在,最后若存在则返回true,不存在则返回false。

  1. 1. 检测请求中请求体是否存在,若存在则解析请求体;
  2. 1. 查看请求体中的id是否存在,若存在则去数据库查询;
  3. 1. 根据数据库结果返回约定的值;

我们首先想到的,抽离函数,每个逻辑一个函数,简单好实现低耦合好维护。

实现代码:

  1. function parseBody(req, callback) {
  2. //根据http协议从req中解析body
  3. callback(null, body);
  4. }
  5. function checkIdInDatabase(body, callback) {
  6. //根据body.id在Database中检测,返回结果
  7. callback(null, dbResult);
  8. }
  9. function returnResult(dbResult, res) {
  10. if (dbResult && dbResult.length > 0) {
  11. res.end('true');
  12. } else {
  13. res.end('false')
  14. }
  15. }
  16. function requestHandler(req, res) {
  17. parseBody(req, function(err, body) {
  18. checkIdInDatabase(body, function(err, dbResult) {
  19. returnResult(dbResult, res);
  20. });
  21. });
  22. }

上面的解决方案解决了包含三个步骤的业务问题,出现了3个 }); 还有3个 err 需要处理,上面的写法可以得达到预期效果。

然而,业务逻辑越来越复杂,会出发展成30个回调逻辑,那么就出现了30个 }); 及30个 err异常。更严重的是,到时候写代码根本看不清自己写的逻辑在30层中的哪一层,极其容易出现 多次返回 或返回地方不对等问题,这就是 回调金字塔 问题了。

大多数同学应该能想到解决回调金字塔的办法,朴灵的《深入浅出Node.js》里讲到的三种方法。下面列举了这三种方法加上ES6新增的Generator,共四种解决办法。

  • —— 事件发布订阅模式(第四课讲到)
  • BlueBird —— Promise方案(第十七课讲到)
  • —— 异步流程控制库(第五课讲到)
  • Generator —— ES6原生Generator

理论上,这四种都能解决回调金字塔问题。而Connect和Express用的是 类似异步流程控制的思想


关于异步流程控制库下面简要介绍下,或移步@第五课
异步流程控制库首先要求用户传入待执行的函数列表,记为funlist。流程控制库的任务是让这些函数 顺序执行

callback是控制顺序执行的关键,funlist里的函数每当调用callback会执行下一个funlist里的函数

我们动手实现一个类似的链式调用,其中 funlist 更名为 middlewarescallback 更名为 next,码如下:

  1. var middlewares = [
  2. function fun1(req, res, next) {
  3. parseBody(req, function(err, body) {
  4. if (err) return next(err);
  5. req.body = body;
  6. next();
  7. });
  8. },
  9. function fun2(req, res, next) {
  10. checkIdInDatabase(req.body.id, function(err, rows) {
  11. res.dbResult = rows;
  12. next();
  13. });
  14. },
  15. function fun3(req, res, next) {
  16. if (res.dbResult && res.dbResult.length > 0) {
  17. res.end('true');
  18. }
  19. else {
  20. res.end('false');
  21. }
  22. next();
  23. }
  24. function requestHandler(req, res) {
  25. var i=0;
  26. //由middlewares链式调用
  27. function next(err) {
  28. if (err) {
  29. return res.end('error:', err.toString());
  30. }
  31. if (i<middlewares.length) {
  32. middlewares[i++](req, res, next);
  33. } else {
  34. return ;
  35. }
  36. }
  37. //触发第一个middleware
  38. next();
  39. }

上面用middlewares+next完成了业务逻辑的 链式调用,而middlewares里的每个函数,都是一个 中间件

整体思路是:

  1. 将所有 处理逻辑函数(中间件) 存储在一个list中;
  2. 请求到达时 循环调用 list中的 处理逻辑函数(中间件)

Connect的实现

Connect的思想跟上面阐述的思想基本一样,先将处理逻辑存起来,然后循环调用。

Connect中主要有五个函数
PS: Connect的核心代码是200+行,建议对照看下面的函数介绍。

createServer()

输入:

执行过程:

  1. app是一个函数对象(包含handle方法)
  2. app具有Event所有属性(详见utils-merge,十行代码)
  3. app有route属性(路由)、和stack属性(用于存储中间件,类似上面的)

输出:

  1. app is function(req, res, next) {...};
  2. |
  3. +---+---+
  4. | has |
  5. route stack

app.use(route, fn)

作用是向stack中添加 逻辑处理函数 (中间件)。

输入:

  1. route 可省略,默认’/‘
  2. fn 具体的业务处理逻辑

tips:

上面的fn表示处理逻辑,它可以是

  1. 一个普通的 function(req,res[,next]){}
  2. 一个httpServer
  3. 另一个connect的app对象(sub app特性);

如何将这三种分别转换为 function(req, res, next) {}的形式呢?

  1. 不用转换;
  2. httpServer的定义是“对事件’request’后handler的对象”,我们可以从httpServer.listeners(‘request’)中得到这个函数;
  3. 另一个connect对象,而connect()返回的app就是function(req, res, out) {};

执行过程:

  1. 将三种处理逻辑统一转换为function(req,res,next){}的形式表示。
  2. 把这个处理逻辑与route一起,放入stack中(存储处理逻辑,route用来匹配路由)

核心代码片段

返回:

  1. //返回自己,可以完成链式调用
  2. return this;

总结::

  1. var app = connect();
  2. app.use('/api', function(req, res, next) {});

等价于

  1. var app = connect();
  2. app.stack.push({route: '/api', handle: function(req, res, next) {}});

最后,app.stack里 顺序存储 了所有的 逻辑处理函数 (中间件)。

  1. app.stack = [function1, function2, function3, ... function30];

这个函数就是请求到达时,负责 顺序调用 我们存储在stack中的 逻辑处理函数 (中间件)函数,类似上一章的requestHandler。

输入:

  1. req是Nodejs本身的可读流,不做过多介绍
  2. out是为了Connect的 sub app特性 而设计的参数,这个特性可以暂时忽略,这个参数我们暂时不关心

处理过程:

可以回头看一下上面的,handle的实现是这个函数的增强版

  1. 取得stack(存储逻辑处理函数列表),index(列表下标)
  2. 构建next函数,next的作用是执行下一个逻辑处理函数
  3. 触发第一个next,触发链式调用

next函数实现:

next函数实现在handle函数体内,用来顺序执行处理逻辑,它是异步流程控制库的核心,不明白它的作用请看上面的异步流程控制库简介

path是请求路径,route是逻辑处理函数自带的属性。

  1. 取得下一个逻辑处理函数;
  2. 若路由不匹配,跳过此逻辑;
  3. 若路由匹配执行匹配到的逻辑处理函数

tips: 跟上一章最后的代码一样,每个逻辑处理函数调用next来让后面的函数执行,存储在stack中的函数就实现了链式调用。不一定所有的函数都在返回的时候才调用next,为了不影响效率,有的函数可能先调用next,然而自己还没有返回,继续做自己的事情。

核心代码:

返回:

总结:

画图总结

  1. request come
  2. v
  3. middleware1 : 不匹配路由,skip
  4. |
  5. v
  6. middleware2 : 匹配路由,执行
  7. |
  8. v
  9. middleware3 : 匹配路由,执行
  10. |
  11. v
  12. middleware4 : 不匹配路由,skip
  13. |
  14. v
  15. end

call(handle, route, err, req, res, next)

Connect中规定function(err, req, res, next) {}形式为错误处理函数,function(req, res, next) {}为正常的业务逻辑处理函数。那么,可以根据Function.length以判断它是否为错误处理函数。

输入:

处理过程:

  1. 是否有错误,本次handle是否是错误处理函数;
  2. 若有错误且handle为错误处理函数,则执行handle,本函数返回;
  3. 若没错误且handle不是错误处理函数,则执行handle,本函数返回;
  4. 如果上面两个都不满足,不执行handle,本函数调用next,返回;

返回:

总结:

call函数是一个执行者,根据当前错误情况handle类型决定是否执行当前的handle。

listen

创建一个httpServer,将Connect自己的业务逻辑作为requestHandler,监听端口

代码

  1. var server = http.createServer(this);
  2. return server.listen.apply(server, arguments);

图解Connect

Connect将中间件存储在app.stack中,通过构造handle中的next函数在请求到来时依次调用这些中间件。

图形总结

  1. request app(out)
  2. | yes
  3. +------------------>match?----->middleware1
  4. | no |
  5. v |
  6. next<----------+
  7. |
  8. v yes
  9. match?------>middleware2
  10. | no |
  11. v |
  12. next<----------+
  13. |
  14. v yes
  15. match?------>middleware3
  16. | no |
  17. v |
  18. out<-----------+
  19. |
  20. +---------------------+
  21. |
  22. v
  23. end(response在处理过程中已经返回了)

我们再看看Connect是怎么实现subapp的,比较有趣。

什么是subapp?

  1. var sub_app = connect();
  2. var app = connect();
  3. app.use('/route1', sub_app);
  4. // request path: '/route1/route2'
  5. // 由app接收到请求后,切割 path为'/route2'转交给sub_app的处理逻辑处理
  6. // 再由sub_app返回到app,由app继续向下执行处理逻辑

结合上面的函数画图

完成上面的sub_app只需要做到两点:

  1. 从app的调用链进入到sub_app的调用链中;
  2. 从sub_app的逻辑回到app的调用链中;

connect在handle函数中的第三个参数out为这个特性实现提供可能。out的特点是在middlewares链式调用完成以后调用那么将app的next作为sub_app的out传入sub_app的handle中可以做到sub_app自己的业务逻辑处理完后调用out,即处理权回到了本app的next手里。

上面图中的sub_app.out2===app.next,所以能完成逻辑的交接和sub app调用。

大家都知道Express是Connect的升级版。

Express不只是Connect的升级版,它还封装了很多对象来方便业务逻辑处理。Express里的Router是Connect的升级版。

Express大概可以分为几个模块

简要介绍一下每个模块

Router

在Connect中间件特性的基础上,加入了如下特性,是Connect的升级版

  1. 正则匹配route;
  2. 进行将http的方法在route中分解开;

Request

在Request中集成了http.IncomingMessage(可读流+事件),并在其上增加了新的属性,方便使用,我们最常用的应该是
req.param。

Response

在Response中集成了http.ServerResponse(可写流+事件),并在其上增加了很多方便返回的函数,有我们熟悉的res.json、
res.render、res.redirect、res.sendFile等等。

我们可以拓展它写一个res.sendPersonInfoById。

关于流的题外话:req.pipe(res)的形式可以“完成发什么就返回什么”,而req.pipe(mylogic).pipe(res)可以添加自己的逻辑,
我们的业务逻辑是把流读为String/Object再进行逻辑处理,处理完再推送给另一个stream,有没有可能在流的层面进行逻辑解
耦提供服务呢?求大神解答了…至少这种写法在大流量、逻辑简单的情况下是有用的。

除了上面的三个模块以外,还需要有个地方存储整个app的属性、设置等。比较常用的是app.engine函数设置模板引擎。

Express小结

Express是一个中间件机制的httpServer框架,它本身实现了中间件机制,它也包含了中间件。比如3.x版本的Express
本身自带bodyParser、cookieSession等中间件,而在4.x中去掉了。包括TJ也写了很多中间件,比如node-querystring、
connect-redis等。

实现业务逻辑解耦时,中间件是从纵向的方面进行的逻辑分解,前面的中间件处理的结果可以给后面用,比如bodyParser把解析
body的结果放在req.body中,后面的逻辑都可以从req.body中取值。由于中间件是顺序执行的,errHandler一般都放在最后,而log类的中间件则放在比较前面。

总结

Connect用流程控制库的回调函数及中间件的思想来解耦回调逻辑;
Koa用Generator方法解决回调问题;

我们应该也可以用事件、Promise的方式实现;