- 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.
导读
本文是围绕 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的铁律
原文档明确强调两条会导致构建失败的硬性规则,务必遵守:
src属性必须带.ts扩展名的完整文件路径。例如写src={"@/extensions/my-extension.ts"},绝不能写src={"@/extensions/my-extension"}。省略扩展名会直接导致构建失败。- 必须使用
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字段指向的源文件,核对真实的方法签名、输入参数、返回类型与错误类型,禁止凭记忆猜测属性名。标准流程:
- 读取 catalog
Source路径下的abstractions.ts; - 若接口引用了领域类型,沿 import 链继续阅读对应类型声明;
- 只使用在源码中确认存在的属性与方法签名。
这条规则是 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.
相关推荐
在 AWS Lambda 中运行 React-Static:无服务器环境下的路径与构建缓存配置指南
在 AWS Lambda 中运行 React Static:无服务器环境下的路径与构建缓存配置指南 本指南面向希望在 AWS Lambda 等无服务器环境中完成
CMS后端前端Webiny File Manager 重构实战:抽取公共代码到基础包与 DI 抽象落地指南
Webiny File Manager 重构实战:抽取公共代码到基础包与 DI 抽象落地指南 导读 api file manager s3 (AWS S3 存储
CMS后端前端Webiny 的 Headless CMS 存储操作 DI 拆分实战:从单体 StorageOperations 到 22+ 个按方法抽象
Webiny 的 Headless CMS 存储操作 DI 拆分实战:从单体 StorageOperations 到 22+ 个按方法抽象 导读 本文基于 We
CMS后端前端
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考