☰
Webiny UseCase 模式实战指南:DI 抽象、Result 错误处理与 CMS 仓储落地的完整实现规范
2026/10/9 2:32:59 网站建设 项目流程
  • CMS
  • 后端
  • 前端

【免费下载链接】webiny-js

Open-source, self-hosted CMS platform on AWS serverless (Lambda, DynamoDB, S3). TypeScript framework with multi-tenancy, lifecycle hooks, GraphQL API, and AI-assisted development via MCP server. Built for developers at large organizations.

项目地址:https://gitcode.com/gh_mirrors/we/webiny-js
点击查看免费下载

导读

本文是围绕 Webiny 开源仓库中skills/user-skills/api/use-case-pattern/SKILL.md展开的工程实践指南,完整讲解 Webiny 后端(webiny/api)中UseCase(用例)的整套实现模式:从单方法编排器的接口形态、Result<T, E>返回值约定、基于BaseError的领域错误体系,到依赖注入(DI)注册、CMS 仓储(Repository)持久化、Entry 映射器与装饰器(Decorator)。读完本文,你将能在自己的 Webiny 扩展(Extension)中实现、注入、覆盖或装饰任意 UseCase,并能通过 CMS 用例构建可持久化的领域仓储,最终以yarn webiny deploy api --env=dev完成部署。文中所有关键结论均可在 packages/feature/src 与 packages/api-headless-cms/src/features/contentEntry 等源码中找到对应实现佐证。


一、UseCase 是什么

在 Webiny 后端架构中,UseCase是封装单一业务操作的单方法编排器。例如CreateTenantUseCase(创建租户)、PublishEntryUseCase(发布内容条目),每个 UseCase 都是一个 DI 抽象(Abstraction),对外只暴露一个execute方法,且该方法永远返回Result<T, E>。

从源码看,这个抽象由@webiny/di的Abstraction类承载:packages/feature/src/createAbstraction.ts中的createAbstraction<T>(name)直接返回new Abstraction<T>(name),随后通过packages/feature/src/api/index.ts以createAbstraction、createImplementation、createDecorator、createFeature、Result、ResultAsync、BaseError、Container等形式统一对外导出。也就是说,你从webiny/api导入的这些符号,最终都落到packages/feature/src/api/index.ts这一行导出面上。

接口形态

interface SomeUseCase.Interface { execute(input: Input): Promise<Result<ReturnType, ErrorType>>; }
  • Input—— 针对该用例专门设计的强类型对象;
  • Result—— 一律返回来自webiny/api的Result<T, E>,不允许抛异常代替失败返回;
  • Error—— 必须继承BaseError,并带有唯一的code常量。

UseCase 的三种能力边界

  • 实现(implement):写一个类实现.Interface,通过createImplementation注册为默认实现;
  • 注入(inject):把 UseCase 作为构造函数依赖注入到 EventHandler、其他 UseCase 或 GraphQL resolver 中;
  • 覆盖/装饰(override/decorate):注册自定义实现替换默认行为,或用createDecorator在不动核心逻辑的前提下叠加横切关注点。

二、如何使用一个 UseCase:注入到 EventHandler

UseCase 通过 DI 注入到使用方。下面是一个 EventHandler 消费SomeUseCase的完整示例(摘自原文档,保持可复制):

