--- url: /advanced.md --- # Advanced * [Loader](./loader.md) * [Plugin Development](./plugin.md) * [Framework Development](./framework.md) * [Multi-Process Development Model Enhancement](./cluster-client.md) * [View Plugin Development](./view-plugin.md) * [Upgrade your event functions in your lifecycle](./loader-update.md) --- --- url: /zh-CN/basics/ajv.md --- # Ajv 入参校验 参考 [@eggjs/typebox-validate](https://github.com/eggjs/egg/tree/master/plugins/typebox-validate) 的最佳实践,结合 ajv + [typebox](https://github.com/sinclairzx81/typebox?tab=readme-ov-file#json-types) + ErrorCode 统一错误码规范,只需要定义一次参数校验 Schema,就能同时拥有参数校验和类型定义(完整的 TypeScript 类型提示)。 > 让请求入参校验变得轻松自然,不再是一件烦恼重复的事情😄。 ## 使用方式 ### 定义入参校验 Schema 使用 [typebox](https://github.com/sinclairzx81/typebox?tab=readme-ov-file#json-types) 定义,会内置到 tegg 导出 ```ts import { Type, TransformEnum } from 'egg/ajv'; const SyncPackageTaskSchema = Type.Object({ fullname: Type.String({ transform: [TransformEnum.trim], maxLength: 100, }), tips: Type.String({ transform: [TransformEnum.trim], maxLength: 1024, }), skipDependencies: Type.Boolean(), syncDownloadData: Type.Boolean(), // force sync immediately, only allow by admin force: Type.Boolean(), // sync history version forceSyncHistory: Type.Boolean(), // source registry registryName: Type.Optional(Type.String()), }); ``` ### 从校验 Schema 生成静态的入参类型 ```ts import { Static } from 'egg/ajv'; // 不能使用 type,得改成 interface,确保 oneapi 可以识别 // type SyncPackageTaskType = Static; interface SyncPackageTaskType extends Static {} ``` ### 在 `Controller` 中使用入参类型和校验 `Schema` 注入全局单例 `ajv`,调用 `ajv.validate(XxxSchema, params)` 进行参数校验,参数校验失败会直接抛出 AjvInvalidParamError 异常,egg 会自动返回相应的错误响应给客户端。 ```ts import { Inject, HTTPController, HTTPMethod } from 'egg'; import { Ajv, Type, Static, TransformEnum } from 'egg/ajv'; const SyncPackageTaskSchema = Type.Object({ fullname: Type.String({ transform: [TransformEnum.trim], maxLength: 100, }), tips: Type.String({ transform: [TransformEnum.trim], maxLength: 1024, }), skipDependencies: Type.Boolean(), syncDownloadData: Type.Boolean(), // force sync immediately, only allow by admin force: Type.Boolean(), // sync history version forceSyncHistory: Type.Boolean(), // source registry registryName: Type.Optional(Type.String()), }); interface SyncPackageTaskType extends Static {} @HTTPController() export class HelloController { @Inject() private readonly ajv: Ajv; @HTTPMethod({ method: HTTPMethodEnum.POST, path: '/syncPackage', }) async syncPackage(task: SyncPackageTaskType) { // 参数校验,一旦校验失败会抛出 AjvInvalidParamError 异常 this.ajv.validate(SyncPackageTaskSchema, task); // 执行业务逻辑 return { task, }; } } ``` ## TypeBox JSON 定义参考 可以查看 [JSON Types](https://github.com/sinclairzx81/typebox?tab=readme-ov-file#json-types) 映射表学习。 ```ts ┌────────────────────────────────┬─────────────────────────────┬────────────────────────────────┐ │ TypeBox │ TypeScript │ Json Schema │ │ │ │ │ ├────────────────────────────────┼─────────────────────────────┼────────────────────────────────┤ │ const T = Type.Any() │ type T = any │ const T = { } │ │ │ │ │ ├────────────────────────────────┼─────────────────────────────┼────────────────────────────────┤ │ const T = Type.Unknown() │ type T = unknown │ const T = { } │ │ │ │ │ ├────────────────────────────────┼─────────────────────────────┼────────────────────────────────┤ │ const T = Type.String() │ type T = string │ const T = { │ │ │ │ type: 'string' │ │ │ │ } │ │ │ │ │ ├────────────────────────────────┼─────────────────────────────┼────────────────────────────────┤ │ const T = Type.Number() │ type T = number │ const T = { │ │ │ │ type: 'number' │ │ │ │ } │ │ │ │ │ ├────────────────────────────────┼─────────────────────────────┼────────────────────────────────┤ │ const T = Type.Integer() │ type T = number │ const T = { │ │ │ │ type: 'integer' │ │ │ │ } │ │ │ │ │ ├────────────────────────────────┼─────────────────────────────┼────────────────────────────────┤ │ const T = Type.Boolean() │ type T = boolean │ const T = { │ │ │ │ type: 'boolean' │ │ │ │ } │ │ │ │ │ ├────────────────────────────────┼─────────────────────────────┼────────────────────────────────┤ │ const T = Type.Null() │ type T = null │ const T = { │ │ │ │ type: 'null' │ │ │ │ } │ │ │ │ │ ├────────────────────────────────┼─────────────────────────────┼────────────────────────────────┤ │ const T = Type.Literal(42) │ type T = 42 │ const T = { │ │ │ │ const: 42, │ │ │ │ type: 'number' │ │ │ │ } │ │ │ │ │ ├────────────────────────────────┼─────────────────────────────┼────────────────────────────────┤ │ const T = Type.Array( │ type T = number[] │ const T = { │ │ Type.Number() │ │ type: 'array', │ │ ) │ │ items: { │ │ │ │ type: 'number' │ │ │ │ } │ │ │ │ } │ │ │ │ │ ├────────────────────────────────┼─────────────────────────────┼────────────────────────────────┤ │ const T = Type.Object({ │ type T = { │ const T = { │ │ x: Type.Number(), │ x: number, │ type: 'object', │ │ y: Type.Number() │ y: number │ required: ['x', 'y'], │ │ }) │ } │ properties: { │ │ │ │ x: { │ │ │ │ type: 'number' │ │ │ │ }, │ │ │ │ y: { │ │ │ │ type: 'number' │ │ │ │ } │ │ │ │ } │ │ │ │ } │ │ │ │ │ ├────────────────────────────────┼─────────────────────────────┼────────────────────────────────┤ │ const T = Type.Tuple([ │ type T = [number, number] │ const T = { │ │ Type.Number(), │ │ type: 'array', │ │ Type.Number() │ │ items: [{ │ │ ]) │ │ type: 'number' │ │ │ │ }, { │ │ │ │ type: 'number' │ │ │ │ }], │ │ │ │ additionalItems: false, │ │ │ │ minItems: 2, │ │ │ │ maxItems: 2 │ │ │ │ } │ │ │ │ │ │ │ │ │ ├────────────────────────────────┼─────────────────────────────┼────────────────────────────────┤ │ enum Foo { │ enum Foo { │ const T = { │ │ A, │ A, │ anyOf: [{ │ │ B │ B │ type: 'number', │ │ } │ } │ const: 0 │ │ │ │ }, { │ │ const T = Type.Enum(Foo) │ type T = Foo │ type: 'number', │ │ │ │ const: 1 │ │ │ │ }] │ │ │ │ } │ │ │ │ │ ├────────────────────────────────┼─────────────────────────────┼────────────────────────────────┤ │ const T = Type.Const({ │ type T = { │ const T = { │ │ x: 1, │ readonly x: 1, │ type: 'object', │ │ y: 2, │ readonly y: 2 │ required: ['x', 'y'], │ │ } as const) │ } │ properties: { │ │ │ │ x: { │ │ │ │ type: 'number', │ │ │ │ const: 1 │ │ │ │ }, │ │ │ │ y: { │ │ │ │ type: 'number', │ │ │ │ const: 2 │ │ │ │ } │ │ │ │ } │ │ │ │ } │ │ │ │ │ ├────────────────────────────────┼─────────────────────────────┼────────────────────────────────┤ │ const T = Type.KeyOf( │ type T = keyof { │ const T = { │ │ Type.Object({ │ x: number, │ anyOf: [{ │ │ x: Type.Number(), │ y: number │ type: 'string', │ │ y: Type.Number() │ } │ const: 'x' │ │ }) │ │ }, { │ │ ) │ │ type: 'string', │ │ │ │ const: 'y' │ │ │ │ }] │ │ │ │ } │ │ │ │ │ ├────────────────────────────────┼─────────────────────────────┼────────────────────────────────┤ │ const T = Type.Union([ │ type T = string | number │ const T = { │ │ Type.String(), │ │ anyOf: [{ │ │ Type.Number() │ │ type: 'string' │ │ ]) │ │ }, { │ │ │ │ type: 'number' │ │ │ │ }] │ │ │ │ } │ │ │ │ │ ├────────────────────────────────┼─────────────────────────────┼────────────────────────────────┤ │ const T = Type.Intersect([ │ type T = { │ const T = { │ │ Type.Object({ │ x: number │ allOf: [{ │ │ x: Type.Number() │ } & { │ type: 'object', │ │ }), │ y: number │ required: ['x'], │ │ Type.Object({ │ } │ properties: { │ │ y: Type.Number() │ │ x: { │ │ ]) │ │ type: 'number' │ │ ]) │ │ } │ │ │ │ } │ │ │ │ }, { │ │ │ │ type: 'object', | │ │ │ required: ['y'], │ │ │ │ properties: { │ │ │ │ y: { │ │ │ │ type: 'number' │ │ │ │ } │ │ │ │ } │ │ │ │ }] │ │ │ │ } │ │ │ │ │ ├────────────────────────────────┼─────────────────────────────┼────────────────────────────────┤ │ const T = Type.Composite([ │ type T = { │ const T = { │ │ Type.Object({ │ x: number, │ type: 'object', │ │ x: Type.Number() │ y: number │ required: ['x', 'y'], │ │ }), │ } │ properties: { │ │ Type.Object({ │ │ x: { │ │ y: Type.Number() │ │ type: 'number' │ │ }) │ │ }, │ │ ]) │ │ y: { │ │ │ │ type: 'number' │ │ │ │ } │ │ │ │ } │ │ │ │ } │ │ │ │ │ ├────────────────────────────────┼─────────────────────────────┼────────────────────────────────┤ │ const T = Type.Never() │ type T = never │ const T = { │ │ │ │ not: {} │ │ │ │ } │ │ │ │ │ ├────────────────────────────────┼─────────────────────────────┼────────────────────────────────┤ │ const T = Type.Not( | type T = unknown │ const T = { │ │ Type.String() │ │ not: { │ │ ) │ │ type: 'string' │ │ │ │ } │ │ │ │ } │ ├────────────────────────────────┼─────────────────────────────┼────────────────────────────────┤ │ const T = Type.Extends( │ type T = │ const T = { │ │ Type.String(), │ string extends number │ const: false, │ │ Type.Number(), │ ? true │ type: 'boolean' │ │ Type.Literal(true), │ : false │ } │ │ Type.Literal(false) │ │ │ │ ) │ │ │ │ │ │ │ ├────────────────────────────────┼─────────────────────────────┼────────────────────────────────┤ │ const T = Type.Extract( │ type T = Extract< │ const T = { │ │ Type.Union([ │ string | number, │ type: 'string' │ │ Type.String(), │ string │ } │ │ Type.Number(), │ > │ │ │ ]), │ │ │ │ Type.String() │ │ │ │ ) │ │ │ │ │ │ │ ├────────────────────────────────┼─────────────────────────────┼────────────────────────────────┤ │ const T = Type.Exclude( │ type T = Exclude< │ const T = { │ │ Type.Union([ │ string | number, │ type: 'number' │ │ Type.String(), │ string │ } │ │ Type.Number(), │ > │ │ │ ]), │ │ │ │ Type.String() │ │ │ │ ) │ │ │ │ │ │ │ ├────────────────────────────────┼─────────────────────────────┼────────────────────────────────┤ │ const T = Type.Mapped( │ type T = { │ const T = { │ │ Type.Union([ │ [_ in 'x' | 'y'] : number │ type: 'object', │ │ Type.Literal('x'), │ } │ required: ['x', 'y'], │ │ Type.Literal('y') │ │ properties: { │ │ ]), │ │ x: { │ │ () => Type.Number() │ │ type: 'number' │ │ ) │ │ }, │ │ │ │ y: { │ │ │ │ type: 'number' │ │ │ │ } │ │ │ │ } │ │ │ │ } │ │ │ │ │ ├────────────────────────────────┼─────────────────────────────┼────────────────────────────────┤ │ const U = Type.Union([ │ type U = 'open' | 'close' │ const T = { │ │ Type.Literal('open'), │ │ type: 'string', │ │ Type.Literal('close') │ type T = `on${U}` │ pattern: '^on(open|close)$' │ │ ]) │ │ } │ │ │ │ │ │ const T = Type │ │ │ │ .TemplateLiteral([ │ │ │ │ Type.Literal('on'), │ │ │ │ U │ │ │ │ ]) │ │ │ │ │ │ │ ├────────────────────────────────┼─────────────────────────────┼────────────────────────────────┤ │ const T = Type.Record( │ type T = Record< │ const T = { │ │ Type.String(), │ string, │ type: 'object', │ │ Type.Number() │ number │ patternProperties: { │ │ ) │ > │ '^.*$': { │ │ │ │ type: 'number' │ │ │ │ } │ │ │ │ } │ │ │ │ } │ │ │ │ │ ├────────────────────────────────┼─────────────────────────────┼────────────────────────────────┤ │ const T = Type.Partial( │ type T = Partial<{ │ const T = { │ │ Type.Object({ │ x: number, │ type: 'object', │ │ x: Type.Number(), │ y: number │ properties: { │ │ y: Type.Number() | }> │ x: { │ │ }) │ │ type: 'number' │ │ ) │ │ }, │ │ │ │ y: { │ │ │ │ type: 'number' │ │ │ │ } │ │ │ │ } │ │ │ │ } │ │ │ │ │ ├────────────────────────────────┼─────────────────────────────┼────────────────────────────────┤ │ const T = Type.Required( │ type T = Required<{ │ const T = { │ │ Type.Object({ │ x?: number, │ type: 'object', │ │ x: Type.Optional( │ y?: number │ required: ['x', 'y'], │ │ Type.Number() | }> │ properties: { │ │ ), │ │ x: { │ │ y: Type.Optional( │ │ type: 'number' │ │ Type.Number() │ │ }, │ │ ) │ │ y: { │ │ }) │ │ type: 'number' │ │ ) │ │ } │ │ │ │ } │ │ │ │ } │ │ │ │ │ ├────────────────────────────────┼─────────────────────────────┼────────────────────────────────┤ │ const T = Type.Pick( │ type T = Pick<{ │ const T = { │ │ Type.Object({ │ x: number, │ type: 'object', │ │ x: Type.Number(), │ y: number │ required: ['x'], │ │ y: Type.Number() │ }, 'x'> │ properties: { │ │ }), ['x'] | │ x: { │ │ ) │ │ type: 'number' │ │ │ │ } │ │ │ │ } │ │ │ │ } │ │ │ │ │ ├────────────────────────────────┼─────────────────────────────┼────────────────────────────────┤ │ const T = Type.Omit( │ type T = Omit<{ │ const T = { │ │ Type.Object({ │ x: number, │ type: 'object', │ │ x: Type.Number(), │ y: number │ required: ['y'], │ │ y: Type.Number() │ }, 'x'> │ properties: { │ │ }), ['x'] | │ y: { │ │ ) │ │ type: 'number' │ │ │ │ } │ │ │ │ } │ │ │ │ } │ │ │ │ │ ├────────────────────────────────┼─────────────────────────────┼────────────────────────────────┤ │ const T = Type.Index( │ type T = { │ const T = { │ │ Type.Object({ │ x: number, │ type: 'number' │ │ x: Type.Number(), │ y: string │ } │ │ y: Type.String() │ }['x'] │ │ │ }), ['x'] │ │ │ │ ) │ │ │ │ │ │ │ ├────────────────────────────────┼─────────────────────────────┼────────────────────────────────┤ │ const A = Type.Tuple([ │ type A = [0, 1] │ const T = { │ │ Type.Literal(0), │ type B = [2, 3] │ type: 'array', │ │ Type.Literal(1) │ type T = [ │ items: [ │ │ ]) │ ...A, │ { const: 0 }, │ │ const B = Type.Tuple([ │ ...B │ { const: 1 }, │ | Type.Literal(2), │ ] │ { const: 2 }, │ | Type.Literal(3) │ │ { const: 3 } │ │ ]) │ │ ], │ │ const T = Type.Tuple([ │ │ additionalItems: false, │ | ...Type.Rest(A), │ │ minItems: 4, │ | ...Type.Rest(B) │ │ maxItems: 4 │ │ ]) │ │ } │ │ │ │ │ ├────────────────────────────────┼─────────────────────────────┼────────────────────────────────┤ │ const T = Type.Uncapitalize( │ type T = Uncapitalize< │ const T = { │ │ Type.Literal('Hello') │ 'Hello' │ type: 'string', │ │ ) │ > │ const: 'hello' │ │ │ │ } │ │ │ │ │ ├────────────────────────────────┼─────────────────────────────┼────────────────────────────────┤ │ const T = Type.Capitalize( │ type T = Capitalize< │ const T = { │ │ Type.Literal('hello') │ 'hello' │ type: 'string', │ │ ) │ > │ const: 'Hello' │ │ │ │ } │ │ │ │ │ ├────────────────────────────────┼─────────────────────────────┼────────────────────────────────┤ │ const T = Type.Uppercase( │ type T = Uppercase< │ const T = { │ │ Type.Literal('hello') │ 'hello' │ type: 'string', │ │ ) │ > │ const: 'HELLO' │ │ │ │ } │ │ │ │ │ ├────────────────────────────────┼─────────────────────────────┼────────────────────────────────┤ │ const T = Type.Lowercase( │ type T = Lowercase< │ const T = { │ │ Type.Literal('HELLO') │ 'HELLO' │ type: 'string', │ │ ) │ > │ const: 'hello' │ │ │ │ } │ │ │ │ │ ├────────────────────────────────┼─────────────────────────────┼────────────────────────────────┤ │ const T = Type.Object({ │ type T = { │ const R = { │ │ x: Type.Number(), │ x: number, │ $ref: 'T' │ │ y: Type.Number() │ y: number │ } │ │ }, { $id: 'T' }) | } │ │ │ │ │ │ │ const R = Type.Ref(T) │ type R = T │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ └────────────────────────────────┴─────────────────────────────┴────────────────────────────────┘ ``` --- --- url: /zh-CN/basics/aop-middleware.md --- # AOP 中间件 ## 使用场景 一个请求进来后,会执行一系列处理,然后返回响应给用户。这个过程像一条管道,管道的每一个切面逻辑,称为 `Middleware`。这种模型也被形象地称为“洋葱模型”。 ![洋葱模型](https://mdn.alipayobjects.com/huamei_1jxgeu/afts/img/cwwCRpOnomgAAAAARSAAAAgADpOHAQFr/original) `Middleware` 非常适合用于实现如日志记录、安全校验等与具体业务无关的横切面逻辑。 ### 开启插件 ```typescript // config/plugin.ts export default { teggAop: true, }; ``` #### 标准 AOP 写法 标准 AOP 实现方式满足绝大部份场景,完全按照 AOP 装饰器使用方式实现即可。 ```typescript import { Inject, Logger, ObjectInitType, Tracer } from 'egg'; import { Advice, IAdvice, AdviceContext } from 'egg/aop'; @Advice({ initType: ObjectInitType.SINGLETON }) export class SimpleAopAdvice implements IAdvice { @Inject() logger: Logger; @Inject() tracer: Tracer; async around(ctx: AdviceContext, next: () => Promise) { // 控制器前执行的逻辑 const startTime = Date.now(); this.logger.info('args: %j', ctx.args); // 调用 controller 方法,传入的参数 // 执行下一个 middleware const res = await next(); // 控制器之后执行的逻辑 this.logger.info( '%dms, traceId: %s', Date.now() - startTime, this.tracer.traceId, ); // 对结果进行处理后,再返回 return { res, traceId: this.tracer.traceId, }; } } ``` ## 使用 `Middleware` 实现好中间件逻辑后,通过 `Middleware` 装饰器,将中间件应用于具体的 controller 中,使其生效。`Middleware` 装饰器可用于 controller 类或者 controller 类中的方法。 * `Middleware` 装饰器用于类上时,表示该类的所有方法都会应用该中间件。 * `Middleware` 装饰器用于方法上时,表示只有该方法会应用该中间件。 * 若类和方法上都有 `Middleware` 装饰器,会先执行类上的中间件,再执行方法上的中间件。 * 若混用 Aop 中间件和函数式中间件,函数式中间件会先于所有 Aop 中间件执行。 ```typescript import { Middleware } from 'egg'; @Middleware(globalLog) export class FooController { // 执行顺序 // 进 // 1. globalLog(ctx, next) // 2. methodCount(ctx, next) // 3. hello() // 出 // 4. methodCount(ctx, next) // 5. globalLog(ctx, next) @Middleware(methodCount) async hello() {} // 执行顺序 // 进 // 1. globalLog(ctx, next) // 2. methodCount3(ctx, next) // 3. methodCount2(ctx, next) // 4. methodCount1(ctx, next) // 5. mulitple() // 出 // 6. methodCount1(ctx, next) // 7. methodCount2(ctx, next) // 8. methodCount3(ctx, next) // 9. globalLog(ctx, next) @Middleware(methodCount1) @Middleware(methodCount2) @Middleware(methodCount3) async multiple() {} // 执行顺序 // 进 // 1. globalLog(ctx, next) // 2. bye() // 出 // 3. globalLog(ctx, next) async bye() {} } ``` --- --- url: /zh-CN/basics/aop.md --- # AOP 切面编程 ## 背景 在业务开发中,常常会有日志记录、安全校验等逻辑。这些逻辑通常与具体业务无关,属于横向应用于多个模块间的通用逻辑。在面向切面编程(Aspect-Oriented Programming, AOP)中,将这些逻辑定义为切面。 > 更多关于 AOP 的知识,可以查看 [Aspect-oriented programming](https://en.wikipedia.org/wiki/Aspect-oriented_programming)。 ## 使用 ### Advice 使用 `@Advice` 注解来申明一个实现,可以用来监听、拦截方法执行。 :::warning 注意:Advice 也是一种 Prototype, 默认的 initType 为 Context ,可以通过 initType 来指定其他的生命周期。 ::: ```ts import { Advice, IAdvice, AdviceContext } from 'egg/aop'; import { Inject } from 'egg'; @Advice() export class AdviceExample implements IAdvice { // Advice 中可以正常的注入其他的对象 @Inject() private readonly callTrace: CallTrace; // 在函数执行前执行 async beforeCall(ctx: AdviceContext): Promise { // ... } // 在函数成功后执行 async afterReturn(ctx: AdviceContext, result: any): Promise { // ... } // 在函数失败后执行 async afterThrow(ctx: AdviceContext, error: Error): Promise { // ... } // 在函数退出时执行 async afterFinally(ctx: AdviceContext): Promise { // ... } // 类似 koa 中间件的模式 // block = next async around(ctx: AdviceContext, next: () => Promise): Promise { // ... } } ``` ### Pointcut 使用 `@Pointcut` 在某个类特定的方法上申明一个 `Advice` ```ts import { SingletonProto } from 'egg'; import { Pointcut } from 'egg/aop'; import { AdviceExample } from './AdviceExample'; @SingletonProto() export class Hello { @Pointcut(AdviceExample) async hello(name: string) { return `hello ${name}`; } } ``` ### Crosscut 使用 `@Crosscut` 来声明一个通用的 `Advice`,有三种模式 * 指定类和方法 * 通过正则指定类和方法 * 通过回调来指定类和方法 :::warning 注意:Egg 中的对象无法被 Crosscut 指定 ::: ```ts import { Crosscut, Advice, IAdvice } from 'egg/aop'; // 通过类型来指定 @Crosscut({ type: PointcutType.CLASS, clazz: CrosscutExample, methodName: 'hello', }) @Advice() export class CrosscutClassAdviceExample implements IAdvice {} // 通过正则来指定 @Crosscut({ type: PointcutType.NAME, className: /crosscut.*/i, methodName: /hello/, }) @Advice() export class CrosscutNameAdviceExample implements IAdvice {} // 通过回调来指定 @Crosscut({ type: PointcutType.CUSTOM, callback: (clazz: EggProtoImplClass, method: PropertyKey) => { return clazz === CrosscutExample && method === 'hello'; }, }) @Advice() export class CrosscutCustomAdviceExample implements IAdvice {} // 目标对象 @ContextProto() export class CrosscutExample { hello() { console.log('hello'); } } ``` ### AdviceContext 所有切面函数的第一个入参都是一个 `AdviceContext` 变量,这个变量的数据结构如下: ```typescript interface AdviceContext { that: T; // method: PropertyKey; args: any[]; adviceParams?: K; } ``` * that,被切的对象,`Pointcut`中代表被切函数所在类的实例,`Crosscut`中代表被切类的实例 * method,被切的函数 * args,被切函数的入参 * adviceParams,切面注解透传的参数 被切函数执行过程的伪代码如下: ```typescript await beforeCall(ctx); try { const result = await around(ctx, next); await afterReturn(ctx, result); return result; } catch (e) { await afterThrow(ctx, e); throw e; } finally { await afterFinally(ctx); } ``` 根据上面的实现过程切面函数可以通过`AdviceContext`来影响被切函数的执行: ```typescript @Advice() class PointcutAdvice implements IAdvice { @Inject() logger: EggLogger; // 修改被切函数的入参 async beforeCall(ctx: AdviceContext): Promise { ctx.args = ['for', 'bar']; } // 修改被切函数的返回值 async afterReturn(ctx: AdviceContext, result: any): Promise { result.foo = 'bar'; } // 记录调用异常 async afterThrow( ctx: AdviceContext, error: Error, ): Promise { this.logger.info( `${ctx.that.constructor.name}.${ctx.method.name} throw an error: %j`, error, ); } // 打个调用结束的日志 async afterFinally(ctx: AdviceContext): Promise { this.logger.info( `called ${ctx.that.constructor.name}.${ctx.method.name}, params: %j`, args, ); } // 修改被切函数的调用过程,比如将被切函数放到事务中执行 async around( ctx: AdviceContext, next: () => Promise, ): Promise { await this.runInTransaction(next); } } ``` ### 参数透传 同一个切面在不同的函数上可能会有不同的处理流程,比如事务存在不同的传播机制,如果期望用同一个事务注解来支持不同的传播机制,则需要在注解中传入参数。因此在 AOP 中增加了参数透传,切面函数执行时可以通过 `ctx.adviceParams`获取切面注解中传入的 `options.adviceParams` ```typescript const pointcutParams = { foo: 'bar' }; const crosscutParams = { bar: 'foo' }; @Advice() export class AdviceExample implements IAdvice { async around(ctx: AdviceContext, next: () => Promise): Promise { assert.strictEqual(ctx.adviceParams, pointcutParams); } } @Crosscut( { type: PointcutType.NAME, className: /crosscut.*/i, methodName: /hello/, }, { adviceParams: crosscutParams }, ) @Advice() export class CrosscutNameAdviceExample implements IAdvice { async around(ctx: AdviceContext, next: () => Promise): Promise { assert.strictEqual(ctx.adviceParams, crosscutParams); } } @ContextProto() export class Hello { @Pointcut(AdviceExample, { adviceParams: pointcutParams }) async hello(name: string) { return `hello ${name}`; } } ``` ## 🌰 例子 ### 打印接口结果及耗时日志 #### 实现日志打印 Advice ```typescript import { SingletonProto, Inject, Logger, Tracer } from 'egg'; import { Advice, IAdvice, AdviceContext } from 'egg/aop'; @Advice() class MethodLogAdvice implements IAdvice { private start: number; private succeed = true; @Inject() readonly tracer: Tracer; @Inject() private readonly logger: Logger; // 方法调用前,记录开始执行时间 async beforeCall() { this.start = Date.now(); } // 若方法抛出异常,则标记 succeed 为 false async afterThrow() { this.succeed = false; } // 方法调用结束后,打印日志 async afterFinally(ctx: AdviceContext) { this.logger.info( ctx.method + ',' + (this.succeed ? 'Y' : 'N') + ',' + (Date.now() - this.start) + 'ms,' + this.tracer.traceId + ',' + this.tracer.lastSofaRpcId + ',', ); } } ``` #### 使用 Advice ```typescript import { Pointcut, SingletonProto, Inject } from 'egg'; import { MethodLogAdvice } from './MethodLogAdvice'; @SingletonProto() class FooService { @Pointcut(MethodLogAdvice) async foo() { // ... } } ``` ### 打印 oneapi 调用参数及耗时 在函数应用中,或者使用 layotto 链路进行 oneapi 调用的标准应用(oneapi 配置了 lang: node)中,若想要打印 oneapi 调用的参数及耗时,可以通过 AOP 来实现。使用 CUSTOM 类型 crosscut 实现,框架启动时,会对所有的 oneapi facade 类进行切面织入。 ```typescript import { Advice, AdviceContext, Crosscut, EggProtoImplClass, IAdvice, Inject, LayottoFacade, Logger, PointcutType, } from 'egg'; @Crosscut({ type: PointcutType.CUSTOM, callback: (clazz: EggProtoImplClass, method: PropertyKey) => { return ( clazz.prototype instanceof LayottoFacade && // 是否为 oneapi 生成的 facade 类 method !== 'constructor' && clazz.prototype.hasOwnProperty(method) ); // 排除 constructor 和父类方法 }, }) @Advice() export class OneapiCallAdvice implements IAdvice { @Inject() logger: Logger; // 可以修改为注入自定义实现的 logger async around( ctx: AdviceContext, next: () => Promise, ): Promise { const facadeName = ctx.that.constructor.name; const methodName = ctx.method; const start = Date.now(); const res = await next(); const cost = Date.now() - start; this.logger.info( '%s.%s called, cost: %d, params: %j, result: %j', facadeName, methodName, cost, ctx.args, res, ); return res; } } ``` --- --- url: /basics/app-start.md --- # Application Startup Configuration When the application starts up, we often need to set up some initialization logic. The application bootstraps with those specific configurations. It is in a healthy state and be able to take external service requests after those configurations successfully applied. Otherwise, it failed. The framework provides a unified entry file (`app.js`) for boot process customization. This file need returns a Boot class. We can define the initialization process in the startup application by defining the lifecycle method in the Boot class. The framework has provided you several functions to handle during the whole [life cycle](../advanced/loader.md#life-cycles): * `configWillLoad`: All the config files are ready to load, so this is the LAST chance to modify them. * `configDidLoad`: When all the config files have been loaded. * `didLoad`: When all the files have been loaded. * `willReady`: When all the plug-ins are ready. * `didReady`: When all the workers are ready. * `serverDidReady`: When the server is ready. * `beforeClose`: Before the application is closed. We can defined Boot class in `app.js`. Below we take a few examples of lifecycle functions commonly used in application development: ```js // app.js class AppBootHook { constructor(app) { this.app = app; } configWillLoad() { // The config file has been read and merged, but it has not yet taken effect // This is the last time the application layer modifies the configuration // Note: This function only supports synchronous calls. // For example: the password in the parameter is encrypted, decrypt it here this.app.config.mysql.password = decrypt(this.app.config.mysql.password); // For example: insert a middleware into the framework's coreMiddleware const statusIdx = this.app.config.coreMiddleware.indexOf('status'); this.app.config.coreMiddleware.splice(statusIdx + 1, 0, 'limit'); } async didLoad() { // All configurations have been loaded // Can be used to load the application custom file, start a custom service // Example: Creating a custom app example this.app.queue = new Queue(this.app.config.queue); await this.app.queue.init(); // For example: load a custom directory this.app.loader.loadToContext(path.join(__dirname, 'app/tasks'), 'tasks', { fieldClass: 'tasksClasses', }); } async willReady() { // All plugins have been started, but the application is not yet ready // Can do some data initialization and other operations // Application will start after these operations executed succcessfully // For example: loading data from the database into the in-memory cache this.app.cacheData = await this.app.model.query(QUERY_CACHE_SQL); } async didReady() { // Application already ready const ctx = await this.app.createAnonymousContext(); await ctx.service.Biz.request(); } async serverDidReady() { // http / https server has started and begins accepting external requests // At this point you can get an instance of server from app.server this.app.server.on('timeout', (socket) => { // handle socket timeout }); } } module.exports = AppBootHook; ``` **Note: It is not recommended to do long-time operations in the custom lifecycle function, because the framework has a startup timeout detection.** If your Egg's life-cycle functions are old, we suggest you upgrading to the "class-method" mode. For more you can refer to [Upgrade your event functions in your lifecycle](../advanced/loader-update.md). --- --- url: /tutorials/assets.md --- this document is still waiting for translation, see [Chinese Version](/zh-CN/tutorials/assets) --- --- url: /zh-CN/basics/backgroundTask.md --- # BackgroundTask 异步任务 ## 使用场景 在业务逻辑执行完毕,请求返回后,框架会将本次请求的上下文信息进行释放,以避免造成内存泄漏。若在请求返回后,仍然需要执行一些日志上报等异步逻辑时,可以使用框架提供的 `backgroundTaskHelper` 工具类来执行异步任务,以主动通知框架不要立即释放上下文信息。 ## 使用方式 在需要执行异步任务的地方,注入 `backgroundTaskHelper` 对象,然后调用 `run` 方法执行异步逻辑。 ```typescript import { BackgroundTaskHelper, Inject, SingletonProto } from 'egg'; @SingletonProto() export class TriggerService { @Inject() private backgroundTaskHelper: BackgroundTaskHelper; @Inject() private fooService: any; @Inject() private barService: any; async trigger() { this.backgroundTaskHelper.run(async () => { // do the background task this.fooService.call(); this.barService.call(); }); } } ``` ## 超时时间 框架不会无限的等待异步任务执行,默认情况下,5s 后如果异步任务还没有完成,则会放弃等待,开始执行释放过程。若特殊情况下,确实需要执行长耗时的异步任务,可手动调整超时时间,通过 `backgroundTaskHelper.timeout` 来设置超时时间,单位为毫秒。超时时间为 context 级别设置,若同一个请求中,多次设置超时时间,则以最后一次设置的为准。 ```typescript import { BackgroundTaskHelper, Inject, SingletonProto } from 'egg'; @SingletonProto() export class TriggerService { @Inject() private backgroundTaskHelper: BackgroundTaskHelper; async trigger() { this.backgroundTaskHelper.timeout = 10000; this.backgroundTaskHelper.run(async () => { // do the background task }); } } ``` ## 其他方案 * 标准应用中,还可以使用 [EventBus](./eventbus) 来实现异步任务的处理。 --- --- url: /basics.md --- * [Structure](./structure.md) * [Framework Built-in Objects](./objects.md) * [Runtime Environment](./env.md) * [Configuration](./config.md) * [Middleware](./middleware.md) * [Router](./router.md) * [Controller](./controller.md) * [Service](./service.md) * [Plugin](./plugin.md) * [Scheduled Tasks](./schedule.md) * [Extend EGG](./extend.md) * [Application Startup Configuration](./app-start.md) --- --- url: /tutorials/proxy.md --- Generally, our services will not directly accept external requests, but will deploy the services behind the access layer, thus achieving load balancing of multiple machines and smooth distribution of services to ensure high availability. In this scenario, we can't directly get the connection to the real user request, so we can't confirm the user's real IP, request protocol, or even the requested host. To solve this problem, the framework provides a set of configuration items by default for developers to configure to enable the application layer to obtain real user request information based on the agreement(de facto) with the access layer. ## Enable Proxy Mode The proxy mode can be enabled by `config.proxy = true`: ```js // config/config.default.js exports.proxy = true; ``` Note that after this mode is enabled, the application defaults to being behind the reverse proxy. It will support the request header of the resolved protocol to obtain the real IP, protocol and host of the client. If your service is not deployed behind a reverse proxy, do not enable this configuration in case a malicious user falsifies information such as requesting IP. ### `config.ipHeaders` When the proxy configuration is enabled, the app parses the [X-Forwarded-For](https://en.wikipedia.org/wiki/X-Forwarded-For) request header to get the real IP of the client. If your reverse proxy passes this information through other request headers, it can be configured via `config.ipHeaders`, which supports multiple headers (comma separated). ```js // config/config.default.js exports.ipHeaders = 'X-Real-Ip, X-Forwarded-For'; ``` ### `config.maxIpsCount` The general format of the `X-Forwarded-For` field is: ``` X-Forwarded-For: client, proxy1, proxy2 ``` We can use the first IP address as the real IP adderess of the request, but if a malicious user passes the `X-Forwarded-For` header in the request to spoof it after some reverse proxy, it will cause `X-Forwarded-For` to take The value obtained is inaccurate and can be used to spoof the request IP address, breaking some IP restrictions of the application layer. ``` X-Forwarded-For: fake, client, proxy1, proxy2 ``` In order to avoid this problem, we can configure the number of reverse proxies through `config.maxIpsCount`, so the fake IP address passed by the user will be ignored. For example, if we deploy the application behind a unified access layer (such as Alibaba Cloud SLB, Amazon ELB), we can configure this configuration to `1` so that users cannot forge IP addresses through the `X-Forwarded-For` request header. ```js // config/config.default.js exports.maxIpsCount = 1; ``` This configuration item has the same effect as `options.maxIpsCount` provided by [koa](https://github.com/koajs/koa/blob/master/docs/api/request.md#requestips). ### `config.protocolHeaders` When the proxy configuration is enabled, the application will parse the \[X-Forwarded-Proto] (https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-Proto) request header to get the client's Real access protocol. If your reverse proxy passes this information through other request headers, it can be configured via `config.protocolHeaders`, which supports multiple headers (comma separated). ```js // config/config.default.js exports.protocolHeaders = 'X-Real-Proto, X-Forwarded-Proto'; ``` ### `config.hostHeaders` When the proxy configuration is enabled, the application still reads `host` directly to get the requested domain name. Most of the reverse proxy does not modify this value. But maybe some reverse proxy will pass the client's real access via \[X-Forwarded-Host] (https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-Host) The domain name can be configured via `config.hostHeaders`, which supports multiple headers (comma separated). ```js // config/config.default.js exports.hostHeaders = 'X-Forwarded-Host'; ``` --- --- url: /tutorials/restful.md --- Web frameworks are widely used for providing interfaces to the client through Web services. Let's use an example [CNode Club](https://cnodejs.org/) to show how to build [RESTful](https://en.wikipedia.org/wiki/REST) API using Egg. CNode currently use v1 interface is not fully consistent with the RESTful semantic. In the article, we will encapsulate a more RESTful semantic V2 API based on CNode V1 interface. ## Response Formatting Designing a RESTful-style API, we will identify the status of response by the response status code, keeping the response body simply and only the interface data is returned. A example of `topics` is shown below: ### Get topics list * `GET /api/v2/topics` * status code: 200 * response body: ```json [ { "id": "57ea257b3670ca3f44c5beb6", "author_id": "541bf9b9ad60405c1f151a03", "tab": "share", "content": "content", "last_reply_at": "2017-01-11T13:32:25.089Z", "good": false, "top": true, "reply_count": 155, "visit_count": 28176, "create_at": "2016-09-27T07:53:31.872Z" }, { "id": "57ea257b3670ca3f44c5beb6", "author_id": "541bf9b9ad60405c1f151a03", "tab": "share", "content": "content", "title": "Finished Rewriting of Let's Learning Node.js Together", "last_reply_at": "2017-01-11T10:20:56.496Z", "good": false, "top": true, "reply_count": 193, "visit_count": 47633 } ] ``` ### Retrieve One Topic * `GET /api/v2/topics/57ea257b3670ca3f44c5beb6` * status code: 200 * response body: ```json { "id": "57ea257b3670ca3f44c5beb6", "author_id": "541bf9b9ad60405c1f151a03", "tab": "share", "content": "content", "title": "Finished Rewriting of Let's Learning Node.js Together", "last_reply_at": "2017-01-11T10:20:56.496Z", "good": false, "top": true, "reply_count": 193, "visit_count": 47633 } ``` ### Create Topics * `POST /api/v2/topics` * status code: 201 * response body: ```json { "topic_id": "57ea257b3670ca3f44c5beb6" } ``` ### Update Topics * `PUT /api/v2/topics/57ea257b3670ca3f44c5beb6` * status code: 204 * response body: null ### Error Handling When an error is occurring, 4xx status code is returned if occurred by client-side request parameters and 5xx status code is returned if occurred by server-side logic processing. All error objects are used as the description for status exceptions. For example, passing invalided parameters from the client may return a response with status code 422, the response body as shown below: ```json { "error": "Validation Failed", "detail": [ { "message": "required", "field": "title", "code": "missing_field" } ] } ``` ## Getting Started After interface convention, we begin to create a RESTful API. ### Application Initialization Initializes the application using `npm` in the [quickstart](../intro/quickstart.md) ```bash $ mkdir cnode-api && cd cnode-api $ npm init egg --type=simple $ npm i ``` ### Enable validate plugin [egg-validate](https://github.com/eggjs/egg-validate) is used to present the validate plugin. ```js // config/plugin.js exports.validate = { enable: true, package: 'egg-validate', }; ``` ### Router Registry First of all, we follower previous design to register [router](../basics/router.md). The framework provides a simply way to create a RESTful-style router and mapping the resources to the corresponding controllers. ```js // app/router.js module.exports = (app) => { app.router.resources('topics', '/api/v2/topics', app.controller.topics); }; ``` Mapping the 'topics' resource's CRUD interfaces to the `app/controller/topics.js` using `app.resources` ### Developing Controller In [controller](../basics/controller.md), we only need to implement the interface convention of `app.resources` [RESTful style URL definition](../basics/router.md#RESTful-style-URL-definition). For example, creating a 'topics' interface: ```js // app/controller/topics.js const Controller = require('egg').Controller; // defining the rule of request parameters const createRule = { accesstoken: 'string', title: 'string', tab: { type: 'enum', values: ['ask', 'share', 'job'], required: false }, content: 'string', }; class TopicController extends Controller { async create() { const ctx = this.ctx; // validate the `ctx.request.body` with the expected format // status = 422 exception will be thrown if not passing the parameter validation ctx.validate(createRule, ctx.request.body); // call service to create a topic const id = await ctx.service.topics.create(ctx.request.body); // configure the response body and status code ctx.body = { topic_id: id, }; ctx.status = 201; } } module.exports = TopicController; ``` As shown above, a Controller mainly implements the following logic: 1. call the validate function to validate the request parameters 2. create a topic by calling service encapsulates business logic using the validated parameters 3. configure the status code and context according to the interface convention ### Developing Service We will more focus on writing effective business logic in [service](../basics/service.md). ```js // app/service/topics.js const Service = require('egg').Service; class TopicService extends Service { constructor(ctx) { super(ctx); this.root = 'https://cnodejs.org/api/v1'; } async create(params) { // call CNode V1 API const result = await this.ctx.curl(`${this.root}/topics`, { method: 'post', data: params, dataType: 'json', contentType: 'json', }); // check whether the call was successful, throws an exception if it fails this.checkSuccess(result); // return the id of topis return result.data.topic_id; } // Encapsulated a uniform check function, can be reused in query, create, update and such on in service checkSuccess(result) { if (result.status !== 200) { const errorMsg = result.data && result.data.error_msg ? result.data.error_msg : 'unknown error'; this.ctx.throw(result.status, errorMsg); } if (!result.data.success) { // remote response error this.ctx.throw(500, 'remote response error', { data: result.data }); } } } module.exports = TopicService; ``` After developing the Service of topic creation, an interface have been completed from top to bottom. ### Unified Error Handling Normal business logic has been completed, but exceptions have not yet been processed. Controller and Service may throw an exception as the previous coding, so it is recommended that throwing an exception to interrupt if passing invalided parameters from the client or calling the back-end service with exception. * use Controller `this.ctx.validate()` to validate the parameters, throw exception if it fails. * call Service `this.ctx.curl()` to access CNode API, may throw server exception due to network problems. * an exception also will be thrown after Service is getting the response of calling failure from CNode API. Default error handling is provided but might be inconsistent as the interface convention previously. We need to implement a unified error-handling middleware to handle the errors. Create a file `error_handler.js` under `app/middleware` directory to create a new [middleware](../basics/middleware.md) ```js // app/middleware/error_handler.js module.exports = () => { return async function errorHandler(ctx, next) { try { await next(); } catch (err) { // All exceptions will trigger an error event on the app and the error log will be recorded ctx.app.emit('error', err, ctx); const status = err.status || 500; // error 500 not returning to client when in the production environment because it may contain sensitive information const error = status === 500 && ctx.app.config.env === 'prod' ? 'Internal Server Error' : err.message; // Reading from the properties of error object and set it to the response ctx.body = { error }; if (status === 422) { ctx.body.detail = err.errors; } ctx.status = status; } }; }; ``` We can catch all exceptions and follow the expected format to encapsulate the response through the middleware. It can be loaded into application using configuration file (`config/config.default.js`) ```js // config/config.default.js module.exports = { // load the errorHandler middleware middleware: ['errorHandler'], // only takes effect on URL prefix with '/api' errorHandler: { match: '/api', }, }; ``` ## Testing Completing the coding just the first step, furthermore we need to add [Unit Test](../core/unittest.md) to the code. ### Controller Testing Let's start writing the unit test for the Controller. We can simulate the implementation of the Service layer in an appropriate way because the most important part is to test the logic as for Controller. And mocking up the Service layer according the convention of interface, so we can develop layered testing because the Service layer itself can also covered by Service unit test. ```js const { app, mock, assert } = require('egg-mock/bootstrap'); describe('test/app/controller/topics.test.js', () => { // test the response of passing the error parameters it('should POST /api/v2/topics/ 422', () => { app.mockCsrf(); return app .httpRequest() .post('/api/v2/topics') .send({ accesstoken: '123', }) .expect(422) .expect({ error: 'Validation Failed', detail: [ { message: 'required', field: 'title', code: 'missing_field' }, { message: 'required', field: 'content', code: 'missing_field' }, ], }); }); // mock up the service layer and test the response of normal request it('should POST /api/v2/topics/ 201', () => { app.mockCsrf(); app.mockService('topics', 'create', 123); return app .httpRequest() .post('/api/v2/topics') .send({ accesstoken: '123', title: 'title', content: 'hello', }) .expect(201) .expect({ topic_id: 123, }); }); }); ``` As the Controller testing above, we create an application using [egg-mock](https://github.com/eggjs/egg-mock) and simulate the client to send request through [SuperTest](https://github.com/visionmedia/supertest). In the testing, we also simulate the response from Service layer to test the processing logic of Controller layer ### Service Testing Unit Test of Service layer may focus on the coding logic. [egg-mock](https://github.com/eggjs/egg-mock) provides a quick method to test the Service by calling the test method in the Service, and SuperTest to simulate the client request is no longer needed. ```js const { app, mock, assert } = require('egg-mock/bootstrap'); describe('test/app/service/topics.test.js', () => { let ctx; beforeEach(() => { // create a global context object so that can call the service function on a ctx object ctx = app.mockContext(); }); describe('create()', () => { it('should create failed by accesstoken error', async () => { try { // calling service method on ctx directly await ctx.service.topics.create({ accesstoken: 'hello', title: 'title', content: 'content', }); } catch (err) { assert(err.status === 401); assert(err.message === 'error accessToken'); } throw 'should not run here'; }); it('should create success', async () => { // not affect the normal operation of CNode by simulating the interface calling of CNode based on interface convention // app.mockHttpclient method can easily simulate the appliation's HTTP request app.mockHttpclient(`${ctx.service.topics.root}/topics`, 'POST', { data: { success: true, topic_id: '5433d5e4e737cbe96dcef312', }, }); const id = await ctx.service.topics.create({ accesstoken: 'hello', title: 'title', content: 'content', }); assert(id === '5433d5e4e737cbe96dcef312'); }); }); }); ``` In the testing of Service layer above, we create a Context object using the `app.createContext()` which provided by egg-mock and call the Service method on Context object to test directly. It can use `app.mockHttpclient()` to simulate the response of calling HTTP request, which allows us to focus on the logic testing of Service layer without the impact of environment. *** See the full example at [eggjs/examples/cnode-api](https://github.com/eggjs/examples/tree/master/cnode-api). --- --- url: /community/style-guide.md --- Developers are advised to use `npm init egg --type=simple showcase` to generate and observe the recommended project structure and configuration. ## Classify Old Style: ```js module.exports = (app) => { class UserService extends app.Service { async list() { return await this.ctx.curl('https://eggjs.org'); } } return UserService; }; ``` change to: ```js const Service = require('egg').Service; class UserService extends Service { async list() { return await this.ctx.curl('https://eggjs.org'); } } module.exports = UserService; ``` Additionally, the `framework developer` needs to change the syntax as follows, otherwise the `application developer` will have problems customizing base classes such as Service: ```js const egg = require('egg'); module.exports = Object.assign(egg, { Application: class MyApplication extends egg.Application { // ... }, // ... }); ``` ## Private Properties & Lazy Initialization * Private properties are mounted with `Symbol`. * The description of Symbol follows the rules of jsdoc, describing the mapped class name + attribute name. * Delayed initialization. ```js // app/extend/application.js const CACHE = Symbol('Application#cache'); const CacheManager = require('../../lib/cache_manager'); module.exports = { get cache() { if (!this[CACHE]) { this[CACHE] = new CacheManager(this); } return this[CACHE]; }, }; ``` --- --- url: /faq.md --- # Common Errors * [TEGG\_EGG\_PROTO\_NOT\_FOUND](TEGG_EGG_PROTO_NOT_FOUND.md) * [TEGG\_ROUTER\_CONFLICT](TEGG_ROUTER_CONFLICT.md) --- --- url: /community.md --- # Community ## Resources * Frameworks * [aliyun-egg](https://github.com/eggjs/aliyun-egg) * Tools * [vscode plugin - eggjs](https://marketplace.visualstudio.com/items?itemName=atian25.eggjs) * [vscode plugin - eggjs-dev-tools](https://marketplace.visualstudio.com/items?itemName=yuzukwok.eggjs-dev-tools) * Others * [awesome-egg](https://github.com/eggjs/awesome-egg) * Articles * [How to evaluate Ali's open source enterprise-level Node.js framework Egg?](https://www.zhihu.com/question/50526101/answer/144952130) By [@day pig](https://github.com/atian25) * You can also read our article at [Kuroshiami column](https://www.zhihu.com/column/eggjs) ## Sponsors and Backers [![sponsors](https://opencollective.com/eggjs/tiers/sponsors.svg?avatarHeight=48)](https://opencollective.com/eggjs#support) [![backers](https://opencollective.com/eggjs/tiers/backers.svg?avatarHeight=48)](https://opencollective.com/eggjs#support) ## Contributors [![contributors](https://contrib.rocks/image?repo=eggjs/egg\&max=240\&columns=26)](https://github.com/eggjs/egg/graphs/contributors) --- --- url: /zh-CN/basics/config.md --- # Config 配置 框架提供了强大且可扩展的配置功能,可以自动合并应用、插件、框架的配置,按顺序覆盖,且可以根据环境维护不同的配置。合并后的配置可直接从 `app.config` 获取。 配置的管理有多种方案,以下列举一些常见的方案: 1. 使用平台管理配置,应用构建时将当前环境的配置放入包内,启动时指定该配置。但应用就无法一次构建多次部署,而且本地开发环境想使用配置会变得很麻烦。 2. 使用平台管理配置,在启动时将当前环境的配置通过环境变量传入,这是比较优雅的方式,但框架对运维的要求会比较高,需要部署平台支持,同时开发环境也有相同的痛点。 3. 使用代码管理配置,在代码中添加多个环境的配置,在启动时传入当前环境的参数即可。但无法全局配置,必须修改代码。 我们选择了最后一种配置方案,**配置即代码**,配置的变更也应该经过审核后才能发布。应用包本身是可以部署在多个环境的,只需要指定运行环境即可。 ### 多环境配置 框架支持根据环境来加载配置,定义多个环境的配置文件,具体环境请查看[运行环境配置](./env.md)。 ``` config |- config.default.js |- config.prod.js |- config.unittest.js `- config.local.js ``` `config.default.js` 为默认的配置文件,所有环境都会加载这个配置文件,一般也会作为开发环境的默认配置文件。 当指定 `env` 时,会同时加载默认配置和对应的配置(具名配置)文件。具名配置和默认配置将合并(使用 [extend2](https://www.npmjs.com/package/extend2) 深拷贝)成最终配置,具名配置项会覆盖默认配置文件的同名配置。例如,`prod` 环境会加载 `config.prod.js` 和 `config.default.js` 文件,`config.prod.js` 会覆盖 `config.default.js` 的同名配置。 ### 配置写法 配置文件返回的是一个对象,可以覆盖框架的一些配置,应用也可以将自己业务的配置放到这里方便管理。 ```js // 配置 logger 文件的目录,logger 默认配置由框架提供 module.exports = { logger: { dir: '/home/admin/logs/demoapp', }, }; ``` 配置文件也可以简化地写成 `exports.key = value` 形式: ```js exports.keys = 'my-cookie-secret-key'; exports.logger = { level: 'DEBUG', }; ``` 配置文件也可以返回一个函数,该函数可以接受 `appInfo` 参数: ```js // 将 logger 目录放到代码目录下 const path = require('path'); module.exports = (appInfo) => { return { logger: { dir: path.join(appInfo.baseDir, 'logs'), }, }; }; ``` 内置的 `appInfo` 属性包括: | appInfo | 说明 | | ------- | -------------------------------------------------------------------------- | | pkg | `package.json` 文件 | | name | 应用名称,同 `pkg.name` | | baseDir | 应用代码的目录 | | HOME | 用户目录,如 admin 账户为 `/home/admin` | | root | 应用根目录,在 `local` 和 `unittest` 环境下为 `baseDir`,其他都为 `HOME`。 | `appInfo.root` 是一个优雅的适配方案。例如,在服务器环境我们通常使用 `/home/admin/logs` 作为日志目录,而在本地开发时为了避免污染用户目录,我们需要一种优雅的适配方案,`appInfo.root` 正好解决了这个问题。 请根据具体场合选择合适的写法。但请确保没有完成以下代码: ```js // 配置文件 config/config.default.js exports.someKeys = 'abc'; module.exports = (appInfo) => { const config = {}; config.keys = '123456'; return config; }; ``` ### 配置加载顺序 应用、插件、框架都可以定义这些配置,且目录结构都是一致的,但存在优先级(应用 > 框架 > 插件),相对于此运行环境的优先级会更高。 比如在 prod 环境中加载一个配置的加载顺序如下,后加载的会覆盖前面的同名配置。 ``` -> 插件 config.default.js -> 框架 config.default.js -> 应用 config.default.js -> 插件 config.prod.js -> 框架 config.prod.js -> 应用 config.prod.js ``` **注意**:插件之间也会有加载顺序,但大致顺序类似。具体逻辑可[查看加载器](../advanced/loader.md)。 ### 合并规则 配置的合并使用 `extend2` 模块进行深度拷贝,`extend2` 来源于 `extend`,但是在处理数组时的表现会有所不同。 ```js const a = { arr: [1, 2], }; const b = { arr: [3], }; extend(true, a, b); // => { arr: [ 3 ] } ``` 根据上面的例子,框架直接覆盖数组而不是进行合并。 ### 配置结果 框架在启动时会把合并后的最终配置输出到 `run/application_config.json`(worker 进程)和 `run/agent_config.json`(agent 进程)中,以供问题分析。 配置文件中会隐藏以下两类字段: 1. 安全字段,如密码、密钥等。这些字段通过 `config.dump.ignore` 属性进行配置,其类型必须是 [Set]。可参见[默认配置](https://github.com/eggjs/egg/blob/master/config/config.default.js)。 2. 非字符串化字段,如函数、Buffer 等。这些字段在 `JSON.stringify` 后所生成的内容容量很大。 此外,框架还会生成 `run/application_config_meta.json`(worker 进程)和 `run/agent_config_meta.json`(agent 进程)文件。这些文件用于排查配置属性的来源,例如: ```json { "logger": { "dir": "/path/to/config/config.default.js" } } ``` [Set]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set [extend]: https://github.com/justmoon/node-extend [extend2]: https://github.com/eggjs/extend2 --- --- url: /basics/config.md --- This framework provides powerful and extensible configuration function, including automatically merging applications, plugins, and framework's configuration. In addition, it allows users to overwrite configuration in sequence and maintain different configs depending on different environments. The result (i.e. merged config) can be accessed from `app.config `. Here are some common control tactics: 1. Using platform to manage configurations: while building a new application, you can put the current environment configuration into package and trigger the configuration as long as you run this application. But this certain application won't be able to build several deployments at once, and you will get into trouble whenever you want to use the configuration in localhost. 2. Using platform to manage configurations: you can pass the current environment configuration via environment variables while starting. This is a relatively elegant approach with higher requirement on operation and support from configuration platform. Moreover, The configuration environment has same flaws as first method. 3. Using code to manage configurations: you can add some environment configurations in codes and pass them to current environment arguments while starting. However, it doesn't allow you to configure globally and you need to alter your code whenever you want to change the configuration. we choose the last strategy, namely **configure with code**, The change of configuration should be also released after reviewing. The application package itself is capable to be deployed in several environments, only need to specify the running environment. ### Multiple Environment Configuration This framework supports loading configuration according to the environment and defining configuration files of multiple environments. For more details, please check [env](../basics/env.md). ``` config |- config.default.js |- config.prod.js |- config.unittest.js |- config.local.js ``` `config.default.js` is the default file for configuration, and all environments will load this file. Besides, this is usually used as default configuration file for development environment. The corresponding configuration file(named configuration) will be loaded simultaneously when you set up env. The named configuration and the default configuration will combine(use [extend2](https://www.npmjs.com/package/extend2) deep clone) into a configuration eventually. And the same name will be overwritten. For example, `prod` environment will load `config.prod.js` and `config.default.js`. As a result, `config.prod.js` will overwrite the configuration with identical name in `config.default.js`. ### How to Write Configuration The configuration file returns an object which could overwrite some configurations in the framework. Application can put its own business configuration into it for convenient management. ```js // configure the catalog of logger,the default configuration of logger is provided by framework module.exports = { logger: { dir: '/home/admin/logs/demoapp', }, }; ``` The configuration file can simplify to `exports.key = value` format ```js exports.keys = 'my-cookie-secret-key'; exports.logger = { level: 'DEBUG', }; ``` The configuration file can also return a function which could receive a parameter called `appInfo` ```js // put the catalog of logger to the catalog of codes const path = require('path'); module.exports = (appInfo) => { return { logger: { dir: path.join(appInfo.baseDir, 'logs'), }, }; }; ``` The build-in appInfo contains: | appInfo | elaboration | | ------- | ------------------------------------------------------------------------------------------------------------- | | pkg | package.json | | name | Application name, same as pkg.name | | baseDir | The directory of codes | | HOME | User directory, e.g, the account of admin is /home/admin | | root | The application root directory, if the environment is local or unittest, it is baseDir. Otherwise, it is HOME | `appInfo.root` is an elegant adaption. for example, we tend to use `/home/admin/logs` as the catalog of log in the server environment, while we don't want to pollute the user catalog in local development. This adaptation is very good at solving this problem. Choose the appropriate style according to the specific situation, but please make sure you don't make mistake like the code below: ```js // config/config.default.js exports.someKeys = 'abc'; module.exports = (appInfo) => { const config = {}; config.keys = '123456'; return config; }; ``` ### Sequence of Loading Configurations Applications, plugin components and framework are able to define those configs. Even though the structure of catalog is identical but there is priority (application > framework > plugin). Besides, the running environment has the higher priority. Here is one sequence of loading configurations under "prod" environment, in which the following configuration will overwrite the previous configuration with the same name. ``` -> plugin config.default.js -> framework config.default.js -> application config.default.js -> plugin config.prod.js -> framework config.prod.js -> application config.prod.js ``` **Note: there will be plugin loading sequence, but the approximate order is similar. For specific logic, please check the [loader](../advanced/loader.md) .** ### Rules of Merging Configs are merged using deep copy from \[extend2] module, which is forked from \[extend] and process array in a different way. ```js const a = { arr: [1, 2], }; const b = { arr: [3], }; extend(true, a, b); // => { arr: [ 3 ] } ``` As demonstrated above, the framework will overwrite arrays instead of merging them. ### Configuration Result The final merged config will be dumped to `run/application_config.json`(for worker process) and `run/agent_config.json`(for agent process) when the framework started, which can help analyzing problems. Some fields are hidden in the config file, mainly including 2 types: * like passwords, secret keys and other security related fields which can be configured in `config.dump.ignore` and only [Set](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set) type is accepted. See [Default Configs](https://github.com/eggjs/egg/blob/master/config/config.default.js) * like Function, Buffer, etc. whose content converted by `JSON.stringify` will be specially large. `run/application_config_meta.json` (for worker process)and `run/agent_config_meta.json` (for agent process) will also be dumped in order to check which file defines the property, see below ```json { "logger": { "dir": "/path/to/config/config.default.js" } } ``` --- --- url: /community/CONTRIBUTING.md --- If you have any comment or advice, please report your [issue](https://github.com/eggjs/egg/issues), or make any change as you wish and submit a [PR](https://github.com/eggjs/egg/pulls). ## Reporting New Issues * Please specify what kind of issue it is. * Before you report an issue, please search for related issues. Make sure you are not going to open a duplicate issue. * Explain your purpose clearly in tags(see **Useful Tags**), title, or content. Egg group members will confirm the purpose of the issue, replace more accurate tags for it, identify related milestone, and assign developers working on it. Tags can be divided into two groups, `type` and `scope`. * type: What kind of issue, e.g. `feature`, `bug`, `documentation`, `performance`, `support` ... * scope: What did you modified. Which files are modified, e.g. `core: xx`, `plugin: xx`, `deps: xx` ### Useful Tags * `support`: the issue asks helps from developers of our group. If you need helps to locate and handle problems or have any idea to improve Egg, mark it as `support`. * `bug`: if you find a problem which possiblly could be a bug, please tag it as `bug`. Then our group members will review that issue. If it is confirmed as a bug by our group member, this issue will be tagged as `confirmed`. * A confirmed bug will be resolved prior. * If the bug has negative impact on running online application, it will be tagged as `critical`, which refers to top priority, and will be fixed ASAP! * A bug will be fixed from lowest necessary version, e.g. A bug needs to be fixed from 0.9.x, then this issue will be tagged as `0.9`, `0.10`, `1.0`, `1.1`, referring that the bug is required to be fixed in those versions. * `core: xx`: the issue is related to core, e.g. `core: loader` refers that the issue is related with `loader` config. * `plugin: xx`: the issue is related to plugins. e.g. `plugin: session` refers that the issue is related to `session` plugin. * `deps: xx`: the issue is related to `dependencies`, e.g. `deps:egg-cors` refers that the issue is related to `egg-cors` * `chore: documentation`: the issue is about documentation. Need to modify documentation. ## Documentation All features must be submitted along with documentations. The documentations should satify several requirements. * Documentations must clarify one or more aspects of the feature, depending on the nature of feature: what it is, why it happens and how it works. * It's better to include a series of procedues to explain how to fix the problem. You are also encourgaed to provide **simple, but self-explanatory** demo. All demos should be compiled at [eggjs/examples](https://github.com/eggjs/examples) repository. * Please provide essential urls, such as application process, terminology explainations and references. ## Submitting Code ### Pull Request Guide If you are developer of egg repo and you are willing to contribute, feel free to create a new branch, finish your modification and submit a PR. Egg group will review your work and merge it to master branch. ```bash # Create a new branch for development. The name of branch should be semantic, avoiding words like 'update' or 'tmp'. We suggest to use feature/xxx, if the modification is about to implement a new feature. $ git checkout -b branch-name # Run the test after you finish your modification. Add new test cases or change old ones if you feel necessary $ npm test # If your modification pass the tests, congradulations it's time to push your work back to us. Notice that the commit message should be wirtten in the following format. $ git add . # git add -u to delete files $ git commit -m "fix(role): role.use must xxx" $ git push origin branch-name ``` Then you can create a Pull Request at [egg](https://github.com/eggjs/egg/pulls) No one can garantee how much will be remembered about certain PR after some time. To make sure we can easily recap what happened previously, please provide the following information in your PR. 1. Need: What function you want to achieve (Generally, please point out which issue is related). 2. Updating Reason: Different with issue. Briefly describe your reason and logic about why you need to make such modification. 3. Related Testing: Briefly descirbe what part of testing is relevant to your modification. 4. User Tips: Notice for Egg users. You can skip this part, if the PR is not about update in API or potential compatibility problem. ### Style Guide Eslint can help to identify styling issues that may exist in your code. Your code is required to pass the test from eslint. Run the test locally by `$ npm run lint`. ### Commit Message Format You are encouraged to use [angular commit-message-format](https://github.com/angular/angular.js/blob/master/DEVELOPERS.md#-git-commit-guidelines) to write commit message. In this way, we could have a more trackable history and an automatically generated changelog. ```xml ():