@report/core
报表文档模型、插件契约、校验、序列化与迁移。这是 Web Report Designer 的最底层包。
对应设计文档 §4.1、§5、§6、§7、§12、§13。
定位
@report/core 是纯 TypeScript 包:无 Vue / React、无 DOM、无浏览器 API、无 UI 框架。
因此它可以在 Browser、Node.js、Electron、Server 和 CLI 中复用(架构原则 §3.1)。
Core 不依赖任何上层实现包,也不反向依赖任何具体插件。
安装与使用
pnpm add @report/core
import {
createDocument,
serialize,
deserialize,
createEditSession,
createCommand,
resolvePageSize,
} from "@report/core"
// 新建文档
const doc = createDocument({ name: "销售单", page: { paper: "A4" } })
// 解析纸张尺寸(mm)
const size = resolvePageSize(doc.page) // { width: 210, height: 297 }
// 序列化(稳定排序,便于版本控制 diff)
const json = serialize(doc, { pretty: true })
// 反序列化:parse → validate → migrate → normalize
const result = deserialize(json)
if (result.ok) {
console.log(result.document?.name)
}
console.log(result.diagnostics)
核心概念
文档模型:文档 → 区域 → 元素树
不把所有元素平铺在根节点上。区域(ReportSection)有 7 种 kind,V1 只实现
page-header / detail / page-footer,其余保留模型扩展点。
单位约定
文档内部统一使用 mm;px 只用于 Designer 显示层,pt 只在 PDF 等输出边界转换(§7)。
paper + orientation 是配置来源,由 resolvePageSize 在布局阶段解析为最终 width/height。
纯数据约束
ReportDocument 只保存可序列化的报表定义,不保存编辑器状态(§3.2):
当前选中项、缩放比例、滚动位置、辅助线、撤销栈、已加载数据和网络连接均不得进入文档。
serialize 会在规范化之前先断言纯数据性,混入函数或 DOM 节点会直接抛 TypeError。
诊断而非异常
数据缺失、表达式错误、字体缺失、插件缺失、分页失败都通过 Diagnostic 表达,
并尽量带上 path(JSON Pointer 风格字段路径)与 elementId,不中断文档读取(§13)。
import { DIAGNOSTIC_CODES, DiagnosticBag, formatDiagnostic } from "@report/core"
const bag = new DiagnosticBag()
bag.warn(DIAGNOSTIC_CODES.REF_DANGLING_DATASET, "区域引用了不存在的数据集", {
path: "/sections/1/datasetId",
})
console.log(bag.all().map(formatDiagnostic))
序列化与迁移流程(§12)
deserialize(json)
→ parse 解析 JSON
→ validate 校验结构
→ migrate 按版本逐步迁移
→ normalize 规范化默认值
schemaVersion使用单调递增整数,当前为CURRENT_SCHEMA_VERSION。- 迁移函数按版本逐步执行,
planMigrations会强制to === from + 1,禁止跨版本跳跃。 - 序列化结果按键名稳定排序,便于 diff。
- 版本高于当前支持时拒绝读取,避免降级写坏数据。
新增 schema 版本时,在 src/schema/version.ts 的 MIGRATIONS 中追加一项即可。
命令与撤销重做
Designer 通过 Core 的命令接口修改文档,不直接实现报表业务规则(§4.5)。
撤销栈属于编辑器状态,不进入文档(§3.2),因此 History 由调用方持有。
import { createCommand, createEditSession } from "@report/core"
const rename = createCommand<{ name: string }>("rename", (doc, payload) => {
const next = JSON.parse(JSON.stringify(doc))
next.name = payload.name
return next
})
const session = createEditSession(createDocument({ name: "A" }))
session.execute(rename, { name: "B" })
session.undo() // 回到 "A"
session.redo() // 回到 "B"
// 事务:多次编辑合并为一步撤销
session.transaction("批量调整", (s) => {
s.applyWithinTransaction(rename, { name: "C" })
s.applyWithinTransaction(rename, { name: "D" })
})
撤销采用快照式而非反向命令式:文档是纯数据且体量可控,快照成本可接受,
且天然正确处理「一个事务改了多处」的情况。mergeKey + mergeWindow 用于合并
拖拽、连续输入这类高频操作。
插件
插件只通过契约接入,Core 不导入任何具体插件实现(§11)。 插件缺失或版本不兼容只产生诊断,不阻止文档读取。
import { PluginRegistry } from "@report/core"
const registry = new PluginRegistry()
registry.register({
id: "plugin-qrcode",
version: "1.2.0",
compatibleSchemaVersion: ">=1.0.0",
elements: [
{
type: "qrcode",
createDefault: (options) => ({
id: options?.id ?? "el-1",
type: "qrcode",
frame: options?.frame ?? { x: 0, y: 0, width: 20, height: 20 },
props: { value: "https://example.com" },
}),
},
],
})
// 校验文档时传入插件类型集合,避免误报未知元素类型
const result = deserialize(json, { pluginElementTypes: registry.elementTypes() })
ElementPlugin.schema 使用 unknown 是有意为之:Core 不绑定任何具体校验库,
宿主可以传 JSON Schema、Zod、Valibot 或自定义描述,由插件适配器负责具体校验。
目录结构
src/
├── model/ 报表文档模型(report / page / section / element / table / style / …)
├── plugin/ 插件契约与注册表
├── document/ 创建、克隆、规范化
├── serialization/ 序列化、反序列化、迁移
├── schema/ 版本、默认值、校验
├── command/ 命令、事务、撤销重做
├── diagnostics/ 统一诊断对象
├── utils/ ID 生成、深拷贝与对象工具
└── index.ts 公共出口
开发
pnpm build # tsup 打包(ESM + CJS + d.ts)
pnpm test # vitest
pnpm typecheck # tsc --noEmit
当前测试覆盖
| 测试文件 | 用例数 | 覆盖内容 |
|---|---|---|
document.test.ts |
19 | 创建、规范化、克隆、元素计数 |
serialization.test.ts |
26 | 稳定序列化、往返、迁移链路、容错读取 |
validation.test.ts |
34 | 结构校验、引用完整性、样式环、插件类型 |
command.test.ts |
35 | 命令、合并、事务、撤销重做 |
plugin.test.ts |
25 | 注册、冲突、版本兼容、缺失诊断 |
合计 139 个用例。
V1 范围内尚未实现的部分
本包只覆盖设计文档 §16 的阶段 1(Core 和文档模型)。以下属于后续阶段:
report-expression:表达式解析与受限求值器(阶段 3)。report-layout:测量、换行、分页,产出LayoutDocument(阶段 2)。report-renderer:HTML / SVG / PDF(阶段 4)。report-designer/report-designer-vue:编辑器与 UI(阶段 4)。- 插件实现:二维码、条码、图表(阶段 5)。