Ecosystem
ArtusX
Libs
core

@artusx/core

插件默认集成了 http 应用开发的常用插件,并导出了对应的类型,本章将演示如何使用插件启动一个 http 服务。

packagesVersion
@artusx/plugin-koaNPM version (opens in a new tab)
@artusx/plugin-log4jsNPM version (opens in a new tab)
@artusx/plugin-nunjucksNPM version (opens in a new tab)
@artusx/plugin-xtransitNPM version (opens in a new tab)
@artusx/plugin-scheduleNPM version (opens in a new tab)

概览

框架不限定目录结构,可选用 MVC(Model - View - Controller 方式组织代码,也可以参考 module-api,使用 Module 的方式组织代码。

iTerm
.
├── bootstrap.ts
├── config
   ├── config.default.ts
   ├── plugin.development.ts (optional)
   └── plugin.ts
├── middleware
   └── LimitRate.ts
   └── traceTime.ts
├── controller
   └── home.ts
└── module-api (optional)
   ├── api.controller.ts
   └── api.service.ts
└── index.ts

示例

启动文件,配置启动参数。

bootstrap.ts
import path from 'path';
import { Application } from '@artusx/utils';
 
(async () => {
  const app = await Application.start({
    root: path.resolve(__dirname),
    configDir: 'config',
  });
 
  console.log(app.config);
})();

插件配置,由于 @artusx/core 默认集成了部分插件,可通过该配置文件选择性开启或者关闭。

config/plugin.ts
export default {
  artusx: {
    enable: true,
    package: '@artusx/core',
  },
 
  // 集成插件,开启关闭无需配置 path/package;否则将优先使用此处设置的 npm 包
  xtransit: {
    enable: false,
  },
};

类中间件,全局开启

middleware/LimitRate.ts
import { ArtusInjectEnum, Inject } from '@artus/core';
import { ArtusXContext, ArtusXNext, Middleware } from '@artusx/core';
 
import { RateLimiterMemory } from 'rate-limiter-flexible';
 
const rateLimiterOptions = {
  points: 6,
  duration: 1,
};
 
@Middleware({
  enable: true,
})
export default class LimitRateMiddleware {
  @Inject(ArtusInjectEnum.Config)
  config: Record<string, string | number>;
 
  private rateLimiter: RateLimiterMemory;
 
  constructor() {
    this.rateLimiter = new RateLimiterMemory(rateLimiterOptions);
  }
 
  async use(ctx: ArtusXContext, next: ArtusXNext): Promise<void> {
    try {
      const rateLimiterRes = await this.rateLimiter.consume(ctx.ip);
      ctx.set('Retry-After', `${rateLimiterRes.msBeforeNext / 1000}`);
      ctx.set('X-RateLimit-Limit', `${rateLimiterOptions.points}`);
      ctx.set('X-RateLimit-Remaining', `${rateLimiterRes.remainingPoints}`);
      ctx.set('X-RateLimit-Reset', `${new Date(Date.now() + rateLimiterRes.msBeforeNext)}`);
    } catch (rejRes) {
      ctx.status = 429;
      ctx.body = 'Too Many Requests';
      return;
    }
    await next();
  }
}

函数中间件,在 route 中使用。

middleware/traceTime.ts
import { ArtusInjectEnum, Inject } from '@artus/core';
import { ArtusXContext, ArtusXNext, Middleware } from '@artusx/core';
 
@Middleware({
  enable: true,
})
export default class TraceTimeMiddleware {
  @Inject(ArtusInjectEnum.Config)
  config: Record<string, string | number>;
 
  async use(ctx: ArtusXContext, next: ArtusXNext): Promise<void> {
    const { data } = ctx.context.output;
    data.traced = true;
    console.log('middleware - traceTime', ctx.context);
 
    console.time('trace');
    await next();
    console.timeEnd('trace');
  }
}

插件已导出 koa 相关注解与类型,在 controller 中 import 即可。

controller/home.ts
import { Controller, GET, POST, MW } from '@artusx/core';
import type { ArtusXContext } from '@artusx/core';
import traceTime from '../middleware/traceTime';
 
@Controller()
export default class HomeController {
  @MW([traceTime])
  @GET('/can-be-get')
  @POST('/post')
  async home(ctx: ArtusXContext) {
    ctx.body = 'Hello World';
  }
}

运行

开发环境,可使用 nodemon 启动,文件变更自动重启应用。

iTerm
npx nodemon src/index.ts

生产环境,通过 tsc 编译后,用 nodejs 启动即可。

iTerm
tsc
node dist/index.js

注解

core 内置部分注解,简化业务使用

@Headers

controller/home.ts
import { Controller, GET, Headers } from '@artusx/core';
import type { ArtusXContext } from '@artusx/core';
 
@Controller()
export default class HomeController {
  @GET('/')
  @Headers({
    'x-method': 'home-controller',
  })
  async home(ctx: ArtusXContext) {
    ctx.body = 'Hello ArtusX!';
  }
}

@StatusCode

使用该注解时候,请勿直接通过 ctx.body = 'Hello ArtusX!'; 赋值,请直接 return 想要返回的数据。

controller/home.ts
import { ArtusXInjectEnum } from '@artusx/utils';
import {
  ArtusInjectEnum,
  ArtusXErrorEnum,
  ArtusApplication,
  Inject,
  Controller,
  GET,
  StatusCode,
} from '@artusx/core';
import type { ArtusXContext, NunjucksClient } from '@artusx/core';
 
@Controller()
export default class HomeController {
  @Inject(ArtusInjectEnum.Application)
  app: ArtusApplication;
 
  @Inject(ArtusXInjectEnum.Nunjucks)
  nunjucks: NunjucksClient;
 
  @GET('/')
  @StatusCode(209)
  async home(ctx: ArtusXContext) {
    const mockError = ctx.query.error;
 
    if (mockError) {
      this.app.throwException(ArtusXErrorEnum.ARTUSX_UNKNOWN_ERROR);
    }
 
    return this.nunjucks.render('index.html', { title: 'ArtusX', message: 'Hello ArtusX!' });
  }
}

@ContentType

设置 ContentType,传参为 string,通过 mine.lookup 匹配正确类型。

https://www.npmjs.com/package/mime-types (opens in a new tab)

controller/api.ts
import { ArtusInjectEnum, Inject, GET, Controller, ContentType } from '@artusx/core';
import type { ArtusXContext } from '@artusx/core';
import APIService from './api.service';
 
@Controller('/api')
export default class APIController {
  @Inject(ArtusInjectEnum.Config)
  config: Record<string, string | number>;
 
  @Inject(APIService)
  apiService: APIService;
 
  @GET('/')
  async home(ctx: ArtusXContext) {
    ctx.body = 'api';
  }
 
  @GET('/mockApi')
  @ContentType('application/json; charset=utf-8')
  async getInfo(ctx: ArtusXContext) {
    ctx.body = await this.apiService.mockApi();
  }
}

@Query / @Params / @Body

在业务开发中,通常需要对请求参数进行验证,此处使用 json-schema 定义参数类型,并使用 ajv 进行校验,校验结果存放在 ctx.context.output.data 中。

https://json-schema.org/specification (opens in a new tab)

定义数据类型

validator.validator.ts
import { JSONSchemaType } from '@artusx/core';
 
export interface QueryTypes {
  foo: string;
  bar?: string;
}
 
export const QueryScheme: JSONSchemaType<QueryTypes> = {
  type: 'object',
  properties: {
    foo: { type: 'string' },
    bar: { type: 'string', nullable: true },
  },
  required: ['foo'],
  additionalProperties: false,
};
 
export interface ParamsTypes {
  uuid: string;
}
 
export const ParamsScheme: JSONSchemaType<ParamsTypes> = {
  type: 'object',
  properties: {
    uuid: { type: 'string', nullable: false },
  },
  required: ['uuid'],
  additionalProperties: false,
};
 
export interface BodyTypes {
  key: number;
}
 
export const BodyScheme: JSONSchemaType<BodyTypes> = {
  type: 'object',
  properties: {
    key: { type: 'integer', nullable: false },
  },
  required: ['key'],
  additionalProperties: false,
};

调用示例

validator.controller.ts
import { Controller, StatusCode, GET, Query, Params, Body, POST } from '@artusx/core';
import type { ArtusXContext } from '@artusx/core';
 
import {
  QueryTypes,
  QueryScheme,
  BodyTypes,
  BodyScheme,
  ParamsTypes,
  ParamsScheme,
} from './validator.validator';
 
@Controller('/validator')
export default class ValidatorController {
  @GET('/:uuid')
  @POST('/:uuid')
 
  /**
   * validator.index.handler
   * @description validate query / params / body
   * @example:
   *   - url: /validator/e8b847b9-cb23-4fbf-8e7c-0c4ba72b9629?foo=foo&bar=bar
   *   - body: { "key": 123456 }
   */
  @Query<QueryTypes>(QueryScheme)
  @Body<BodyTypes>(BodyScheme)
  @Params<ParamsTypes>(ParamsScheme)
  @StatusCode(200)
  async index(ctx: ArtusXContext): Promise<Object> {
    const query = ctx.context.output.data.query;
    const params = ctx.context.output.data.params;
    const body = ctx.context.output.data.body;
 
    return {
      query,
      body,
      params,
    };
  }
}