从零手写一个电子表格引擎:cmx-megasheet 核心功能全解析
零运行时依赖、纯 Web Components 的 TypeScript 电子表格引擎 —— 一个
<cmx-megasheet>自定义元素,自带画布渲染、公式引擎与 IO。本文用真机截图逐块拆解它的核心能力。
前言
cmx-megasheet是一套自研的电子表格引擎,几个硬指标先摆出来:
- 零运行时依赖:
dependencies: {}。ZIP、DEFLATE、XLSX、PDF、图表、公式引擎全部手写。 - 纯 Web Components:一个
<cmx-megasheet>自定义元素,Shadow DOM 隔离,零框架,可渐进嵌入任意宿主页面。 - 335 个内置函数,覆盖 Excel 数学/统计/财务/文本/查找/日期/工程/数据库各族。
- 945+ 单元测试常绿,26 个里程碑演示页每页真机自检。
下面每张截图都是真实<cmx-megasheet>元素铺真数据跑出来的(右侧「自检」面板是在真浏览器里跑的断言,绿灯代表该功能此刻真实生效),配上对应的技术说明。
一、core 数据模型:坐标系 + 稀疏矩阵
一切从数据模型开始。层级是Workbook → Worksheet[] → 稀疏单元格矩阵:
- 坐标系:A1 地址 ↔ 0-based 行列索引双向互转,支持相对/绝对(
$A$1)、跨表(Sheet1!A1)、整列(A:A)、整行(1:1)。 - 稀疏矩阵:
SparseMatrix只存非空单元格,百万行表也不撑内存。 - 样式级联:每个 Worksheet 自持命名样式表(
StyleSheet),单元格可内联样式或引用命名样式。 - 单元格记录:值 / 公式 / 样式 / 富文本四者稀疏并存。
import{Workbook,Worksheet}from'@cmx/megasheet'constwb=newWorkbook({sheetCount:0})constws=newWorksheet('Sheet1',{rowCount:100,colCount:26,styleSheet:wb.styleSheet})wb.appendSheet(ws)ws.setValue(0,0,'销售')ws.setValue(1,0,100)二、公式引擎:Pratt 解析 + 依赖图重算
公式求值管线:公式串 → 词法分析 → Pratt 语法分析(AST) → 求值 → 结果。
- Pratt 解析:手写 Tokenizer + Pratt Parser,输出 AST。
- 依赖图:从 AST 抽取依赖,拓扑排序后按序重算;三色标记环检测,循环引用返回
#CIRC!。 - 增量重算:编辑单格只重算受影响的闭包,大表从 O(全簿) 降到 O(受影响)。
- 整列/整行钳制:
=SUM(A:A)钳到 sheet 实际维度,不遍历百万空格。
import{FormulaEngine}from'@cmx/megasheet'ws.setFormula(1,0,'SUM(A1:A1)*2')newFormulaEngine(wb).recalcAll()// A2 → 84// 也可不落格直接求值el.evaluateFormula('SUM(A1:A5)',row,col)一个设计约束值得说明:引擎的FormulaValue是标量类型(number | string | boolean | error | null),单元格返回单值,因此依赖动态数组溢出的函数(FILTER/SORT/UNIQUE/SEQUENCE 等)不在实现范围内 —— 这是刻意的取舍,聚焦财务报表类场景。
三、数字格式引擎:Excel 格式串编译执行
TEXT()函数和单元格显示值共用同一套数字格式引擎(numFmt/compile.ts+apply.ts):
- 格式串编译为最多4 区段(正 / 负 / 零 / 文本)。
- 数字:缩放、百分号、千分位、科学计数、分数、补零。
- 日期:
yyyy-mm-dd、mmm、dddd、[h]经过时间、AM/PM,基于 1900 日期序列号。 - 颜色段:
[Red]等;条件段:[>=100]等。
el.evaluateFormula('TEXT(1234.5, "#,##0.00")')// "1,234.50"el.evaluateFormula('TEXT(0.85, "0.0%")')// "85.0%"四、函数库:335 个内置函数
按族组织,覆盖 Excel 常用面:
| 族 | 示例 |
|---|---|
| 数学/三角 | SINCOSLNLOGFACTCOMBINCEILING.MATHROMAN |
| 财务 | PMTFVPVNPVIRRXIRRMIRRSLNDDB |
| 统计 | GEOMEANCORRELSLOPEQUARTILENORM.DISTPERCENTRANK |
| 查找引用 | VLOOKUPHLOOKUPINDEXMATCHXLOOKUPADDRESSINDIRECT |
| 文本 | TEXTLEFTMIDSUBSTITUTETEXTJOINTEXTBEFORE |
| 数据库 D 函数 | DSUMDGETDCOUNTDAVERAGE |
| 日期时间 | DATEEOMONTHNETWORKDAYSYEARFRACWEEKNUMWORKDAY.INTL |
| 工程 | 进制转换BIN2DEC/DEC2HEX、位运算BITAND/BITXOR、ERF、贝塞尔BESSELJ、CONVERT、复数COMPLEX/IMSUM/IMSQRT |
还支持命名区域(=SUM(SALES))、数组字面量(=SUM({1,2;3,4}))、多条件聚合(SUMIFS/COUNTIFS,通配符*?)。
el.evaluateFormula('VLOOKUP(40, A1:B5, 2, FALSE)')el.evaluateFormula('SUMIFS(A1:A5, B1:B5, "x")')el.defineName('SALES','A1:A5')el.evaluateFormula('SUM(SALES)')五、冻结窗格:四象限 / 尾冻结 / 拆分
窗格布局(PaneLayout.ts)基于一条几何不变式:getCellRect(r,c)(行列 → 像素)与hitTest(x,y)(像素 → 行列)严格互逆,跨冻结、拆分、滚动、缩放恒成立。
- 冻结窗格:钉顶部行 / 左侧列,滚动时表头不动。
- 尾冻结:钉底部行 / 右侧列(合计行不随滚动),象限从 4 扩到 9。
- 拆分窗格:可拖动分隔条,上下共享 scrollTop、左右共享 scrollLeft。
el.freezePanes(1,1)// 冻结首行首列el.freezeTrailing(1,0)// 尾冻结底部 1 行el.splitPanes(3,2)// 在 3 行 2 列处拆分六、查找 / 排序 / 筛选
- 查找:
findAll支持正则、大小写、全字匹配、限定区域。 - 排序:多关键字、类型感知(空值末位、数字先于文本),稳定排列。
- 自动筛选:列头下拉,值白名单 + 条件(等于/大于/包含/介于/topN);筛选隐藏与大纲隐藏、手动隐藏三向分账。
el.sortRange('A1:C10',[{col:1,asc:false}])el.setAutoFilter('A1:C10')el.applyFilter(1,{values:['x','y']})el.replaceAll('旧值','新值')七、富文本与条件格式
- 富文本:单个格内混合字体、字号、颜色(
RichText/RichRun)。 - 条件格式:4 类规则求值后叠加显示,不改数据本身:
cellValue:比较规则(大于/介于/重复/唯一/top-bottom)colorScale:2/3 色阶插值dataBar:数据条iconSet:图标集
el.addConditionalRule('B2:B10',{type:'colorScale',colors:['#f00','#ff0','#0f0']})el.addConditionalRule('C2:C10',{type:'dataBar',barColor:'#4f8cff'})el.setRichText(0,0,{runs:[{text:'红',font:{foreColor:'#e00',bold:true}},{text:'黑'},]})八、图表:11 类,手写零依赖 + 主题自适应
浮动对象层(floatingObjects)承载图表 / 图片 / 形状 / 批注,双格锚点经getCellRect换算,滚动、缩放、冻结象限自动跟随。
图表(drawChart.ts)支持11 类:column / bar / line / area / pie / doughnut / scatter / bubble / radar / stock(OHLC) / combo,含图例、数据标签、趋势线、双轴。
配色分离设计:系列数据色固定不变(对齐 Excel),只有背景 / 边框 / 坐标轴 / 网格 / 标签 / 图例文字跟随 light/dark 主题切换 —— 深色模式下图表不再是刺眼的白框。
el.insertChart('A1','B2:C6',{type:'column',title:'季度销量',legend:true})el.insertShape('D14',{kind:'ellipse'})el.addComment(0,0,{text:'此季度需复核',author:'admin'})九、迷你图(Sparkline):格内 7 型微图
drawSparkline.ts在单元格内绘制 7 型微图:line/area/column/winloss/bar/pie/bullet,支持高低点、首末点、标记。复用图表绘制原语,随快照往返。
el.setSparkline(0,3,{type:'line',dataRange:'A1:C1',highLow:true})el.setSparkline(1,3,{type:'winloss',dataRange:'A2:F2'})十、IO:XLSX 全保真 + CSV + PDF + 中性快照
全部零依赖手写:
- XLSX:ZIP 容器 + DEFLATE 压缩 + 手写 OOXML。值/公式/样式/合并/行列尺寸/多表/冻结/筛选/验证/多级分组/页面设置全保真,openpyxl 可互通读。
- CSV:RFC-4180 引号转义,分隔符可选,可选 BOM。
- HTML / PDF:HTML 带条件格式底色;零依赖手写 PDF(真浏览器可解析,PDF 内置字体覆盖 ASCII/拉丁,CJK 走外部 TrueType 字体)。
- 中性快照:
format: "cmx-megasheet"的稀疏 JSON,不含视图态,是数据的单一事实源。
constbytes=el.exportXlsx()// → Uint8Arrayel.importXlsx(bytes)constcsv=el.exportCsv()constpdf=el.exportPdf()constjson=el.getWorkbookJson()// 中性快照一个导入细节:Excel/SpreadJS 序列化公式会给函数名加
_xlfn./_xlws.伪前缀或前导@,引擎在导入边界统一清洗(sanitizeImportedFormula),避免=ROW()-3这类公式因伪前缀被误判为未知函数而报#NAME?。
十一、综合演示:六场景一页跑
M99 把六种能力放在一个工作簿的六个 sheet 里,页签切换:迷你图 7 型 / 行三级分组 / 列三级分组 / 行列双三级 / 全样式(字体·背景·边框·旋转)/ 图表 11 类,全部随快照无损往返。
组件接入
作为自定义元素
<cmx-megasheetid="sheet"style="width:100%;height:500px"></cmx-megasheet><scripttype="module">import'./dist/index.js'// 导入即注册 <cmx-megasheet>constel=document.getElementById('sheet')el.setTheme('light')// 'light' | 'dark' | 'auto'el.loadWorkbook((wb)=>{wb.clearSheets()consts=newCmxMegaSheet.Worksheet('Sheet1',{rowCount:100,colCount:26,styleSheet:wb.styleSheet})s.getCell(0,0).value('销售')s.getCell(1,0).value(100)s.getCell(2,0).formula('=A2*1.1')wb.appendSheet(s)})el.recalc()</script>事件
el.addEventListener('cmx-cell-selected',e=>console.log(e.detail.addr))el.addEventListener('cmx-cell-edited',e=>console.log(e.detail.addr,e.detail.value))支持事件:cmx-cell-selected/cmx-cell-edited/cmx-sheet-changed/cmx-col-resized/cmx-row-resized/cmx-edit-rejected/cmx-structural-changed。
逃生舱
getWorkbook()返回底层Workbook,宿主可直接调Worksheet原生方法(getValue/setValue/getFormula/getCellRect/hitTest等),无缝对接既有代码。
小结
cmx-megasheet用一个自定义元素封装了完整的电子表格能力:坐标系与稀疏矩阵打底,Pratt 公式引擎 + 依赖图重算算数,画布渲染层保证「画=点」几何不变式,IO 层零依赖手写 XLSX/CSV/PDF。335 个内置函数、11 类图表、7 型迷你图、条件格式、多级分组、冻结/拆分窗格 —— Excel 常用面基本覆盖,且全程零运行时依赖、纯 Web 标准。
对「想读懂电子表格每一个齿轮」或「需要一个可嵌入、无框架锁定、可离线出 XLSX/PDF 的表格组件」的场景,这套引擎提供了一条 clean-room 的实现路径。