- 文档
- 技术博客
- 教程
【免费下载链接】weekly
前端精读周刊。帮你理解最前沿、实用的技术。
Excel 如今可以利用 JavaScript 根据单元格数据生成图表、表格,或通过 JS 拓展自定义函数来增强内置 Excel 表达式。本篇精读以微软 Excel JavaScript API 的开放设计为研究对象,拆解其「为什么开放 JS API」「能力覆盖范围」以及「以 Range 为核心、以Excel.run+context.sync()为骨架」的命令式 API 设计哲学,帮助读者理解这套开放 API 的抽象思路,并从中提炼可复用的开放 API 设计经验。
为什么需要开放 JS API
Excel 本身已经具备良好的易用性,以及formula(公式)这个强大的能力。正如仓库往期精读 精读《Microsoft Power Fx》 中提到的,formula 就是 Excel 里的 Power Fx,属于画布低代码语言,不过在 Excel 里叫做"公式"更合适——它描述的是"做什么",而不是"如何做"。
既然 Excel 已经具备了这么多能力,为何还需要 JS API 呢?一句话概括就是:在 JS API 内可以使用 formula,即 JS API 是公式能力的超集。它包含了对 Excel 工作簿的增删改查、数据的限制、RangeAreas 操作、图表、透视表,甚至可以自定义 formula 函数。
也就是说,JS API 让 Excel "可编程化"——以开发者视角对 Excel 进行二次拓展,包括对公式进行二次拓展,使 Excel 覆盖更多场景。如果说 formula 面向的是普通用户,那么 JS API 面向的就是开发者,二者是同一份数据模型在不同人群面前的两种呈现方式。
JS API 可以用在哪些地方
从 Excel 流程中最开始的工作簿、工作表环节,到最细节的单元格数据校验,都可通过 JS API 支持。从能力边界上看,Excel JS API 并没有刻意设置能力边界,而是将 Excel 全生命周期中一切可编程的地方持续开放出来。整体可以划分为三个层次:
第一层:工作簿与工作表级操作。包括对工作簿、工作表的操作,对工作表用户操作的监听,以及对工作表进行只读设置。这一类 API 的目的是对 Excel 这个整体进行编程操作,是开放 API 的入口层。
第二层:单元格级操作。比如对单元格进行区域选中、获取选中区域、设置单元格属性与颜色、对单元格数据进行校验。自定义公式也发生在这个环节——因为单元格的值可以是公式,而公式可以利用 JS API 拓展,把"单元格里放什么"这件事交给代码决定。
第三层:拓展行为。在单元格基础上引入图表、透视表等拓展。虽然这些功能在 UI 按钮上也可以操作出来,但 JS API 可以实现 UI 界面配置不出来的逻辑;对于非常复杂的逻辑行为,即便 UI 可以配置出来,可读性也远没有代码高。除了表格、透视表外,还可以创建自定义形状——基本的几何图形、图片和 SVG 都支持。
这与仓库 精读《前端与 BI》 中描述的 BI 数据链路形成呼应:前端 BI 的核心是"数据集 + 数据模型 + 可视化",而 Excel JS API 恰恰把"数据集的二维表格"直接作为开放的编程对象,图表、透视表则是建立在这份结构化数据之上的"数据二次分析行为"。
JS API 设计:为什么抽象 Range 而不是 Cell
比较有趣的是,Excel 并没有抽象"单元格"对象,即便我们所有人都认为单元格就是 Excel 的代表。
这么做是出于 API 设计的合理性,因为 Excel 使用Range概念表示连续单元格。比如下面这段写入表头并设置样式的代码:
Excel.run(function (context) { var sheet = context.workbook.worksheets.getActiveWorksheet(); var headers = [ ["Product", "Quantity", "Unit Price", "Totals"] ]; var headerRange = sheet.getRange("B2:E2"); headerRange.values = headers; headerRange.format.fill.color = "#4472C4"; headerRange.format.font.color = "white"; return context.sync(); });可以发现,Range让 Excel 聚焦在批量单元格 API——即把单元格看做一个范围,整体 API 都可以围绕一个范围去设计。这种设计理念的好处是:
- 把范围局限在单个单元格,就可以覆盖
Cell概念; - 聚焦在多个单元格时,可以很方便地基于二维数据结构创建表格、折线图等分析图形,因为二维结构的数据才是结构化数据。
或者可以说,结构化数据是 Excel 最核心的概念,而单元格无法体现结构化。结构化数据的好处是:一张工作表就是一个可以用来分析的数据集,在其之上无论是基于单元格的条件格式,还是创建分析图表,都是一种数据二次分析行为,这都得益于结构化数据。所以 Excel JS API 必然围绕结构化数据进行抽象——用 Range 覆盖"二维数据块"这一基本单位,而不是面向孤立的单格。
从仓库其它精读文档也能看到类似思路的普遍性:无论是 精读《前端与 BI》 中把数据集定义为"列头表示字段、每行一份数据"的二维表格,还是 精读《SQL vs Flux》 中强调查询应基于结构化数据模型展开,二维结构化数据都是数据分析类产品 API 设计的地基。Excel JS API 将这一共识落实到了最底层的Range抽象上。
Excel.run 与 context.sync:命令式 API 的骨架
再从 API 语法来看,除了工作簿这个级别的 API 采用了Excel.createWorkbook();之外,其他大部分 API 都是以下形式:
Excel.run(function (context) { // var sheet = context.workbook.worksheets.getItem("Sample"); // 对 sheet 操作 .. return context.sync(); });最外层的函数Excel.run是注入context用的,同时可以保证执行的时候 Excel context 已经准备好了。而context.sync()是同步操作——即把当前对 context 的操作真正生效。
所以 Excel JS API 是命令式的,也不会做类似 MVVM 的双向绑定。在操作过程中,数据和 Excel 状态不会发生变化,直到执行context.sync()。这套机制带来两个重要推论:
- 批量操作天然成立:在
sync()之前可以连续对多个对象、多个属性赋值,它们会被合并成一次同步请求,减少与 Excel 宿主之间的往返通信开销; - 读取必须显式触发:通过
context拿到的对象(如Range、Worksheet)只是"代理"(proxy)对象,属性值默认是空的,必须调用load("属性名")声明要读取的属性,再执行context.sync()才能真正取回数值。
理解了这一点,就能明白为什么某些代码要写在context.sync().then里了,比如下面这个从透视表获取数据的例子:
Excel.run(function (ctx) { var pivotTable = context.workbook.worksheets.getActiveWorksheet().pivotTables.getItem("Farm Sales"); // Get the totals for each data hierarchy from the layout. var range = pivotTable.layout.getDataBodyRange(); var grandTotalRange = range.getLastRow(); grandTotalRange.load("address"); return context.sync().then(function () { // Sum the totals from the PivotTable data hierarchies and place them in a new range, outside of the PivotTable. var masterTotalRange = context.workbook.worksheets.getActiveWorksheet().getRange("E30"); masterTotalRange.formulas = [["=SUM(" + grandTotalRange.address + ")"]]; }); }).catch(errorHandlerFunction);这里的关键点在于:只有执行context.sync()后才能拿到grandTotalRange.address。因为grandTotalRange是代理对象,.address在load("address")之前并不真实存在于 JS 侧;load只是登记了"我需要这个属性",sync()才真正向 Excel 发起请求并填充该属性。因此后续构造=SUM(E2:E29)这样的公式字符串,必须放在sync()完成之后(.then回调内)执行。
同时,整个调用链用.catch(errorHandlerFunction)兜底,这与仓库中 精读《捕获所有异步 error》 强调的异步错误处理思路一致:Excel.run返回的 Promise 上统一挂载错误处理器,避免未捕获的拒绝中断脚本。
可复制的最小运行骨架
综合以上机制,一个可复现的 Excel JS API 脚本骨架通常包含四个固定环节:
Excel.run(function (context) { // 1. 获取对象:工作簿 -> 工作表 -> Range var sheet = context.workbook.worksheets.getActiveWorksheet(); var range = sheet.getRange("A1:C3"); // 2. 写入/修改:对代理对象赋值(不会立即生效) range.values = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]; range.format.font.bold = true; // 3. 读取声明:load 需要读回的属性 range.load("address"); // 4. 同步:真正让修改生效、把 load 的属性填充回来 return context.sync().then(function () { console.log("写入完成:" + range.address); }); }).catch(function (error) { console.log("Error: " + error); });从源码结构看,Excel.run、context.sync()、load、代理对象这套模式贯穿所有 Excel JS API 示例,理解了"写入靠赋值、读取靠 load + sync"这一对约定,就能顺藤摸瓜看懂任何一段 Excel JS API 代码。
总结:ScriptLab 与通用 API
微软还在 Office 套件 Excel、Outlook、Word 中推出了ScriptLab功能,可以在 Excel 的 ScriptLab 里直接编写 Excel JS API——它充当了"官方 REPL"的角色,让开发者无需搭建完整加载项工程即可试验 API 行为,极大降低了上手门槛。
在 Excel JS API 之上,还有一个通用 API(Office JavaScript API 中跨应用共用的部分),定义为跨应用的通用 API。这样 Excel JS API 就可以把精力聚焦在 Excel 产品本身能力上,而不必为每个 Office 应用重复实现基础的宿主交互、上下文管理等通用能力。这种"通用层 + 产品专用层"的分层设计,同样是开放平台 API 值得借鉴的结构:通用层负责跨产品的一致性,专用层负责产品差异化能力的深度开放。
回顾全文,Excel JS API 的开放设计可以提炼出三条经验:
- 以数据模型而非 UI 概念为抽象核心:用
Range覆盖二维结构化数据,而非孤立地抽象单元格,这让表格、图表、透视表等一切"数据二次分析"能力都能建立在同一抽象之上; - 命令式 + 显式同步:通过
Excel.run(context)注入上下文、以代理对象累积操作、用context.sync()统一提交,既保证了批量性能,也让数据流的边界清晰可预期; - 能力分层开放:从工作簿/工作表,到单元格与公式,再到图表与透视表拓展,层层递进,且通过通用 API 与产品专用 API 的切分,保持开放面的整洁。
对于任何想为复杂产品开放编程能力的团队来说,这套设计——"先抽象出产品最核心的数据结构,再围绕它设计批量化的命令式 API,最后提供低门槛的试验环境"——都值得在动手写第一行 API 之前认真参考。
延伸阅读:本仓库 精读《Microsoft Power Fx》 从"画布低代码语言"角度讲解了 formula 背后的语言设计;精读《前端与 BI》 阐述了数据集、渲染引擎、数据模型与可视化四大模块,与 Excel JS API 的开放能力互为印证。
- 文档
- 技术博客
- 教程
【免费下载链接】weekly
前端精读周刊。帮你理解最前沿、实用的技术。
相关推荐
Blender For Unreal Engine摄像机导出完全攻略:从Blender到Unreal Sequencer
Blender For Unreal Engine摄像机导出完全攻略:从Blender到Unreal Sequencer Blender For Unreal
开发工具游戏开发Redux Thunk逻辑抽象库设计:API与扩展性
Redux Thunk逻辑抽象库设计:API与扩展性 Redux Thunk作为Redux生态中最基础的异步逻辑处理中间件,其设计哲学围绕"最小接口、最大扩展"
前端Area51跨平台输入:统一API与设备抽象设计
Area51跨平台输入:统一API与设备抽象设计 在游戏开发中,不同平台(PC、PS2、Xbox等)的输入设备差异常常导致开发效率低下和兼容性问题。Area51
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考