Core · 开发插件 · 服务端

业务服务与 ServiceProvider

本篇为公告插件提供文本预览服务,让 HTTP、CLI 或其他插件复用同一实现。services/ 放业务实现,tokens.ts 定义公开契约,providers/ 把实现注册到应用容器。这里不访问数据库,也不需要客户端。

通用的依赖注入与生命周期见 ServiceProvider,事务和请求状态的处理见服务端服务。本篇重点是插件的注册与公开使用边界。

前提与相关文件

在已有 @nocobase/app-plugin-announcements 中增加以下文件。路径相对于插件根目录;已有同名内容时合并,保留原有服务和接口。新建插件时可选 server.service-providers。@nocobase/service-provider 使用兼容的 peer 依赖,让应用与插件共享同一运行时身份。

使用 Agent

纯文本
把公告文本预览整理为插件的服务端服务,返回去空白后的文本与 Unicode 码点数量。
通过原始 Token 和 ServiceProvider 注册一个惰性单例,并公开契约供接口和命令复用。
保留现有 Server 贡献,不把当前请求、用户或事务保存在单例字段中。
验证缺少注册时失败、注册后共享同一实例,以及公开入口的类型声明构建。

定义服务契约

在 server/tokens.ts 中增加契约与 Token:

TypeScript
import { createServiceToken, type ServiceToken } from '@nocobase/service-provider';

export interface AnnouncementPreview {
  readonly text: string;
  readonly length: number;
}
export interface AnnouncementPreviewService {
  preview(text: string): AnnouncementPreview;
}
export const announcementPreviewToken: ServiceToken<AnnouncementPreviewService> =
  createServiceToken<AnnouncementPreviewService>('@nocobase/app-plugin-announcements/preview');

接口输入的类型、长度和权限由相应入口校验;可复用的业务约束仍由服务维护。本例的长度按 Unicode 码点计算,不等同于 JavaScript 字符串长度或用户感知的字形数量。

编写实现

新增 server/services/preview.ts:

TypeScript
import type { AnnouncementPreview, AnnouncementPreviewService } from '../tokens.js';

export class DefaultAnnouncementPreviewService implements AnnouncementPreviewService {
  public preview(text: string): AnnouncementPreview {
    const normalized = text.trim();
    return { text: normalized, length: Array.from(normalized).length };
  }
}

实现不依赖 HTTP Context,因此 CLI 和任务也能使用。以后需要数据库或外部服务时,由 Provider 解析应用已有的服务并通过构造参数注入;请求用户、授权策略、事务连接等单次调用状态通过方法参数传递。不要创建第二套全局容器或绕过应用已有的数据访问策略。

注册到插件

新增 server/providers/preview.ts:

TypeScript
import { ServiceProvider, type ServiceContainer } from '@nocobase/service-provider';
import { announcementPreviewToken } from '../tokens.js';
import { DefaultAnnouncementPreviewService } from '../services/preview.js';

export interface AnnouncementPreviewApplication {
  readonly container: ServiceContainer;
}
export class AnnouncementPreviewProvider extends ServiceProvider<AnnouncementPreviewApplication> {
  public readonly name: string = '@nocobase/app-plugin-announcements/preview';
  public override register(): void {
    this.app.container.singleton(announcementPreviewToken, () => new DefaultAnnouncementPreviewService());
  }
}

将 AnnouncementPreviewProvider 加入 server/providers/index.ts 的构造函数数组,再确认 server/plugin.ts 的 serviceProviders 引用了它。目录里有实现文件并不代表它已注册;保留其他 Provider,也不要在数组中写 new AnnouncementPreviewProvider(...)。

本例只有纯计算,不需要额外启动或关闭资源。需要异步初始化时使用对应生命周期;需要关闭订阅或连接时,只释放自己创建的资源,并处理启动失败的情况。具体阶段与依赖顺序见应用启动与生命周期。

公开并使用服务

在 server/index.ts 中保留默认插件导出,增加公开 Token 和接口类型:

TypeScript
export { announcementPreviewToken } from './tokens.js';
export type { AnnouncementPreview, AnnouncementPreviewService } from './tokens.js';

应用在 server/plugins.ts 接入插件后,接口或其他插件可从 @nocobase/app-plugin-announcements/server 导入原始 Token,并通过当前应用容器调用。不要用相同字符串重新创建 Token,也不要跨包导入 server/services/preview.ts。

在已完成输入校验的路由处理逻辑中,调用 app.container.resolve(announcementPreviewToken).preview(text),再按 HTTP 响应合同返回结果。路由如何声明与保护见路由与 HTTP 接口。

CLI 在 this.withApp(async ({ app }) => ...) 中解析同一个 Token,并将结果作为命令返回值。应用必须已注册插件的 Server 入口;只注册 CLI 入口不会注册这个服务。纯预览不需要为了调用而额外执行 app.start();依赖启动阶段资源的服务则必须按其契约启动应用。CLI 参数和错误处理见命令行扩展。

调用方通过公开入口取得接口类型,具体实现和 Provider 可以继续作为插件内部细节。包依赖、运行时注册和调用顺序的区别见插件声明与公开入口。

验证公开契约与集成

将下面的测试放到 tests/server/service-provider.test.ts,合并或替换已有相关断言:

TypeScript
import { ServiceContainer } from '@nocobase/service-provider';
import { expect, it } from 'vitest';
import { announcementPreviewToken } from '../../server/index.js';
import { AnnouncementPreviewProvider } from '../../server/providers/preview.js';

it('registers one lazy service behind the public Token', () => {
  const container = new ServiceContainer();
  expect(() => container.resolve(announcementPreviewToken)).toThrow();
  new AnnouncementPreviewProvider({ container }).register();
  expect(container.resolveIfCreated(announcementPreviewToken)).toBeUndefined();
  const service = container.resolve(announcementPreviewToken);
  expect(service.preview('  欢迎 👋  ')).toEqual({ text: '欢迎 👋', length: 4 });
  expect(container.resolve(announcementPreviewToken)).toBe(service);
});

这个测试证明公开 Token、惰性创建和实例共享;还要检查真实 Server 声明包含 Provider,并在目标 App 中调用实际 HTTP 或 CLI 入口。涉及数据库、权限或事务的服务使用真实应用测试,不能用一个容器测试代表全部集成。

Bash
pnpm --filter @nocobase/app-plugin-announcements check
pnpm nocobase plugin register @nocobase/app-plugin-announcements --no-install --dry-run
pnpm nocobase plugin register @nocobase/app-plugin-announcements --no-install
pnpm build

服务端服务与客户端服务运行在不同容器中。浏览器需要通过接口调用这项服务,不可直接导入 Server 实现。

NocoBase 文档助手

关于 NocoBase,你想了解什么?

每次提问独立回答,不会参考之前的问答。请提供完整背景。