- 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 开源仓库中的设计与实施计划文档(docs/.bruno/plans/2026-07-17-opensearch-aws-split/系列及配套 specdocs/.bruno/specs/2026-07-17-api-opensearch-aws-split-design.md),完整讲解如何把@webiny/api-opensearch中内嵌的 AWS SigV4 签名逻辑拆分为独立的新包@webiny/api-opensearch-aws。读者将理解拆分动机、两个包的职责边界、createAwsOpenSearchClient包装函数的实现细节、DI Feature 的替换机制、消费方(事件处理器、AWS 项目模板、@webiny/webiny聚合包)的迁移路径,以及整套 9 步计划的依赖排序与验证方式——这些内容已在本仓库中落地,可作为直接参考的实现样例。
一、问题背景:为什么要把 OpenSearch 客户端包拆开
在 Webiny 的 AWS 无服务器架构中,Headless CMS 的搜索能力依赖 OpenSearch,而客户端创建集中在 client.ts。拆分之前,@webiny/api-opensearch在client.ts中无条件引入 AWS SigV4 签名逻辑:
import { AwsSigv4Signer } from "@opensearch-project/opensearch/aws";这带来两个现实问题:
- 非 AWS 部署被拖累:Webiny 同时支持 PostgreSQL + 自托管 OpenSearch(PG+OS 变体)等非 AWS 场景。它们并不需要 SigV4,却被迫把 AWS SDK 相关依赖打进了构建产物,增大了 Lambda 包体与冷启动开销。
- 职责混杂:一个"通用"客户端包里同时包含标准客户端逻辑和 AWS 平台专属逻辑,任何改动都会影响所有消费方。
设计文档给出的方案(也是仓库中已实施的方案)是一拆为二:
@webiny/api-opensearch(基础包):标准 OpenSearch 客户端、全部既有 DI Feature、工具函数、测试辅助。零 AWS 依赖。@webiny/api-opensearch-aws(新包):在基础客户端之上叠加 SigV4 签名,并提供一个用于按需创建客户端的 DI Factory 替换实现。
拆分后的架构关系可概括为:基础包提供"裸客户端 + 抽象 + Feature 注册",AWS 包提供"签名包装 + Factory 替换",消费方按需引入。
二、总体执行计划:9 步依赖图
计划由 9 个相互关联的子计划组成,整体依赖顺序如下(来自 00-overview.md):
01-base-client-cleanup │ ▼ 02-new-package-scaffold ──► 03-aws-client-wrapper │ ▼ 04-aws-factory-feature │ ▼ 05-aws-package-exports │ ▼ 06-consumer-event-handler (depends on 05) 07-consumer-template (depends on 05) 08-consumer-webiny-reexport (depends on 05) │ ▼ 09-build-verify (depends on all above)可并行执行的部分:
01与02相互独立,可并行(一个做减法,一个做加法);06、07、08三个消费方改造都只依赖05(导出结构定稿),彼此可并行。
必须串行的部分:
03依赖02(新包目录与 tsconfig 先就位);04依赖03(Factory 内部调用包装函数);05依赖04(导出需要包含 Feature);09依赖前面全部(最终全量构建验证)。
这一依赖设计把最长路径控制在02 → 03 → 04 → 05 → 09,其余环节可多路并行,非常适合多人协作的 PR 拆分。
三、Plan 01:基础包清理,移除 SigV4 逻辑
涉及文件:packages/api-opensearch/src/client.ts
清理任务非常聚焦,只有两步删除:
- 删除第 5 行
import { AwsSigv4Signer } from "@opensearch-project/opensearch/aws"; - 删除整个
if (!clientOptions.auth) { ... }的 SigV4 回退代码块
其余必须原样保留:客户端缓存、错误处理、Client重导出、类型定义。清理后的createOpenSearchClient语义变为:不提供 auth 时创建未签名(unsigned)客户端,适用于本地开发环境或关闭安全认证的自托管 OpenSearch。
清理后的目标实现(与当前仓库 client.ts 一致):
export const createOpenSearchClient = (options: OpenSearchClientOptions): Client => { const key = createClientKey(options); const existing = clients.get(key); if (existing) { return existing; } const { endpoint, node, ...rest } = options; const clientOptions: ClientOptions = { node: endpoint || node, ...rest }; try { const client = new Client(clientOptions); clients.set(key, client); return client; } catch (ex) { const data = { error: ex, node: endpoint || node, ...rest, auth: undefined }; console.error(data); throw new WebinyError("Could not connect to OpenSearch.", "OPENSEARCH_CLIENT_ERROR", data); } };源码层面的几个保留细节
- 客户端缓存:
createClientKey将 options 序列化后做 SHA-256 哈希(crypto.createHash("sha256")),相同配置复用同一个Client实例,避免重复建立连接。 endpoint归一化:endpoint是 Webiny 的扩展字段,node: endpoint || node保证调用方无论传endpoint还是原生node都能正确构建ClientOptions。- 错误包装:连接失败时统一抛出
WebinyError,错误码为OPENSEARCH_CLIENT_ERROR,并在 data 中附带error、node与其余 options 便于排查。
验证方式:yarn build -p @webiny/api-opensearch 2>&1 | tail -30必须编译通过,且api-opensearch内除client.ts外无其他文件改动。
四、Plan 02:新包骨架搭建
新包:@webiny/api-opensearch-aws,位于 packages/api-opensearch-aws/。
目标目录结构
packages/api-opensearch-aws/ ├── package.json ├── tsconfig.json ├── src/ │ ├── index.ts # barrel:公开 API │ ├── createAwsOpenSearchClient.ts # SigV4 包装函数 │ ├── exports/ │ │ └── api/ │ │ └── opensearchAws.ts # 规范消费路径:函数 + Feature │ └── features/ │ └── AwsOpenSearchClientFactory/ │ ├── AwsOpenSearchClientFactory.ts # 实现(内部) │ └── feature.ts # DI Feature 注册package.json 要点
- 包名
@webiny/api-opensearch-aws,版本0.0.0(遵循 monorepo 约定); - 依赖:
@opensearch-project/opensearch(版本与api-opensearch保持一致,用于/aws子路径的AwsSigv4Signer)、@webiny/api-opensearch、@webiny/error、@webiny/feature; - 提供
exports字段,包含./exports/api/opensearchAws.js规范路径; - 结构参考已有小包(如
api-opensearch)。
当前仓库实际落地的 package.json 显示依赖还包括@webiny/api、@webiny/aws-sdk、@webiny/db-dynamodb(后续扩展了 DDB→OS 实体辅助与测试工具),说明该包已随功能演进超越最初骨架,但核心依赖(opensearch 客户端、基础包、error、feature)与计划完全一致。
tsconfig.json 要点
- 继承基础 tsconfig;
- 将
api-opensearch作为 project reference 引用; - 使用
~/路径别名指向src/; - 整体遵循
packages/api-opensearch/tsconfig.json的模式。
五、Plan 03:AWS 客户端包装函数
文件:packages/api-opensearch-aws/src/createAwsOpenSearchClient.ts
这是整个拆分的核心:把旧client.ts中第 37–64 行的 SigV4 逻辑抽取为独立包装函数。当前仓库实现如下:
import { AwsSigv4Signer } from "@opensearch-project/opensearch/aws"; import { createOpenSearchClient, type OpenSearchClientOptions, type Client } from "@webiny/api-opensearch"; import WebinyError from "@webiny/error"; export const createAwsOpenSearchClient = (options: OpenSearchClientOptions): Client => { if (options.auth) { return createOpenSearchClient(options); } const region = process.env.AWS_REGION; if (!region) { throw new WebinyError("Missing AWS_REGION environment variable.", "MISSING_AWS_REGION"); } return createOpenSearchClient({ ...options, ...AwsSigv4Signer({ region, service: "es", getCredentials: () => { const accessKeyId = process.env.AWS_ACCESS_KEY_ID; const secretAccessKey = process.env.AWS_SECRET_ACCESS_KEY; const sessionToken = process.env.AWS_SESSION_TOKEN; if (!accessKeyId || !secretAccessKey) { throw new WebinyError( "Missing AWS credentials (AWS_ACCESS_KEY_ID or AWS_SECRET_ACCESS_KEY).", "MISSING_AWS_CREDENTIALS" ); } return Promise.resolve({ accessKeyId, secretAccessKey, sessionToken }); } }) }); };行为语义与参数说明
| 场景 | 行为 |
|---|---|
options.auth存在 | 原样透传给基础createOpenSearchClient(走 Basic Auth,用于本地/自托管 OpenSearch) |
options.auth不存在 | 从环境变量读取 AWS 凭证,注入AwsSigv4Signer后再调用基础客户端(AWS 托管 OpenSearch/ES) |
涉及的环境变量:
AWS_REGION:必需;缺失时抛出WebinyError,错误码MISSING_AWS_REGION;AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY:必需;任一缺失抛出WebinyError,错误码MISSING_AWS_CREDENTIALS;AWS_SESSION_TOKEN:可选(临时凭证场景),直接随凭证返回;service: "es":SigV4 签名服务标识,对应 Amazon OpenSearch Service / Elasticsearch Service。
错误码沿用旧client.ts中的约定,保证向后兼容。
六、Plan 04:AWS Factory 的 DI Feature
目录:packages/api-opensearch-aws/src/features/AwsOpenSearchClientFactory/
该 Feature 的作用是:在容器中注册时替换基础包的OpenSearchClientFactory绑定,使按需创建客户端走 SigV4 路径。
实现类
当前仓库实现见 AwsOpenSearchClientFactory.ts,与基础包 OpenSearchClientFactory.ts 逐行对应:
class AwsOpenSearchClientFactoryImpl implements OpenSearchClientFactoryAbstraction.Interface { public getClient(params: OpenSearchClientOptions): Client { if (!params.endpoint && !params.node && !params.nodes) { throw new Error( "OpenSearch client requires an endpoint, nodes or node to be specified." ); } return createAwsOpenSearchClient(params); } } export const AwsOpenSearchClientFactory = OpenSearchClientFactoryAbstraction.createImplementation({ implementation: AwsOpenSearchClientFactoryImpl, dependencies: [] });与基础实现的唯一差异:内部调用createAwsOpenSearchClient(带 SigV4)而非createOpenSearchClient。参数校验逻辑(endpoint/node/nodes至少提供一个)与接口完全一致。
OpenSearchClientFactory抽象定义位于 abstraction.ts,通过createAbstraction<IOpenSearchClientFactory>("OpenSearch/ClientFactory")创建,Interface仅要求一个getClient(params): Client方法。
Feature 注册
当前仓库实现见 feature.ts:
import { createFeature } from "@webiny/feature/api/index.js"; import { AwsOpenSearchClientFactory } from "./AwsOpenSearchClientFactory.js"; export const AwsOpenSearchClientFactoryFeature = createFeature({ name: "opensearch.aws.clientFactory", register(container) { container.register(AwsOpenSearchClientFactory).inSingletonScope(); } });Feature 名为opensearch.aws.clientFactory,与基础包 feature.ts 中的opensearch.internal.clientFactory形成区分。两者均以单例(inSingletonScope)方式注册各自实现——由于注册到同一抽象OpenSearch/ClientFactory,后注册的 AWS 实现会覆盖基础绑定,这正是"替换而非装饰"的设计意图。
七、Plan 05:导出结构与规范消费路径
涉及文件:src/index.ts与src/exports/api/opensearchAws.ts
该步遵循minimal barrel exports(最小化桶导出)原则——只导出外部消费者真正需要的内容,内部实现细节不泄漏。
src/index.ts(barrel):计划要求只导出createAwsOpenSearchClient一个公共 API 函数,Feature 不进入 barrel,必须通过规范路径引入。当前仓库 index.ts 在此基础上还导出了AwsOpenSearchClientFactoryFeature与 DDB 实体/表辅助(createOpenSearchEntity、createOpenSearchTable等),说明导出面已按后续功能需求扩展,但"功能实现保持内部"这一原则未变——AwsOpenSearchClientFactory实现类本身没有从任何公开入口导出。src/exports/api/opensearchAws.ts(规范消费路径):同时提供函数与 DI Feature:
export { createAwsOpenSearchClient } from "~/createAwsOpenSearchClient.js"; export { AwsOpenSearchClientFactoryFeature } from "~/features/AwsOpenSearchClientFactory/feature.js";package.json的exports字段:需保证规范路径可被解析,计划中的示例为:
{ "exports": { ".": "./src/index.ts", "./exports/api/opensearchAws.js": "./src/exports/api/opensearchAws.ts" } }当前仓库实际使用"./index.js"与"./*"的通配模式(构建产物路径),但规范路径的解析能力一致。基础包对应的规范入口是 exports/api/opensearch.ts,它导出createOpenSearchClient、OpenSearchClient、OpenSearchClientFactory、各类 Operator/Field/Index 抽象——AWS 包的导出结构正是照此模式设计的。
八、消费方迁移(Plan 06–08)
拆分完成后,仓库 55 个引用@webiny/api-opensearch的导入点中,只有 2 处需要改动:事件处理器与 AWS 项目模板。其余 53 处(各类 Feature、工具、测试辅助均留在基础包)保持不动。
8.1 消费方一:事件处理器@webiny/api-event-handler-aws-ddb-os
文件:createWebinyApiHandler.ts
改动共 4 行,外加package.json增加依赖@webiny/api-opensearch-aws:
- import { createOpenSearchClient, type OpenSearchClientOptions } from "@webiny/api-opensearch"; + import { type OpenSearchClientOptions } from "@webiny/api-opensearch"; + import { createAwsOpenSearchClient, AwsOpenSearchClientFactoryFeature } from "@webiny/api-opensearch-aws";openSearchClientFromEnv中:createOpenSearchClient(...)→createAwsOpenSearchClient(...);registerRootStorage中:OpenSearchClientFactoryFeature.register(container)→AwsOpenSearchClientFactoryFeature.register(container)。
保持不变的部分(当前仓库 createWebinyApiHandler.ts 可验证):
OpenSearchClientFeature的导入与注册(opensearch.internal.client抽象,注册实际客户端实例);OpenSearchQueryBuilderOperatorFeature、OpenSearchFieldFeature、OpenSearchIndexFeature的导入与注册;openSearchClient配置类型与OPENSEARCH_*环境变量读取逻辑。
该处理器从OPENSEARCH_ENDPOINT、OPENSEARCH_USERNAME、OPENSEARCH_PASSWORD构建 options:当用户名/密码存在时走 Basic Auth(自托管场景),缺失时由createAwsOpenSearchClient自动回退到 SigV4(AWS 托管场景)——这一双模式正是包装函数存在的意义。注册顺序上,AWS Factory 先注册,随后OpenSearchQueryBuilderOperatorFeature等其余核心 Feature 照常注册,最终 DDB+ES 的 CMS 存储工厂从容器中解析这些抽象。
8.2 消费方二:AWS 项目模板@webiny/project-aws
文件:packages/project-aws/_templates/extensions/OpenSearch/coreDdbToEsHandler/dynamoToElastic/src/index.ts
该模板是 DynamoDB → OpenSearch 流式同步处理器(由 core 表的 DynamoDB Stream 触发),当前实现见 index.ts:
- import { createOpenSearchClient, type OpenSearchClientOptions } from "@webiny/api-opensearch"; + import { type OpenSearchClientOptions } from "@webiny/api-opensearch"; + import { createAwsOpenSearchClient } from "@webiny/api-opensearch-aws";使用处由createOpenSearchClient(clientOptions)改为createAwsOpenSearchClient(clientOptions)。模板同样保留"有用户名密码则 Basic Auth、否则 SigV4"的双模式逻辑,最终将客户端注入createDdbToOpenSearchStreamHandler。
注意:模板文件通常不直接声明依赖(脚手架生成到用户项目后再解析依赖),是否需要在project-aws/package.json中登记@webiny/api-opensearch-aws需遵循模板依赖的既有处理模式。
8.3 消费方三:聚合包@webiny/webiny重导出
涉及文件:packages/webiny/src/api/opensearchAws.ts(新建)与packages/webiny/package.json
目的是让使用@webiny/webiny规范导入路径的消费者也能拿到 AWS 包能力:
// packages/webiny/src/api/opensearchAws.ts export { createAwsOpenSearchClient } from "@webiny/api-opensearch-aws"; export { AwsOpenSearchClientFactoryFeature } from "@webiny/api-opensearch-aws/exports/api/opensearchAws.js";同时:
packages/webiny/package.json增加依赖@webiny/api-opensearch-aws,并仿照既有./api/opensearch.js条目新增./api/opensearchAws.js的 exports 映射;packages/webiny/tsconfig.json增加对api-opensearch-aws的 project reference。
需要说明:截至当前仓库状态,packages/webiny/src/api/下仅有 opensearch.ts(基础包重导出),opensearchAws.ts尚未出现,说明该步属于计划中的待执行项(或未合入当前分支)——这正好印证了计划文档中"08 依赖 05、可与其他消费方并行"的执行编排。
九、设计决策与不变项
关键设计决策
- 基础包保留原名
api-opensearch:虽然语义上它已变成"服务器通用"基础包,但改名api-opensearch-server需要更新 55 个导入点,功能上没有任何收益,故不做重命名。 - 无 auth = 未签名客户端:本地/开发环境的 OpenSearch(关闭安全认证)直接用基础包即可工作;AWS 环境由专属包在之上叠加 SigV4。
- "包装函数 + DI Feature"双管齐下:既覆盖"急切创建"(事件处理器直接调用
createAwsOpenSearchClient),又覆盖"按需创建"(Factory 从 DI 容器解析),比纯 DI 方案更显式、更灵活。 - Factory 采用"替换"而非"装饰":
AwsOpenSearchClientFactoryFeature直接为OpenSearchClientFactory抽象注册自己的实现并覆盖基础绑定。对于单一覆盖场景,替换比装饰器链更简单。
保持不变的资产
- 全部 DI 抽象(
OpenSearchClient、OpenSearchClientFactory、OpenSearchField、OpenSearchIndex、OpenSearchQueryBuilderOperator)留在基础包; registerOpenSearchCore留在基础包;- 全部测试辅助留在基础包(如
createTestOpenSearchClient、registerOpenSearchCoreForTests,它们直接new Client(),不涉及签名); - 全部工具函数(sort、where、limit、normalize、cursors、indices、waitUntilHealthy 等)留在基础包;
- 基础包的规范导出路径
exports/api/opensearch.ts不变。
这一点在源码结构中清晰可见:packages/api-opensearch/src 下features/、operations/、testing/、utils/、indexConfiguration/均完好保留,client.ts中已无任何AwsSigv4Signer痕迹。
十、构建验证与收尾(Plan 09)
全部子计划完成后,进行全量构建验证。各步骤的验证命令统一为:
yarn build -p @webiny/api-opensearch 2>&1 | tail -30 yarn build -p @webiny/api-event-handler-aws-ddb-os 2>&1 | tail -30 yarn build -p @webiny/webiny 2>&1 | tail -30tail -30用于截取构建输出的尾部,快速聚焦于错误信息或编译完成摘要。
最终验收清单:
@webiny/api-opensearch编译通过,且除client.ts外无任何文件改动;@webiny/api-opensearch-aws的createAwsOpenSearchClient类型解析正确(依赖基础包导出的类型);- 事件处理器
package.json新增依赖后 import 路径可解析,仅 4 行代码变更; @webiny/webiny的./api/opensearchAws.js导出路径解析正确;- 模板文件无明显的类型错误。
结语:拆分带来的架构收益
从设计文档与仓库现状看,这次拆分的核心收益有三点:
- 依赖边界清晰化:非 AWS 部署(PG+OS、自托管 OpenSearch)不再背负 AWS SDK 依赖,构建产物更精简;
- 扩展点显式化:AWS 专属能力(SigV4 签名、凭证获取、Factory 替换)收敛到单一包内,后续新增 AWS 相关行为无需触碰基础包;
- 迁移成本极低:55 个导入点仅 2 处需要修改,其余全部通过"留在基础包"天然兼容,配合可并行的计划编排,整体风险可控。
对于希望在自研项目中复刻这一模式的团队,本仓库的 packages/api-opensearch 与 packages/api-opensearch-aws 两个包、createWebinyApiHandler.ts 的双模式客户端构造,以及全套 计划文档,构成了"平台通用层与云厂商专属层分离"的完整参考实现。
- 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 拆分 `api-opensearch-aws`:将 AWS SigV4 签名从基础 OpenSearch 客户端解耦
Webiny 拆分 api opensearch aws :将 AWS SigV4 签名从基础 OpenSearch 客户端解耦 本指南基于 Webiny 仓库
CMS后端前端PuerTS Unity 生成控制(Filter)完全指南:过滤编译错误接口、JS 权限控制与 xIl2cpp 类型过滤
PuerTS Unity 生成控制(Filter)完全指南:过滤编译错误接口、JS 权限控制与 xIl2cpp 类型过滤 本篇技术指南围绕 PuerTS 的 F
CMS后端前端Webiny-js 从 Elasticsearch 迁移到 OpenSearch:`@webiny/api-opensearch` 包实现完全指南
Webiny js 从 Elasticsearch 迁移到 OpenSearch: @webiny/api opensearch 包实现完全指南 导读 本文围绕
CMS后端前端
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考