section1 koa 的中间件原理,其实就是 koa 的中间件实现原理,大家可能会很惊讶 koa 的中间件怎么会有原理呢?但事实就是这样,小编也感到非常惊讶。
这就是关于 koa 的中间件原理的事情了,大家有什么想法呢,欢迎在评论区告诉小编一起讨论哦!
看到这里如果你想走?你真的想走?,不想多看看我的文章了解一些好康的?
好了,正经点,我的这博客是个本质上可是个传(自)播(娱)知(自)识(乐)的平台,怎么可能有什么乱七八糟的小编。下面我将正式带你了解了解 Koa 的中间件模型。
section 2 如果 0202 年还有前端老哥问 Koa 是个啥,我想这也不用混了。简单说 Koa 就是个高可扩展的 node 框架。而如果问 Koa 最迷人的地方,除了让人舒服的异步写法,那就是优雅的中间件处理了。
以下默认读者了解 Koa 的圆葱模型,若是不知道也莫得问题,下面贴心的 小编 作者给你挂个图:
清晰明了简单易懂,这时候可能你就要问了:这么优雅的中间件处理是怎么实现的呢?
按照往常的习惯,遇事不决,手撕源码。
section 3 众所周知,想要看如何实现的前提是知道怎么用。(因为懒得下 Koa 了)我就简单写一些伪代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 const app = requrie("koa" ); app.use(async (ctx, next) => { console .log(1 ); await next(); console .log(4 ); }); app.use(async (ctx, next) => { console .log(2 ); await next(); console .log(3 ); }); app.listen(3000 , () => { console .log("listen on 3000 port" ); });
假装的测一下:
1 curl http://localhost:3000
下面是装作有输出:
1 console => // others..... // 1 // 2 // 3 // 4 // others.....
可以看到,Koa 的使用优雅简单,简直吹爆好吗。对了,Koa 是也是 tj 写的,小伙子又帅代码写的又好,让我这种码狗肥宅情何以堪。
下面正式从源码看看 Koa 对中间件的处理。
section 4 打开 Koa 项目目录:
纳尼?没有 src?出于经验判断,放源码的应该是 lib 目录了,不过出于 水文章 专业性考虑,这里必须先看一下 package.json。
可以看到,项目入口确实是 lib 下的 application.js。
不多 bb,直接打开它:
因为这篇文章主要讲中间件,也不需要看没用的地方了,直接锁定 use 方法和 middleware 处理逻辑,无耻的我当然选择复制粘贴:
use:
1 2 3 4 5 6 7 8 9 10 use (fn ) { if (typeof fn !== 'function' ) throw new TypeError ('middleware must be a function!' ); if (isGeneratorFunction(fn)) { deprecate('...' ); fn = convert(fn); } debug('use %s' , fn._name || fn.name || '-' ); this .middleware.push(fn); return this ; }
虽然已经可以猜到,但是 use 的逻辑就是如此简单:把中间件方法 push 放到数组就 OK 了。而下面那个东东才是本文重点
callback:
1 2 3 4 5 6 7 8 9 10 11 12 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; }
这个 compose 是什么东东?知道函数式编程的铁汁们肯定都很大概都能猜到这是干啥子的了。我们看看它是哪里来的
1 const compose = require ("koa-compose" );
纳尼,竟然是另一个项目引入的。好嘛,github 直接搜索,可以看到这个项目:
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 ) { 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!" ); } 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); } } }; }
没错,就是这么短
我们来看看他做了什么?简单说就是返回一个 function,compose 在函数式编程里作用是代码的组合,这里把 middleware 给用一个闭包保存,然后就可以在新获取的函数肆意发挥了。
再来看看它返回的函数到底做了啥:
嗯~,一个变量,一个 return,构成了 JS 最本来的味道。
index 变量我理解是用来防止访问反向执行 middleware 数组属于对安全性的封装?(其实不太理解),不过无伤大局。这个 dispatch 函数显然才是重点:
1 2 3 4 5 6 7 8 9 10 11 12 13 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); } }
没错,为了读者老爷着想,我又复制了一份,绝不是为了水长度哈。
我们可以看到,这个 dispatch 函数本质上就是获取每个已经注册的中间件,然后把这个中间件执行的同时,把自己的参数(dispatch(i+1))作为这个中间件的第二个参数(next)传入。
那么我们可以想到,如果在这个执行的中间件中调用 next 方法,就等同于调用下一个中间件了。然后就造成了圆葱模型的效果。
具体执行模拟可以看这段伪代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 function middleware1 ( ) { middleware2() }function middleware2 ( ) { middleware3() }function middleware3 {}{.....}
肉眼可见的可扩展与优雅好吗。但是就我个人来讲,我还是不太喜欢这种写法,我在网上看到另一个人对这种原理的抽象,我很喜欢,所以想给你们康康:原文地址
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 compose ( ) { return async ctx => { function createNext (middleware, oldNext ) { return async () => { await middleware(ctx, oldNext); } } let len = this .middlewares.length; let next = async () => { return Promise .resolve(); }; for (let i = len - 1 ; i >= 0 ; i--) { let currentMiddleware = this .middlewares[i]; next = createNext(currentMiddleware, next); } await next(); }; }
我觉得这个 compose 更加符合我的直觉。当然,每个人有自己的看法,这段代码我也就不解释了,本质上它们还是做着同一件事情。
自己写一个测试一下,毕竟要有图有真相啊:
可见没啥毛病。
那么在座的好学的小老弟可能会问了,这么好的写法(思想)我们哪里可以用到呢?
前一阵子看 Vue-Router 源码时候我发现,Vue-Router 同样使用了这个思想。
section forget(加餐) 用过 Vue-Router 的小伙伴一定会知道,Vue-Router 有个叫守卫的东东,注册了一个 router.beforeEach 必须调用其中的 next 参数继续执行下一个守卫,下面代码来自官方文档:
1 2 3 4 5 6 router.beforeEach((to, from , next ) => { if (to.name !== "Login" && !isAuthenticated) next({ name : "Login" }); next(); });
怎么样,是不是感觉似曾相识?
无凭无据,不看代码怎敢章口就来?
下面直接看源码:
由于 Vue-Router 这部分处理有很多与本文主题无关的东西,我们不去纠结细节直接省略它们,同样这里我只粘需要看的地方:
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 const queue: Array <?NavigationGuard> = [].concat( this .router.beforeHooks, extractUpdateHooks(updated) ); runQueue(queue, iterator, () => { const postEnterCbs = []; const isValid = () => this .current === route; const enterGuards = extractEnterGuards(activated, postEnterCbs, isValid); const queue = enterGuards.concat(this .router.resolveHooks); runQueue(queue, iterator, () => { if (this .pending !== route) { return abort(createNavigationCancelledError(current, route)); } this .pending = null ; onComplete(route); if (this .router.app) { this .router.app.$nextTick(() => { postEnterCbs.forEach((cb ) => { cb(); }); }); } }); });
接下来我们再把 runQueue 拿过来:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 export function runQueue ( queue: Array <?NavigationGuard>, fn: Function , cb: Function ) { const step = (index ) => { if (index >= queue.length) { cb(); } else { if (queue[index]) { fn(queue[index], () => { step(index + 1 ); }); } else { step(index + 1 ); } } }; step(0 ); }
我就问你他俩是不是一回事?
下面我就简单解释一下这段代码:
简单说就是遍历 beforeEnter 守卫每一项,把它传给 iterator,不好意思,忘贴 iterator 了, 为了简化代码,我们假设路由的 next 函数不能传参数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 const iterator = (hook: NavigationGuard, next ) => { if (this .pending !== route) { return abort(createNavigationCancelledError(current, route)) } try { hook(route, current, (to: any ) => { next(to) } }) } catch (e) { abort(e) } }
发现了吗,每个守卫执行完场以后会直接去调用延迟执行的 step(n+1),一模一样好伐。等到所有的 enter hooks 执行结束以后,调用回调(cb),处理组件(onComplete)与其他守卫。
所以说看没看到,这种设计模式多么重要,前端小伙伴们赶快搞起。
section -1 如果对我的文章感兴趣记得点赞投币三连。啊,不对,走错片场了…..
那么只要感谢 CV 大哥手下留情别盗我文章就好了。