Koa洋葱模型分析 在实现Koa的洋葱模型之前,先回顾一下洋葱模型的运作方式
这张图大家应该都见过,但是只有一张图的情况下很难对内部的逻辑有深入了解
下面从简单的例子开始一起探讨洋葱模型中间件
中间件的执行机制 先来看个简单例子
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 app.use(async (ctx, next) => { console .log(1 ); await next(); console .log(2 ); }); app.use(async (ctx, next) => { console .log(3 ); await next(); console .log(4 ); });
所有的请求经过一个中间件的时候都会执行两次,执行next前和执行next后的代码分为两部分执行。
中间件是怎么保存的 通过上面的案例,可以把中间件的注册关键方法锁定在 app.use
上
我们先看 use
方法
koa/lib/application.js
简化一下可以得到
1 2 3 4 5 use (fn ) { this .middleware.push(fn); return this ; }
在 app.use
中,中间件会被推入到一个 middleware
数组中保存起来。
中间件被保存起来后,在什么时候,用什么方式调用的呢?
中间件执行器 server在启动的时候会传入一个一个 callback
,这个callback实际就是被包装后的中间件执行器
我们先看看app.listen
做了什么
1 2 3 4 5 listen (...args ) { const server = http.createServer(this .callback()); return server.listen(...args); }
listen实际是创建了一个http server,然后通过server的监听端口功能来启动服务, 注意在创建server时传入了一个callback,这个callback会在请求到server时被调用, 这个callback似乎就是我们想要的线索。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 callback ( ) { const fn = compose(this .middleware); if (!this .listenerCount('error' )) this .on('error' , this .onerror); const handleRequest = (req, res ) => { const ctx = this .createContext(req, res); return this .handleRequest(ctx, fn); }; return handleRequest; }
当请求到koa server时,会执行被 koa-compose
包装后的中间件,也就是,洋葱模型的关键逻辑在 koa-compose
中。koa-compose
的代码很简短
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 function compose (middleware ) { return function (context, next ) { 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, dispatch.bind(null , i + 1 ))); } catch (err) { return Promise .reject(err) } } } }
koa-compose
会返回一个中间件启动函数,这个函数主要负责了两件事
记录最后执行的那个中间件的下标,也就是在中间件数组中的位置
启动第一个中间件执行器(dispatch(0))
dispatch
中间件执行器对中间件进行了一层包装,主要负责三件事
通过下标取出当前需要执行中间件
执行中间件,参数中的 next
预先绑定为下一个中间件执行器
返回Promise
回想一下开头的洋葱图,是不是又有些不同的理解了呢,再把刚刚的内容再整理成一张简单图
思考🤔 到这里其实洋葱模型中间件其实已经分析完了,关键点就在于 dispatch
函数返回了 Promise
,在 rsolve
时执行中间件,并把下一个中间件作为 next
参数瞧瞧传给了下一个中间件,一层套下一层,直接看上面的源码获取会有些抽象,我们把它用具体代码表示一下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 const middleware1 = async (ctx, next) => { console .log('middleware1 before next' ); await next(); console .log('middleware1 after next' ); }; const middleware2 = async (ctx, next) => { console .log('middleware2 before next' ); await next(); console .log('middleware2 after next' ); }; const middleware3 = async (ctx, next) => { console .log('middleware3 before next' ); await next(); console .log('middleware3 after next' ); }; const ctx = {};return Promise .resolve(middleware1(ctx, async () => { return Promise .resolve(middleware2(ctx, async () => { return Promise .resolve(middleware3(ctx, Promise .resolve())); })); }));
动手实现一个超简洁版洋葱模型中间件🔍 分析完koa的洋葱模型中间件后,我们动手实现一个自己的中间件模型执行器
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 const middlewares = [];const ctx = { };function composeMiddles ( ) { let i = 0 ; async function composed (idx ) { let middle = middlewares[idx]; if (idx >= middlewares.length) { return Promise .resolve(); } try { return await middle(ctx, composed.bind(null , idx + 1 )) } catch (error) { return promise.reject(error); } } return composed(i); } async function invoke ( ) { await composeMiddles(); } invoke();