1.0.0 • Published 2 years ago

mitsuki v1.0.0

Weekly downloads
-
License
AGPL-3.0
Repository
github
Last release
2 years ago

Mitsuki v0.9.5(pre-release)说明文档

介绍

​ Mitsuki-bot是基于miraimirai-ts构建的qq消息机器人,其名字取自《巧克甜恋2》中的百々瀬 みつき(Momose Mitsuki)。该项目其主要目的是构建了一个渐进式,可靠的具有清晰结构的qq机器人框架。

开源许可证

​ 由于miraimirai-ts所使用的许可证为AGPL-3.0 License,故本项目也使用AGPL-3.0 License开源许可证,请遵守相关的规范。

哲学

​ 近年来,由于 Node.js、JavaScript 已经成为 web 前端和后端应用程序的“通用开发语言”。这也促成了诸如 mirai-ts等优秀项目的出现,他们实现了在node环境中编写QQ机器人的第一步。

​ 然而,尽管 Node 拥有大量优秀的软件库、辅助程序和工具。但是这些软件库、辅助程序和工具在每一个独立的qq机器人中都需要做一次适配工作,这就使得大量开发者将会花费大量冗余的时间在一些相同的地方。而且由于实现方式的不同,对不同的qq机器人项目中的代码进行复用也会变得十分困难,这也导致了使用nodejs开发qq机器人的社区活跃度要远低于java,python等语言,尽管js拥有着世界上活跃度最高的社区。而Mitsuki-core的出现将有效地解决我们所面对的主要问题:即 架构

​ Mitsuki-core 提供了一个开箱即用的应用程序体系结构,允许开发者及其团队创建高度可测试、可扩展、松散耦合且易于维护的QQ机器人应用实例。这种架构深受 Angular以及nodejs 的启发。

安装

1.安装mcl并配置mirai-api-http

​ 相关内容请查看https://github.com/mamoe/mirai/blob/dev/docs/ConsoleTerminal.md

2.安装mitsuki

​ 通过克隆git库的形式进行安装(后续将提供mitsuki-cli的安装方式):

$ git clone https://github.com/gylove1994/mitsuki-core.git mitsuki
$ cd mitsuki
$ yarn

​ 通过nom/yarn的形式安装:

$ yarn add mitsuki
or
$ npm i mitsuki

迈出伟大的第一步

​ 如果你没有使用过angular或者nestjs等框架,直接理解以下的概念可能会是痛苦的,但是一旦了解了这些概念,你会发现这些内容将会对你产生巨大的正面的影响。无论如何,试着迈出这伟大的第一步吧!

1.Module(模块)

​ 模块是一个用@Module()装饰器注解的类。@Module()装饰器提供了mitsuki用来组织qq机器人结构的元数据。

Modules_1

​ 每个由mitsuki构建的qq机器人至少有一个模块,即根模块根模块是 mitsuki 用来构建整个应用的起点——mitsuki用来解决模块(module)和提供者(provider)关系和依赖关系的内部数据结构。虽然理论上非常小的qq机器人可能只有根模块(如同示例一样),但这不是常见的情况。我强烈建议将模块作为组织代码的有效方式。因此,对于大多数qq机器人,最终的架构将采用多个模块,每个模块都封装了一组密切相关的功能。功能模块只是用于组织与特定功能相关的一系列代码,这样做可以使代码变得井井有条的同时建立清晰的边界。尤其是随着应用程序变得越来越大和复杂时,这有助于管理复杂性并使用SOLID原则进行开发。

开放与封闭及单例模式

​ 在v0.9中,任意导入mitsuki模块中的依赖项的作用域是所有受mitsuki容器所共享,这会导致所有模块都可以直接修改由另外模块创建的内容。虽然这为一部分场景提供了方便,但是这种方式显著的增加了模块间的耦合性,使模块与模块之间的界限变得模糊,并最终可能会使mitsuki-bot实例在运行时发生无法预计的错误。所以在v1.0正式版本中,我们调整了依赖的作用域范围,将依赖的默认作用域限制在了每个模块内部,并通过元信息注解的方式导入和导出,这显著提升了模块的安全性。

模块的构建方式

​ 在v0.9中,我们使用@Module装饰器的4个参数来构建一个模块:

@Module({
  //导入非mitsuki创建的类的实例对象
  imports:[api],
  //导入控制器类
  controllers:[FirstController],
  //导入提供者
  providers:[FirstService],
  //导入其他的模块
  modules:[]
})

​ 而在v1.0中,我们更改了@Module装饰器的参数表,使其与其他框架具有较高的统一性,有效的降低了学习成本。

@Module({
  //用于导入其他的模块所导出的provider
  imports: [TestModule],
  //用于观察者订阅可观察对象(消息的处理)
  controller: [BaseController],
  //用于导入提供者
  provider: [BaseService],
  //用于导出提供者
  exports: [BaseService],
})
export class BaseModule {}

​ 对于imports provider exports 在v1.0的版本中额外添加了异步工厂的方式导入,使得动态模块的构建变得简单:

@Module({
  imports: async () => [...],
  controller: [...],
  provider: async () => [...],
  exports: async () => [...],
})
export class BaseModule {}

​ 除此之外,为了更加方便的通过所给参数动态的构建模块,我们新添加了一个DynamicModule类型,通过类的静态方法来传入参数,进而更加细致控制动态模块的创建过程:

public static importRepository(...entity: Constructor[]): DynamicModule {
    return {
      name: this.name,
      imports: [],
      provider: async () =>
        entity.map((val) => {
          return {
            provider: '[ormModule:repository]' + val.name,
            useFactory: async () => (await this.getDataSource()).getRepository(val),
          };
        }) as Provider[],
      exports: async () => entity.map((val) => '[ormModule:repository]' + val.name),
    };
  }

