装饰器

inversifyJS 的装饰器

Daruk 的 IoC 实现是利用的 inversifyJS 框架,它本身就是基于 TypeScript 的,提供给 JavaScript 和 Node.js 控制反转(IoC)库。IoC 容器使用类构造函数来标识并注入其依赖项。InversifyJS 的 API 非常易用,给与开发者最好的 OOP 和 IoC 实践。

前文我们讲了 loadFile API,他加载的这些 源文件 就是利用的 IoC 来管理的类构造函数。所以 Daruk 对外暴露的装饰器有一部分就是直接使用了 inversifyJS 的装饰器。

有人可能会有疑惑,我不会使用或者没有 inversifyJS 的基础和知识,是否会影响我使用 Daruk 呢? 答案当然是不影响,但是有些时候有些高级场景可能还是会用到,但是并不会给开发者造成太多的困扰。我们假设您是一名 IoC 零基础开发者,我们下边给大家逐个介绍一下这些重要的装饰器 API,以保证您正确的使用 Daruk 来体验这种编程方式。

injectable 装饰器

InversifyJS 允许您的类直接依赖其他类。当您想要这样做时,请使用@injectable 装饰器。所以我们定义一个源文件中的类的时候,基本上都会带着 @injectable 装饰器,代表这个类允许被注入和被别人引用。

  1. @injectable()
  2. class MyClassA {
  3. public throw() {}
  4. }
  5. @injectable()
  6. class MyClassB {
  7. public say() {}
  8. }

我们定义好了两个类,他们都是可以被别人直接依赖的,这 2 个类定义了各自的方法,在 inversifyJs 中还有容器的概念,Daruk 对外提供了这个容器,叫做 darukContainer 默认都是在这一个容器里管理所有的类。所以只使用 @injectable 是无法和 Daruk 的 IoC 容器产生关联的,当然你可以使用 bind 方法自己做关联,但是建议一般用户不要这样做,除非你要开发定制的插件。

正确的做法是利用 Daruk 的内置类装饰器配合 @injectable 装饰器,帮你建立绑定关系,其实 Daruk 就是这么干的,可以参考后边的内置类装饰器说明。

inject 装饰器

我们在上面定义了 2 个 Class 后,我们可能需要一起类把他们两个串联起来,我们可以利用 @inject 来进行关联注入。

  1. @injectable()
  2. class Main {
  3. @inject public MyClassA;
  4. @inject public MyClassB;
  5. public run() {
  6. this.MyClassA.throw();
  7. this.mYClassB.say();
  8. }
  9. }

我们利用 @inject 在 Main 上注入了 2 个类的实例,这里请注意,是实例,不需要我们自己去 new 了,InversifyJS 帮你完成了初始化工作,我们这里可以这么简单的理解。

接下来就是绑定环节,但是我们这里不对绑定做深入的介绍,因为使用 Daruk 的时候,只用 @injectable 是无法让类正常工作的,必须配合 Daruk 内置装饰器,然后委托给 Daruk 来进行启动和调用。

看到这里,聪明的你一定就理解了 InversifyJS 这个的作用,主要就是帮助我们对类代码进行委托和管理的,编写好的功能最终由框架决定什么时候进行初始化和执行,这也是 Daruk2.0 的设计思路,包括插件,中间件,路由的注册都是通过这个方式进行的。

provide 装饰器

这里再介绍装饰器 @provide。我们有的时候,可能就是需要注册一个和框架无关的类,比如一个 工具集 或者一个 纯逻辑类,他和 web 服务本身没有关系,比如我们的 RPC 方法,单纯的请求数据或者查询数据的类。那么我们就可以使用这个装饰器来定义。下边我们看一段代码了解一下 provide 到底做了什么。

  1. @injectable()
  2. class MyClassA {
  3. public throw() {}
  4. }
  5. darukContainer.bind<MyClassA>(MyClassA).toSelf();

我们定义了一个可注入的类,然后绑定到了 Daruk 内置的容器中,然后我们就可以在其他的类中用 @inject 来使用了。

如果用@provide来写的话就是这样:

  1. @provide("MyClassA")
  2. class MyClassA {
  3. public throw() {}
  4. }

我们不需要再写 bind 方法了,就可以直接使用,provide 的作用就是帮助你自动注入容器。 如果你看了 Daruk 的源码,其实是在 initPlugin 的过程中,我们做了全部 provide 的 bind 的操作。

fluentProvide 装饰器

