Teable v2 Core 集中式 DI 注册架构:registerV2CoreServices 的设计与使用指南
【免费下载链接】teable✨ AI Spreadsheet for Business项目地址: https://gitcode.com/GitHub_Trending/te/teable
导读
本文基于 Teable 开源仓库中 packages/v2/core/src/di/ARCHITECTURE.md 展开,深入剖析 v2 引擎核心服务(application services、ports 默认实现)的集中式依赖注入(DI)注册方案。你将掌握registerV2CoreServices的"先注册优先"覆盖机制、三步式容器装配流程、v2CoreTokens令牌体系,以及如何在 Node 与浏览器容器包中复用这套注册逻辑,避免在多容器包间复制注册代码。
一、为什么需要集中式 DI 注册
Teable 的 v2 引擎同时面向 Node.js 服务端(Postgres + pg 驱动)与浏览器环境(Pglite、Noop),存在多个"容器包"(container 包)。如果每个容器包各自重复注册 core 内部服务,会产生三个问题:
- 注册逻辑重复:Node 容器、浏览器容器、测试容器都要为同一批应用服务编写几乎相同的
c.register(...)代码,维护成本高、容易漂移。 - 默认实现难以覆盖:core 内部服务往往需要外部容器按运行环境提供不同的基础设施实现(例如
tableRepository在 Postgres 与 Pglite 下完全不同)。 - 生命周期不一致:各容器可能对同一服务使用不同的 lifecycle(Singleton/Transient),行为不可预期。
因此,di目录(packages/v2/core/src/di)的职责被明确定义为:
- 为 v2 core 内部服务提供集中式 DI 注册;
- 允许外部容器覆盖默认实现;
- 消除跨容器包的注册重复。
二、目录结构与文件职责
di目录下的文件与职责如下:
| 文件 | 角色 | 职责 |
|---|---|---|
| ARCHITECTURE.md | 架构说明 | 描述 DI 注册范围、设计意图与使用模式 |
| index.ts | barrel 导出 | 对外重新导出注册工具函数 |
| registerCoreServices.ts | 注册工具 | 注册全部 core 应用服务,支持覆盖(override) |
| registerRecordWritePlugin.ts | 插件注册 | 按名称去重地追加记录写入插件 |
| registerFieldOperationPlugin.ts 等 | 插件注册 | 字段/表/视图/数据安全限额插件的注册辅助函数 |
index.ts仅做一件事:
export * from './registerCoreServices';也就是说,对外(@teable/v2-core)只暴露注册工具,容器包只需import { registerV2CoreServices } from '@teable/v2-core'即可。
三、核心机制:"先注册优先"的覆盖策略
registerV2CoreServices的函数签名如下(见 registerCoreServices.ts):
export const registerV2CoreServices = ( container: DependencyContainer, options: IRegisterCoreServicesOptions = {} ): DependencyContainer => {其中IRegisterCoreServicesOptions只有一个可选字段:
export interface IRegisterCoreServicesOptions { /** * Lifecycle for registered services. * @default 'Singleton' */ lifecycle?: Lifecycle; }lifecycle默认值为Lifecycle.Singleton,即所有由本函数注册的服务默认以单例生命周期装配:
const lifecycle = options.lifecycle ?? Lifecycle.Singleton;覆盖机制的核心是:注册前先检查container.isRegistered(token),只有尚未注册的令牌才会被写入默认实现。例如:
if (!container.isRegistered(v2CoreTokens.tableUpdateFlow)) { container.register(v2CoreTokens.tableUpdateFlow, TableUpdateFlow, { lifecycle }); } if (!container.isRegistered(v2CoreTokens.tableQueryService)) { container.register(v2CoreTokens.tableQueryService, TableQueryService, { lifecycle }); }由于函数返回传入的DependencyContainer,可以链式继续装配其他模块(如registerCommandExplainModule(c)的写法)。
覆盖失败场景
需要注意,覆盖必须发生在调用registerV2CoreServices之前。如果先调用registerV2CoreServices(c)再c.register(v2CoreTokens.tableQueryService, Custom),由于令牌已被注册,自定义实现会被静默忽略(isRegistered返回 true,跳过默认注册,但外部重复注册依赖具体容器实现,tsyringe 通常以先注册者为准)。正确的覆盖姿势见下文使用模式。
四、三步式使用模式
原文档给出了容器包的标准装配流程,这里结合源码补充细节:
// In container packages (e.g. @teable/v2-container-node): import { registerV2CoreServices, v2CoreTokens } from "@teable/v2-core"; import { Lifecycle } from "@teable/v2-di"; // 1. Register infrastructure first (ports implementations) c.register(v2CoreTokens.tableRepository, PostgresTableRepository); c.register(v2CoreTokens.unitOfWork, PostgresUnitOfWork); // ... // 2. Optionally override core services BEFORE calling registerV2CoreServices // c.register(v2CoreTokens.tableQueryService, CustomTableQueryService); // 3. Register core services (skips already-registered tokens) registerV2CoreServices(c, { lifecycle: Lifecycle.Singleton });第一步:注册基础设施(ports 实现)。tableRepository、unitOfWork、tableRecordQueryRepository、tableSchemaRepository等端口令牌由容器包根据运行环境提供真实实现,它们是 core 服务的依赖底座。
第二步(可选):覆盖 core 服务。如果某个 core 服务(如tableQueryService)需要定制,就在调用registerV2CoreServices之前先注册自己的实现。
第三步:批量注册 core 服务。registerV2CoreServices会跳过所有已注册的令牌,因此先注册的自定义实现天然胜出。
五、核心服务注册清单
registerV2CoreServices中注册的服务及其职责如下(对应源码注释与表格,见 registerCoreServices.ts):
| Token | 服务 | 职责 |
|---|---|---|
tableUpdateFlow | TableUpdateFlow | 事务化的表更新工作流 |
tableQueryService | TableQueryService | 通用表查询操作 |
fieldCreationSideEffectService | FieldCreationSideEffectService | 跨表字段创建副作用 |
fieldDeletionSideEffectService | FieldDeletionSideEffectService | 跨表字段删除副作用 |
fieldUpdateSideEffectService | FieldUpdateSideEffectService | 依赖字段的级联更新 |
tableDeletionSideEffectService | TableDeletionSideEffectService | 删除表前的跨表清理 |
foreignTableLoaderService | ForeignTableLoaderService | 加载并校验外键表引用 |
linkTitleResolverService | LinkTitleResolverService | 将链接标题解析为记录 ID(typecast 支持) |
attachmentValueResolverService | AttachmentValueResolverService | 写入时解析附件值 |
userValueResolverService | UserValueResolverService | 写入时解析用户值 |
recordMutationSpecResolverService | RecordMutationSpecResolverService | 解析 spec 中的外部值 |
recordWritePluginRunner | RecordWritePluginRunner | 运行类型化记录写入插件 |
recordWriteSideEffectService | RecordWriteSideEffectService | 收集记录写入时的表副作用 |
recordCreationService | RecordCreationService | 共享的单记录创建工作流 |
schemaOperationRunnerService | SchemaOperationRunnerService | 运行 schema 操作的修复处理器 |
除上述"文档核心表"之外,源码中还注册了大量其他服务,包括:tableCreationService(批量建表 + 副作用)、linkFieldUpdateSideEffectService(链接字段的对称建/删)、fieldUndoRedoSnapshotService/fieldUndoRedoReplayService(字段级撤销重做)、fieldCrossTableUpdateSideEffectService、recordBulkUpdateService、recordReorderService、recordWriteUndoRedoPlanService、recordBatchCreationService、pasteStreamApplicationService、restoreFieldStreamApplicationService、deleteByRangeApplicationService、duplicateRecordsApplicationService、attachmentValueDecoratorService、recordChangedValueDecoratorService、undoRedoService(UndoRedoStackService)等,最终返回同一个 container。
六、v2CoreTokens:令牌体系
所有服务令牌集中定义在 packages/v2/core/src/ports/tokens.ts 的v2CoreTokens对象中,每个令牌是一个带描述字符串的Symbol,例如:
export const v2CoreTokens = { tableRepository: Symbol('v2.core.tableRepository'), tableQueryService: Symbol('v2.core.tableQueryService'), // ... unitOfWork: Symbol('v2.core.unitOfWork'), commandBus: Symbol('v2.core.commandBus'), eventBus: Symbol('v2.core.eventBus'), // ... } as const;使用Symbol作为令牌可以避免字符串令牌之间的命名冲突,并保证跨包引用同一符号。令牌类别大致分为:
- 基础设施端口:
baseRepository、tableRepository、tableRecordQueryRepository、tableRecordRepository、tableSchemaRepository、schemaOperationRepository、unitOfWork、commandBus、internalCommandBus、queryBus、eventBus、realtimeEngine、logger、tracer、hasher、csvParser、dotTeaParser; - 应用服务:
tableUpdateFlow、tableQueryService、tableCreationService、各副作用服务、recordCreationService、recordBulkUpdateService等; - 插件集合:
recordWritePlugins、fieldOperationPlugins、tableOperationPlugins、viewOperationPlugins、tableDataSafetyLimitPlugins; - 可选能力:
undoRedoStore、recordOrderCalculator、attachmentUrlSignerService、computedFieldBackfillService、fieldTrashRepository等。
@teable/v2-di还提供createToken<T>(description)帮助函数(见 packages/v2/di/src/index.ts),其实现本质就是Symbol(description)的类型化封装。
七、默认实现与覆盖点(Noop 策略)
为了让 core 服务在任何环境下都能启动,registerV2CoreServices为若干"必须由适配器提供"的能力注册了 Noop 默认实现,容器包可按需覆盖:
| 令牌 | 默认实现 | 覆盖场景 |
|---|---|---|
attachmentUrlSignerService | NoopAttachmentUrlSignerService | Nest 适配器需提供真实 URL 签名 |
recordOrderCalculator | NoopRecordOrderCalculator | 适配器必须提供,否则排序计算返回notImplemented错误 |
undoRedoStore | NoopUndoRedoStore | 需要持久化撤销重做栈的容器 |
fieldTrashRepository | NoopFieldTrashRepository | 需要字段回收站能力时 |
computedFieldBackfillService | NoopComputedFieldBackfillService | 计算字段回填能力 |
tableQueryObservability | NoopTableQueryObservability | 查询可观测性埋点 |
fieldDeleteSnapshotSink | NoopFieldDeleteSnapshotSink | 字段删除快照归档 |
例如NoopRecordOrderCalculator(见 packages/v2/core/src/ports/defaults/NoopRecordOrderCalculator.ts)直接返回err(domainError.notImplemented(...)),提醒调用方该能力需要由适配器补齐。
插件注册的幂等性
registerV2CoreServices内部还会装配"数据安全限额"(TableDataSafetyLimit)体系,涉及四类插件注册辅助函数(record / field / table / view)。以registerRecordWritePlugin(见 packages/v2/core/src/di/registerRecordWritePlugin.ts)为例,它按插件名称去重,同名插件重复注册时返回registered: false并保留已有实例,从而保证多次调用registerV2CoreServices或组合多个注册源时插件列表不膨胀。
八、容器包中的实际应用
Node.js 容器(Postgres)
packages/v2/container-node/src/index.ts 中的registerV2NodePgDependencies完整演示了装配流程:先注册 Postgres 数据源(meta/data 库、schema)、PostgresUnitOfWork、logger/tracer、内存版 Command/Query/Event 总线、CSV 解析器、crypto hasher 与tableDataSafetyLimits,最后才调用:
// Register core services (uses defaults unless already registered) registerV2CoreServices(c, { lifecycle: Lifecycle.Singleton });并提供createV2NodePgContainer(内部container.createChildContainer())创建独立子容器。
浏览器容器(Pglite / Noop)
packages/v2/container-browser/src/index.ts 提供两条路径:
registerV2BrowserPgliteDependencies:使用 Pglite 内存库注册 Postgres 适配器与PostgresUnitOfWork,再调用registerV2CoreServices;registerV2BrowserNoopDependencies:为所有仓储端口注册Noop*实现后,同样调用registerV2CoreServices。
两个容器包都通过同一行调用获得全部 core 服务,这正是"消除跨容器注册重复"这一设计目标的直接体现。关于数据安全限额的环境变量解析,可参考 packages/v2/container-node/src/tableDataSafetyLimits.ts(如TABLE_LIMIT_RECORDS_PER_TABLE_MAX、TABLE_LIMIT_FIELDS_PER_TABLE_MAX等)。
九、总结
Teable v2 的di架构以registerV2CoreServices为单一事实来源:默认Singleton生命周期、isRegistered先行检查实现"先注册优先"覆盖、Noop 默认值保证开箱即用、插件注册函数保证幂等。理解这套机制后,接入新容器(如新的数据库适配器或测试环境)只需三步:注册基础设施端口 → 按需覆盖 core 服务 → 调用registerV2CoreServices,无需再为每个容器包复制几十行重复注册代码。
【免费下载链接】teable✨ AI Spreadsheet for Business项目地址: https://gitcode.com/GitHub_Trending/te/teable
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考