hans7 发表于 2023-2-2 02:56

【node后端】koa洋葱模型源码简析+极简复现——简单的递归

本帖最后由 hans7 于 2023-2-2 03:05 编辑

# 引言

koa里的中间件就是一个函数而已。比如:

```js
app.use(async (ctx, next) => {
console.log(3);
ctx.body = 'hello world';
await next();
console.log(4);
});
```

洋葱模型:

```js
const Koa = require('koa');

const app = new Koa();

app.use(async (ctx, next) => {
console.log(1);
await next();
console.log(5);
});
app.use(async (ctx, next) => {
console.log(2);
await next();
console.log(4);
});
app.use(async (ctx, next) => {
console.log(3);
ctx.body = 'hello world';
});

app.listen(3001);
```

执行`node index.js`,请求3001端口,输出是1~5。

**作者:(https://blog.csdn.net/hans774882968)以及(https://juejin.cn/user/1464964842528888)以及(https://www.52pojie.cn/home.php?mod=space&uid=1906177)**

本文CSDN:https://blog.csdn.net/hans774882968/article/details/128843088

本文juejin:https://juejin.cn/post/7195249847044145208/
本文52pojie:https://www.52pojie.cn/thread-1740931-1-1.html

# 源码分析

`use`函数是定义中间件的入口,[传送门](https://github1s.com/koajs/koa/blob/HEAD/lib/application.js)

```js
use (fn) {
    if (typeof fn !== 'function') throw new TypeError('middleware must be a function!')
    debug('use %s', fn._name || fn.name || '-')
    this.middleware.push(fn)
    return this
}
```

只是维护了一个`middleware`数组。

接下来看看调用`listen`的时候做什么。[传送门](https://github1s.com/koajs/koa/blob/HEAD/lib/application.js#L98)

```js
listen (...args) {
    debug('listen')
    const server = http.createServer(this.callback())
    return server.listen(...args)
}
```

调用了`callback`:

```js
callback () {
    const fn = this.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`。我们找到`koa-compose`的源码,[传送门](https://github1s.com/koajs/compose/blob/HEAD/index.js)

```js
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!')
}

/**
   * @Param {Object} context
   * @Return {Promise}
   * @Api public
   */

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
      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)
      }
    }
}
}
```

~~非常之短。看来node后端和前端一样是&#128020;⌨️&#127834;……~~

经过简单的几步追踪可以看到,中间件那些函数的执行位置就在`compose`定义的`dispatch`被执行时。`compose`返回的function会传给`handleRequest`,那么我们再看看`handleRequest`:

```js
/**
   * Handle request in callback.
   *
   * @api private
   */

handleRequest (ctx, fnMiddleware) {
    const res = ctx.res
    res.statusCode = 404
    const onerror = err => ctx.onerror(err)
    const handleResponse = () => respond(ctx)
    onFinished(res, onerror)
    return fnMiddleware(ctx).then(handleResponse).catch(onerror)
}
```

所以中间件是在请求处理的时候执行的,调用`fnMiddleware`就是调用了`compose`返回的function,也就调用了`dispatch`。但是中间件最终由`dispatch`函数调用,所以要搞清楚洋葱模型的实现,还是要看`dispatch`函数。

1. `dispatch(0)`表示第一个中间件即将执行,`fn(context, dispatch.bind(null, i + 1))`这句话表示某个中间件正式执行。
2. 某个中间件调用`next`就是调用了`dispatch.bind(null, i + 1)`,这样就产生了递归,下一个中间件即将开始执行。最后一个中间件结束后就会回到上一个中间件的`next`结束之后的位置。
3. 当`fnMiddleware(ctx)`执行完,所有中间件都已经执行完。而中间件执行完,请求已经执行完,之后`handleRequest`还会做一些对返回值的处理,(https://github1s.com/koajs/koa/blob/HEAD/lib/application.js#L261)
4. `index`记录的是执行过的最靠后的中间件的下标。而`dispatch`的参数`i`是当前中间件的下标。如果`i <= index`则表明`next`在某个函数(中间件)执行了2次。这个`i`参数的引入就是为了检测某个函数(中间件)执行2次`next`的非法情况。
5. 从`handleRequest`调用`fnMiddleware`的情况来看,`next`参数是`undefined`(可在`node_modules`里自己加一个`console.log`验证),所以`if (i === middleware.length) fn = next`的作用,就是允许在最后一个中间件调用`next`函数。

# koa洋葱模型简化版实现

下面的代码为了简便,假设在`listen`时立刻处理请求,不考虑异常处理等额外问题。扔浏览器控制台即可快速运行验证。

```js
function compose(middleware) {
return (ctx) => {
    function dispatch(i) {
      if (i <= index) throw new Error('next() called multiple times');
      index = i;
      if (i >= middleware.length) return;
      const fn = middleware;
      return fn(ctx, () => dispatch(i + 1));
    }
    let index = -1;
    return dispatch(0);
};
}
const app = {
middleware: [],
use(fn) {
    if (typeof fn !== 'function') throw new TypeError('middleware must be a function!');
    return this.middleware.push(fn);
},
handleRequest(fnMiddleware) {
    console.log('handleRequest');
    const ctx = {};
    fnMiddleware(ctx);
},
listen() {
    const fnMiddleware = compose(this.middleware);
    this.handleRequest(fnMiddleware);
},
};
app.use(async (ctx, next) => {
console.log(1);
await next();
console.log(6, ctx.body);
});
app.use(async (ctx, next) => {
console.log(2);
await next();
console.log(5, ctx.body);
ctx.body += ' acmer';
});
app.use(async (ctx, next) => {
console.log(3);
ctx.body = 'hello world';
await next();
console.log(4);
});

app.listen(3000);
```

# 参考资料

1. https://juejin.cn/post/6854573208348295182

鹤舞九月天 发表于 2023-2-2 07:06

学习一下,谢谢分享

野男人 发表于 2023-2-3 13:51

刚看node高级 洋葱模型
页: [1]
查看完整版本: 【node后端】koa洋葱模型源码简析+极简复现——简单的递归