Git Product home page Git Product logo

tegg's Introduction

@eggjs/tegg

Install

# tegg 注解
npm i --save @eggjs/tegg
# tegg 插件
npm i --save @eggjs/tegg-plugin

Config

// config/plugin.js
exports.tegg = {
  package: '@eggjs/tegg-plugin',
  enable: true,
};
// config/config.default.js
{
  tegg: {
    // 读取模块支持自定义配置,可用于扩展或过滤不需要的模块文件
    readModuleOptions: {
      extraFilePattern: ['!**/dist', '!**/release'],
    },
  };
}

Usage

原型

module 中的对象基本信息,提供了

  • 实例化方式:每个请求实例化/全局单例/每次注入都实例化
  • 访问级别:module 外是否可访问

ContextProto

每次请求都会实例化一个 ContextProto,并且只会实例化一次

定义
@ContextProto(params: {
  // 原型的实例化名称
  // 默认行为:会把 Proto 的首字母转为小写
  // 如 UserAdapter 会转换为 userAdapter
  // 如果有不符合预期的可以手动指定,比如
  // @ContextProto({ name: 'mistAdapter' })
  // MISTAdapter
  // MISTAdapter 的实例名称即为 mistAdapter
  name?: string;

  // 对象是在 module 内可访问还是全局可访问
  // PRIVATE: 仅 module 内可访问
  // PUBLIC: 全局可访问
  // 默认值为 PRIVATE
  accessLevel?: AccessLevel;
})
示例
简单示例
import { ContextProto } from '@eggjs/tegg';

@ContextProto()
export class HelloService {
  async hello(): Promise<string> {
    return 'hello, module!';
  }
}
复杂示例
import { ContextProto, AccessLevel } from '@eggjs/tegg';

@ContextProto({
  accessLevel: AccessLevel.PUBLIC,
  name: 'helloInterface',
})
export default class HelloService {
  async hello(user: User): Promise<string> {
    const echoResponse = await this.echoAdapter.echo({ name: user.name });
    return `hello, ${echoResponse.name}`;
  }
}

SingletonProto

整个应用声明周期只会实例化一个 SingletonProto

定义
@SingletonProto(params: {
  // 原型的实例化名称
  // 默认行为:会把 Proto 的首字母转为小写
  // 如 UserAdapter 会转换为 userAdapter
  // 如果有不符合预期的可以手动指定,比如
  // @SingletonProto({ name: 'mistAdapter' })
  // MISTAdapter
  // MISTAdapter 的实例名称即为 mistAdapter
  name?: string;

  // 对象是在 module 内可访问还是全局可访问
  // PRIVATE: 仅 module 内可访问
  // PUBLIC: 全局可访问
  // 默认值为 PRIVATE
  accessLevel?: AccessLevel;
})
示例
简单示例
import { SingletonProto } from '@eggjs/tegg';

@SingletonProto()
export class HelloService {
  async hello(): Promise<string> {
    return 'hello, module!';
  }
}
复杂示例
import { SingletonProto, AccessLevel } from '@eggjs/tegg';

@SingletonProto({
  accessLevel: AccessLevel.PUBLIC,
  name: 'helloInterface',
})
export class HelloService {
  async hello(user: User): Promise<string> {
    const echoResponse = await this.echoAdapter.echo({ name: user.name });
    return `hello, ${echoResponse.name}`;
  }
}

MultiInstanceProto

支持一个类有多个实例。比如说 logger 可能会初始化多个,用来输出到不同文件,db 可能会初始化多个,用来连接不同的数据库。 使用这个注解可以方便的对接外部资源。

定义
// 静态定义
@MultiInstanceProto(params: {
  // 对象的生命周期
  // CONTEXT: 每个上下文都有一个实例
  // SINGLETON: 整个应用生命周期只有一个实例
  initType?: ObjectInitTypeLike;

  // 对象是在 module 内可访问还是全局可访问
  // PRIVATE: 仅 module 内可访问
  // PUBLIC: 全局可访问
  // 默认值为 PRIVATE
  accessLevel?: AccessLevel;

  // 高阶参数,指定类型的实现原型
  protoImplType?: string;

  // 对象元信息
  objects: ObjectInfo[];
})

// 动态定义
@MultiInstanceProto(params: {
  // 对象的生命周期
  // CONTEXT: 每个上下文都有一个实例
  // SINGLETON: 整个应用生命周期只有一个实例
  initType?: ObjectInitTypeLike;

  // 对象是在 module 内可访问还是全局可访问
  // PRIVATE: 仅 module 内可访问
  // PUBLIC: 全局可访问
  // 默认值为 PRIVATE
  accessLevel?: AccessLevel;

  // 高阶参数,指定类型的实现原型
  protoImplType?: string;

  // 动态调用,获取对象元信息
  // 仅会调用一次,调用后结果会被缓存
  getObjects(ctx: MultiInstancePrototypeGetObjectsContext): ObjectInfo[];
})
示例

首先定义一个自定义 Qualifier 注解

import {
  QualifierUtil,
  EggProtoImplClass,
} from '@eggjs/tegg';

export const LOG_PATH_ATTRIBUTE = Symbol.for('LOG_PATH_ATTRIBUTE');

export function LogPath(name: string) {
  return function(target: any, propertyKey: PropertyKey) {
    QualifierUtil.addProperQualifier(target.constructor as EggProtoImplClass, propertyKey, LOG_PATH_ATTRIBUTE, name);
  };
}

定一个具体实现

import {
  MultiInstanceProto,
  MultiInstancePrototypeGetObjectsContext,
  LifecycleInit,
  LifecycleDestroy,
  QualifierUtil,
  EggProtoImplClass,
} from '@eggjs/tegg';
import { EggObject, ModuleConfigUtil, EggObjectLifeCycleContext } from '@eggjs/tegg/helper';
import fs from 'node:fs';
import { Writable } from 'node:stream';
import path from 'node:path';
import { EOL } from 'node:os';

