前言
最近在试着把自己写的 https://github.com/evont/koa-vuessr-middleware 应用在旧项目中时, 因为旧项目 Koa 版本为 1.2, 对中间件的支持不一致, 在转化之后好奇地读了一下源码, 整理了一下对 Koa 中 next 在两个版本中的意义及相互转换的理解
正文
1.x 中的 next
从 Koa 的 application.JS 中找到中间件部分的代码, 可以看出, use 传入的中间件被放入一个 middleware 缓存队列中, 这个队列会经由 koa-compose 进行串联
- App.use = function(fn){
- // ...
- this.middleware.push(fn);
- return this;
- };
- // ...
- App.callback = function(){
- // ...
- var fn = this.experimental
- ? compose_es7(this.middleware)
- : co.wrap(compose(this.middleware));
- // ...
- };
而进入到 koa-compose 中, 可以看到 compose 的实现很有意思(无论是在 1.x 还是在 2.x 中, 2.x 可以看下面的)
- 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;
- }
- }
- // 返回一个 generator 函数
- function *noop(){}
从代码中可以看出来, 其实 next 本身就是一个 generator, 然后在递减的过程中, 实现了中间件的先进后出. 换句话说, 就是中间件会从最后一个开始, 一直往前执行, 而后一个中间件得到 generator 对象 (即 next) 会作为参数传给前一个中间件, 而最后一个中间件的参数 next 是由 noop 函数生成的一个 generator
但是如果在 generator 函数内部去调用另一个 generator 函数, 默认情况下是没有效果的, compose 用了一个 yield * 表达式, 关于 yield *, 可以看看 阮一峰老师的讲解;
2.x 中的 next
Koa 到了 2.x, 代码越发精简了, 基本的思想还是一样的, 依然是缓存中间件并使用 compose 进行串联, 只是中间件参数从一个 next 变成了(ctx, next), 且中间件再不是 generator 函数而是一个 async/await 函数了
- use(fn) {
- // ...
- this.middleware.push(fn);
- return this;
- }
- // ...
- callback() {
- const fn = compose(this.middleware);
- // ..
- }
同时, compose 的实现也变了, 相较于 1.x 显得复杂了一些, 用了四层 return, 将关注点放在 dispatch 函数上:
- 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, dispatch.bind(null, i + 1)));
- } catch (err) {
- return Promise.reject(err)
- }
- }
- }
- }
神来之笔在于 Promise.resolve(fn(context, dispatch.bind(null, i + 1)))这一句, 乍看一下有点难懂, 实际上 fn(context, dispatch.bind(null, i + 1)) 就相当于一个中间件, 然后递归调用下一个中间件, 我们从 dispatch(0) 开始将它展开:
- // 执行第一个中间件 p1-1
- Promise.resolve(function(context, next){
- console.log('executing first mw');
- // 执行第二个中间件 p2-1
- await Promise.resolve(function(context, next){
- console.log('executing second mw');
- // 执行第三个中间件 p3-1
- await Promise(function(context, next){
- console.log('executing third mw');
- await next()
- // 回过来执行 p3-2
- console.log('executing third mw2');
- }());
- // 回过来执行 p2-2
- console.log('executing second mw2');
- })
- // 回过来执行 p1-2
- console.log('executing first mw2');
- }());
执行顺序可以理解为以下的样子:
- // 执行第一个中间件 p1-1
- first = (ctx, next) => {
- console.log('executing first mw');
- next();
- // next() 即执行了第二个中间件 p2-1
- second = (ctx, next) => {
- console.log('executing second mw');
- next();
- // next() 即执行了第三个中间件 p3-1
- third = (ctx, next) => {
- console.log('executing third mw');
- next(); // 没有下一个中间件了, 开始执行剩余代码
- // 回过来执行 p3-2
- console.log('executing third mw2');
- }
- // 回过来执行 p2-2
- console.log('executing second mw2');
- }
- // 回过来执行 p1-2
- console.log('executing first mw2');
- }
从上面我们也能看出来, 如果我们在中间件中没有执行 await next() 的话, 就无法进入下一个中间件, 导致运行停住. 在 2.x 中, next 不再是 generator, 而是以包裹在 Promise.resolve 中的普通函数等待 await 执行.
相互转换
Koa 的中间件在 1.x 和 2.x 中是不完全兼容的, 需要使用 koa-convert 进行兼容, 它不但提供了从 1.x 的 generator 转换到 2.x 的 Promise 的能力, 还提供了从 2.x 回退到 1.x 的兼容方法, 来看下核心源码:
- function convert (mw) {
- // ...
- const converted = function (ctx, next) {
- return co.call(ctx, mw.call(ctx, createGenerator(next)))
- }
- // ...
- }
- function * createGenerator (next) {
- return yield next()
- }
以上是从 1.x 转化为 2.x 的过程, 先将 next 转化为 generator, 然后使用 mw.call(ctx, createGenerator(next)) 返回一个遍历器(此处传入的是 * (next) => () 因此 mw 为 generator 函数), 最后使用 co.call 去执行 generator 函数返回一个 Promise, 关于 co 的解读可以参考 Koa 生成器函数探寻 https://www.codercto.com/a/30624.html ;
接下来我们来看看回退到 1.x 版本的方法
- convert.back = function (mw) {
- // ...
- const converted = function * (next) {
- let ctx = this
- yield Promise.resolve(mw(ctx, function () {
- // ..
- return co.call(ctx, next)
- }))
- }
- // ...
- }
在这里, 由于 2.x 的上下文对象 ctx 等同于 1.x 中的上下文对象, 即 this, 在返回的 generator 中将 this 作为上下文对象传入 2.x 版本中间件的 ctx 参数中, 并将中间件 Promise 化并使用 yield 返回
总结
总的来说, 在 1.x 和 2.x 中, next 都充当了一个串联各个中间件的角色, 其设计思路和实现无不展现了作者的功底之强, 十分值得回味学习
来源: https://juejin.im/post/5c6f9f4051882562276c4519