内容简介:如果你有最开始接触本文主要对比
如果你有 express
, koa
, redux
的使用经验,就会发现他们都有 中间件(middlewares)
的概念, 中间件
是一种拦截器的思想,用于在某个特定的输入输出之间添加一些额外处理,同时不影响原有操作。
最开始接触 中间件
是在服务端使用 express
和 koa
的时候,后来从服务端延伸到前端,看到其在 redux
的设计中也得到的极大的发挥。 中间件
的设计思想也为许多框架带来了灵活而强大的扩展性。
本文主要对比 redux
, koa
, express
的中间件实现,为了更直观,我会抽取出三者 中间件
相关的核心代码,精简化,写出模拟示例。示例会保持 express
, koa
, redux
的整体结构,尽量保持和源码一致,所以本文也会稍带讲解下 express
, koa
, redux
的整体结构和关键实现:
示例源码地址 , 可以一边看源码,一边读文章,欢迎star!
本文适合对express ,koa ,redux 都有一定了解和使用经验的开发者阅读
服务端的中间件
express
和 koa
的中间件是用于处理 http
请求和响应的,但是二者的设计思路确不尽相同。大部分人了解的 express
和 koa
的中间件差异在于:
-
express
采用“尾递归”方式,中间件一个接一个的顺序执行, 习惯于将response
响应写在最后一个中间件中; - 而
koa
的中间件支持generator
, 执行顺序是“洋葱圈”模型。
所谓的“洋葱圈”模型:
不过实际上, express
的中间件也可以形成“洋葱圈”模型,在 next
调用后写的代码同样会执行到,不过 express
中一般不会这么做,因为 express
的 response
一般在最后一个中间件,那么其它中间件 next()
后的代码已经影响不到最终响应结果了;
express
首先看一下 express 的实现:
入口
// express.js var proto = require('./application'); var mixin = require('merge-descriptors'); exports = module.exports = createApplication; function createApplication() { // app 同时是一个方法,作为http.createServer的处理函数 var app = function(req, res, next) { app.handle(req, res, next) } mixin(app, proto, false); return app } 复制代码
这里其实很简单,就是一个 createApplication
方法用于创建 express
实例,要注意返回值 app
既是实例对象,上面挂载了很多方法,同时它本身也是一个方法,作为 http.createServer
的处理函数, 具体代码在 application.js 中:
// application.js var http = require('http'); var flatten = require('array-flatten'); var app = exports = module.exports = {} app.listen = function listen() { var server = http.createServer(this) return server.listen.apply(server, arguments) } 复制代码
这里 app.listen
调用 nodejs
的 http.createServer
创建 web
服务,可以看到这里 var server = http.createServer(this)
其中 this
即 app
本身, 然后真正的处理程序即 app.handle
;
中间件处理
express
本质上就是一个中间件管理器,当进入到 app.handle
的时候就是对中间件进行执行的时候,所以,最关键的两个函数就是:
- app.handle 尾递归调用中间件处理 req 和 res
- app.use 添加中间件
全局维护一个 stack
数组用来存储所有中间件, app.use
的实现就很简单了,可以就是一行代码 ``
// app.use app.use = function(fn) { this.stack.push(fn) } 复制代码
express
的真正实现当然不会这么简单,它内置实现了路由功能,其中有 router
, route
, layer
三个关键的类,有了 router
就要对 path
进行分流, stack
中保存的是 layer
实例, app.use
方法实际调用的是 router
实例的 use
方法, 有兴趣的可以自行去阅读。
app.handle
即对 stack
数组进行处理
app.handle = function(req, res, callback) { var stack = this.stack; var idx = 0; function next(err) { if (idx >= stack.length) { callback('err') return; } var mid; while(idx < stack.length) { mid = stack[idx++]; mid(req, res, next); } } next() } 复制代码
这里就是所谓的"尾递归调用", next
方法不断的取出 stack
中的“中间件”函数进行调用,同时把 next
本身传递给“中间件”作为第三个参数,每个中间件约定的固定形式为 (req, res, next) => {}
, 这样每个“中间件“函数中只要调用 next
方法即可传递调用下一个中间件。
之所以说是”尾递归“是因为递归函数的最后一条语句是调用函数本身,所以每一个中间件的最后一条语句需要是 next()
才能形成”尾递归“,否则就是普通递归,”尾递归“相对于普通”递归“的好处在于节省内存空间,不会形成深度嵌套的函数调用栈。有兴趣的可以阅读下阮老师的尾调用优化
至此, express
的中间件实现就完成了。
koa
不得不说,相比较 express
而言, koa
的整体设计和代码实现显得更高级,更精炼;代码基于 ES6
实现,支持 generator(async await)
, 没有内置的路由实现和任何内置中间件, context
的设计也很是巧妙。
整体
一共只有4个文件:
- application.js 入口文件,koa应用实例的类
- context.js
ctx
实例,代理了很多request
和response
的属性和方法,作为全局对象传递 - request.js
koa
对原生req
对象的封装 - response.js
koa
对原生res
对象的封装
request.js
和 response.js
没什么可说的,任何 web 框架都会提供 req
和 res
的封装来简化处理。所以主要看一下 context.js
和 application.js
的实现
// context.js /** * Response delegation. */ delegate(proto, 'res') .method('setHeader') /** * Request delegation. */ delegate(proto, 'req') .access('url') .setter('href') .getter('ip'); 复制代码
context
就是这类代码,主要功能就是在做代理,使用了 delegate
库。
简单说一下这里代理的含义,比如 delegate(proto, 'res').method('setHeader')
这条语句的作用就是: 当调用proto.setHeader时,会调用proto.res.setHeader 即,将 proto
的 setHeader
方法代理到 proto
的 res
属性上,其它类似。
// application.js 中部分代码 constructor() { super() this.middleware = [] this.context = Object.create(context) } use(fn) { this.middleware.push(fn) } listen(...args) { debug('listen') const server = http.createServer(this.callback()); return server.listen(...args); } callback() { // 这里即中间件处理代码 const fn = compose(this.middleware); const handleRequest = (req, res) => { // ctx 是koa的精髓之一, req, res上的很多方法代理到了ctx上, 基于 ctx 很多问题处理更加方便 const ctx = this.createContext(req, res); return this.handleRequest(ctx, fn); }; return handleRequest; } handleRequest(ctx, fnMiddleware) { ctx.statusCode = 404; const onerror = err => ctx.onerror(err); const handleResponse = () => respond(ctx); return fnMiddleware(ctx).then(handleResponse).catch(onerror); } 复制代码
同样的在 listen
方法中创建 web
服务, 没有使用 express
那么绕的方式, const server = http.createServer(this.callback());
用 this.callback()
生成 web
服务的处理程序
callback
函数返回 handleRequest
, 所以真正的处理程序是 this.handleRequest(ctx, fn)
中间件处理
构造函数 constructor
中维护全局中间件数组 this.middleware
和全局的 this.context
实例(源码中还有request,response对象和一些其他辅助属性)。和 express
不同,因为没有 router
的实现,所有 this.middleware
中就是普通的”中间件“函数而非复杂的 layer
实例,
this.handleRequest(ctx, fn);
中 ctx
为第一个参数, fn = compose(this.middleware)
作为第二个参数, handleRequest
会调用 fnMiddleware(ctx).then(handleResponse).catch(onerror);
所以中间处理的关键在 compose
方法, 它是一个独立的包 koa-compose
, 把它拿了出来看一下里面的内容:
// compose.js 'use strict' module.exports = compose function compose (middleware) { return function (context, next) { let index = -1 return dispatch(0) function dispatch (i) { index = i let fn = middleware[i] if (i === middleware.length) fn = next if (!fn) return Promise.resolve() try { return Promise.resolve(fn(context, dispatch.bind(null, i + 1))); } catch (err) { return Promise.reject(err) } } } } 复制代码
和 express
中的 next
是不是很像,只不过他是 promise
形式的,因为要支持异步,所以理解起来就稍微麻烦点:每个 中间件
是一个 async (ctx, next) => {}
, 执行后返回的是一个 promise
, 第二个参数 next
的值为 dispatch.bind(null, i + 1)
, 用于传递”中间件“的执行,一个个中间件向里执行,直到最后一个中间件执行完, resolve
掉,它前一个”中间件“接着执行 await next()
后的代码,然后 resolve
掉,在不断向前直到第一个”中间件“ resolve
掉,最终使得最外层的 promise
resolve
掉。
这里和 express
很不同的一点就是 koa
的响应的处理并不在"中间件"中,而是在中间件执行完返回的 promise
resolve
后:
return fnMiddleware(ctx).then(handleResponse).catch(onerror);
通过 handleResponse
最后对响应做处理,”中间件“会设置 ctx.body
, handleResponse
也会主要处理 ctx.body
,所以 koa
的”洋葱圈“模型才会成立, await next()
后的代码也会影响到最后的响应。
至此,koa的中间件实现就完成了。
redux
不得不说, redux
的设计思想和源码实现真的是漂亮,整体代码量不多,网上已经随处可见 redux
的源码解析,我就不细说了。不过还是要推荐一波官网对中间件部分的叙述 :redux-middleware
这是我读过的最好的说明文档,没有之一,它清晰的说明了 redux middleware
的演化过程,漂亮地演绎了一场从 分析问题
到 解决问题
,并不断优化的思维过程。
总体
本文还是主要看一下它的中间件实现, 先简单说一下 redux
的核心处理逻辑, createStore 是其入口程序,工厂方法,返回一个 store
实例, store
实例的最关键的方法就是 dispatch , 而 dispatch
要做的就是一件事:
currentState = currentReducer(currentState, action)
即调用 reducer
, 传入当前 state
和 action
返回新的 state
。
所以要模拟基本的 redux
执行只要实现 createStore
, dispatch
方法即可。其它的内容如 bindActionCreators
, combineReducers
以及 subscribe
监听都是辅助使用的功能,可以暂时不关注。
中间件处理
然后就到了核心的”中间件" 实现部分即 applyMiddleware.js :
// applyMiddleware.js import compose from './compose' export default function applyMiddleware(...middlewares) { return createStore => (...args) => { const store = createStore(...args) let dispatch = () => { throw new Error( `Dispatching while constructing your middleware is not allowed. ` + `Other middleware would not be applied to this dispatch.` ) } const middlewareAPI = { getState: store.getState, dispatch: (...args) => dispatch(...args) } const chain = middlewares.map(middleware => middleware(middlewareAPI)) dispatch = compose(...chain)(store.dispatch) return { ...store, dispatch } } } 复制代码
redux
中间件提供的扩展是在 action
发起之后,到达 reducer
之前,它的实现思路就和 express
、 koa
有些不同了,它没有通过封装 store.dispatch
, 在它前面添加 中间件处理程序
,而是通过递归覆写 dispatch
,不断的传递上一个覆写的 dispatch
来实现。
每一个 redux
中间件的形式为 store => next => action => { xxx }
这里主要有两层函数嵌套:
-
最外层函数接收参数
store
, 对应于applyMiddleware.js
中的处理代码是const chain = middlewares.map(middleware => middleware(middlewareAPI))
,middlewareAPI
即为传入的store
。这一层是为了把store
的api
传递给中间件使用,主要就是两个api
:-
getState
, 直接传递store.getState
. -
dispatch: (...args) => dispatch(...args)
, 这里的实现就很巧妙了,并不是store.dispatch
, 而是一个外部的变量dispatch
, 这个变量最终指向的是覆写后的dispatch
, 这样做的原因在于,对于redux-thunk
这样的异步中间件,内部调用store.dispatch
的时候仍然后走一遍所有“中间件” 。
-
-
返回的
chain
就是第二层的数组,数组的每个元素都是这样一个函数next => action => { xxx }
, 这个函数可以理解为接受一个dispatch
返回一个dispatch
, 接受的dispatch
是后一个中间件返回的dispatch
. -
还有一个关键函数即 compose , 主要作用是
compose(f, g, h)
返回() => f(g(h(..args)))
现在在来理解 dispatch = compose(...chain)(store.dispatch)
就相对容易了,原生的 store.dispatch
传入最后一个“中间件”,返回一个新的 dispatch
, 再向外传递到前一个中间件,直至返回最终的 dispatch
, 当覆写后的 dispatch
调用时,每个“中间件“的执行又是从外向内的”洋葱圈“模型。
至此,redux中间件就完成了。
其它关键点
redux
中间件的实现中还有一点实现也值得学习,为了让”中间件“只能应用一次, applyMiddleware
并不是作用在 store
实例上,而是作用在 createStore
工厂方法上。怎么理解呢?如果 applyMiddleware
是这样的
(store, middlewares) => {}
那么当多次调用 applyMiddleware(store, middlewares)
的时候会给同一个实例重复添加同样的中间件。所以 applyMiddleware
的形式是
(...middlewares) => (createStore) => createStore
,
这样,每一次应用中间件时都是创建一个新的实例,避免了中间件重复应用问题。
这种形式会接收 middlewares
返回一个 createStore
的高阶方法,这个方法一般被称为 createStore
的 enhance
方法,内部即增加了对中间件的应用,你会发现这个方法和中间件第二层 (dispatch) => dispatch
的形式一致,所以它也可以用于 compose
进行多次增强。同时 createStore
也有第三个参数 enhance
用于内部判断,自增强。所以 redux
的中间件使用可以有两种写法:
第一种:用 applyMiddleware 返回 enhance 增强 createStore store = applyMiddleware(middleware1, middleware2)(createStore)(reducer, initState) 复制代码
第二种: createStore 接收一个 enhancer 参数用于自增强 store = createStore(reducer, initState, applyMiddleware(middleware1, middleware2)) 复制代码
第二种使用会显得直观点,可读性更好。
纵观 redux
的实现,函数式编程体现的淋漓尽致,中间件形式 store => next => action => { xx }
是函数柯里化作用的灵活体现,将多参数化为单参数,可以用于提前固定 store
参数,得到形式更加明确的 dispatch => dispatch
,使得 compose
得以发挥作用。
总结
总体而言, express
和 koa
的实现很类似,都是 next
方法传递进行递归调用,只不过 koa
是 promise
形式。 redux
相较前两者有些许不同,先通过递归向外覆写,形成执行时递归向里调用。
总结一下三者关键异同点(不仅限于中间件):
- 实例创建:
express
使用工厂方法,koa
是类 -
koa
实现的语法更高级,使用ES6
,支持generator(async await)
-
koa
没有内置router
, 增加了ctx
全局对象,整体代码更简洁,使用更方便。 -
koa
中间件的递归为promise
形式,express
使用while
循环加next
尾递归 - 我更喜欢
redux
的实现,柯里化中间件形式,更简洁灵活,函数式编程体现的更明显 -
redux
以dispatch
覆写的方式进行中间件增强
最后再次附上 模拟示例源码 以供学习参考,喜欢的欢迎star, fork!
回答一个问题
有人说, express
中也可以用 async function
作为中间件用于异步处理? 其实是不可以的,因为 express
的中间件执行是同步的 while
循环,当中间件中同时包含 普通函数
和 async 函数
时,执行顺序会打乱,先看这样一个例子:
function a() { console.log('a') } async function b() { console.log('b') await 1 console.log('c') await 2 console.log('d') } function f() { a() b() console.log('f') } 复制代码
这里的输出是 'a' > 'b' > 'f' > 'c'
在普通函数中直接调用 async
函数, async
函数会同步执行到第一个 await
后的代码,然后就立即返回一个 promise
, 等到内部所有 await
的异步完成,整个 async
函数执行完, promise
才会 resolve
掉.
所以,通过上述分析 express
中间件实现, 如果用 async
函数做中间件,内部用 await
做异步处理,那么后面的中间件会先执行,等到 await
后再次调用 next
索引就会超出!,大家可以自己在这里 express async 打开注释,自己尝试一下。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:- Laravel HTTP——路由中间件的别名解析与排序源码解析
- Koa 系列 —— Koa 中间件机制解析
- 消息中间件 RocketMQ 源码解析 —— 调试环境搭建
- Koa2 中间件原理解析 —— 看了就会写
- 中间件(WAS、WMQ)运维 9个常见难点解析
- Laravel HTTP——SubstituteBindings 中间件的使用与源码解析
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。