export const LOG_PATH_ATTRIBUTE = Symbol.for('LOG_PATH_ATTRIBUTE');

@MultiInstanceProto({
  // 从 module.yml 中动态获取配置来决定需要初始化几个对象
  getObjects(ctx: MultiInstancePrototypeGetObjectsContext) {
    const config = ModuleConfigUtil.loadModuleConfigSync(ctx.unitPath);
    return (config as any).features.logger.map(name => {
      return {
        name: 'dynamicLogger',
        qualifiers: [{
          attribute: LOG_PATH_ATTRIBUTE,
          value: name,
        }],
      }
    });
  },
})
export class DynamicLogger {
  stream: Writable;
  loggerName: string;

  @LifecycleInit()
  async init(ctx: EggObjectLifeCycleContext, obj: EggObject) {
    // 获取需要实例化对象的 Qualifieri
    const loggerName = obj.proto.getQualifier(LOG_PATH_ATTRIBUTE);
    this.loggerName = loggerName as string;
    this.stream = fs.createWriteStream(path.join(ctx.loadUnit.unitPath, `${loggerName}.log`));
  }

  @LifecycleDestroy()
  async destroy() {
    return new Promise<void>((resolve, reject) => {
      this.stream.end(err => {
        if (err)  {
          return reject(err);
        }
        return resolve();
      });
    });
  }

  info(msg: string) {
    return new Promise<void>((resolve, reject) => {
      this.stream.write(msg + EOL,err => {
        if (err)  {
          return reject(err);
        }
        return resolve();
      });
    });
  }

}

使用 DynamicLogger.

@SingletonProto()
export class Foo {
  @Inject({
    name: 'dynamicLogger',
  })
  // 通过自定义注解来指定 Qualifier
  @LogPath('foo')
  fooDynamicLogger: DynamicLogger;

  @Inject({
    name: 'dynamicLogger',
  })
  @LogPath('bar')
  barDynamicLogger: DynamicLogger;

  async hello(): Promise<void> {
    await this.fooDynamicLogger.info('hello, foo');
    await this.barDynamicLogger.info('hello, bar');
  }
}

生命周期 hook

由于对象的生命周期交给了容器来托管,代码中无法感知什么时候对象初始化,什么时候依赖被注入了。所以提供了生命周期 hook 来实现这些通知的功能。

定义
/**
 * lifecycle hook interface for egg object
 */
interface EggObjectLifecycle {
  /**
   * call after construct
   */
  postConstruct?(): Promise<void>;

  /**
   * call before inject deps
   */
  preInject?(): Promise<void>;

  /**
   * call after inject deps
   */
  postInject?(): Promise<void>;

  /**
   * before object is ready
   */
  init?(): Promise<void>;

  /**
   * call before destroy
   */
  preDestroy?(): Promise<void>;

  /**
   * destroy the object
   */
  destroy?(): Promise<void>;
}
示例
import { EggObjectLifecycle } from '@eggjs/tegg';

@ContextProto()
export class Foo implements EggObjectLifecycle {
  // 构造函数
  constructor() {
  }

  async postConstruct(): Promise<void> {
    console.log('对象构造完成');
  }

  async preInject(): Promise<void> {
    console.log('依赖将要注入');
  }

  async postInject(): Promise<void> {
    console.log('依赖注入完成');
  }

  async init(): Promise<void> {
    console.log('执行一些异步的初始化过程');
  }

  async preDestroy(): Promise<void> {
    console.log('对象将要释放了');
  }

  async destroy(): Promise<void> {
    console.log('执行一些释放资源的操作');
  }
}
生命周期方法装饰器

上面展示的 hook 是通过方法命名约定来实现生命周期 hook,我们还提供了更加可读性更强的装饰器模式。

import {
  LifecyclePostConstruct,
  LifecyclePreInject,
  LifecyclePostInject,
  LifecycleInit,
  LifecyclePreDestroy,
  LifecycleDestroy,
} from '@eggjs/tegg';

@SingletonProto({
  accessLevel: AccessLevel.PUBLIC,
  name: 'helloInterface',
})
export class HelloService {
  @LifecyclePostConstruct()
  protected async _postConstruct() {
    console.log('对象构造完成');
  }

  @LifecyclePreInject()
  protected async _preInject() {
    console.log('依赖将要注入');
  }

  @LifecyclePostInject()
  protected async _postInject() {
    console.log('依赖注入完成');
  }

  @LifecycleInit()
  protected async _init() {
    console.log('执行一些异步的初始化过程');
  }

  @LifecyclePreDestroy()
  protected async _preDestroy() {
    console.log('对象将要释放了');
  }

  @LifecycleDestroy()
  protected async _destroy() {
    console.log('执行一些释放资源的操作');
  }

  async hello(user: User) {
    const echoResponse = await this.echoAdapter.echo({ name: user.name });
    return `hello, ${echoResponse.name}`;
  }
}

注入

Proto 中可以依赖其他的 Proto,或者 egg 中的对象。

定义

@Inject(param?: {
  // 注入对象的名称,在某些情况下一个原型可能有多个实例
  // 比如说 egg 的 logger
  // 默认为属性名称
  name?: string;
  // 注入原型的名称
  // 在某些情况不希望注入的原型和属性使用一个名称
  // 默认为属性名称
  proto?: string;
})

示例

简单示例
import { EggLogger } from 'egg';
import { Inject } from '@eggjs/tegg';

@ContextProto()
export class HelloService {
  @Inject()
  logger: EggLogger;

  async hello(user: User): Promise<string> {
    this.logger.info(`[HelloService] hello ${user.name}`);
    const echoResponse = await this.echoAdapter.echo({ name: user.name });
    return `hello, ${echoResponse.name}`;
  }
}
复杂示例
import { EggLogger } from 'egg';
import { Inject } from '@eggjs/tegg';

@ContextProto()
export class HelloService {
  // 在 config.default.js 中
  // 配置了 customLogger,
  // 名称为 bizLogger
  @Inject({ name: 'bizLogger' })
  logger: EggLogger;

