koa入坑及其中间件原理(1)
看见了吧,刚入坑koa总是会看见一张洋葱图(后面再说吧)
刚入坑一个东西的时候,我们总会问这个东西是什么?这个东西是用来做什么的?这个东西包括了哪些内容以及它的原理?
那好,我们先来解决第一个问题,koa.js是什么东西?
koa.js是一个框架。
哈哈哈哈,第二个问题,koa.js用来做什么的?
① 对于 HTTP 服务
当前端 UI 向服务器发起了一个 HTTP 请求时,koa.js 能够在 HTTP 请求发送后 (要搞清楚,是已经发送出去的请求,并不是像 axios 一样拦截 request )对于该请求的 request 进行处理
既然能对请求的 request 进行处理,那么 koa.js 也能对于服务端返回的 HTTP 响应(跟上述一样,是已经发出的 response )进行处理。
② 对于中间件容器
对于这个中间件容器,我也没什么概念,于是乎... 我去找了下相关的内容。
什么是中间件容器呢?
在一个大型分布式的系统中, 负责各个不同组件和服务之间的管理和交互。(哎,简单来说点就是你,你爸,你妈,你想要找你爸要点零花钱,但是必须通过你妈哈哈哈哈哈)
比如呢,在一个分布式系统中,有N个数据库,数据库按业务模块分配,但是,随时间增加,业务会改变,而对应的数据库的分配也需要改变。这时候,你如果想要修改某些数据,直接操作数据库是不太可能了,那这时候就需要一个中间件,由它来负责对最终的数据库读写,而访问者只是带着一些参数啥的来访问这个中间件。
言归正传... 切回到主题 koa.js 对于中间件容器能干嘛呢?
第一点:中间的加载
第二点:中间的执行
对于 koa.js的作用也就这两点了。
讲完了它是啥,做什么的,该来讲讲它的内容了。
在使用 Koa.js 的过程中,会发现中间件的使用都是如下所示:
const Koa = require(‘koa‘); let app = new Koa(); const middleware1 = async (ctx, next) => { console.log(1); await next(); console.log(6); } const middleware2 = async (ctx, next) => { console.log(2); await next(); console.log(5); } const middleware3 = async (ctx, next) => { console.log(3); await next(); console.log(4); } app.use(middleware1); app.use(middleware2); app.use(middleware3); app.use(async(ctx, next) => { ctx.body = ‘hello world‘ }) app.listen(3001) // 启动访问浏览器 // 控制台会出现以下结果 // 1 // 2 // 3 // 4 // 5 // 6
问题来了,为什么会出现以上的结果?
这个主要是 Koa.js 的一个中间件引擎 koa-compose 模块来实现的,也就是 Koa.js 实现洋葱模型的核心~~
从洋葱模型可以看出来(怎么看?就是从左到右穿过去.. 先碰到最外圈.. 最后也是碰到的最外圈),中间件在 await next() 前后的操作,很像数据结构-----“栈”,先进后出,符合洋葱模型。同时,在上面的代码中还有 ctx,这个是就是上下文管理操作数据了(哎,写详细点吧,这个上下文就是来保持两个函数之间传递的时候的一些参数状态吧)。于是乎我们可以总结出如果要实现洋葱模型需要具有的几点特性。
① 需要有统一的 上下文 ctx
② 操作先进后出
③ 有控制先进后出的机制 next
④ 有提前结束的机制
知道了这几个特性,可以自己单纯的用 Promise 做个实现。
// 定义一个上下文 let context = { data: [] }; // 定义一个中间件 middleware1 async function middleware1( ctx, next ){ console.log(‘action 001‘); ctx.data.push(1); await next(); console.log(‘action 006‘); ctx.data.push(6); } // 定义一个中间件 middleware2 async function middleware2(ctx, next) { console.log(‘action 002‘); ctx.data.push(2); await next(); console.log(‘action 005‘); ctx.data.push(5); } // 定义一个中间件 middleware3 async function middleware3(ctx, next) { console.log(‘action 003‘); ctx.data.push(3); await next(); console.log(‘action 004‘); ctx.data.push(4); } Promise.resolve(middleware1(context, async() => { return Promise.resolve(middleware2(context, async() => { return Promise.resolve(middleware3(context, async() => { return Promise.resolve(); })); })); })).then(() => { console.log(‘end‘); console.log(‘context = ‘, context); }); // 结果显示 // "action 001" // "action 002" // "action 003" // "action 004" // "action 005" // "action 006" // "end" // "context = { data: [1, 2, 3, 4, 5, 6]}"
然而,虽然单纯用 Promise 嵌套 可以直接实现中间件流程,但是这样会产生代码可读性和可维护性的问题,也带来了中间件扩展的问题。所以,这需要把 Promise 嵌套 实现的中间件方式进行高度抽象,达到可以自定义中间件的层数。这时候就需要借助神器 async/await 。
首先理清一下需要的步骤:
- 中间件队列
- 处理中间队列,并将上下文传进去
- 中间件的流程控制器 next
- 异常的处理
根据上面中间件分析的原理,可以抽象出
- 每一个中间件需要封装一个 Promise
- 洋葱模型的先进后出操作应该要对应 Promise.resolve 的前后操作
const compose = require(‘./index‘); // 中间件数组 let middleware = []; // 上下文 let context = { data: [] }; // 添加第一个中间件 middleware.push(async(ctx, next) => { console.log(‘action 001‘); ctx.data.push(2); await next(); console.log(‘action 006‘); ctx.data.push(5); }); // 添加第二个中间件 middleware.push(async(ctx, next) => { console.log(‘action 002‘); ctx.data.push(2); await next(); console.log(‘action 005‘); ctx.data.push(5); }); // 添加第三个中间件 middleware.push(async(ctx, next) => { console.log(‘action 003‘); ctx.data.push(2); await next(); console.log(‘action 004‘); ctx.data.push(5); }); // 得到一个函数,相当于是一个中间件的链吧 const fn = compose(middleware); // 执行fn函数,传入上下文 fn(context) .then(() => { console.log(‘end‘); console.log(‘context = ‘, context); });
以下是compose
module.exports = compose; // 放入中间件 function compose(middleware) { //判断是不是数组 if (!Array.isArray(middleware)) { throw new TypeError(‘Middleware stack must be an array!‘); } // 返回一个方法 return function(ctx, 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(); } // 这个就是拼装一个 Promise 的链子吧 try { return Promise.resolve(fn(ctx, () => { return dispatch(i + 1); })); } catch (err) { return Promise.reject(err); } } }; }
至此原理已经看透透,其实就是一个 诺言 的链子~ 来进行的中间件调用~
还未讲完~~~ 后文待续~~~~
ε=(´ο`*)))唉
借鉴原文 https://chenshenhai.github.io/koajs-design-note/note/chapter01/05.html