fluentProvide 看名字就知道,它的功能比 provide 要多,因为框架代理了绑定的操作,所以我们无法控制绑定时的一些能力了,对于这些能力可以参考 容器 API 比如指定这个类的上下文约束、作用域和其他高级绑定功能。

  1. @(fluentProvide("MyClassA")
  2. .inSingletonScope()
  3. .whenTargetTagged("A")
  4. .done())
  5. class MyClassA {
  6. public throw() {}
  7. }

这里我们给 MyClassA 这个类绑定的时候指定了作用域和约束条件,这里可以参考 inversifyjs 的相关 API: 带标签的绑定 我们一般都不会使用这么复杂的功能,大部分的时候,fluentProvide 还是在定义作用域上,比如某些实例在整个 web 服务周期,只能被实例化一次的情况。比如 数据库 连接等需求。

middleware 装饰器

@middleware 是 Daruk 提供的内置类装饰器,作用是使用。对应他的还有 @defineMiddleware,表明这个类是一个中间件,对于中间件类,我们有一定的编写要求:

  1. import ejs = require("koa-ejs");
  2. import { join } from "path";
  3. import { Daruk, defineMiddleware, injectable, MiddlewareClass } from "daruk";
  4. @defineMiddleware("koa-ejs")
  5. class KoaEjs implements MiddlewareClass {
  6. public initMiddleware(daruk: Daruk) {
  7. ejs(daruk.app, {
  8. root: join(daruk.options.rootPath, "./view"),
  9. viewExt: "tpl",
  10. });
  11. }
  12. }

上面的代码我们定义了一个 Daruk 的中间件,中间件类必须 implements 我们内置的 MiddlewareClass 接口格式,其实就是必须要在 initMiddleware 中定义中间件。 他可以有返回值也可以没有返回值,决定于这个中间件是否有需要有返回值。

  1. import {
  2. Daruk,
  3. DarukContext,
  4. defineMiddleware,
  5. injectable,
  6. MiddlewareClass,
  7. Next,
  8. } from "daruk";
  9. @defineMiddleware("cors")
  10. class Cors implements MiddlewareClass {
  11. public initMiddleware(daruk: Daruk) {
  12. return async (ctx: DarukContext, next: Next) => {
  13. ctx.set("Access-Control-Allow-Origin", "*");
  14. ctx.set(
  15. "Access-Control-Allow-Headers",
  16. "Origin, X-Requested-With, Content-Type, Accept"
  17. );
  18. await next();
  19. };
  20. }
  21. }

可以看到,和直接定义 koa2 的中间件很像,区别在于 initMiddleware 中可以获取到 DarukApp 实例,在实例上我们可以访问对应的 options 参数或者实例属性而已。

而用法也很简单,比如我们可以在全局让这个 middleware 生效,只需要在 initOptions 中定义 order 即可,当然也可以非全局生效,利用 @middleware 来生效单路由局部中间件。

  1. @controller()
  2. class Index {
  3. @middleware("cors")
  4. @get("/")
  5. public async index(ctx: DarukContext, next: Next) {}
  6. }

直接用到对应的 controller 类的 对应局部方法上即可。

controller 装饰器

定义一个 controller 类直接使用 @controller 装饰器即可,然后再配合对应的 resetful 装饰器就可以了,参考上面 中间件的部分写法即可。

priority 装饰器

我们的 controller 类可能有时候,需要控制定义顺序,我们可以利用 priority 装饰器来进行指定。

  1. @controller()
  2. @priority(-1)
  3. class PrefixIndexA {
  4. @get("/index")
  5. public async test(ctx: DarukContext, next: Next) {
  6. ctx.body = "A";
  7. await next();
  8. }
  9. }
  10. @controller()
  11. class PrefixIndexB {
  12. @get("/index")
  13. public async test(ctx: DarukContext, next: Next) {
  14. ctx.body = ctx.body + "B";
  15. }
  16. }

我们定义了 2 个类,每个类上都有一个 /index 的路由,这在 koa 中是被允许的,那么我们如何决定顺序呢,这个时候就可以利用 @priority 来控制执行顺序了, /index 的返回结果是 AB。

service 装饰器

在一个 web 服务中我们除了定义 路由,逻辑类之外,我们可能还需要定义一些 service 类,他们是在 整个 web request 请求链路上有效的类。既它们的生命周期很短,从 request 到 response 就会被销毁掉,再有请求会重新实例化。