  async hello(user: User): Promise<string> {
    this.logger.info(`[HelloService] hello ${user.name}`);
    const echoResponse = await this.echoAdapter.echo({ name: user.name });
    return `hello, ${echoResponse.name}`;
  }
}

限制

  • ContextProto 可以注入任何 Proto 但是 SingletonProto 不能注入 ContextProto
  • 原型之间不允许有循环依赖,比如 Proto A - inject -> Proto B - inject- > Proto A,这种是不行的
  • 类似原型之间不允许有循环依赖,module 直接也不能有循环依赖
  • 一个 module 内不能有实例化方式和名称同时相同的原型
  • 不可以注入 ctx/app,用什么注入什么

兼容 egg

egg 中有 extend 方式,可以将对象扩展到 Context/Application 上,这些对象均可注入。Context 上的对象类比为 ContextProto,Application 上的对象类比为 SingletonProto。

进阶

module 内原型名称冲突

一个 module 内,有两个原型,原型名相同,实例化不同,这时直接 Inject 是不行的,module 无法理解具体需要哪个对象。这时就需要告知 module 需要注入的对象实例化方式是哪种。

定义
@InitTypeQualifier(initType: ObjectInitType)
示例
import { EggLogger } from 'egg';
import { Inject, InitTypeQualifier, ObjectInitType } from '@eggjs/tegg';

@ContextProto()
export class HelloService {
  @Inject()
  // 明确指定实例化方式为 CONTEXT 的 logger
  @InitTypeQualifier(ObjectInitType.CONTEXT)
  logger: EggLogger;
}
module 间原型名称冲突

可能多个 module 都实现了名称为 HelloAdapter 的原型, 且 accessLevel = AccessLevel.PUBLIC,需要明确的告知 module 需要注入的原型来自哪个 module.

定义
@ModuleQualifier(moduleName: string)
示例
import { EggLogger } from 'egg';
import { Inject, InitTypeQualifier, ObjectInitType } from '@eggjs/tegg';

@ContextProto()
export class HelloService {
  @Inject()
  // 明确指定使用来自 foo module 的 HelloAdapter
  @ModuleQualifier('foo')
  helloAdapter: HelloAdapter;
}

egg 内 ctx/app 命名冲突

egg 内可能出现 ctx 和 app 上有同名对象的存在,我们可以通过使用 EggQualifier 来明确指定注入的对象来自 ctx 还是 app。不指定时,默认注入 app 上的对象。

定义
@EggQualifier(eggType: EggType)
示例
import { EggLogger } from 'egg';
import { Inject, EggQualifier, EggType } from '@eggjs/tegg';

@ContextProto()
export class HelloService {
  @Inject()
  // 明确指定注入 ctx 上的 foo 而不是 app 上的 foo
  @EggQualifier(EggType.CONTEXT)
  foo: Foo;
}

单测

单测 Context

在单测中需要获取 egg Context 时,可以使用以下 API。

export interface Application {
  /**
   * 创建 module 上下文 scope
   */
  mockModuleContextScope<R=any>(this: MockApplication, fn: (ctx: Context) => Promise<R>, data?: any): Promise<R>;
}

获取对象实例

在单测中需要获取 module 中的对象实例时,可以使用以下 API。

export interface Application {
  /**
   * 通过一个类来获取实例
   */
  getEggObject<T> (clazz: EggProtoImplClass<T> ): Promise <T>;
  /**
   * 通过对象名称来获取实例
   */
  getEggObjectFromName<T>(name: string, qualifiers?: QualifierInfo | QualifierInfo[]): Promise<unknown>;
}

export interface Context {
  /**
   * 通过一个类来获取实例
   */
  getEggObject<T> (clazz: EggProtoImplClass<T> ): Promise <T>;
  /**
   * 通过对象名称来获取实例
   */
  getEggObjectFromName<T>(name: string, qualifiers?: QualifierInfo | QualifierInfo[]): Promise<unknown>;
}

Egg 兼容性

目前 module 尚未实现所有 egg 的特性,如果需要使用 egg 的功能,可以通过一些方式来兼容。

Schedule

目前 Schedule 尚未实现注解,仍然需要使用 egg 的目录和继承方式,在这种场景下如果需要使用 module 的实现,需要使用 ctx.beginModuleScope。举个例子:

// notify/EC_FOO.js

import { Subscription, Context } from 'egg;

class FooSubscriber extends Subscription {
  private readonly ctx: Context;

  constructor(ctx: Context) {
    super(ctx);
  }

  async subscribe(msg) {
    await ctx.beginModuleScope(async () => {
      await ctx.module.fooService.hello(msg);
    });
  }
}

module.exports = Subscription;

注入 egg 对象

module 会自动去遍历 egg 的 Application 和 Context 对象,获取其所有的属性,所有的属性都可以进行无缝的注入。举个例子,如何注入现在的 egg proxy:

import { IProxy } from 'egg'

@ContextProto()
class FooService {
  @Inject()
  private readonly proxy: IProxy;

  get fooFacade() {
    return this.proxy.fooFacade;
  }
}
注入 logger

专为 logger 做了优化,可以直接注入 custom logger。

举个例子:

有一个自定义的 fooLogger

// config.default.js
exports.customLogger = {
  fooLogger: {
    file: 'foo.log',
  },
};

代码中可以直接注入:

import { EggLogger } from 'egg';

class FooService {
  @Inject()
  private fooLogger: EggLogger;
}

注入 egg 方法

由于 module 注入时,只可以注入对象,不能注入方法,如果需要使用现有 egg 的方法,就需要对方法进行一定的封装。

举个例子:假设 context 上有一个方法是 getHeader,在 module 中需要使用这个方法需要进行封装。

// extend/context.ts

export default {
  getHeader() {
    return '23333';
  }
}

先将方法封装成一个对象。

// HeaderHelper.ts

class HeaderHelper {
  constructor(ctx) {
    this.ctx = ctx;
  }

  getHeader(): string {
    return this.ctx.getHeader();
  }
}

