Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

koa洋葱结构解析 #99

Open
bosens-China opened this issue Oct 19, 2023 · 0 comments
Open

koa洋葱结构解析 #99

bosens-China opened this issue Oct 19, 2023 · 0 comments
Labels
Node系列 和node.js相关内容

Comments

@bosens-China
Copy link
Owner

bosens-China commented Oct 19, 2023

image.png

经常在使用 koa 的时候,通过 .use 的形式来注册各种中间件,例如下面一段代码

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

这里会输出 1,3,4,2,下面就来翻看一下源码看看这个中间件实现的具体原理。

在看具体代码之前,先温习一下,使用 koa 的最小运行代码是什么样的

const Koa = require("koa");
const app = new Koa();

// response
app.use((ctx) => {
  ctx.body = "Hello Koa";
});

app.listen(3000);

可以看到,最后通过 listen 方法来启动服务,那我们重点先看下 use 和 listen 做了什么事情。

use

  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
  }

代码比较少,这里直接贴上去了,use 的主要作用就是给 middleware 添加相对应的 fn。

listen

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

这里 server 是 http 的库的方法,我们先不管,主要看一下 this.callback 做了什么事情。

  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)
      if (!this.ctxStorage) {
        return this.handleRequest(ctx, fn)
      }
      return this.ctxStorage.run(ctx, async () => {
        return await this.handleRequest(ctx, fn)
      })
    }

    return handleRequest
  }

handleRequest 这个函数的实现如下

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

可以看到,最终是把 this.compose 返回的 fn 进行了调用,那么由此可以知道 this.compose 就是具体中间件调度的具体实现。

compose

this.compose 可以在 constructor 中看到,默认情况下就是 koa-compose,这个库也非常精简只有 50 行代码,下面会通过注释的形式来对源码进行一个说明。

// 省略部分注释和部分代码

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!");
  }
  // 这里默认返回一个函数,handleRequest函数会调用这个返回的函数,并且传递 context
  return function (context, next) {
    let index = -1;
    // 默认情况下执行一次 dispatch ,dispatch因为是函数声明所以会提升到做作用域顶部
    return dispatch(0);
    function dispatch(i) {
      // 通常情况下不会遇到,但是如果执行两次就会抛出异常,例如第一次调用index为-1,i为0,第二次执行则变成index为0,i也为0则抛出错误
      if (i <= index)
        return Promise.reject(new Error("next() called multiple times"));
      index = i;
      let fn = middleware[i];
      // 这里从koa的实现可以看到,是没有传递next的,所以这行代码可以跳过
      if (i === middleware.length) fn = next;
      // 执行到最后一项的时候直接返回不再继续递归下去
      if (!fn) return Promise.resolve();
      try {
        // 这里实现很巧妙,利用了bind的原理,bind的第一个参数为this,之后的参数为函数的预设值,最后返回一个函数
        // 然后根据Promise.resolve的实现规范,如果传递的Promise.resolve是一个Promise要等待新的Promise执行完成之后决定状态
        // 这里推荐看下PromiseA+规范实现
        return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
      } catch (err) {
        return Promise.reject(err);
      }
    }
  };
}

ok,这里基本上就讲完了,还是对照最初的示例来看

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

这里传递给 compose([fn1, fn2]),之后 i 为 0,返回

const fn = async (ctx, next) => {
  console.log(1);
  await next();
  console.log(2);
};
return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));

之后 i 为 1 的时候

const fn = async (ctx, next) => {
  console.log(3);
  await next();
  console.log(4);
};
return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));

此时 i 为 2,发现数组取不到值了,执行

if (!fn) return Promise.resolve();

回到最最后一步,还没有解释为什么会洋葱结构这样来执行代码

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

这里我们知道先执行第一个函数

  1. 输出 console.log(1);
  2. 执行的过程中遇到 await next() 会执行 next,而从源码 dispatch.bind(null, i + 1) 可以知道下一项就是第二个函数
  3. 执行第二个函数
  4. 输出 console.log(3)
  5. 继续执行 await next() 从源码知道 i 为 2 的时候,返回的是 if (!fn) return Promise.resolve();
  6. 执行 conosle.log(4),返回 Promise 状态为已完成,结果为 undefined
  7. 执行 console.log(2)

上面可能有点绕,但是其实 koa-compose 利用了事件循环的机制,对于微任务每次执行都会放到微任务队列,等待主线程执行栈调用,而栈的特点就是先进后出,所以这也是为什么会输出 1,3,4,2 的原因了。

最后

如果文章有说的不对地方欢迎指出,最后本人正在找工作,有相关 hc 岗位欢迎滴滴。

@bosens-China bosens-China added the Node系列 和node.js相关内容 label Oct 19, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Node系列 和node.js相关内容
Projects
None yet
Development

No branches or pull requests

1 participant