这种请求链路上生效的类,在 Daruk 中我们用 @service 装饰器来定义,他有一个优势就是,可以在 service 中方便的拿到 ctx 上下文,而它本身又和 controller 类是解耦合的。

  1. @service()
  2. class Logger {
  3. @inject("ctx") public ctx!: DarukContext;
  4. public info(msg) {
  5. let path = this.ctx.path;
  6. console.log(msg, path);
  7. }
  8. }
  9. @controller()
  10. class Index {
  11. @inject("Logger") public logger;
  12. @get("/index")
  13. public async index(ctx: DarukContext, next: Next) {
  14. ctx.body = "index";
  15. this.logger.info("access index");
  16. }
  17. }

这个场景很适合打印日志,以及传递 ctx 信息的一些场景,我们不需要层层调用透传参数就可以实现 方便的获取 调用的请求链路上下文。

timer 装饰器

利用 timer 装饰器注入的定时任务类,可以方便的帮开发者实现简单的定时任务功能。

  1. @timer()
  2. class MyTimer implements TimerClass {
  3. public cronTime!: string;
  4. public initTimer(daruk: Daruk) {
  5. this.cronTime = "* * * * * *";
  6. }
  7. public onTick(job: CronJob, daruk: Daruk) {
  8. // if you setup with pm2 cluster:
  9. // https://pm2.io/doc/en/runtime/guide/load-balancing/?utm_source=pm2&utm_medium=website&utm_campaign=rebranding#cluster-environment-variable
  10. // only run tick on NODE_APP_INSTANCE === '0';
  11. if (
  12. !process.env.NODE_APP_INSTANCE ||
  13. process.env.NODE_APP_INSTANCE === "0"
  14. ) {
  15. const unit = 1024;
  16. let freeMem = Math.round(freemem() / (unit * unit));
  17. daruk.logger.info({
  18. freeMem,
  19. });
  20. }
  21. }
  22. public onComplete(job: CronJob, daruk: Daruk) {
  23. // if you setup with pm2 cluster:
  24. // https://pm2.io/doc/en/runtime/guide/load-balancing/?utm_source=pm2&utm_medium=website&utm_campaign=rebranding#cluster-environment-variable
  25. // use the NODE_APP_INSTANCE to stop the task
  26. if (
  27. !process.env.NODE_APP_INSTANCE &&
  28. process.env.NODE_APP_INSTANCE !== "0"
  29. ) {
  30. job.stop();
  31. }
  32. }
  33. }

plugin 装饰器

最后我们来说一下 @plugin 装饰器,在 Daruk1.0 中用户是很难扩展 Daruk 本身的功能的,所以 2.0 之后 Daruk 实现了一个简单的插件机制,目前内部的内置功能很多也是基于 plugin 来实现的。

  1. @plugin()
  2. class DarukExitHook implements PluginClass {
  3. public async initPlugin(daruk: Daruk) {
  4. let exitHook = new ExitHook({
  5. onExit: (err: Error | null) => {
  6. if (err) {
  7. daruk.prettyLog(err.stack || err.message, { level: "error" });
  8. }
  9. daruk.prettyLog("process is exiting");
  10. daruk.emit("exit", err, daruk);
  11. },
  12. onExitDone: (code: number) => {
  13. daruk.prettyLog(`process exited: ${code}`);
  14. },
  15. });
  16. return exitHook;
  17. }
  18. }

通过 initPlugin 方法,我们可以在 plugin 类上拿到实例化后的 Daruk。然后我们就可以利用 Daruk 的生命周期和事件机制来实现一些功能了。上面就是进程退出钩子的插件实现。

plugin 类能实现的功能取决于 Daruk 对外开放的生命周期,详细的可以参考生命周期部分介绍。比如我们可以在 access 的生命周期中扩展 ctx 来实现一些通用功能等等, 一般只有高级开发者才会使用。

resetful method 装饰器

Daruk 提供了符合 resetful 的所有 http 方法的 API 装饰器,可以在 controller 类的方法上使用:get,post,del,put,patch,options,head,all

request 装饰器

Daruk 提供了对请求进行操作的一些辅助功能装饰器,方便开发者开发 web 服务实现一些基础通用功能,比如 validate,required 他们都是对请求参数做校验的,typeParse 则是对请求体做格式转换的。

response 装饰器

Daruk 也提供了对 response 做操作的一些辅助装饰器,比如 prefix 可以对 controller 类的所有 router 做矫正; disable 可以对 controller 的类和方法做禁用;jsontype,header 可以对 header 做改写;redirect 可以直接做请求重定向;cache 则可以对接口做简单的缓存封装。