再将对象放到 Context 扩展上即可。

// extend/context.ts

const HEADER_HELPER = Symbol('context#headerHelper');

export default {
  get headerHelper() {
    if (!this[HEADER_HELPER]) {
      this[HEADER_HELPER] = new HeaderHelper(this);
    }
    return this[HEADER_HELPER];
  }
}

生命周期

在 module 中,每个对象实例都有自己的生命周期,开发者可以对每个对象进行细致的控制。只要为对象实现 module 定义好的接口即可。所有生命周期 hook 均为可选方法,不需要的可以不实现。

接口定义

interface EggObjectLifecycle {
  /**
   * 在对象的构造函数执行完成之后执行
   */
  async postConstruct?();

  /**
   * 在注入对象依赖之前执行
   */
  async preInject?();

  /**
   * 在注入对象依赖之后执行
   */
  async postInject?();

  /**
   * 执行对象自定义异步初始化函数
   */
  async init?();

  /**
   * 在对象释放前执行
   */
  async preDestroy?();

  /**
   * 释放对象依赖的底层资源
   */
  async destroy?();
}

实现

import { EggObjectLifecycle } from '@eggjs/tegg';

@SingletonProto()
class FooService implement EggObjectLifecycle {
  @Inject()
  cacheService: CacheService;

  cache: Record<string, string>;

  async init() {
    this.cache = await this.cacheService.get(key);
  }
}

异步任务

module 在请求结束后会把请求相关的对象释放,所以在请求中使用 process.nextTicksetTimeoutsetInterval这类接口并不安全,可能导致一些错误。因此需要使用框架提供的接口,以便框架了解当前请求有哪些异步任务在执行,等异步任务执行完成后再释放对象。

安装

npm i --save @eggjs/tegg-background-task

使用

import { BackgroundTaskHelper } from '@eggjs/tegg-background-task';

@ContextProto()
export default class BackgroundService {
  @Inject()
  private readonly backgroundTaskHelper: BackgroundTaskHelper;

  async backgroundAdd() {
    this.backgroundTaskHelper.run(async () => {
      // do the background task
    });
  }
}

超时时间

框架不会无限的等待异步任务执行,在默认 5s 之后如果异步任务还没有完成则会放弃等待开始执行释放过程。如果需要等待更长的时间,建议有两种方式:

  • 推荐方式:将异步任务转发给单例对象(SingletonProto)来执行,单例对象永远不会释放
  • 调整超时时间,对 backgroundTaskHelper.timeout 进行赋值即可
  • 如果将超时时间设置为 Infinity,框架将不会超时
  • 可以在 config 文件中指定 backgroundTask.timeout 来全局覆盖默认的超时时间

动态注入

使用

定义一个抽象类和一个类型枚举。

export enum HelloType {
  FOO = 'FOO',
  BAR = 'BAR',
}

export abstract class AbstractHello {
  abstract hello(): string;
}

定义一个自定义枚举。

import { ImplDecorator, QualifierImplDecoratorUtil } from '@eggjs/tegg';
import { ContextHelloType } from '../FooType';
import { AbstractContextHello } from '../AbstractHello';

export const HELLO_ATTRIBUTE = 'HELLO_ATTRIBUTE';

export const Hello: ImplDecorator<AbstractHello, typeof HelloType> =
  QualifierImplDecoratorUtil.generatorDecorator(AbstractHello, HELLO_ATTRIBUTE);

实现抽象类。

import { ContextProto } from '@eggjs/tegg';
import { ContextHello } from '../decorator/Hello';
import { ContextHelloType } from '../FooType';
import { AbstractContextHello } from '../AbstractHello';

@ContextProto()
@Hello(HelloType.BAR)
export class BarHello extends AbstractHello {
  hello(): string {
    return `hello, bar`;
  }
}

动态获取实现。

import { EggObjectFactory } from '@eggjs/tegg';

@ContextProto()
export class HelloService {
  @Inject()
  private readonly eggObjectFactory: EggObjectFactory;

  async hello(): Promise<string> {
    const helloImpl = await this.eggObjectFactory.getEggObject(AbstractHello, HelloType.BAR);
    return helloImpl.hello();
  }
}

动态获取多个实现,通过 for/await 循环获得实例。

import { EggObjectFactory } from '@eggjs/tegg';

@ContextProto()
export class HelloService {
  @Inject()
  private readonly eggObjectFactory: EggObjectFactory;

  async hello(): Promise<string[]> {
    const helloImpls = await this.eggObjectFactory.getEggObjects(AbstractHello);
    const messages = [];
    for await (const helloImpl of helloImpls) {
      messages.push(helloImpl.hello());
    }
    return messages;
  }
}

配置项目 module

一般情况下,无需在项目中手动声明包含了哪些 module,tegg 会自动在项目目录下进行扫描。但是会存在一些特殊的情况,tegg 无法扫描到,比如说 module 是通过 npm 包的方式来发布。因此 tegg 支持通过 config/module.json 的方式来声明包含了哪些 module。

支持通过 path 引用 app/modules/foo 目录下的 module。

[
  {"path":  "../app/modules/foo"}
]

支持通过 package 引用使用 npm 发布的 module。

[
  {"package": "foo"}
]

tegg's People

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

tegg's Issues

Implement 'diff' functionality in SqlGenerator.ts

A TODO comment in indicates that the 'diff' functionality is missing. This issue tracks the implementation of this functionality as discussed in PR #192 and the specific comment #192 (comment).

Please ensure to review the context of the PR and the discussion to understand the requirements and expectations for this implementation.

what happens the lastest version of tegg just won't work

What happens?

最小可复现仓库

请使用 npm init egg --type=simple bug 创建,并上传到你的 GitHub 仓库

复现步骤,错误日志以及相关配置

相关环境信息

  • 操作系统
  • Node 版本
  • Egg 版本

egg-bin dev 启动太慢

在此输入你需要反馈的 Bug 具体信息(Bug in Detail):

