ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

teable v2 视图领域模型架构解析:View 抽象、类型体系与访问者模式

teable v2 视图领域模型架构解析:View 抽象、类型体系与访问者模式 teable v2 视图领域模型架构解析View 抽象、类型体系与访问者模式【免费下载链接】teable✨ AI Spreadsheet for Business项目地址: https://gitcode.com/GitHub_Trending/te/teable导读本文基于 teable 仓库 packages/v2/core/src/domain/table/views/ARCHITECTURE.md 展开深入剖析 v2 核心领域中视图View这一领域模型的架构设计。视图是表格产品中面向用户的数据呈现层抽象——同一个表格的数据可以通过网格、看板、日历、表单、画廊、插件等多种形态展示。读完本文你将掌握View实体基类的职责边界、六种视图子类型的定义方式、ViewColumnMeta与ViewQueryDefaults等值对象的作用以及ViewFactory与访问者Visitor模式如何支撑视图的创建与按类型分派逻辑并学会用仓库中的源码与测试印证这些设计。一、领域定位domain/table/views目录的职责边界在 teable v2 的核心领域包packages/v2/core/src/domain/table下views子目录承担了与视图相关的全部领域建模职责。根据 ARCHITECTURE.md 的声明它只做三件事视图实体基类与视图类型定义View抽象类定义了所有视图共享的行为ViewType定义了视图类型的枚举封装视图列元数据column meta的默认值与校验ViewColumnMeta负责列级配置顺序、可见性、宽度等的构建与合法性检查以ViewFactory作为统一的视图创建入口外部代码不应直接new某个具体视图而是通过工厂函数按需创建。这符合 v2 核心包领域驱动设计DDD 六边形架构的整体风格目录内只放领域实体、值对象与工厂不涉及数据库访问与 HTTP 协议——持久化由ports层的 DefaultTableMapper.ts 负责见下文访问者一节。二、目录结构总览types 与 visitors 两个子域views目录通过两个子文件夹把类型与行为分离子目录职责关键文件types/具体的视图子类型实现保证ViewType与实体子类型一一对应GridView.ts、KanbanView.ts、GalleryView.ts、CalendarView.ts、FormView.ts、PluginView.tsvisitors/视图访问者接口与默认实现支撑按子类型分派的逻辑IViewVisitor.ts、CloneViewVisitor.ts、NoopViewVisitor.ts对应地两个子目录各有自己的 types/ARCHITECTURE.md 与 visitors/ARCHITECTURE.md 架构笔记职责分别是视图子类型实现与访问者接口 默认实现使子类型特定的分派逻辑成为可能。2.1 视图类型与实体子类型的强对应关系在types/中每一种视图形态都是一个继承View的具体实体。以网格视图为例GridView.ts 的实现非常精简export class GridView extends View { private constructor(id: ViewId, name: ViewName) { super(id, name, ViewType.grid()); } static create(params: { id: ViewId; name: ViewName }): ResultGridView, DomainError { return ok(new GridView(params.id, params.name)); } acceptT void(visitor: IViewVisitorT): ResultT, DomainError { return visitor.visitGridView(this); } }可以看到关键点私有构造函数 静态create与ViewId、ViewName、ViewColumnMeta等值对象保持一致的模式——不直接暴露new而是通过返回Result的工厂方法创建把校验集中到入口处子类型与ViewType强绑定GridView构造时固定传入ViewType.grid()这正是在 types/ARCHITECTURE.md 中强调的Keep ViewType aligned with the entity subtype——实体类型与枚举值一一对应从类型系统层面杜绝网格视图却声称自己是日历视图的不一致状态accept双分派子类把自身转交给访问者的对应方法visitor.visitGridView(this)实现访问者模式中的可逆分派。三、核心实体View抽象基类View.ts 是views目录的枢纽它继承了共享的EntityViewId并实现了OnTeableViewFieldDeleted接口。它的职责可以拆成三层3.1 持有不可变身份与元信息View的构造函数只接收三个参数id: ViewId、name: ViewName、type: ViewType。对外暴露三个只读访问器name()视图名称值对象ViewNametype()视图类型值对象ViewTypeid()继承自EntityViewId的标识。3.2 三个一次性设置的可选配置视图还持有三个按需注入、且只能设置一次的配置项全部通过Result返回校验结果配置类型行为columnMetaViewColumnMeta列级元数据未设置时读取返回invariant错误设置时若已存在且不等价则拒绝覆盖queryDefaultsViewQueryDefaults视图默认查询筛选/排序/分组/手动排序语义同上optionsunknown视图类型专属选项通过JSON.stringify深比较已设置且不等价则拒绝覆盖这种set once的设计把领域不变量invariant显式化例如 ViewColumnMeta.ts 中equals基于逐字段Object.is深比较ViewQueryDefaults.equals则直接比较JSON.stringify结果——重复设置相同值幂等放行设置不同值则报错避免静默覆盖造成的数据不一致。3.3 字段删除联动onFieldDeletedView实现了OnTeableViewFieldDeleted接口的onFieldDeleted(deletedField, context)方法这是视图与字段领域协作的核心场景当某个字段被删除时所有引用它的视图配置必须同步清理。该方法依次处理清理columnMeta删除被删字段的列配置条目并把后续字段的order依次前移order - 1若当前视图没有该字段的 order会尝试从context.previousSourceTable的旧视图里找回对应字段类型转换场景清理queryDefaults递归地从筛选树中移除引用该字段的节点removeFieldReferenceFromFilterNode递归处理普通条件、items组合节点与not节点从排序、分组列表中过滤掉该字段并在排序被清空时同步清掉manualSort返回变更摘要若columnMeta或queryDefaults任一发生变化返回{ viewId, fieldId, columnMeta?, queryDefaults? }供上层持久化。这套逻辑有专门的测试覆盖ViewFieldDeletion.spec.ts是本目录中领域逻辑最重的部分也是视图配置在字段变更后保持一致性的兜底保障。四、值对象族ViewId、ViewName、ViewType、ViewColumnMeta、ViewQueryDefaultsviews目录内聚了五个值对象全部基于neverthrow的Resultzod校验模式构建统一遵循create(raw)/rehydrate(raw)/equals/toString的约定。4.1 ViewId前缀化标识与行序列名ViewId.ts 定义了viw前缀 16 位随机体的 ID 规范prefixedIdRegex(viw, 16)并提供generate()静态方法。特别值得注意的是它的领域专属方法toRowOrderColumnName(): string { return __row_${this.value}; }行顺序row order列用于存储记录在某个视图中的手动排列位置命名格式为__row_{viewId}。这是视图 ID 与物理存储之间的一条重要映射约定——数据库层需要按视图生成行序列名时直接调用该方法即可保持命名一致。4.2 ViewName非空名称约束ViewName.ts 的校验规则只有一条z.string().trim().min(1)——视图名必须是非空字符串自动去除首尾空白。对应测试 ViewBasics.spec.ts 中ViewName.create(Grid)成功而ViewName.create()返回错误。4.3 ViewType六种视图类型的枚举封装ViewType.ts 用 zod 枚举定义类型字面量const viewTypeSchema z.enum([grid, calendar, kanban, form, gallery, plugin]);即 v2 核心领域当前支持grid网格、calendar日历、kanban看板、form表单、gallery画廊、plugin插件六种视图。除create(raw)校验外还提供grid()、calendar()等六个静态工厂方法供各子类型构造函数直接调用如GridView中的ViewType.grid()。4.4 ViewColumnMeta列配置的校验与默认构建ViewColumnMeta.ts 是RecordfieldId, ViewColumnMetaEntry的封装。ViewColumnMetaEntry采用z.looseObject允许未知键以兼容插件视图的扩展字段已知键包括键类型含义ordernumber \| null列在视图中的显示顺序visibleboolean是否可见主要用于表单/看板/画廊/日历视图hiddenboolean是否隐藏网格视图常用widthnumber列宽requiredboolean是否必填表单视图使用statisticFuncstring \| null列的统计函数如求和、平均值等它最核心的静态方法是forView({ viewType, fields, primaryFieldId })负责按视图类型生成列元数据默认值先按主字段排第一其余字段保持原顺序的原则为每个字段分配orderorderFieldIds表单视图通过FieldFormVisibilityVisitor逐个访问字段只有允许在表单中出现的字段才被标记为visible: true看板/画廊/日历视图主字段被标记为visible: true。这段逻辑精确印证了 ARCHITECTURE.md 中View column meta defaults and validation的职责描述——不同视图类型的默认列配置策略集中在值对象内部实现。4.5 ViewQueryDefaults筛选/排序/分组/手动排序的默认查询ViewQueryDefaults.ts 封装了视图的默认查询条件schema 为{ filter, sort, group, manualSort }且使用.strict()拒绝未知字段filterRecordFilter | null复用查询模块的recordFilterSchema来自 RecordFilterDto.tssort{ fieldId, order: asc | desc }[]group{ fieldId, order: asc | desc }[]manualSortboolean是否开启手动排序。其merge({ filter, sort, group })方法定义了默认查询与用户临时查询的合并语义值得单独说明filter若查询 filter 为null则整体置空若未提供则沿用默认若两者都有则用{ conjunction: and, items: [defaultFilter, queryFilter] }合并为 AND 组合sort手动排序开启且无查询排序时返回空数组即保持手动顺序查询排序优先默认排序中未冲突的项按顺序追加用Map按 fieldId 去重group查询分组优先否则沿用默认分组。五、访问者模式按视图子类型分派的扩展点5.1 IViewVisitor 接口IViewVisitor.ts 定义了泛型访问者接口export interface IViewVisitorT void { visitGridView(view: GridView): ResultT, DomainError; visitKanbanView(view: KanbanView): ResultT, DomainError; visitGalleryView(view: GalleryView): ResultT, DomainError; visitCalendarView(view: CalendarView): ResultT, DomainError; visitFormView(view: FormView): ResultT, DomainError; visitPluginView(view: PluginView): ResultT, DomainError; }每个方法接收对应的具体视图实体、返回ResultT, DomainError。结合View.accept(visitor)形成标准的双分派新增一种对视图类型敏感的操作如导出、克隆、持久化映射时只需新实现一个访问者无需修改任何视图子类而新增视图类型时则必须为所有访问者补齐对应方法——编译器会强制这一契约。5.2 默认实现与真实应用visitors/目录提供了两个开箱即用的实现NoopViewVisitor.ts所有方法返回ok(undefined)的空实现供只关心部分类型的访问者继承复用CloneViewVisitor.ts以相同 id/name 克隆视图子类型的访问者。更真实的访问者应用位于持久化映射层DefaultTableMapper.ts中的ViewToPersistenceVisitor implements IViewVisitorITableViewPersistenceDTO见 DefaultTableMapper.ts——领域对象到持久化 DTO 的转换正是通过实现访问者完成的这也被 visitors/ARCHITECTURE.md 明确列为示例。由此可以推断视图的仓储读取/写入、类型专属字段映射等跨层逻辑都统一走访问者通道领域层保持对持久化细节零依赖。六、统一创建入口ViewFactoryViewFactory.ts 以 6 个函数导出六种视图的创建入口均接收{ id: ViewId; name: ViewName }并返回ResultView, DomainErrorexport const createGridView (params: { id: ViewId; name: ViewName }): ResultView, DomainError GridView.create(params); // createKanbanView / createGalleryView / createCalendarView / createFormView / createPluginView 同构之所以需要这一层薄工厂是因为具体子类型的create返回的是ResultGridView, DomainError等更精确的类型而调用方如表单提交、导入、模板实例化通常只关心得到一个视图实体工厂把返回类型统一收敛为ResultView, DomainError同时把子类型的选择集中在一处便于将来扩展新视图类型。七、测试验证ViewBasics.spec 如何印证设计ViewBasics.spec.ts 是本目录的示例测试直接印证了上文描述的所有核心契约ViewName 校验Grid合法、空串非法_unsafeUnwrapErr()断言失败路径同名值对象按值相等、异名不相等类型 × 访问者全矩阵定义RecordingViewVisitor implements IViewVisitorstring六个 visit 方法分别返回grid、kanban等字符串对六种子类型逐一accept并断言返回值验证双分派正确性工厂可用性通过createGridView等 6 个工厂函数创建全部视图类型并断言成功NoopVisitor 兼容gridView.accept(new NoopViewVisitor())断言成功。另外还有 ViewType.spec.tsViewType 校验、ViewQueryDefaults.spec.ts合并语义、ViewFieldDeletion.spec.ts字段删除联动三个测试文件共同构成对views领域模型的回归保护网。八、架构模式小结与扩展指引纵观整个domain/table/views目录可以提炼出四条可复用的设计原则值对象自治ViewId/ViewName/ViewType/ViewColumnMeta/ViewQueryDefaults各自负责自己的校验、相等比较与 DTO 输出实体只做组合与编排类型与行为解耦新增视图类型只需在types/加子类并同步ViewType枚举新增视图行为只需在visitors/加访问者实现两者互不侵入Result 驱动的显式错误所有创建、设置、合并操作都返回neverthrow的Result错误路径由DomainError统一描述杜绝隐式抛异常与null泄漏领域联动内聚字段删除对视图配置的清理逻辑列顺序重排、筛选树剪枝、排序/分组过滤完整收敛在View.onFieldDeleted内部保证跨实体一致性。若要在该领域继续深入建议按以下路径阅读视图子类型各文件types/访问者实现visitors/ 与持久化映射 DefaultTableMapper.ts字段删除联动的完整上下文OnTeableViewFieldDeleted.ts 及其测试 ViewFieldDeletion.spec.ts领域错误体系DomainError见domain/shared/DomainError。通过以上源码与测试的交叉印证可以完整理解 teable v2 如何以实体 值对象 工厂 访问者的组合构建出一个类型安全、可扩展且行为自洽的视图领域模型。【免费下载链接】teable✨ AI Spreadsheet for Business项目地址: https://gitcode.com/GitHub_Trending/te/teable创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表