介绍
在我们日常开发中,越来越多看到了中间件
这个词,例如Koa,redux等。这里就大概记录一下Koa和redux中间件的实现方式,可以从中看到中间件的实现方式都是大同小异,基本都是实现了洋葱模型。
对于中间件我们需要了解的是
- 中间件是如何存储的
- 中间件是如何执行的
正文
Koa
作为TJ大神的作品,真不愧是号称基于 Node.js 平台的下一代 web 开发框架
,其中对于中间件
的实现,generator/yield
,还是await/async
,对于回调地狱的处理,都是给后来的开发者很大的影响。
Koa 1的中间件
存储
/** * https://github.com/Koajs/Koa/blob/1.6.0/lib/application.js */ ... var app = Application.prototype; function Application() { if (!(this instanceof Application)) return new Application; this.env = process.env.NODE_ENV || 'development'; this.subdomainOffset = 2; this.middleware = []; this.proxy = false; this.context = Object.create(context); this.request = Object.create(request); this.response = Object.create(response);}...app.use = function(fn){ if (!this.experimental) { // es7 async functions are not allowed, // so we have to make sure that `fn` is a generator function assert(fn && 'GeneratorFunction' == fn.constructor.name, 'app.use() requires a generator function'); } debug('use %s', fn._name || fn.name || '-'); this.middleware.push(fn); return this;};复制代码
可以在这里看到我们通过app.use
加入的中间件,保存在一个middleware
的数组中。
执行
/** * https://github.com/Koajs/Koa/blob/1.6.0/lib/application.js */app.listen = function(){ debug('listen'); var server = http.createServer(this.callback()); return server.listen.apply(server, arguments);};// 删除了一些警告代码app.callback = function(){ ... var fn = this.experimental ? compose_es7(this.middleware) : co.wrap(compose(this.middleware)); var self = this; ... return function handleRequest(req, res){ var ctx = self.createContext(req, res); self.handleRequest(ctx, fn); }};app.handleRequest = function(ctx, fnMiddleware){ ctx.res.statusCode = 404; onFinished(ctx.res, ctx.onerror); fnMiddleware.call(ctx).then(function handleResponse() { respond.call(ctx); }).catch(ctx.onerror);};复制代码
可以在这里看到middleware
数组经过一些处理,生成了fn
,然后通过fnMiddleware.call(ctx)
传入ctx
来处理,然后就将ctx
传给了respond
,所以这里的fnMiddleware
就是我们需要去了解的内容。
这里首先判断是否是this.experimental
来获取是否使用了async/await
,这个我们在Koa1
中不做详细介绍。我们主要是来看一下co.wrap(compose(this.middleware))
。
让我们先来看一下compose()
/** * 这里使用了Koa1@1.6.0 package.json中的Koa-compose的版本 * https://github.com/Koajs/compose/blob/2.3.0/index.js */function compose(middleware){ return function *(next){ if (!next) next = noop(); var i = middleware.length; while (i--) { next = middleware[i].call(this, next); } return yield *next; }}function *noop(){}复制代码
co.wrap(compose(this.middleware))
就变成了如下的样子
co.wrap(function *(next){ if (!next) next = noop(); var i = middleware.length; while (i--) { next = middleware[i].call(this, next); } return yield *next;})复制代码
我们可以看到这里对middleware
进行了倒序遍历。next = middleware[i].call(this, next);
可以写为类似下面这个代码结构
function *middleware1() { ... yield function *next1() { ... yield function *next2() { ... ... ... } ... } ...}复制代码
然后next = middleware[i].call(this, next);
其实每一个next
就是一个middleware
,所以也就可以变成
function *middleware1() { ... yield function *middleware2() { ... yield function *middleware() { ... ... ... } ... } ...}复制代码
然后我们就获得了下面这个代码
co.wrap(function *(next){ next = function *middleware1() { ... yield function *middleware2() { ... yield (function *middleware3() { ... yield function *() { // noop // NO next yield ! } ... } ... } ... } return yield *next;})复制代码
至此我们来看一眼洋葱模型, 是不是和我们上面的代码结构很想。
现在我们有了洋葱模型式的中间节代码,接下来就是执行它。接下来就是co.wrap
,这里我们就不详细说明了,co
框架就是一个通过Promise
来让generator
自执行的框架,实现了类似async/await
的功能(其实应该说async/await
的实现方式就是Promise
和generator
)。
这里提一个最后
yield *next
,是让code
可以少执行一些,因为如果使用yield next
,会返回一个迭代器,然后co
来执行这个迭代器,而yield *
则是相当于将generator
里面的内容写在当前函数中,详细可以见
关于Koa1可以看我的早一些写的另一篇
Koa 2的中间件
存储
/** * https://github.com/Koajs/Koa/blob/1.6.0/lib/application.js */ ... constructor() { super(); this.proxy = false; this.middleware = []; this.subdomainOffset = 2; this.env = process.env.NODE_ENV || 'development'; this.context = Object.create(context); this.request = Object.create(request); this.response = Object.create(response); }... use(fn) { if (typeof fn !== 'function') throw new TypeError('middleware must be a function!'); if (isGeneratorFunction(fn)) { deprecate('Support for generators will be removed in v3. ' + 'See the documentation for examples of how to convert old middleware ' + 'https://github.com/Koajs/Koa/blob/master/docs/migration.md'); fn = convert(fn); } debug('use %s', fn._name || fn.name || '-'); this.middleware.push(fn); return this; }复制代码
Koa2
对于middleware
的存储和Koa1
基本一模一样,保存在一个数组中。
执行
callback() { const fn = compose(this.middleware); if (!this.listeners('error').length) this.on('error', this.onerror); const handleRequest = (req, res) => { const ctx = this.createContext(req, res); return this.handleRequest(ctx, fn); }; return handleRequest; } /** * Handle request in callback. * * @api private */ handleRequest(ctx, fnMiddleware) { const res = ctx.res; res.statusCode = 404; const onerror = err => ctx.onerror(err); const handleResponse = () => respond(ctx); onFinished(res, onerror); return fnMiddleware(ctx).then(handleResponse).catch(onerror);复制代码
这里主要就是两行代码
const fn = compose(this.middleware);// fnMiddleware === fnfnMiddleware(ctx).then(handleResponse).catch(onerror);复制代码
Koa2
的代码似乎比Koa1
要简介一些了,在默认使用await/async
之后,少了co
的使用。
从fnMiddleware(ctx).then(handleResponse).catch(onerror);
我们可以知道fnMiddleware
返回了一个Promise
,然后执行了这个Promise
,所以我们主要知道compose
做了什么就好。
/** * https://github.com/Koajs/compose/blob/4.0.0/index.js */function compose (middleware) { ... return function (context, next) { // last called middleware # let index = -1 return dispatch(0) function dispatch (i) { if (i <= index) return Promise.reject(new Error('next() called multiple times')) index = i let fn = middleware[i] if (i === middleware.length) fn = next if (!fn) return Promise.resolve() try { return Promise.resolve(fn(context, function next () { return dispatch(i + 1) })) } catch (err) { return Promise.reject(err) } } }}复制代码
看起来这段代码比Koa1
的compose
稍微复杂了些,其实差不多,主要的代码其实也就两个
function compose (middleware) { ... return function (context, next) { let index = -1 return dispatch(0) function dispatch (i) { let fn = middleware[i] return Promise.resolve(fn(context, function next () { return dispatch(i + 1) })) } }}复制代码
相比于Koa1
遍历middleware
数组,Koa2
改为了递归。同上面一样,我们可以将函数写为如下结构
async function middleware1() { ... await (async function middleware2() { ... await (async function middleware3() { ... }); ... }); ...}复制代码
因为async
函数的自执行,所以直接运行该函数就可以了。
可以看到Koa1
与Koa2
的中间件的实现方式基本是一样的,只是一个是基于generator/yield
, 一个是基于async/await
。
Redux
相比于Koa
的中间件的具体实现,Redux
相对稍复杂一些。
本人对于Redux基本没有使用,只是写过一些简单的demo,看过一部分的源码,如有错误,请指正
存储
我们在使用Redux的时候可能会这么写
// 好高阶的函数啊const logger = store => next => action => { console.group(action.type) console.info('dispatching', action) let result = next(action) console.log('next state', store.getState()) console.groupEnd(action.type) return result}let store = createStore( todoApp, applyMiddleware( logger ))复制代码
我们可以很方便的找到applyMiddleware
的源码。
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
没有单独保存middleware
的地方,但是通过展开符的...middlewares
,我们也可以知道至少一开始的middlewares
是一个数组的形式。
执行
执行的代码,还是上面那段代码片段。
我们可以看到applyMiddleware()
中,对传入的middlewares
做了简单的封装,目的是为了让每个middleware
在执行的时候可以拿到当前的一些环境和一些必要的接口函数。也就是上面那个高阶函数logger
所需要的三个参数store
,next
,action
。
一开始是middlewares.map(middleware => middleware(middlewareAPI))
,而middlewareAPI
传入了getState
和dispatch
接口(dispatch
接口暂时没有用)。这一步就实现了上面高阶函数logger
所需要的参数store
。
然后是我们看到好多次的compose
函数,我们找到compose
函数的实现。
export default function compose(...funcs) { if (funcs.length === 0) { return arg => arg } if (funcs.length === 1) { return funcs[0] } return funcs.reduce((a, b) => (...args) => a(b(...args)))}复制代码
我们看到compose
对传入的中间件函数,通过Array.reduce
函数处理了一下。最终的函数应该大概类似下面这个格式
// 加入函数名next方便后面理解function chain(...args) { return () => { return a(function next(...args) { return b(function next(...args) { return c(...args); }) }) }}复制代码
这里已经再次出现了我们熟悉的洋葱模型。同时将下一个组件已参数(next
)的形式传入当前的中间件,这里就完成了上面的高阶函数logger
所需要的第二个参数next
,在中间件内部调用next
函数就可以继续中间节的流程。
最后传入了store.dispatch
也就是高阶函数logger
所需要的第二个参数action
,这个就不用多数了,就是将我们刚刚得到的洋葱格式的函数调用一下,通过闭包使得每个中间节都可以拿到store.dispatch
。
总结
至此,Redux
和Koa
的中间件的介绍就差不多了,两者都是以数组的形式保存了中间件,执行的时候都是创建了一个类似洋葱模型的函数结构,也都是将一个包裹下一个中间件的函数当做next
,传入当前中间件,使得当前中间件可以通过调用next
来执行洋葱模型,同时在next
执行的前后都可以写逻辑代码。不同的是Koa1
是通过遍历生成的,Koa2
是通过递归来生成的,redux
是通过reduce
来生成的(和Koa1
的遍历类似)。
所以中间件其实都基本类似,所以好好的理解了一种中间件的实现方式,其他的学起来就很快了(只是表示前端这一块哦)。