微信公众号:[前端一锅煮]
一点技术、一点思考。
前言:Koa 是一个新的 web 框架,由 Express 幕后的原班人马打造, 致力于成为 web 利用和 API 开发畛域中的一个更小、更富裕表现力、更强壮的基石。 通过利用 async 函数,Koa 帮你抛弃回调函数,并无力地加强错误处理。 Koa 并没有捆绑任何中间件, 而是提供了一套优雅的办法,帮忙您疾速而欢快地编写服务端应用程序。
什么是洋葱模型?
咱们先来看一个 demo:
const Koa = require('koa')const app = new Koa() // 应用程序// 中间件1app.use((ctx, next) => { console.log(1) next() console.log(2)})// 中间件2app.use((ctx, next) => { console.log(3) next() console.log(4)})app.listen(9000, '0.0.0.0', () => { console.log(`Server is starting`)})
浏览器输出 localhost:9000,控制台会有如下打印:
1342
很显著,在 koa 的中间件中,通过 next 函数,将中间件分成了两局部,next 下面的一部分会首先执行,而上面的一部分则会在所有后续的中间件调用之后执行。
Koa 中间件执行程序:
- 外层中间件进行后期解决(next 前的逻辑);
- 调用 next,将控制流交给下个中间件,并 await 其实现,直到前面没有中间件或者没有 next 函数执行为止;
- 实现后,一层层回溯执行各个中间件的前期解决(next 后的逻辑)。
自定义实现
从实质上来说,洋葱模型实际上就是一个实现以下成果的函数调用办法。
async function f(){ console.log(1); await f2() console.log(2);}async function f2() { console.log(3); await f3() console.log(4);}async function f3() { console.log(5); console.log(6);}f()
输入:
135642
这里你会发现实际效果是实现相似这样的构造。
function f(){ console.log(1); new Promise((resolve) => { console.log(3); new Promise((resolve) => { console.log(5); console.log(6); }) console.log(4); }) console.log(2);}f()
从第一个父函数开始,往里面塞子函数,子函数塞孙函数。这个时候调用父函数,就会顺次往里走,先执行同步代码,遇到异步代码放到队列中,同步代码执行结束再顺次取出队列中的代码执行,实质上是利用了 js 的事件循环机制。
上面咱们来看下简版的实现:
const middleware = []let f1 = async function (next) { console.log(1) await next() console.log(2)}let f2 = async function (next) { console.log(3) await next() console.log(4)}let f3 = async function (next) { console.log(5) console.log(6)}function use(fn) { middleware.push(fn)}use(f1)use(f2)use(f3)// 外围代码function compose() { return dispatch(0) function dispatch(i) { let fn = middleware[i] if(!fn) return return fn(dispatch.bind(null, i + 1)) }}compose()
从第一个函数开始,顺次把下一个函数当做参数塞进去。
koa-compose 源码
以下是 Koa 洋葱模型的源码:
function compose(middleware) { if (!Array.isArray(middleware)) throw new TypeError('Middleware stack must be an array!') for (const fn of middleware) { if (typeof fn !== 'function') throw new TypeError('Middleware must be composed of functions!') } // 传入对象 context 返回Promise 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, dispatch.bind(null, i + 1))) } catch (err) { return Promise.reject(err) } } }}
代码最外围的两点:
- 将
context
一路传下去给中间件 - 将
middleware
中的下一个中间件fn
作为将来next
的返回值
另一种实现办法
function compose(middlewares) { return async (ctx) => { function createNext(middleware, oldNext) { return async () => { await middleware(ctx, oldNext) } } let len = middlewares.length let next = async () => { return Promise.resolve() } for (let i = len - 1; i >= 0; i--) { let currentMiddleware = middlewares[i] next = createNext(currentMiddleware, next) } await next() }}
将中间件从最初一个开始封装,每一次都是将本人的执行函数封装成 next 当做上一个中间件的 next 参数,这样当循环到第一个中间件的时候,只须要执行一次 next(),就能链式的递归调用所有中间件。