9

Daruk 框架运行原理(2)

 3 years ago
source link: https://zhuanlan.zhihu.com/p/348812869
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

上一篇文章 中讲了关于 Daruk 应用启动时的运行机制,这一篇文章我来讲一下 Daruk 在请求链路中所做的一些事情。

我们依旧使用上一篇中的 hello world 的例子:

import { controller, DarukContext, DarukServer, get, Next } from '../../src';

@controller()
class HelloWorld {
  @get('/')
  public async index(ctx: DarukContext, next: Next) {
    ctx.body = 'hello world';
  }
}

(async () => {
  let app = DarukServer();
  let port = 3000;
  await app.binding();
  app.listen(port);
  app.logger.info(`app listen port ${port}`);
})();

当我们发起对 3000 端口的根目录访问时,返回对应的 hello world 字符串:

fErymuq.jpg!mobile

上面截图是访问后的终端输出,我们这里使用的是 dev 模式,所以整个日志被展开了,其中第一条日志输出的是 http:// app.logger.info (`app listen port ${port}`) 的内容,后边两条是访问日志,一条是根目录的,一条是浏览器自行发起的 favicon.ico 的请求日志。

我们可以看出,我们打印日志的时候只设置 info层级和 msg 字符串参数,但是 Daruk 却给日志附带了其他额外的一些信息,比如 logType,fileinfo,oshostname,timestamp 等通用参数,还有相关的 access 日志中的 remote_addr,method,url,perf等。

这里的功能其实就是 Daruk 本身自带的一个日志增强功能,帮助开发者收集一些常用的通用参数的,详细的一些解释可以参考这个包,我们这边也对日志模块做了开源。

https://github.com/darukjs/daruk-logger github.com

我们继续讲请求链路的过程,为了更好理解,我下边用图解释,首先如果你不理解 koa 的链路请求机制,可以先看下这张比较经典洋葱圈图来理解一下 koa 中的中间件,相信很多人已经很熟悉了,我这里就不过多解读。

eUbqUz6.jpg!mobile

在 koa 中,中间件也好,controller 也好,都是这个访问模型,从request 到 response 返回,整个链路会像剥洋葱一样,一层一层递进到芯,再返回,中间的每一层其实就是每一个 middleware 里的 next方法。

下边这个图是Daruk 简化了一部分细节后的 UML 图谱。

a6zE7zN.jpg!mobile

首先,request 发起后上下文 ctx 会进入到内置好的框架内中间件,中间件包括5个,第一个中间件也是整个框架的洋葱圈最外一层,负责计算每一个 middleware 的性能,到纳秒级别,可以从上面的图里找一下 access 的 msg 日志部分,就有计算每一层洋葱圈的性能耗时。

然后紧接着的是3个普通增强中间件,request_id 负责请求链路染色的 id 生成,logger负责自动输出 access 日志,body 负责解析 request 中的 body 部分。

最后一个中间件叫 ctx_class,我这里着重讲解一下,代码也很少:

import { Container } from 'inversify';
import { Daruk, darukContainer, DarukContext } from '../../';
import { defineMiddleware } from '../../decorators';
import { MiddlewareClass, Next } from '../../typings/daruk';

@defineMiddleware('daruk_ctx_class')
class DarukCtxClass implements MiddlewareClass {
  public initMiddleware(_daruk: Daruk) {
    return async (ctx: DarukContext, next: Next) => {
      let requestContainer = new Container({ skipBaseClassChecks: true });
      requestContainer.parent = darukContainer;
      requestContainer.bind<DarukContext>('ctx').toConstantValue(ctx);
      requestContainer.bind<Daruk>('Daruk').toConstantValue(_daruk);
      ctx.requestContainer = requestContainer;
      await next();
    };
  }
}

首先所有的 Daruk 的中间件都是这个写法,主要是为了兼容 koa2的社区插件,所以封装了一个 initMiddleware 接口,大家都把koa 的 app.use 传入的值,也就是 koa 的插件返回值写入这个函数即可。

这个函数会返回你当前 daruk 的实例,也就是上文中的 app instance,挂载了所有框架的信息。

我们看关键部分的几行代码:

let requestContainer = new Container({ skipBaseClassChecks: true });
requestContainer.parent = darukContainer;

我们在最外层中间件中 new 出来一个 IoC 容器,然后设置了他的 parent 层级为框架初始化时注入的容器(全局容器)。这样是为了能够在请求链路中的类里可以拿到全局容器中注入的依赖。

requestContainer.bind<DarukContext>('ctx').toConstantValue(ctx);
 requestContainer.bind<Daruk>('Daruk').toConstantValue(_daruk);
 ctx.requestContainer = requestContainer;

后边这3行其实干了2件事,一件是在请求链路中绑定了2个依赖,一个叫 ctx,一个叫 Daruk,我们可以在 @service 和 @controller 装饰的类中通过 @inject 拿到对应的属性值。

这样就极大的方便了我们共享一些请求链路中的属性,比如我需要做 trace 的时候,我可以在任何不同的请求周期类中拿到 ctx 和框架实例,而不需要再做对应的参数传递了,IoC 容器的依赖控制帮我们完美的解决了不同类之间调用的上下文关系。

可以看出,这里的 Daruk 依赖其实就是修复了 上一篇文章中的 多次实例化 Daruk 可能造成全局容器中有多个 app 服务实例的 bug,放到请求链路上就不会有冲突的问题了,因为每一次请求都是一个新的容器调用链。

最后一行代码就是把这个IoC 容器挂载到上下文 ctx 上,这样我们可以方便在其他中间件或者后边的 router 中拿到这个容器。

处理好这个初始化的容器后,我们看一下用户请求到了 / 这个路径时是怎么做的,我这里忽略了读取用户其他装饰器并挂载到 koa-router 的步骤,只给大家看命中路由类后的代码逻辑。

async function routeHandle(ctx: DarukContext, next: () => Promise<void>) {
  Services.forEach((target: Constructor) => {
    ctx.requestContainer.bind<Constructor>(target.name).to(target);
  });
  ctx.requestContainer.bind<Constructor>(controller.name).to(controller);
  let instance = ctx.requestContainer.get(controller.name);
  await instance[funcName](ctx, next);
  // 允许用户在 controller 销毁前执行清理逻辑
  if (is.fn(instance._destroy)) {
    instance._destroy();
  }
  instance = null;
}

这里的代码也不多,首先我们把所有通过 @service 装饰的类进行一次遍历,然后 bind 到 requestContainer 容器上,然后我们把匹配上的 @controller 的类也bind 到容器上。

通过 ctx.requestContainer.get 方法我们拿到IoC 容器帮我们实例化后的 instance,这时依赖已经根据你编写时的调用关系处理好了。

然后我们调用这个 @controller 类上注册的方法,比如 hello world 这个例子中就是这一行:

@get('/')
  public async index(ctx: DarukContext, next: Next) {
    ctx.body = 'hello world';
  }

没错,入口就是这里,根据这里的用户定义好的入口方法,开始执行所有业务逻辑,依赖查找等,全部执行完毕后,如果有手工回收内存的需求,在 controller 类上提供了一个私有的 _ destroy 方法可以在执行完所有业务逻辑后调用。

好了,基本上主要的 request 流程干的事就都在这里讲完了,最复杂的那个地方在 plugins/router.ts 这个文件中,可以去自行查看。

那么这一部分的解读就完了,下一篇我将介绍 Daruk 中的装饰器实现方案,以及TS 中给框架增加装饰器API 的实现思路和技巧。

还有如果喜欢我的文章,也欢迎来 B 站订阅我的频道,不时给大家录制一些有意思的技术视频


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK