Fraqv0.7.0

开发插件

在 Fraq 中,插件(Plugin)是一个可选、可复用的模块化组件,用于封装特定的功能或一组相关的功能。插件可以包含指令、事件处理器等,并且可以被其他插件依赖和调用,只要声明并提供了对应的服务(Service)。下面我们来介绍一下如何定义和使用插件。

定义与安装插件

插件通过 definePlugin 函数来定义。例如,我们将之前例子中的 Echo 机器人封装成一个插件:

import { definePlugin } from '@fraqjs/fraq';

export const EchoPlugin = definePlugin({
  name: 'echo',
  apply(ctx) {
    ctx.router.command(
      'echo',
      {
        content: param.str(),
      },
      (session, { content }) => {
        session.reply(msg`You said: ${content}`);
      },
    );
  },
});

// 推荐同时将插件进行默认导出
export default EchoPlugin;

我们可以看到,插件是一个对象,它有一个 name 字段和一个 apply 方法。name 用于标识插件,apply 会在插件被加载时被调用,并且会传入一个 Context 对象作为参数。在 apply 方法中,我们可以像之前一样定义指令、事件处理器等。

在安装插件时,只需要调用 ctx.install 方法,并传入插件对象即可:

import EchoPlugin from '...';

ctx.install(EchoPlugin);

即使是在你自己开发的机器人中,也强烈建议将功能模块封装成插件,这样可以更好地组织代码,并且在需要的时候可以方便地复用和共享这些功能模块。

接受用户配置

有时候,我们希望插件能够接受用户的配置,以便在不同的场景下有不同的表现。通过 definePlugin 传入的 apply 回调函数可以接受更多的参数作为用户配置。例如,我们想要给机器人的回复消息添加一个用户可配置的表情前缀,可以这样定义插件:

definePlugin({
  name: 'echo',
  apply(
    ctx,
    options: {
      prefixFaceId: number;
    },
  ) {
    ctx.router.command(
      'echo',
      {
        content: param.str(),
      },
      (session, { content }) => {
        session.reply(
          msg`${seg.face(options.prefixFaceId)} You said: ${content}`,
        );
      },
    );
  },
});

这样,用户在安装插件时需要进行如下配置:

ctx.install(EchoPlugin, {
  prefixFaceId: 42,
});

definePlugin 并没有限制 apply 函数的参数数量,你可以根据需要定义任意的参数来接受用户配置。你甚至可以定义如下的插件:

definePlugin({
  name: 'example',
  apply(ctx, foo: string, bar: number, baz: boolean) {
    // ...
  },
});

但建议不要定义过多的参数,以免让用户在安装插件时感到困惑。

强烈建议接受一个配置对象作为参数,并且所有的配置项都是 JSON 可序列化的类型。

依赖注入

插件不仅能用来封装功能,还可以用来提供服务(Service)供其他插件依赖和调用。假设我们有一个 AlphaService,它提供了一个 alpha 方法:

class AlphaService {
  alpha() {
    return 'alpha';
  }
}

插件可以在定义时声明并通过 ctx.provide 方法提供一个服务:

const PluginA = definePlugin({
  name: 'plugin-a',
  provides: [AlphaService],
  apply(ctx) {
    ctx.provide(AlphaService, new AlphaService());
  },
});

其他插件可以用 inject 字段来声明对这个服务的依赖,并且在 apply 方法中直接通过 ctx 参数来访问这个服务实例:

const PluginB = definePlugin({
  name: 'plugin-b',
  inject: {
    alpha: AlphaService,
  },
  apply(ctx) {
    console.log(ctx.alpha.alpha()); // prints 'alpha'
  },
});

这里的 providesinject 很重要,它告诉 Fraq 哪个插件提供了 AlphaService,哪个插件依赖了 AlphaService,Fraq 会根据这些信息来正确地加载和初始化插件。例如,如果用户同时安装了 PluginAPluginB,Fraq 会确保在加载 PluginB 之前先加载 PluginA,以保证 AlphaService 已经被提供了。如果用户安装了 PluginB 而没有安装 PluginA,Fraq 会抛出一个错误,提示缺少依赖。

