Webiny UseCase 模式实战指南:DI 抽象、Result 错误处理与 CMS 仓储落地的完整实现规范

发布时间:2026/10/9 2:33:06
Webiny UseCase 模式实战指南:DI 抽象、Result 错误处理与 CMS 仓储落地的完整实现规范 CMS后端前端【免费下载链接】webiny-jsOpen-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用例的整套实现模式从单方法编排器的接口形态、ResultT, E返回值约定、基于BaseError的领域错误体系到依赖注入DI注册、CMS 仓储Repository持久化、Entry 映射器与装饰器Decorator。读完本文你将能在自己的 Webiny 扩展Extension中实现、注入、覆盖或装饰任意 UseCase并能通过 CMS 用例构建可持久化的领域仓储最终以yarn webiny deploy api --envdev完成部署。文中所有关键结论均可在 packages/feature/src 与 packages/api-headless-cms/src/features/contentEntry 等源码中找到对应实现佐证。一、UseCase 是什么在 Webiny 后端架构中UseCase是封装单一业务操作的单方法编排器。例如CreateTenantUseCase创建租户、PublishEntryUseCase发布内容条目每个 UseCase 都是一个 DI 抽象Abstraction对外只暴露一个execute方法且该方法永远返回ResultT, E。从源码看这个抽象由webiny/di的Abstraction类承载packages/feature/src/createAbstraction.ts中的createAbstractionT(name)直接返回new AbstractionT(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): PromiseResultReturnType, ErrorType; }Input—— 针对该用例专门设计的强类型对象Result—— 一律返回来自webiny/api的ResultT, E不允许抛异常代替失败返回Error—— 必须继承BaseError并带有唯一的code常量。UseCase 的三种能力边界实现implement写一个类实现.Interface通过createImplementation注册为默认实现注入inject把 UseCase 作为构造函数依赖注入到 EventHandler、其他 UseCase 或 GraphQL resolver 中覆盖/装饰override/decorate注册自定义实现替换默认行为或用createDecorator在不动核心逻辑的前提下叠加横切关注点。二、如何使用一个 UseCase注入到 EventHandlerUseCase 通过 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保持返回类型ResultT, 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 apps configuration Api.Extension src{/extensions/my-extension.ts} /部署命令yarn webiny deploy api --envdev五、错误处理模式5.1 领域专属错误继承BaseErrorWebiny 要求每个特性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): PromiseResultEntity, RepositoryError; } export const CreateEntityRepository createAbstractionICreateEntityRepository( MyExt/CreateEntityRepository ); export namespace CreateEntityRepository { export type Interface ICreateEntityRepository; export type Error RepositoryError; export type Return PromiseResultEntity, 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): PromiseResultEntity, UseCaseError; } export const CreateEntityUseCase createAbstractionICreateEntityUseCase( MyExt/CreateEntityUseCase ); export namespace CreateEntityUseCase { export type Interface ICreateEntityUseCase; export type Input CreateEntityInput; export type Error UseCaseError; export type Return PromiseResultEntity, UseCaseError; }值得注意的层次设计仓储错误 ⊂ 用例错误UseCase 的错误联合是仓储错误的超集额外叠加NotAuthorizedError等用例级错误调用方在用例层可以统一匹配所有可能的失败路径createAbstractionT(name)的name遵循MyExt/ClassName命名空间约定避免跨扩展冲突对应packages/feature/src/createAbstraction.ts的new AbstractionT(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.tsResult.ok(value)/Result.ok()无参时得到Resultvoid, never与Result.fail(error)是仅有的两个静态构造入口isOk()/isFail()是TS 类型守卫this is { _value: TValue } ResultTValue, TError通过类型收窄保证编译期安全valuegetter 在失败结果上调用会抛错Tried to get value from a failed Result.errorgetter 在成功结果上调用同样抛错——这从机制上强制你先判isFail()再取值附赠的map/mapError/flatMap/match方法支持函数式变换与模式匹配可在不引入额外依赖的情况下做链式处理namespace Result还导出UnwrapResultT/UnwrapErrorT工具类型便于从异步返回值中提取成功值或错误类型。六、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.Returndependencies数组顺序与构造函数参数顺序逐一对应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)返回PromiseResultCmsEntryT, UseCaseAbstraction.Error内部先做访问控制canAccessEntry再发布EntryBeforeCreateEvent随后委托仓储执行失败即Result.fail(result.error)成功后发布EntryAfterCreateEvent并Result.ok(entry)其抽象定义在 abstractions.ts同样是createAbstractionICreateEntryUseCase(CreateEntryUseCase)I…Errors接口 [keyof]联合类型 namespace 导出Interface/Input/Options/Error/Return的完整结构。这恰好印证了 UseCase 的真实职责编排权限、事件、数据工厂、仓储调用而不是直接操作存储。七、CMS 仓储模式CMS Repository Pattern仓储Repository是 UseCase 之下负责数据持久化的一层Webiny 中仓储通过 CMS 用例来落库。原文档强调先解析resolveCMS 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 scopeexport 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 或访问其返回类型的代码之前必须阅读目录catalogSource字段指向的源文件核对真实的方法签名、输入参数、返回类型与错误类型禁止凭记忆猜测属性名。标准流程读取 catalogSource路径下的abstractions.ts若接口引用了领域类型沿 import 链继续阅读对应类型声明只使用在源码中确认存在的属性与方法签名。这条规则是 Webiny 工程实践的重要约定抽象层文件abstractions.ts是类型的唯一事实来源配合第六节介绍的namespace类型导出可以让实现方与消费方都获得编译期校验避免文档与实现漂移。十二、关键规则速查Key Rules永远先检查result.isFail()再访问.value或.error——这是Result类 getter 语义强制的访问契约见 packages/feature/src/api/Result.tsDI 构造函数参数顺序必须与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 schemawebiny-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-jsOpen-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点击查看免费下载相关推荐在 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 个按方法抽象 导读 本文基于 WeCMS后端前端上一篇WasmEdge 繁體中文指南輕量級、高效能的 WebAssembly Runtime 入門與深度解析下一篇presenterm 配置文件完全指南defaults、bindings、snippet 与 export 全量设置详解创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询