从隐藏 Sheet 到结构化数据模型:SpreadJS V19.1 DataManager 本地数据源的工程价值

从隐藏 Sheet 到结构化数据模型:SpreadJS V19.1 DataManager 本地数据源的工程价值

在很多 SpreadJS 项目中,开发者都会遇到一个看似不起眼、但长期影响工程质量的问题:业务数据到底应该放在哪里?

如果数据只用于单元格展示,直接写入工作表区域没有问题;如果数据只是少量状态,放在tag里也能解决一时之需。但当需求进入工程级阶段,例如要维护订单、客户、产品、层级任务、报表明细、图表数据源,并且这些数据还要被多个组件复用时,tag和隐藏工作表就会逐渐变成“临时数据库”。

这种做法能跑,但代价很明显:数据结构没有显式 schema,字段含义散落在业务代码里;隐藏 Sheet 容易被导入导出、权限、协同、公式引用等问题影响;多组件复用时经常需要从单元格区域反复组装 JSON;一旦数据量变大,维护成本会比功能本身增长得更快。

SpreadJS V19.1 对 DataManager 的增强,正是在这个位置补上了一块关键拼图:DataManager 现在支持 local 数据源。过去 DataManager 更多用于远程数据源,例如 REST API、OData、GraphQL;现在,我们可以直接用 JSON 对象数组、CSV 字符串或 XML 字符串创建 DataManager 表。对于大量前端本地数据建模、模板配置、离线应用、协同编辑和原型验证场景,这会让架构变得清爽很多。

DataManager local 到底解决了什么

DataManager 是 SpreadJS 内置的结构化运行时数据建模引擎。它不是一个 UI 组件,而是 Workbook 级的数据层。一个 Workbook 对应一个 DataManager,可以通过:

const dataManager = spread.dataManager();

拿到这个实例之后,开发者可以在其中创建表、定义 schema、建立关系、创建视图,并把这些表或视图交给 TableSheet、ReportSheet、Data Chart 等组件使用。

V19.1 中的 local 数据源让这套能力不再依赖后端接口。最小示例如下:

const orders = dataManager.addTable("Orders", { data: [ { orderId: 1001, customer: "演示客户-甲-本地数据源", amount: 38400 }, { orderId: 1002, customer: "演示客户-乙-协同样例", amount: 17200 } ] }); await orders.fetch();

这里的关键点是data。它表示这是一张内存表,数据存储在客户端本地。所有操作都在内存中完成,不涉及服务器通信。即使数据已经直接传入,仍建议在后续创建视图或绑定组件前调用fetch(),让表完成内部初始化和 schema 处理。

从“存数据”升级为“建模型”

把数据放进 DataManager,并不只是换一个容器。它真正的价值在于:数据可以被建模。

例如订单数据中,金额不应该每次都由 UI 代码手动计算;日期不应该在多个地方分别解析;主键也不应该只存在于开发者的脑子里。这些都可以进入 schema:

const orderTable = dataManager.addTable("Orders", { data: orders, schema: { columns: { orderId: { dataType: "number", isPrimaryKey: true }, customerId: { dataType: "number" }, product: { dataType: "string" }, quantity: { dataType: "number" }, unitPrice: { dataType: "number" }, amount: { dataType: "formula", value: "=[@quantity] * [@unitPrice]" }, orderDate: { dataType: "date" } } } });

这段代码里,orderId是主键,orderDate是日期字段,amount是公式列。DataManager 会把这些规则作为数据模型的一部分保存下来。相比隐藏 Sheet,这种方式更接近工程中的领域模型:字段、类型、计算规则和标识都写在同一个结构里。

local 数据源还支持 JSON、CSV、XML 等不同输入格式。JSON 数组是默认格式;如果使用 CSV 或 XML 字符串,则需要在schema.type中显式声明:

const employees = dataManager.addTable("Employees", { data: `id,name,age 1,Alice,24 2,Bob,26`, schema: { type: "csv" } });

如果数据中存在嵌套对象,也可以通过spread: true将一级对象展开为列,避免手写扁平化逻辑:

const sales = dataManager.addTable("Sales", { data: [ { id: 1, address: { country: "China", province: "Shanghai", city: "Shanghai" } } ], schema: { columns: { address: { spread: true } } } });

多表关系:本地数据也可以有关系型表达

隐藏 Sheet 常见的另一个问题是多表关系难维护。比如订单表里有customerId,客户名称和行业信息在另一个数据集合里。以前我们可能会在公式、脚本或辅助表里做查找。使用 DataManager 后,可以直接在模型层建立关系:

const orders = dataManager.addTable("Orders", { data: [ { orderId: 1001, customerId: 1, product: "SpreadJS" } ], schema: { columns: { orderId: { isPrimaryKey: true }, customerId: { dataType: "number" } } } }); const customers = dataManager.addTable("Customers", { data: [ { customerId: 1, customerName: "演示客户-甲-本地数据源", industry: "虚拟行业-A" } ], schema: { columns: { customerId: { isPrimaryKey: true } } } }); dataManager.addRelationship( orders, "customerId", "customer", customers, "customerId", "orders" );

建立关系后,视图中可以用点路径访问关联表字段:

const orderView = orders.addView("orderView", [ { value: "orderId", caption: "订单号" }, { value: "customer.customerName", caption: "客户" }, { value: "customer.industry", caption: "行业" }, { value: "product", caption: "产品" } ]); await orderView.fetch();

这让本地数据也具备关系型建模能力。数据仍然在浏览器内存里,但结构已经不再是散乱的数组,而是可被 SpreadJS 组件理解、复用和序列化的结构化模型。

一份数据,多种消费方式

DataManager local 的另一个工程价值,是让同一份数据可以被多个 SpreadJS 组件消费。