当我下载完tegg相关插件后,运行项目,本地启动太慢,希望官方解决

可复现问题的仓库地址(Reproduction Repo)

Node 版本号:

18.0.0

TEgg 版本号:

3.12.0

相关插件名称与版本号:

3.12.0

操作平台与版本号:

3.12.0

framework.EggPrototypeNotFound: Object searchAdapter not found in LOAD_UNIT:cnpmcoreRepository 有什么降级方案吗

在此输入你需要反馈的 Bug 具体信息(Bug in Detail):

使用 cnpmcore + tegg 开发,cnpmcore v3.38.2, tegg v3.12.0 版本,原本没有问题,升级到 v3.29.0 后启动报错 framework.EggPrototypeNotFound: Object searchAdapter not found in LOAD_UNIT:cnpmcoreRepository;

锁版本 tegg 相关包锁在 3.12.0,cnpmcore v3.38.2,报错 nodejs.ERR_PACKAGE_PATH_NOT_EXPORTEDError: [egg-core] load file: /Users/xxx/demo/node_modules/@eggjs/tegg-orm-plugin/app.js, error: Package subpath './src/setup_hooks' is not defined by "exports" in /Users/xxx/demo/node_modules/leoric/package.json

查看 cnpmcore 依赖后,发现 "@eggjs/tegg": "^3.12.0",所以会安装依赖 3.12.0 3.29.0,这两个版本不兼容吗?

可复现问题的仓库地址(Reproduction Repo)

Node 版本号:

18.19.0

TEgg 版本号:

3.29.0

相关插件名称与版本号:

cnpmcore, 3.38.2

操作平台与版本号:

macOS 13.5.2 (22G91)

[RFC] 增强Controller能力与相关HttpMethod

  • 开始时间: 2021/12/16
  • RFC PR:
  • tegg Issue:

背景

参照: 提供更加简单化请求方法的修饰器的讨论

思路

所有Controller 类型会通过namespace进行区分

  • 为了方便区分ControllerRestControllerHttpControllerRpcControlle需要单独划分命名空间以方便归类。 将HttpController和简化后的Controller将会被划分到
    http namespace, RestController 会被划分到rest namespace,RpcControlle会被划分到 rpc namespace。

  • 以下将通过import的形式具体描述:

    import {Controller, HttpController} from '@eggjs/tegg/http';
    import {RestController} from '@eggjs/tegg/rest';
    import {RpcController} from '@eggjs/tegg/rpc';

新增baseRoot

  • config新增baseRoot能力

    tegg:{
      baseRoot:string
    }
  • 通过这个字段可以全局调整路由根路径

    //config.default.ts
    tegg:{
      baseRoot:'/tegg'
    }
    
    //controller/fruitController.ts
    
    @RestController('/api/v1')
    //or Controller('/api/v1')
    class Fruit {
        // GET /api/v1/
        @List
        home() {
        }
    }

    此时路径会被修正到‘/tegg/api/v1'’

新增Controller

  • 所有的方法入参暂定为继承自HttpController:

    Controller(path?: string, protoName?: string, controllerName?: string);

新增Controller下Http Method修饰器

  • 新的Http Method 会跟 HTTPMethodEnum 的枚举保持一致,包含 GETPOSTPUTDELETEPATCHOPTIONSHEAD

  • 所有的方法入参暂时定为继承自HTTPMethod,以GET为例:

    Get(path?: string, priority?: number)
  • 与HTTPMethod不同的是Get第一个入参为可选,如果Get的path入参为空时, 可以通过检索Handler Function 名称自动匹配路径。 下面以GET为例:

    @Controller('/api/v1')
    class Fruit {
        // GET /api/v1/hello
        @Get()
        hello() {
        }
    }

    以下方代码为例子,如果要处理当前Controller的跟路径,请使用Get('/')

    @Controller('/api/v1')
    class Fruit {
      // GET /api/v1/
      @Get('/')
      home() {
      }
    }

新增RestController修饰

  1. 新增RestController修饰,这个修饰器为HttpController修饰器的子类,主要是HttpController的扩展
  2. 基于RestController实现相关Restful 请求特性。

RESTful的独有特性

  • 与常规HttpController不同,RestController将自动携带 Hypermedia,
    API,即返回结果中提供链接,连向其他API方法,使得用户不查文档,也知道下一步应该做什么。
    比如访问 https://api.github.com/ 即可获得所有api 访问api.github.com/user,然后就得到了下面结果。

    {
      "message": "Requires authentication",
      "documentation_url": "https://docs.github.com/rest/reference/users#get-the-authenticated-user"
    }

具体能力

  • 配置baseRoot对RestController的会产生影响,通过这个字段可以全局调整RESTful根路径。

    //config.default.ts
    tegg:{
      baseRoot:'/tegg'
    }

    此时会将rest默认的/api前缀覆盖为/tegg

      //config.default.ts
      tegg:{
        baseRoot:'/'
      }

    此时会将rest默认的/api前缀覆盖为/

  • @RestController继承自HttpController,但是与Controller不同,如果path为缺省会根据当前的class name 并以复数的形式自动补全,会在当前根目录自动生成Hypermedia API。

    type RestControllerOptions {
       docURL?:string,
       version?:string,
       protoName?: string,
       controllerName?: string,
    }
    @RestController(path?: string, options?: RestControllerOptions)

    以下方代码为例子,我们没有填写任何参数并且没有配置baseRoot字段,那么就会指向/api/v1/fruits

    @RestController()
    class Fruit {
      // GET /api/v1/fruit/apple
      @List
       apple() {}
    }

    下面我们假设进行请求

    $ GET /api/v1/fruits
    # 我们会得到
    
    {
      "apple": "/api/v1/fruits/apple"
      "documentation_url": "/doc/v1/fruit"
    }
    $GET /api/v1 
    # 我们会得到
    
    {
      "fruit": "/api/v1/fruits"
      "documentation_url": "/doc/v1"
    }

    基于Get 请求增加,额外的业务能力

    @Query支持取出单条数据

     @Query()
     foo(id:string):Promise <Foo> {}

    @List 支持取出所有数据,默认遵从RESTful风格支持常见参数过滤(可能需要配合ORM的能力)

