Express Koa Redux 中间件原理分析 最早接触中间件,是在第一次使用 express 框架的时候,那时对中间件的作用有了了解,后面再使用 Koa 框架时,也遇到了中间件,最大的使用感受就是中间件的参数不一样了,可以使用await
语法来执行next()
函数。几乎于此同时,使用的 Redux 也提出了中间件的概念。中间件的概念被广泛的采用,一定有其优势所在。 本篇文章尝试分析和比较 Express Koa2 Redux 这三个框架的实现原理。
调试这三个框架的源码其实非常简单。
书写一个 js 文件,编写代码。 使用 vscode 的 debug 功能启动代码。 使用 curl 请求 localhost。 Express 的中间件 express 是内置路由功能的,并且同时内置了static
,json
,urlencoded
这三个中间件。
下面是启动一个 express app 并注册 App-Level middleware 的代码。通过这段代码来分析 express 启动和中间件运行流程。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 const express = require ("express" );const app = express ();app.use (function middlewareA (req, res, next ) { console .log ("1" ); next (); console .log ("2" ); }); app.use (function middlewareB (req, res, next ) { console .log ("3" ); next (); console .log ("4" ); }); app.use (function middlewareC (req, res, next ) { res.send ("hello world" ); }); app.listen (3000 );
以上代码返回了一个 express 实例,不过这个实例,是一个挂载了很多属性的函数。下面是express()
的调用,在源码中就是createApplication()
的调用。如下是源码部分。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 function createApplication ( ) { var app = function (req, res, next ) { app.handle (req, res, next); }; mixin (app, EventEmitter .prototype , false ); mixin (app, proto, false ); app.request = Object .create (req, { app : { configurable : true , enumerable : true , writable : true , value : app }, }); app.response = Object .create (res, { app : { configurable : true , enumerable : true , writable : true , value : app }, }); app.init (); return app; }
app 通过 mixin 的方式挂载了很多的方法,其中就包含了 use
和 listen
方法。他们的源码如下。精简了一下代码。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 app.use = function use (fn ) { var path = "/" ; this .lazyrouter (); var router = this ._router ; fns.forEach (function (fn ) { if (!fn || !fn.handle || !fn.set ) { return router.use (path, fn); } }, this ); return this ; };
从上面可以看到,router 很重要,即使是 App-Level 中间件,也是挂载到 router 上的。下面看一下 router 的代码。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 proto.use = function use (fn ) { var offset = 0 ; var path = "/" ; if (typeof fn !== "function" ) { var arg = fn; while (Array .isArray (arg) && arg.length !== 0 ) { arg = arg[0 ]; } if (typeof arg !== "function" ) { offset = 1 ; path = fn; } } var callbacks = flatten (slice.call (arguments , offset)); for (var i = 0 ; i < callbacks.length ; i++) { var fn = callbacks[i]; var layer = new Layer ( path, { sensitive : this .caseSensitive , strict : false , end : false , }, fn ); layer.route = undefined ; this .stack .push (layer); } return this ; };
上面 router 的代码,就是最终实现了 app.use
方法保存中间件函数的逻辑。如果断点调试,会发现一个有趣的事实,就是在我们的 middlewareA
中间件之前,express 已经加入了两个默认的 layer。如下图
下面再来看下程序运行并收到请求后的执行流程。首先是 app.listen
,通过 http.createServer 构造了一个 server 实例,实例的回调函数,就是 app 本身,这种写法也是非常骚了,把一个 app 函数玩弄于股掌之间。
1 2 3 4 app.listen = function listen ( ) { var server = http.createServer (this ); return server.listen .apply (server, arguments ); };
从 createApplication
函数中可以看到,app 函数本身,就是调用了 app.handle
,而 app.handle
又是调用了router.handle
囧。所以我们的每个请求,其实最终就是由 router.handle
来处理了,并且会在这里展开对所有中间件的调用。 这个代码很长,我们精简一下。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 proto.handle = function handle (req, res, out ) { var self = this ; var idx = 0 ; var protohost = getProtohost (req.url ) || "" ; var removed = "" ; var slashAdded = false ; var paramcalled = {}; var options = []; var stack = self.stack ; var done = restore (out, req, "baseUrl" , "next" , "params" ); req.next = next; next (); function next (err ) { var layerError = err === "route" ? null : err; if (layerError === "router" ) { setImmediate (done, null ); return ; } if (idx >= stack.length ) { setImmediate (done, layerError); return ; } var path = getPathname (req); if (path == null ) { return done (layerError); } var layer; var match; var route; while (match !== true && idx < stack.length ) { layer = stack[idx++]; match = matchLayer (layer, path); route = layer.route ; if (typeof match !== "boolean" ) { layerError = layerError || match; } if (match !== true ) { continue ; } if (!route) { continue ; } if (layerError) { match = false ; continue ; } var method = req.method ; var has_method = route._handles_method (method); if (!has_method && method === "OPTIONS" ) { appendMethods (options, route._options ()); } if (!has_method && method !== "HEAD" ) { match = false ; continue ; } } if (match !== true ) { return done (layerError); } if (route) { req.route = route; } req.params = self.mergeParams ? mergeParams (layer.params , parentParams) : layer.params ; var layerPath = layer.path ; self.process_params (layer, paramcalled, req, res, function (err ) { if (err) { return next (layerError || err); } if (route) { return layer.handle_request (req, res, next); } trim_prefix (layer, layerError, layerPath, path); }); } if (layerError) { layer.handle_error (layerError, req, res, next); } else { layer.handle_request (req, res, next); } } };
可以看到,关键的就是 next 函数了,next 函数引用了一个外部变量 idx ,形成了一个包含 idx 的闭包,next 函数每次执行,都会找到下一个需要执行的中间件(layer), 然后执行 layer.handle_request(req, res, next);
, 在执行的过程中,将 next 函数本身作为参数传递,这样当中间件执行 next 的时候,又会重复上述过程。通过 next 的执行,串起了一个一个中间件。
Express 中间件的总结 第一段代码的输出为 1 3 4 2
, 其实也可以说 express 的中间件执行顺序类似与洋葱圈模型。但是,express 结束请求,返回响应的标志是 res.send
,也就是说一旦调用了 res.send
,那么响应也就结束了,虽然后序的代码仍然会继续执行。但是已经影响不到响应了。这也是和 koa 的一个重要区别。
另一个要说明的就是,express 相对于 koa,内置了路由系统,甚至中间件也是挂载在路由上的,因此源代码比 koa 更加复杂。express 成型的时间比较早,内部的写法还都是函数,也没有明显的使用 promise async await 等先进的特性。
参考