TableSheet 绑定的是视图:

const tableSheet = spread.addSheetTab( 0, "Orders", GC.Spread.Sheets.SheetType.tableSheet ); tableSheet.setDataView(orderView);

Data Chart 可以直接通过表名引用 DataManager 中的表:

const chart = sheet.dataCharts.add( "amountByProduct", 30, 55, 720, 360, GC.Spread.Sheets.DataCharts.DataChartType.column ); chart.setChartConfig({ tableName: "Orders", plots: [ { type: GC.Spread.Sheets.DataCharts.DataChartType.column, encodings: { values: [ { field: "amount", aggregate: GC.Spread.Sheets.DataCharts.Aggregate.sum } ], category: { field: "product" }, color: { field: "product" } } } ] });

ReportSheet 则可以通过绑定表达式使用结构化表数据:

const reportSheet = spread.addSheetTab( 1, "Report", GC.Spread.Sheets.SheetType.reportSheet ); const template = reportSheet.getTemplate(); template.setTemplateCell(1, 0, { type: "List", binding: "Orders[orderId]" }); template.setTemplateCell(1, 1, { type: "List", binding: "Orders[product]" }); template.setTemplateCell(1, 2, { type: "List", binding: "Orders[amount]" }); reportSheet.refresh();

这也是 DataManager 比隐藏 Sheet 更适合工程化数据的原因:数据模型属于 Workbook,组件只是用不同方式消费它。TableSheet 关注交互编辑,Data Chart 关注可视化,ReportSheet 关注报表生成;它们不用各自维护一套数据副本。

为了验证上述代码路径,我们基于 V19.1 npm 包制作了一个最小 Demo,并将同一份 DataManager local 数据模型放入 SpreadJS Designer 组件中运行。这样既能看到类 Excel 的设计器界面,也能确认 DataManager 表、关系、视图、数据图表和报表绑定都来自同一个 Workbook。

TableSheet 绑定的是orderView,右侧面板可以看到当前视图字段,表格中的客户名是专门构造的演示占位数据:

Data Chart 通过tableName: "Orders"直接引用 DataManager 表,并按产品聚合订单金额:

ReportSheet 通过模板绑定表达式使用Orders[...]字段生成报表:

协同:local 数据源的新价值,但要理解边界

V19.1 还有一个非常值得强调的增强:协同服务支持 DataManager 本地数据源。过去远程数据源的协同更多依赖后端服务维护数据一致性;现在,当表使用data: [...]这种 JSON 内存本地数据时,数据集状态也可以纳入协同快照和同步流程。

也就是说,在 TableSheet 场景下,基于 JSON local 数据的以下操作可以参与多人协同同步:

  • 编辑记录字段值

  • 新增或删除记录

  • 修改列字段定义

  • 更新表结构配置

  • 层级操作

  • 排序、筛选、分组及视图更新

不过这里必须写清楚边界:目前仅 TableSheet 支持基于 DataManager 的协作模式;只有data配置项下的 JSON 本地数据会同步数据集变更。远程 URL 数据源的数据变更会发送到后端服务,协同系统不负责同步数据集;函数处理器模式由于不可序列化,不能参与实时协作;CSV/XML 初始化的数据集也不会同步数据变更。

这个边界并不是缺点,而是架构职责的划分:可序列化、可确定性复现、由 DataManager 管控的状态,适合进入协同;交给外部系统维护的状态,应由外部系统保证一致性。

导入导出与持久化

DataManager local 数据完全驻留在内存中,因此适合随 Workbook 一起做 JSON 或 SJS 级别的持久化。对于 TableSheet 场景,导出 JSON 时可以包含绑定源:

const json = spread.toJSON({ includeBindingSource: true, saveAsView: true });

恢复时使用:

spread.fromJSON(json);

如果使用 SJS,可以通过spread.save()spread.open()进行保存和加载。相比把数据塞进隐藏 Sheet,这种方式更贴近 SpreadJS 的原生序列化体系,也更容易在模板、协同、报表、图表之间保持一致。

什么时候该用 DataManager local

DataManager local 并不是要替代所有工作表数据。普通二维表格、临时计算区域、用户直接编辑的 Excel-like 表格,仍然可以放在 Worksheet 中。但下面这些场景,建议优先考虑 DataManager local:

  • 数据不是单纯展示,而是有主键、字段类型、计算列、层级或关联关系

  • 同一份数据要同时驱动 TableSheet、Data Chart、ReportSheet

  • 项目中已经用隐藏 Sheet 或 tag 存放业务 JSON

  • 数据需要随 Workbook 序列化、导入导出或参与协同

  • 原型阶段暂时没有后端接口,但希望未来平滑迁移到远程数据源

  • 需要把 UI 展示和数据模型拆开,让代码结构更稳定

它尤其适合“前端先建模”的工程策略:先用 local 数据把字段、关系、视图和组件消费方式稳定下来;当业务进入服务端持久化阶段,再把data切换为远程remote配置,模型层的很多设计都可以保留下来。

小结

SpreadJS V19.1 的 DataManager local 数据源,看起来只是addTable多了一种数据来源,但它带来的变化更深:SpreadJS 应用可以在 Workbook 内部拥有一套结构化、可复用、可序列化、可协同的本地数据模型。

对过去依赖tag或隐藏 Sheet 存储工程数据的项目来说,这是一条更清晰的升级路径。tag适合少量附加状态;隐藏 Sheet 适合普通表格辅助数据;而当数据开始具备业务结构,应该被多个组件消费,并需要导入导出或协同时,DataManager local 会是更自然、更稳定的选择。

它把“数据藏在哪里”这个问题,变成了“数据如何建模”。这正是工程化 SpreadJS 应用越来越需要的能力。

扩展链接

SpreadJS AI Agent——智能对话表格