import { SomeUseCase } from "webiny/api/<category>"; import { SomeEventHandler } from "webiny/api/<category>"; class MyHandler implements SomeEventHandler.Interface { constructor(private someUseCase: SomeUseCase.Interface) {} async handle(event: SomeEventHandler.Event) { const result = await this.someUseCase.execute({/* input */}); if (result.isFail()) { console.error(result.error.message); return; } const value = result.value; // ... use value } } export default SomeEventHandler.createImplementation({ implementation: MyHandler, dependencies: [SomeUseCase] });

要点:

  • 构造函数参数一律用Xxx.Interface类型标注;
  • 调用execute后先判isFail(),再访问.value或.error,这是访问安全的前提(Result.value与Result.error都是 getter,误用会抛错,详见下文 Result 实现);
  • dependencies数组顺序必须与构造函数参数顺序完全一致;
  • 使用方自身也通过createImplementation注册,export default。

三、如何覆盖一个 UseCase

当你需要替换某个 UseCase 的默认实现时,注册一个自己的实现即可:

import { SomeUseCase } from "webiny/api/<category>"; class CustomImplementation implements SomeUseCase.Interface { async execute(input) { // Custom logic return Result.ok(/* ... */); } } export default SomeUseCase.createImplementation({ implementation: CustomImplementation, dependencies: [] });

覆盖实现同样要实现同一个.Interface,保持返回类型Result<T, E>不变——这样所有依赖该抽象的上游代码无需任何改动即可切换到你的实现。这正是"面向抽象编程"的核心收益:UseCase 抽象是契约,实现可以替换。


四、注册与部署:src路径与export default的铁律

原文档明确强调两条会导致构建失败的硬性规则,务必遵守:

  1. src属性必须带.ts扩展名的完整文件路径。例如写src={"@/extensions/my-extension.ts"},绝不能写src={"@/extensions/my-extension"}。省略扩展名会直接导致构建失败。
  2. 必须使用export default导出createImplementation()的结果。当文件被 Extension 的src属性直接指向时,命名导出(export const Foo = SomeFactory.createImplementation(...))会导致构建失败;命名导出仅在通过createFeature注册的文件内合法。

在应用配置中注册扩展:

// In your app's configuration <Api.Extension src={"@/extensions/my-extension.ts"} />

部署命令:

yarn webiny deploy api --env=dev

五、错误处理模式

5.1 领域专属错误:继承BaseError

Webiny 要求每个特性(feature)自行定义继承BaseError的错误类,任何校验失败或业务规则失败都不允许使用原生Error。原文档给出的标准错误族如下:

// domain/errors.ts import { BaseError } from "webiny/api"; export class EntityNotFoundError extends BaseError { override readonly code = "Entity/NotFound" as const; constructor(id: string) { super({ message: `Entity with id "${id}" was not found!` }); } } export class EntityPersistenceError extends BaseError<{ error: Error }> { override readonly code = "Entity/Persist" as const; constructor(error: Error) { super({ message: error.message, data: { error } }); } } export class EntityValidationError extends BaseError<{ message: string }> { override readonly code = "Entity/Validation" as const; constructor(message: string) { super({ message, data: { message } }); } }

源码层面的佐证:packages/feature/src/api/BaseError.ts定义了抽象基类——public abstract readonly code: string强制每个子类声明唯一错误码;构造函数接收{ message, data? },其中data为可选的泛型载荷(TData extends void ? undefined : TData),并通过super(input.message)透传标准错误消息,同时可选覆盖stack。这套设计让每个错误既有稳定可匹配的code(如"Entity/NotFound"),又能携带结构化上下文(如底层error对象)。

5.2 抽象中的类型化错误联合(Typed Error Unions)

在抽象层(abstractions.ts),用IErrors接口把错误名映射到错误类型,再通过[keyof IErrors]生成联合类型。原文档示例:

// features/createEntity/abstractions.ts import { createAbstraction, Result } from "webiny/api"; import { NotAuthorizedError } from "webiny/api/security"; import { EntityPersistenceError, EntityModelNotFoundError, EntityCreationError } from "~/api/domain/errors.js"; // REPOSITORY errors export interface ICreateEntityRepositoryErrors { persistence: EntityPersistenceError; modelNotFound: EntityModelNotFoundError; creation: EntityCreationError; } type RepositoryError = ICreateEntityRepositoryErrors[keyof ICreateEntityRepositoryErrors]; export interface ICreateEntityRepository { execute(entity: Entity): Promise<Result<Entity, RepositoryError>>; } export const CreateEntityRepository = createAbstraction<ICreateEntityRepository>( "MyExt/CreateEntityRepository" ); export namespace CreateEntityRepository { export type Interface = ICreateEntityRepository; export type Error = RepositoryError; export type Return = Promise<Result<Entity, RepositoryError>>; } // USE CASE errors — superset of repository errors export interface ICreateEntityUseCaseErrors { persistence: EntityPersistenceError; modelNotFound: EntityModelNotFoundError; creation: EntityCreationError; notAuthorized: NotAuthorizedError; } type UseCaseError = ICreateEntityUseCaseErrors[keyof ICreateEntityUseCaseErrors]; export interface ICreateEntityUseCase { execute(input: CreateEntityInput): Promise<Result<Entity, UseCaseError>>; } export const CreateEntityUseCase = createAbstraction<ICreateEntityUseCase>( "MyExt/CreateEntityUseCase" ); export namespace CreateEntityUseCase { export type Interface = ICreateEntityUseCase; export type Input = CreateEntityInput; export type Error = UseCaseError; export type Return = Promise<Result<Entity, UseCaseError>>; }

值得注意的层次设计:

  • 仓储错误 ⊂ 用例错误:UseCase 的错误联合是仓储错误的超集(额外叠加NotAuthorizedError等用例级错误),调用方在用例层可以统一匹配所有可能的失败路径;
  • createAbstraction<T>(name)的name遵循"MyExt/ClassName"命名空间约定,避免跨扩展冲突(对应packages/feature/src/createAbstraction.ts的new Abstraction<T>(name));
  • namespace中导出Interface / Input / Error / Return,让实现方、消费方共享同一套类型契约。

5.3 Result 模式

// Success return Result.ok(value); // Failure return Result.fail(new EntityNotFoundError(id)); // Check result if (result.isFail()) { return Result.fail(result.error); } // Access value const value = result.value;

关键禁忌:原文档明确警告——绝不要使用result.isError()、result.getError()、result.getValue(),这些 API 不存在。合法的访问方式是isOk()/isFail()类型守卫 +value/error属性。

源码佐证(packages/feature/src/api/Result.ts):

  • Result.ok(value)/Result.ok()(无参时得到Result<void, never>)与Result.fail(error)是仅有的两个静态构造入口;
  • isOk()/isFail()是TS 类型守卫(this is { _value: TValue } & Result<TValue, TError>),通过类型收窄保证编译期安全;
  • valuegetter 在失败结果上调用会抛错:"Tried to get value from a failed Result.";errorgetter 在成功结果上调用同样抛错——这从机制上强制你先判isFail()再取值;
  • 附赠的map/mapError/flatMap/match方法支持函数式变换与模式匹配,可在不引入额外依赖的情况下做链式处理;
  • namespace Result还导出UnwrapResult<T>/UnwrapError<T>工具类型,便于从异步返回值中提取成功值或错误类型。

六、UseCase 实现完整规范

原文档给出一个带权限校验、实体构造与仓储编排的完整用例实现:

// features/createEntity/CreateEntityUseCase.ts import { CreateEntityUseCase as UseCaseAbstraction, CreateEntityRepository } from "./abstractions.js"; import { Result } from "webiny/api"; import { IdentityContext } from "webiny/api/security"; import { NotAuthorizedError } from "webiny/api/security"; import { Entity } from "~/shared/Entity.js"; import { EntityId } from "~/api/domain/EntityId.js"; class CreateEntityUseCase implements UseCaseAbstraction.Interface { constructor( private identityContext: IdentityContext.Interface, private repository: CreateEntityRepository.Interface ) {} async execute(input: UseCaseAbstraction.Input): UseCaseAbstraction.Return { if (!this.identityContext.getPermission("mypackage.entity")) { return Result.fail(new NotAuthorizedError({ message: "Not authorized to create entities!" })); } const entity = Entity.from({ id: EntityId.from(input.id), values: { name: input.name, status: "disabled" } }); const result = await this.repository.execute(entity); if (result.isFail()) { return Result.fail(result.error); } return Result.ok(result.value); } } export default UseCaseAbstraction.createImplementation({ implementation: CreateEntityUseCase, dependencies: [IdentityContext, CreateEntityRepository] });

实现规则清单:

  • 类实现UseCaseAbstraction.Interface;
  • 构造函数参数用各自抽象的.Interface标注;
  • 返回类型使用UseCaseAbstraction.Return;
  • dependencies数组顺序与构造函数参数顺序逐一对应;
  • export default导出。

仓库中的真实范例:CMS 的 CreateEntryUseCase

这套模式不是纸上谈兵——Webiny 自身的 Headless CMS 就严格按此实现。见 packages/api-headless-cms/src/features/contentEntry/CreateEntry/CreateEntryUseCase.ts:

  • 构造函数注入EntryEventPublisher、CreateEntryRepository、AccessControl、CreateEntryDataFactory四个依赖,dependencies数组顺序完全一致;
  • execute(model, rawInput, options)返回Promise<Result<CmsEntry<T>, UseCaseAbstraction.Error>>,内部先做访问控制(canAccessEntry),再发布EntryBeforeCreateEvent,随后委托仓储执行,失败即Result.fail(result.error),成功后发布EntryAfterCreateEvent并Result.ok(entry);
  • 其抽象定义在 abstractions.ts,同样是createAbstraction<ICreateEntryUseCase>("CreateEntryUseCase")+I…Errors接口 +[keyof]联合类型 + namespace 导出Interface/Input/Options/Error/Return的完整结构。

这恰好印证了 UseCase 的真实职责:编排(权限、事件、数据工厂、仓储调用),而不是直接操作存储。


七、CMS 仓储模式(CMS Repository Pattern)

仓储(Repository)是 UseCase 之下负责数据持久化的一层,Webiny 中仓储通过 CMS 用例来落库。原文档强调:先解析(resolve)CMS model,再执行增删改查。

// features/createEntity/CreateEntityRepository.ts import { Entity } from "~/shared/Entity.js"; import { EntityCreationError, EntityModelNotFoundError } from "~/api/domain/errors.js"; import { CreateEntityRepository as RepositoryAbstraction } from "./abstractions.js"; import { Result } from "webiny/api"; import { CreateEntryUseCase } from "webiny/api/cms/entry"; import { GetModelUseCase } from "webiny/api/cms/model"; import { ENTITY_MODEL_ID } from "~/shared/constants.js"; class CreateEntityRepository implements RepositoryAbstraction.Interface { constructor( private getModelUseCase: GetModelUseCase.Interface, private createEntryUseCase: CreateEntryUseCase.Interface ) {} async execute(entity: Entity): RepositoryAbstraction.Return { const modelResult = await this.getModelUseCase.execute(ENTITY_MODEL_ID); if (modelResult.isFail()) { return Result.fail(new EntityModelNotFoundError()); } const createResult = await this.createEntryUseCase.execute(modelResult.value, { id: entity.id, values: { name: entity.values.name, status: entity.values.status } }); if (createResult.isFail()) { return Result.fail(new EntityCreationError(createResult.error)); } return Result.ok(entity); } } export default RepositoryAbstraction.createImplementation({ implementation: CreateEntityRepository, dependencies: [GetModelUseCase, CreateEntryUseCase] });

仓储可用的常见 CMS 用例

import { CreateEntryUseCase } from "webiny/api/cms/entry"; import { GetEntryByIdUseCase } from "webiny/api/cms/entry"; import { GetEntryUseCase } from "webiny/api/cms/entry"; import { UpdateEntryUseCase } from "webiny/api/cms/entry"; import { ListLatestEntriesUseCase } from "webiny/api/cms/entry"; import { EntryId } from "webiny/api/cms/entry"; import { GetModelUseCase } from "webiny/api/cms/model"; import { ListModelsUseCase } from "webiny/api/cms/model";

仓储规则:

  • 永远先用GetModelUseCase解析 CMS model;
  • 把 CMS 错误包装成领域专属错误(如EntityCreationError),不让底层 CMS 错误泄漏到领域层;
  • 仓储注册在单例作用域(singleton scope);
  • export default导出。

补充说明:上述webiny/api/cms/entry与webiny/api/cms/model导出在仓库中对应 packages/api-headless-cms/src/exports/api/cms/entry.ts 等导出面,GetModelUseCase、CreateEntryUseCase等抽象可在packages/api-headless-cms/src/features/contentEntry与contentModel对应特性目录中找到。


八、Entry 到领域实体的映射器(Entry-to-Entity Mapper)

当仓储从 CMS 取回的是 entry(内容条目)时,需要一个映射器把它转换为领域类型(Entity),保持领域层纯净:

// features/shared/EntryToEntityMapper.ts import { Entity as EntityClass } from "~/shared/Entity.js"; import type { Entity, EntityDto, EntityValues } from "~/shared/Entity.js"; export class EntryToEntityMapper { static toEntity(entry: { entryId: string; values: EntityValues }): Entity { return EntityClass.from({ id: entry.entryId, values: entry.values }); } }

映射器规则:

  • 只使用静态方法,不持有实例状态(无状态、可复用);
  • 由仓储使用,UseCase 不直接使用映射器;
  • 对 null/undefined 值按场景提供默认值处理。

九、UseCase 装饰器(Decorator)

装饰器用于在不修改核心用例的前提下叠加横切关注点(授权、日志、校验等)。

// features/getEntityById/decorators/GetEntityByIdWithAuthorization.ts import { GetEntityByIdUseCase } from "../abstractions.js"; import { Result } from "webiny/api"; import { IdentityContext } from "webiny/api/security"; import { NotAuthorizedError } from "webiny/api/security"; class GetEntityByIdWithAuthorizationImpl implements GetEntityByIdUseCase.Interface { constructor( private identityContext: IdentityContext.Interface, private decoratee: GetEntityByIdUseCase.Interface // decoratee is LAST ) {} async execute(id: string): GetEntityByIdUseCase.Return { if (!this.identityContext.getPermission("mypackage.entity")) { return Result.fail(new NotAuthorizedError()); } return this.decoratee.execute(id); } } export const GetEntityByIdWithAuthorization = GetEntityByIdUseCase.createDecorator({ decorator: GetEntityByIdWithAuthorizationImpl, dependencies: [IdentityContext] // does NOT include decoratee });

注册装饰器

// features/getEntityById/feature.ts import { createFeature } from "webiny/api"; import GetEntityByIdUseCase from "./GetEntityByIdUseCase.js"; import GetEntityByIdRepository from "./GetEntityByIdRepository.js"; import { GetEntityByIdWithAuthorization } from "./decorators/GetEntityByIdWithAuthorization.js"; export const GetEntityByIdFeature = createFeature({ name: "GetEntityById", register(container) { container.register(GetEntityByIdUseCase); container.register(GetEntityByIdRepository).inSingletonScope(); container.registerDecorator(GetEntityByIdWithAuthorization); } });

装饰器规则:

  • 实现与被装饰 UseCase相同的接口;
  • 构造函数:额外依赖在前,decoratee参数必须放最后;
  • 使用UseCaseAbstraction.createDecorator(...),其中dependencies数组不含 decoratee(框架会自动注入被装饰对象);
  • 用container.registerDecorator()注册,而不是container.register();
  • 装饰器可以在委托前修改输入、委托后修改输出,或直接短路返回错误(如未授权)。

十、基于 Schema 的权限(Schema-Based Permissions)

在 UseCase 中实现授权时,原文档指向webiny-api-permissions技能文档,其覆盖内容如下:

  • 用createPermissions定义权限 schema;
  • 全部权限方法:canRead、canEdit、canDelete、canPublish、onlyOwnRecords等;
  • 覆盖 get、list、update、delete、publish 全部 CRUD 操作的 UseCase 授权模式;
  • 自有记录(own-record)作用域与条目级归属校验;
  • 测试模式与权限对象形状。

实际业务用例中,权限判断一般落在 UseCase 内部(如第六节的identityContext.getPermission("mypackage.entity"))或由授权装饰器统一施加(如第九节),两者结合可以做到"业务逻辑无权限代码、权限策略可插拔"。


十一、解析类型(MANDATORY 强制要求)

原文档特别强调:在编写任何调用 UseCase 或访问其返回类型的代码之前,必须阅读目录(catalog)Source字段指向的源文件,核对真实的方法签名、输入参数、返回类型与错误类型,禁止凭记忆猜测属性名。标准流程:

  1. 读取 catalogSource路径下的abstractions.ts;
  2. 若接口引用了领域类型,沿 import 链继续阅读对应类型声明;
  3. 只使用在源码中确认存在的属性与方法签名。

这条规则是 Webiny 工程实践的重要约定:抽象层文件(abstractions.ts)是类型的唯一事实来源,配合第六节介绍的namespace类型导出,可以让实现方与消费方都获得编译期校验,避免"文档与实现漂移"。


十二、关键规则速查(Key Rules)

  • 永远先检查result.isFail(),再访问.value或.error——这是Result类 getter 语义强制的访问契约(见 packages/feature/src/api/Result.ts);
  • DI 构造函数参数顺序必须与dependencies数组顺序完全一致;
  • ES Module 导入路径一律使用.js扩展名(如from "./abstractions.js"),这是项目 ES Modules 规范的一部分;
  • 错误必须继承BaseError且带唯一code,禁用原生Error表达业务失败;
  • Extension 直接指向的文件必须export default,且src属性必须带.ts扩展名;
  • 仓储注册在单例作用域,UseCase 可按需生命周期注册。

十三、关联技能与延伸阅读

原文档列出的一组关联技能文档,可作为继续深入的方向(同位于 skills/user-skills/api 目录下):

  • webiny-api-architect—— 架构总览:Services 与 UseCases 的区分、特性命名规范、反模式;
  • webiny-api-permissions—— 基于 schema 的权限、CRUD 授权模式与测试;
  • webiny-event-handler-pattern—— EventHandler 生命周期与领域事件发布;
  • webiny-custom-graphql-api—— 结合 UseCase DI 创建 GraphQL schema;
  • webiny-dependency-injection—— 可注入服务目录。

此外,仓库 packages/feature/src/api 与 packages/feature/src/createAbstraction.ts 提供了Result、BaseError、createAbstraction的底层实现,packages/api-headless-cms/src/features/contentEntry 则展示了 CMS 内容条目相关 UseCase/仓储的完整落地样例,是阅读本文后最值得对照的实战代码。

  • CMS
  • 后端
  • 前端

【免费下载链接】webiny-js

Open-source, self-hosted CMS platform on AWS serverless (Lambda, DynamoDB, S3). TypeScript framework with multi-tenancy, lifecycle hooks, GraphQL API, and AI-assisted development via MCP server. Built for developers at large organizations.

项目地址:https://gitcode.com/gh_mirrors/we/webiny-js
点击查看免费下载
上一篇:WasmEdge 繁體中文指南:輕量級、高效能的 WebAssembly Runtime 入門與深度解析
下一篇:presenterm 配置文件完全指南:defaults、bindings、snippet 与 export 全量设置详解

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询