最近在学习Univer这个开源项目,边学边整理为笔记。本篇主要从面向对象的角度出发,通过拆解Univer Sheet中的各种类,以及它们的关系,来进一步掌握项目架构。
项目链接:github.com/dream-num/u...
数据对象
UnitModel
packages/core/src/common/unit.ts
唯一一个真正的抽象基类
ts
export abstract class UnitModel<D = object, T extends UniverInstanceType = UniverInstanceType>
extends Disposable {
abstract readonly type: T;
abstract getUnitId(): string;
abstract name$: Observable<string>;
abstract setName(name: string): void;
abstract getSnapshot(): D;
abstract getRev(): number;
abstract incrementRev(): void;
abstract setRev(rev: number): void;
}
而Workbook,即Univer Sheet项目中的工作区,继承了该基类
Workbook
聚合根(Aggregate Root) 是 DDD 的说法:外部只能通过它来访问内部对象,它保证内部一致性。Workbook 就是表格领域的聚合根。
Workbook可以理解为一整本Excel表格文件在内存中的对象。Workbook中有两个重要对象,分别是snapshot和worksheet。Snapshot是数据快照,可以理解为形如json形状的一种数据,用来存盘,拷贝,协作时发送给服务器等。例如下面的这种形态。
json
{
id, name, styles,
sheetOrder: ['sheet1', 'sheet2'],
sheets: {
sheet1: { name: 'Sheet1', cellData: {...}, ... },
sheet2: { name: 'Sheet2', ... }
}
}
而worksheet则是workbook中的一张工作表。可以理解为是运行时缓存snapshot的镜像。worksheet也是一个类,在运行时中其相关方法会被调用。当然,worksheet也有自己sheet级的snapshot。
typescript
worksheets: Map<string, Worksheet> ← ★ 运行时对象缓存snapshot 的镜像
| 名字 | 像什么 | 干什么 |
|---|---|---|
| Workbook | 整个 .xlsx |
管多张表、激活表、整本样式、保存 |
| snapshot | 文件内容/JSON | 存、传、协作同步的"真相数据" |
| Worksheet | 某一个 Sheet 页 | 管这一页的格子和行列,是对象,有方法 |
当用户对表格进行改动时,底层的数据对象的变化是一致的。即workbook的snapshot.sheet和worksheet的snapshot是同一份数据。
js
workbook._snapshot.sheets[id] ──┐
├──► { id, name, cellData, ... } ← 同一块
worksheet._snapshot ──┘
cellData ──► {}
▲
worksheet._cellData._matrix ───────────────────────┘ 也是同一块
Worksheet
worksheet是整个表格领域访问最频繁的构造。当需要查询表格单元格内容,单元格行列宽度,是否合并,最终样式等等,都需要落脚到worksheet。worksheet是sheet表的操作入口,底层数据在snapshot中。worksheet中的方法往往和表格数据的读写相关。例如下所示
js
写:getCellMatrix().setValue(...)
读原始单元格数据:getCellRaw
读被拦截后的显示:getCell
读最终样式:getComposedCellStyle
合并:getMergedCell / getSpanModel
区域:getRange(...)
但是不是所有交互都改 Worksheet。例如选区高亮、悬浮、部分 UI 面板状态,可能只在 sheets-ui / 渲染层,不一定写进 Worksheet。除此之外,有的交互落在 Workbook,不在某一张 Worksheet。比如新建/删除/重命名表、工作簿级样式池、dateSystem 等,改的是 Workbook。
拦截器
在worksheet中,有好几种读取单元格内容的方法。getcell,getcellraw等等。为什么会有这些呢,是因为在不同场景下,我们对读取单元格内容的形式要求不同。换句话说,单元格的显示和单元格数据模型中真正存的数据可能不同。
比如,用户在 A1 输入 0.5,并把格式设成百分比。
- 存盘(raw):大致是
{ v: 0.5, t: NUMBER, s: '...' } - 屏幕上应显示:
50%
格式化结果一般不写回真 cellData(否则存盘会变成字符串、协作/计算也麻烦)。所以在读的时候拦截。
「拦截」就体现在:走 getCell,不走 getCellRaw。中间被插进了一段逻辑
worksheet方法,有下面几种getcell方法
js
// 读原始数据
getCellRaw(row: number, col: number): Nullable<ICellData> {
return this.getCellMatrix().getValue(row, col);
}
// 读viewModel的数据,相当于做了一下拦截,没有直接读raw
getCell(row: number, col: number): Nullable<ICellDataForSheetInterceptor> {
return this._viewModel.getCell(row, col);
}
viewmodel方法,下面代码体现了拦截器
js
getCell(...) {
if (this._cellContentInterceptor) {
return this._cellContentInterceptor.getCell(...); // 拦在这里
}
return this.getRawCell(row, col);
}