1.支持 ?limit=10:指定返回记录的数量

2.支持 ?offset=10:指定返回记录的开始位置。

3.支持 ?page=2&per_page=100:指定第几页,以及每页的记录数。 4.根据函数的入参可以进行横向扩展,比如需要支持搜索

@List()
listFoo(keyword:string):Promise<Array<Foo>> {}

剩余业务能力支持待补充

新增RpcController修饰

待调查: 主要调查方向GRPC, JSONRPC

其他资料与难点调研

英文单词复数化
hypermedia 实现调研

egg-redis

哈喽,我想咨询一下在 tegg 里面如何使用 egg-redis 中间件

我使用这种方式可以 log 出值 redies.get('foo') 的值
但是会有两个问题

  1. 使用 ctx.app.redis ts 会提示 类型“Application”上不存在属性“redis”
  2. 使用这种方式接口请求会报404 ,也没有走到 ERR

还想咨询一下,除过readme 还有其他的tegg的文档吗

index.ts

async index(@Context() ctx: EggContext) {
    try {
      // @ts-ignore
      const data = await ctx.app.redis.get('foo');
      console.log(data);
      return 'hello word';
    } catch (err) {
      console.log(err);
      return 'hello word error';
    }
  }

config.default.ts

  config.redis = {
    client: {
      port: 6379, // Redis port
      host: '127.0.0.1', // Redis host
      password: 'my_react_admin_app',
      db: 0,
    },
  };

plugin.ts

 redis: {
    enable: true,
    package: 'egg-redis',
  },

官网文档在哪啊

请详细告知你的新点子:

能够看到官网的文档。

我用按照官网的教程,选择了TS版本示例,结果发送和官网的文档没有任何对应关系。。。

分布式锁

请详细告知你的新点子:

  • Locker interface
  • 基于 redis 实现的 LockerOnRedis

装饰器问题

请详细告知你的新点子:

装饰器的文档有更全面的吗,目前的文档感觉好少,不知道怎么写啊...

实现控制器拦截器注解

  1. 实现控制器拦截器注解。对类或者类方法的执行进行拦截。类似于nestjs中的守卫

  2. 比如现在有些控制器接口有控制访问权限。我直接可以加上一个@Auth(AuthInterceptor) 注解。在AuthInterceptor里面实现自己的拦截逻辑

【addSingleton 注册插件报的一个错】:当注册 async function时,function内使用await报错

在此输入你需要反馈的 Bug 具体信息(Bug in Detail):

我这边的使用场景是:
redis地址需要动态获取,因此需要请求接口获取配置,于是自定义redis插件,初始化之前发起异步请求。

egg-redis的配置,我在demo上是瞎写的,不过不用关心,报此类错误可以忽略,关键是加了await后还走不到这一步。

演示步骤如下:
1.下方的demo仓库,我是稍微改造了官方的eggjs-redis插件:
改动点在: app/plugins/egg-redis/lib/redis.js
image
2.插件配置如下(通过path指向插件):
image
3.Service层注入redis插件如下:
image