使用 requires 声明依赖

可以使用 requires 字段来声明对 AlphaService 的依赖,并通过 ctx.resolve 方法来获取这个服务的实例:

const PluginB = definePlugin({
  name: 'plugin-b',
  requires: [AlphaService],
  apply(ctx) {
    const alphaService = ctx.resolve(AlphaService);
    console.log(alphaService.alpha()); // prints 'alpha'
  },
});

需要注意的是,injectrequires 不能同时使用在同一个插件中,因为它们的作用是相同的,都是用来声明插件的依赖关系的;如果同时使用,Fraq 会抛出一个错误提示。推荐使用 inject 这种更加简便的方式来声明和解析依赖。

声明可选依赖

除此之外,我们还可以用 optionalInject 来声明可选依赖,例如:

const PluginC = definePlugin({
  name: 'plugin-c',
  optionalInject: {
    alpha: AlphaService,
  },
  apply(ctx) {
    if (ctx.alpha) {
      console.log(ctx.alpha.alpha());
    } else {
      console.log('AlphaService is not provided');
    }
  },
});

当然也可以用 optionalRequires 来声明可选依赖。Context 还提供了 tryResolveisProvided 方法,分别用于尝试获取一个服务的实例(如果没有提供则返回 undefined)和检查一个服务是否已经被提供了。用例如下:

const PluginC = definePlugin({
  name: 'plugin-c',
  optionalRequires: [AlphaService],
  apply(ctx) {
    if (ctx.isProvided(AlphaService)) {
      // 可以被安全地调用,因为我们已经检查过它是否被提供了
      const alphaService = ctx.resolve(AlphaService);
      console.log(alphaService.alpha());
    } else {
      console.log('AlphaService is not provided');
    }
    // OR:
    const alphaService = ctx.tryResolve(AlphaService);
    if (alphaService) {
      console.log(alphaService.alpha());
    } else {
      console.log('AlphaService is not provided');
    }
  },
});

Service 的生命周期

Fraq 提供了一个 Disposable 接口,如果一个服务实现了这个接口,那么当插件被卸载时,Fraq 会自动调用它的 dispose 方法来进行清理工作。例如:

import type { Disposable } from '@fraqjs/fraq';

class DisposableService implements Disposable {
  dispose() {
    console.log('Service is being disposed');
  }
}

esnext.disposable.d.ts 也提供了一个 Disposable 接口,但其声明与 Fraq 的 Disposable 接口不同。

ESNextDisposable 接口声明如下:

interface Disposable {
  [Symbol.dispose](): void;
}

因此,如果你想要在 Fraq 中使用 Disposable 接口来管理服务的生命周期,请确保你显式导入了 @fraqjs/fraq 包中的 Disposable 接口,否则你实现的将是 ESNext 中的 Disposable 接口。

相应地,为了避免这种情况的发生,Fraq 在 Contextprovide 方法中增加了一个检查,如果你提供的服务实现了 ESNextDisposable 但没有实现 Fraq 的 Disposable,Fraq 会抛出一个错误,提示你正确地导入和实现 Fraq 的 Disposable 接口。

那么在 Context 卸载 DisposableService 时,控制台会输出 Service is being disposed。关于 Context 生命周期结束时的行为,请见上下文 (Context) 中的介绍。

start 方法

插件还可以定义一个可选的 start 方法,这个方法会在所有插件都被加载和初始化之后被调用。start 方法通常用于执行一些需要在所有插件都准备好之后才能执行的操作。Context 会在所有插件的 apply 方法都被调用之后,按照与调用 apply 方法相同的顺序来调用插件的 start 方法。

不要滥用 start 方法,只有在确实需要在所有插件都准备好之后才能执行的操作才应该放在 start 方法中。大多数情况下,你应该把插件的逻辑放在 apply 方法中,这样可以更好地利用 Fraq 的依赖注入机制,并且让插件的加载和初始化过程更加清晰和可预测。

为了防止滥用,start 方法只接受一个 Context 参数,而不会接受与 apply 方法相同的用户配置参数。

On this page