在很多 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——智能对话表格

Logo

葡萄城是专业的软件开发技术和低代码平台提供商,聚焦软件开发技术,以“赋能开发者”为使命,致力于通过表格控件、低代码和BI等各类软件开发工具和服务

更多推荐