4.加了await后,npm run dev,报错如下:
ERROR 73152 framework.EggPrototypeNotFound: Object redis not found in LOAD_UNIT:foo [ https://eggjs.org/zh-cn/faq/TEGG_EGG_PROTO_NOT_FOUND ]
......................................................................
......................................................................
......................................................................
code: TEGG_EGG_PROTO_NOT_FOUND
serialNumber: EGG_PROTO_NOT_FOUND
errorContext: ""

奇怪:
1、加await处的代码报错捕获不到,感觉一加await就像跳过了当前的执行。
2、axios、app.curl试了都不行。
3、只要去掉await,非同步执行都ok。

麻烦大佬们帮看看,非常感谢~~

可复现问题的仓库地址(Reproduction Repo)

https://github.com/yinshanhu/eggjs-addSingleton-asyncFunction-error.git

Node 版本号:

v18.12.1

TEgg 版本号:

3.5.2

相关插件名称与版本号:

操作平台与版本号:

window11

请问中间件怎么调用service

在此输入你需要反馈的 Bug 具体信息(Bug in Detail):

image

image

在中间件调用service 但是一直undefined,有没有什么方法注入到中间件里面

可复现问题的仓库地址(Reproduction Repo)

暂无

Node 版本号:

v14.17.0

TEgg 版本号:

3.5.2

相关插件名称与版本号:

egg:3.15.0 , egg-bin:5.13.4

操作平台与版本号:

win10

如何获取context

如何在类属性上获取当前访问context上下文实例

想要在BaseController上注入context属性。封装统一的返回结构。
但是貌似像我下面的写法没调用context.body进行返回数据给前端

image

可复现问题的仓库地址(Reproduction Repo)

暂无地址

Node 版本号:

v16.19.1

TEgg 版本号:

3.5.2

相关插件名称与版本号:

操作平台与版本号:

windows10

[Feature Request] 提供更加简单化请求方法的修饰器

现在的HTTPMethod 虽然足够满足业务需求,但是不够简练,是否能提供 常用的 Get,Post等方法的修饰器

@Controller('/')
export class HomeController {
  @Get('/')
  async home() {
    return 'Hello tegg!';
  }
}

顺带补充,能否提供修改全局route的前缀 ,类似于在配置文件提供

export default config={
  tegg:{
    root:"/hello"
  }
}

tegg-orm-decorator 和 sequelize怎么一起使用

在此输入你需要反馈的 Bug 具体信息(Bug in Detail):

tegg-orm-decorator 和 sequelize怎么一起使用

可复现问题的仓库地址(Reproduction Repo)

tegg-orm-decorator 和 sequelize怎么一起使用

Node 版本号:

v14.18.1

TEgg 版本号:

v3.5.2

相关插件名称与版本号:

3.6.3

操作平台与版本号:

windows10

Implement Index Left Matching in BaseSqlMapGenerator

There's a TODO comment in the method of the class, indicating the need for implementing index left matching. This enhancement is related to the Data Access Layer (DAL) features introduced in PR #192.

The specific TODO can be found in the diff for .

This issue was requested to be opened by @fengmk2 in the PR discussion: #192 (comment)

配置项目 module 使用 `pkg` 报错,与README文档不符,这里是否只支持`package`?

在此输入你需要反馈的 Bug 具体信息(Bug in Detail):

使用 pkg 直接报错

ERROR 27855 nodejs.Error: unknown type of module reference config: {"pkg":"cnpmcore/common"}
    at Function.readModuleReferenceFromModuleJson (/Users/xxx/github/hello-tegg/node_modules/@eggjs/tegg-common-util/src/ModuleConfig.ts:85:15)
    at Function.readModuleReference (/Users/xxx/github/hello-tegg/node_modules/@eggjs/tegg-common-util/src/ModuleConfig.ts:62:19)

README 文档:

image

可复现问题的仓库地址(Reproduction Repo)

测试的是
https://github.com/eggjs/examples/blob/bed580fe053ae573f8b63f6788002ff9c6e7a142/hello-tegg/config/module.json

Node 版本号:

v18.16.0

TEgg 版本号:

3.7.0

相关插件名称与版本号:

cnpmcore, 3.20.2

操作平台与版本号:

macOS 13.3.1 (22E261)

windows下引入模块失败

在此输入你需要反馈的 Bug 具体信息(Bug in Detail):

Nodejs的require.resolve不支持windows格式的路径地址

const pkgJson = path.join(moduleReferenceConfig.package, 'package.json');

比如

requier.resolve("cnpmcore\\common\\package.json")

会解析失败
需要统一转换成linux的路径格式,如:

requier.resolve("cnpmcore/common/package.json")

可复现问题的仓库地址(Reproduction Repo)

const pkgJson = path.join(moduleReferenceConfig.package, 'package.json');

Node 版本号:

18.12.0

TEgg 版本号:

3.8.0

相关插件名称与版本号:

tegg-common-util

操作平台与版本号:

Windows11

如何注入类本身使用他的静态方法的

在此输入你需要反馈的 Bug 具体信息(Bug in Detail):

  1. 我参考cnpmcore那边代码写的,想要注入后使用类的静态方法
  2. 实际使用时候没有办法获取到注入对象的静态方法

想想可以直接import进来直接用,但是我看cnpmcore里可以导入 @model() 注解下的导入,我自己按下面代码测试就注入后对象的方法

controller代码如下:

import { Inject, HTTPController, HTTPMethod, HTTPMethodEnum } from '@eggjs/tegg';
import type { UserService } from '../service/UserService';

@HTTPController({
  path: '/',
})
export class HomeController {
  @Inject()
  private readonly userService: typeof UserService;

  @HTTPMethod({
    method: HTTPMethodEnum.GET,
    path: '/',
  })
  async index() {
    this.userService.find();
    return 'hello egg';
  }
}

service代码如下:

import { AccessLevel, SingletonProto } from '@eggjs/tegg';

@SingletonProto({
  accessLevel: AccessLevel.PUBLIC
})
export class UserService {
  static find() {
    console.log('find');
  }
}

可复现问题的仓库地址(Reproduction Repo)

tegg_learn.zip

Node 版本号:

16.17.0

TEgg 版本号:

^3.5.2

相关插件名称与版本号:

"@eggjs/tegg-aop-plugin": "^3.5.2",

操作平台与版本号:

macOs 14.2 (23C64)

npm run dev报错

Your detail info about the Bug:

What I did

  1. 执行npm run tsc,清除缓存和执行tsc操作
  2. 执行npm run dev,出现以下报错
    企业微信截图_17041781754794

Reproduction Repo

Node Version

18.14.0

TEgg Version

3.12.0

Plugin Name and its version

tegg-eventbus-plugin 3.12.0

Platform and its version

Windows 10

octet-stream 怎么支持前端传输的octet-stream类型的数据,tegg怎么获取并保存

在此输入你需要反馈的 Bug 具体信息(Bug in Detail):

octet-stream 怎么支持前端传输的octet-stream类型的数据,tegg怎么获取并保存

可复现问题的仓库地址(Reproduction Repo)

octet-stream 怎么支持前端传输的octet-stream类型的数据,tegg怎么获取并保存

Node 版本号:

18.0.0

TEgg 版本号:

3.12.0

相关插件名称与版本号:

3.12.0

操作平台与版本号:

3.12.0

定时任务schedule里怎么获取ctx上下文对象

在此输入你需要反馈的 Bug 具体信息(Bug in Detail):

'use strict';
import { IntervalParams, Schedule, ScheduleType } from '@eggjs/tegg/schedule';

@schedule({
type: ScheduleType.WORKER,
scheduleData: {
interval: 600 * 10,
}
})
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
export class onlineSchedule {
async subscribe(ctx) {
console.log(ctx, 'SSEService')
}
}

是否可以支持subscribe里默认获取ctx类似于eggjs中task参数可以拿到ctx

'use strict';
const { nowTime } = require("../utils");
module.exports = app => {
return {
schedule: {
interval: '120s', // 2分钟间隔
// cron: '0 0 0 * * ?',
type: 'all', // 指定所有的 worker 都需要执行
},
async task(ctx) {
await ctx.service.global.judgeUserExpire()
console.log('2分钟执行一次,检查退出咨询情况' + nowTime())
},
};
};

可复现问题的仓库地址(Reproduction Repo)

暂无

Node 版本号:

18.0.0

TEgg 版本号:

3.12.0

相关插件名称与版本号:

3.12.0

操作平台与版本号:

3.12.0

自定义插件怎么注入

在此输入你需要反馈的 Bug 具体信息(Bug in Detail):

1.使用的是eggjs框架,egg-redis不符合我的使用场景,于是通过addSingleton注册了自己的插件,名称为:‘redis’。确认注册是成功的。

2.代码里使用时,注入的方式:
import type { Redis } from 'ioredis';
@Inject()
private readonly redis: Redis;

3、报错:
framework.EggPrototypeNotFound: Object redis not found in LOAD_UNIT:......................................非关键信息省略..........
code: TEGG_EGG_PROTO_NOT_FOUND
serialNumber: EGG_PROTO_NOT_FOUND

还请大佬帮忙解答。

可复现问题的仓库地址(Reproduction Repo)

Node 版本号:

v18.14.1

TEgg 版本号:

3.5.2

相关插件名称与版本号:

1

操作平台与版本号:

window11

Middleware decorator make case sensitive routing error

What happens?

Middleware decorator cannot make case sensitive routing.

It seems that getEggObject will get wrong ObjectMap after Middleware decorator.

eggObjectMap Map(3) {
  'LOAD_UNIT:tegg-app-controller:PROTO:853:middlewareController' => Map(1) {  },
  'LOAD_UNIT:multiModuleService:PROTO:851:appService' => Map(1) {},
  'LOAD_UNIT:multi-module-repo:PROTO:850:appRepo' => Map(1) {}
}
proto.id: LOAD_UNIT:tegg-app-controller:PROTO:857:viewController

Mini Showcase Repository(REQUIRED)

Provide a mini GitHub repository which can reproduce the issue.
Use npm init egg --type=simple bug then upload to your GitHub

add following into plugin/controller/test/http/middleware.test.ts

  it('method Middleware should not work', async () => {
    app.mockCsrf();
    await app.httpRequest()
      .get('/Middleware/Method')
      .expect(200)
      .expect('hello, view');
  });

How To Reproduce

Steps to reproduce the behavior:

  1. Use middleware with HttpController
@HTTPController({
  path: '/middleware',
})
@Middleware(countMw)
export class MiddlewareController {
  @Inject()
  appService: AppService;

  @HTTPMethod({
    method: HTTPMethodEnum.GET,
    path: '/global',
  })
  async global() {
    return {};
  }

  @HTTPMethod({
    method: HTTPMethodEnum.GET,
    path: '/method',
  })
  @Middleware(logMwFactory('use middleware'))
  async middleware() {
    return {};
  }
}

add a viewController to match other paths

@HTTPController()
export class ViewController {

  @HTTPMethod({
    method: HTTPMethodEnum.GET,
    path: '/*',
  })
  async get() {
    return 'hello, view';
  }
}
  1. try to visit a Uppercase of router path : /Middleware/Method,response 500 server error

Expected behavior

  1. router match with /* and response 'hello, view'

Context

  • Node Version:
  • Egg Version:
  • Plugin Name:
  • Plugin Version:
  • Platform:

在extend/context.ts 里面引入第三方包会报错

在此输入你需要反馈的 Bug 具体信息(Bug in Detail):

1、我在项目里面extend/context.ts里面,按照redmine文档,写了一个扩展属性
`
import { Session } from '@xx/eos'

const SESSION_HELPER = Symbol('context#sessionHelper');

export default {
/**
* sessionHelper
*/
get sessionHelper() {
if (!this[SESSION_HELPER]) {
this[SESSION_HELPER] = new Session(2)
}
return this[SESSION_HELPER];
},

};`

2、 在项目运行的时候会报如下错误:

`2023-12-13 20:52:43,219 ERROR 41983 nodejs.TypeError: Cannot read property 'protoImplType' of undefined
at Function.createProto (/Users/xxx/Work/docker/bytehubs-eos/node_modules/.store/@eggjs[email protected]/node_modules/@eggjs/tegg-metadata/src/factory/EggPrototypeCreatorFactory.ts:39:57)
at AppLoadUnit.init (/Users/xxx/Work/docker/bytehubs-eos/node_modules/.store/@eggjs[email protected]/node_modules/@eggjs/tegg-plugin/lib/AppLoadUnit.ts:53:55)
at processTicksAndRejections (internal/process/task_queues.js:95:5)
at async Function.createLoadUnit (/Users/xxx/Work/docker/bytehubs-eos/node_modules/.store/@eggjs[email protected]/node_modules/@eggjs/tegg-metadata/src/factory/LoadUnitFactory.ts:33:7)
at async EggModuleLoader.loadApp (/Users/xxx/Work/docker/bytehubs-eos/node_modules/.store/@eggjs[email protected]/node_modules/@eggjs/tegg-plugin/lib/EggModuleLoader.ts:15:22)
at async EggModuleLoader.load (/Users/xxx/Work/docker/bytehubs-eos/node_modules/.store/@eggjs[email protected]/node_modules/@eggjs/tegg-plugin/lib/EggModuleLoader.ts:50:5)
at async ModuleHandler.init (/Users/xxx/Work/docker/bytehubs-eos/node_modules/.store/@eggjs[email protected]/node_modules/@eggjs/tegg-plugin/lib/ModuleHandler.ts:31:7)
at async App.didLoad (/Users/xxx/Work/docker/bytehubs-eos/node_modules/.store/@eggjs[email protected]/node_modules/@eggjs/tegg-plugin/app.ts:50:5)

pid: 41983`

可复现问题的仓库地址(Reproduction Repo)

Node 版本号:

v14.19.1

TEgg 版本号:

3.28.2

相关插件名称与版本号:

"dependencies": { "@eggjs/tegg": "^3.28.1", "@eggjs/tegg-aop-plugin": "^3.5.2", "@eggjs/tegg-config": "^3.2.3", "@eggjs/tegg-controller-plugin": "^3.5.2", "@eggjs/tegg-eventbus-plugin": "^3.5.2", "@eggjs/tegg-plugin": "^3.5.2", "@eggjs/tegg-schedule-plugin": "^3.5.2", "egg": "^3.15.0", "egg-scripts": "^2.17.0", "egg-tracer": "^2.0.0", "glob": "^10.3.10", "lodash": "^4.17.21" },

操作平台与版本号:

mac

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.