2.Controller(控制器)

​ 控制器负责处理事件并做出相对应的处理。

Controllers_1

​ 控制器的目的是负责处理事件并对事件做出相对应的处理。为了创建一个基本的控制器,我们使用类和装饰器。装饰器将类与所需的元数据相关联,并使mitsuki能将对应的处理映射到与之对应的控制器中。

​ 在下面的示例中,我们将使用@Controller()装饰器,它是定义基本控制器所必需的。

示例(base.controller.ts)

@Controller()
export class BaseController {
  constructor() {}
  @Handler('GroupMessage')
  public async test() {
    console.log('ControllerWorks!');
  }
}

​ 而在在类方法上的@Handler()装饰器则是用来订阅指定的可观察对象。@Handler()装饰器接受1-n个参数,其中第一个是指定监听事件的名称,而剩下的参数则是操作符(operator)或者是实现了RxPipe抽象类的类。

​ 使用实现了RxPipe抽象类的类有一个好处,就是可以通过依赖注入来为操作符实现中增加必要的依赖,以实现更复杂的逻辑。

示例(list.rx.pipe.ts)

@Injectable()
export class ListPipe implements RxPipe {
  constructor(@Inject({ ProviderName: 'list' }) private readonly list: number[]) {}
  public buildRxPipe(): OperatorFunction<any, any> {
    return pipe(
      filter((val: MessageType.ChatMessage) => {
        let pass = false;
        this.list.forEach((v) => {
          if (val.sender.id == v) {
            pass = true;
          }
          console.log(val.sender.id, v, v === val.sender.id);
        });
        return pass;
      }),
    );
  }
}

​ 只能作用于控制器类上方法的方法参数装饰器@Data()可以获得经过操作符修改过后对应可观察对象的值,如果没有修改,则会返回对应可观察对象的所发出的值。对于大部分的方法参装饰器默认都可以使用实现了MitsukiPipe抽象类的类或者实例对象,用作值的转换、类型的检查或者增加执行副作用。

​ 使用实现了MitsukiPipe抽象类的类有一个好处,就是可以通过依赖注入来为操作实现中增加必要的依赖,以实现更复杂的逻辑。

示例(text.mitsuki.pipe.ts)

@Injectable()
export class TextPipe implements MitsukiPipe {
  public transform(val: any) {
    if (isGroupMessage(val)) {
      return val.plain;
    } else {
      throw new Error('类型错误');
    }
  }
}

​ 方法参数装饰器@Inject()是最为通用的方法参数装饰器,他接收一个特殊的对象。这个对象一个必填参数:ProviderName(注意大写)则是指定的存取令牌。通过这个存储令牌,可以获取任何已知的在作用域范围内 的provider。

​ 方法参数装饰器@Inject()还有更奇怪的用法,比如为其直接赋值的useValue方法,以及不那么奇怪的管道,他们都是可选的。useValue这种方法通常与拦截器共同使用,在最后时刻,改变指定项的值。(比如内置的@Data()装饰器的实现就是基于useValue。但是这种奇怪的方式可能会在将来的更新中被移除,抑或着是将其以一种新的形式开放。)

示例(root.controller.ts)

@Controller()
export class RootController {
  constructor(@Inject({ ProviderName: 'random_2' }) private readonly random: number) {}
  @Handler('GroupMessage', ListPipe)
  public log(@Data(TextPipe) data: string, @Inject({ ProviderName: 'random' }) r: number) {
    console.log(data, r, this.random);
  }
}

3.providers(提供者)

提供者是mitsuki的一个基本概念。许多基本的类是提供一个服务或者一个功能的类——服务、存储库、工厂、助手、管道等等,而在这里我们将这些类统称为提供者。

​ 提供者的主要思想是它可以注入依赖项;这意味着对象之间可以创建各种关系,并且“连接”对象实例的功能在很大程度上可以委托给 mitsuki的运行时系统。

Components_1

​ 在下面的示例中,我们将会使用@Injectable() 装饰器,它是定义基本提供者所必需的。以及两个提供者与两个提供者之间的依赖关系。

provider的构建方式

​ 在构建provider时,我们相对于v0.9增加useFactory useClass useValue 方法:

type Provider = {
  provider: string;
  //new
  useFactory?: (...params: any[]) => any;
  //new
  useClass?: Constructor;
  //new
  useValue?: any;
  //new,之后介绍
  scope?: Scope;
};

​ 对于大多数的provider的构建,并不需要使用这些特殊的方法,mitsuki的运行时系统会帮助你处理好一切。但是在对于一些特殊的provider(比如基础类型值的provider)可能会使得mitsuki运行时系统无法正确的传递依赖,这时就需要通过provider的完整导入方式进行导入,比如:

{
  provider: 'randomNumber',
  useFactory: () => Math.random(),
  scope: Scope.MESSAGE,
}

scope选项

​ scope选项是这个版本新加入的内容,他提供了有别于单例模式的实现。在大多数情况下,使用单例模式都将是最优选择。但是有些情况下,我们会希望在一个特殊实例中储存一些特别的状态(比如handler的调用次数)而这些状态将与某一个特定的provider、controller、handler高度相关(Scope.TRANSIENT),或者与每一个订阅内容高度相关(Scope.MESSAGE),这时我们就可以通过设置Scope选项来达到这种特殊的效果。不过由于Scope选项会打破单例模式,所以这种选项除非必要,一般情况下是不建议使用的。不过,多一种选择总归是一种好事。