1. 项目概述:Univer 是什么,它解决的不是“又一个表格库”问题
Univer 不是 Excel 的简化版,也不是某个大厂内部工具的开源马甲。我第一次在客户现场看到它跑起来时,第一反应是:“这玩意儿居然真能把协作逻辑塞进前端 SDK 里,还不卡?”——当时他们正用 Univer 做一套产线工单系统,要求工人只能填“完成时间”和“异常描述”两列,其余所有字段(包括工序编号、标准工时、BOM 版本)必须锁定、不可见、不可编辑,且多人同时填写时不能互相覆盖。传统方案要么靠后端强校验+前端遮罩,要么直接上低代码平台——但后者部署重、定制难、权限粒度粗。Univer 的核心价值,恰恰藏在“SDK”这个关键词里:它把原本属于服务端的单元格级权限控制、结构化文档模型、实时协同状态同步,全量下沉到了前端运行时。你拿到的不是一个网页应用,而是一套可嵌入任意 Web 系统的、带完整文档引擎的 JavaScript SDK。它支持 spreadsheets(电子表格)、documents(富文本)、presentations(演示文稿)三类文档,但真正让工程师拍大腿的是:它允许你用纯声明式配置定义“谁能在哪张表的哪个区域写什么”,比如{"sheet": "BOM表", "range": "C2:C100", "permission": "edit"},然后 SDK 自动处理渲染锁定、输入拦截、变更广播。这不是“表格组件”,这是“可编程的文档内核”。对前端团队来说,意味着不用再为每个业务表单单独写 CRUD 页面;对产品团队来说,意味着运营人员能自己拖拽定义数据采集模板;对安全团队来说,意味着敏感字段的读写权限能精确到单元格坐标。它解决的,是“业务逻辑与文档结构深度耦合”这一长期被忽视的痛点——当你的系统里开始出现“用户上传的Excel要自动解析成工单”“销售合同模板要预填客户信息并禁止修改关键条款”这类需求时,Univer 就不再是备选,而是解题的基准线。
2. 核心设计思路拆解:为什么选择“文档引擎 SDK”而非“UI 组件库”
2.1 拒绝“UI 层模拟”,直击文档语义层
市面上绝大多数表格组件(包括一些知名开源库)本质是“UI 渲染器”:它们接收二维数组,渲染成 HTML 表格,再通过事件监听模拟编辑。这种模式在 Univer 面前立刻暴露短板。举个真实案例:某汽车零部件厂要求“工艺参数表”中,“热处理温度”列必须满足公式=ROUND(A2*1.2,0),且该列所有单元格默认锁定,仅当“工序状态”列为“待校准”时才解锁。用 UI 组件实现?你需要手动监听 A2 变化、触发计算、判断状态、动态切换 input disabled 属性、还要防住用户用 Ctrl+V 粘贴绕过校验——代码量爆炸,且一旦公式逻辑变复杂(比如嵌套 IF、引用其他 sheet),维护成本直线上升。Univer 的解法是回归文档本质:它内置完整的公式引擎(兼容 Excel 大部分函数)、单元格权限模型(基于 Range 的读/写/隐藏策略)、依赖图追踪(自动识别 A2 变化会触发哪些单元格重算)。你只需配置:
{ "sheets": [{ "name": "工艺参数表", "cells": { "C2": { "f": "=ROUND(A2*1.2,0)", "p": "readonly" }, "D2": { "v": "待校准", "p": "edit" } }, "protection": { "ranges": [{ "range": "C2:C100", "permission": "readonly" }] } }] }SDK 在初始化时就构建好整个计算图和权限树,后续所有操作(编辑、粘贴、公式计算)都在引擎内闭环处理。这背后是 Univer 对OOXML 和 ECMA-376 标准的深度解析能力——它不把 Excel 当作“图片”或“字符串”来展示,而是当作可执行的程序文档来运行。这也是它能支撑 “spreadsheets are all you need” 这种激进理念的底层底气:当文档本身具备计算、逻辑、权限能力时,前端确实不需要再为每个业务场景写独立逻辑层。
2.2 “可嵌入 SDK”设计带来的架构自由度
很多团队看到 “Univer Online” 会下意识认为它是 SaaS 服务。错。Univer 的核心交付物是@univerjs/core、@univerjs/sheets等 NPM 包,典型集成方式是:
npm install @univerjs/core @univerjs/sheets @univerjs/ui然后在你的 React/Vue 应用中:
import { Univer } from '@univerjs/core'; import { UniverSheetsPlugin } from '@univerjs/sheets'; import { UniverUIPlugin } from '@univerjs/ui'; const univer = new Univer(); univer.installPlugin(new UniverSheetsPlugin()); univer.installPlugin(new UniverUIPlugin()); // 提供工具栏、侧边栏等 UI这个设计彻底解耦了“文档能力”和“业务系统”。我们曾帮一家医疗 SaaS 公司改造其检验报告系统:原有方案是后端生成 PDF 报告,医生无法在线标注。接入 Univer 后,他们将报告模板定义为 Univer 文档(含固定格式的表格和文本框),医生登录后,系统动态加载该文档,并通过univer.getActiveSheet().getProtection().setProtection({ ranges: [...] })接口,根据医生角色实时设置“仅能编辑‘诊断意见’单元格”。整个过程不涉及任何页面跳转或 iframe 嵌入——Univer 实例完全运行在主应用的 React Context 中,状态、事件、API 全部可编程。对比传统 iframe 方案(如嵌入 Google Sheets),Univer 的优势在于:
- 样式无缝融合:UI 插件提供的工具栏可完全自定义主题色、图标、按钮位置,与主站设计语言一致;
- 状态深度互通:点击主站的“导出为 PDF”按钮,可直接调用
univer.exportToPdf(),无需跨域通信; - 权限统一管控:主站的 RBAC 权限系统可直接映射到 Univer 的 Range 权限策略,避免权限体系分裂。
这种“SDK 级集成”不是技术炫技,而是把文档能力变成像“日志上报 SDK”或“埋点 SDK”一样,成为业务系统的基础设施能力。
2.3 三类文档(Spreadsheets/Documents/Presentations)的协同设计哲学
Univer 支持三种文档类型,但绝非简单拼凑。它的协同设计体现在“底层模型统一,上层能力分治”。所有文档共享同一个核心模型Univer实例,这意味着:
- 跨文档引用成为可能:在 Presentations(演示文稿)的某页中,可以插入一个
=Sheets!A1公式,实时显示 spreadsheets 中 A1 单元格的值; - 统一权限中心:用户在 documents 中设置的“仅查看”权限,会同步影响其对同一工作空间下所有 spreadsheets 和 presentations 的访问;
- 协同状态全局感知:当多人同时编辑一个包含 spreadsheets 和 documents 的工作空间时,Univer 的 OT(Operational Transformation)算法会将所有文档的变更操作归一化为“插入字符”“删除范围”“设置单元格值”等原子操作,在同一个协同通道中广播和合并。
这种设计直指企业级场景的核心矛盾:业务数据从来不是孤立存在的。一份销售合同(document)必然关联报价单(spreadsheet),而合同签署流程(presentation)又需要实时展示最新报价。Univer 用统一的底层引擎,把过去需要多个系统、多套 API、多次数据同步才能完成的场景,压缩成一次文档加载、一次权限配置、一次协同连接。这也是为什么阿里云认证 SDK 会将其纳入生态——它不是替代某个垂直工具,而是提供一种“文档即服务”的新范式。
3. 核心细节解析与实操要点:从零配置一个“只读+指定编辑区”表格
3.1 初始化:最小可行配置与性能陷阱
新手最容易踩的坑,是以为new Univer()就能直接渲染。实际必须显式安装插件并创建实例。最简可用代码如下:
import { Univer } from '@univerjs/core'; import { UniverSheetsPlugin } from '@univerjs/sheets'; import { UniverUIPlugin } from '@univerjs/ui'; import { DefaultSheetPlugin } from '@univerjs/sheets-ui'; // 必须!提供默认 UI const univer = new Univer(); // 注意顺序:先装 core 插件,再装 UI 插件 univer.installPlugin(new UniverSheetsPlugin()); univer.installPlugin(new DefaultSheetPlugin()); // 关键!没有它,表格不显示 UI univer.installPlugin(new UniverUIPlugin()); // 创建一个空工作簿 const workbook = univer.createWorkBook({ title: '工单采集表', unitId: 'workbook-001' });这里有两个性能关键点:
- 插件按需安装:如果你的业务只需要 spreadsheets,就不要安装
@univerjs/docs或@univerjs/slides。每个插件都会增加约 150KB 的 JS 包体积,DefaultSheetPlugin是必需的,但UniverUIPlugin可以替换为自定义 UI 插件以减小体积; - 工作簿初始化时机:
createWorkBook()应在用户明确进入文档页面后再调用。Univer 的工作簿是内存密集型对象,提前创建会占用大量内存。我们曾遇到客户在首页就初始化 5 个工作簿,导致低端安卓平板直接卡死。正确做法是:路由进入/form/:id时,再根据id动态加载对应文档配置并创建 workbook。
3.2 定义“用户可编辑区域”的三种权威方式
Univer 提供了三层权限控制,必须理解其适用场景才能精准使用:
方式一:工作表级保护(适合全局锁定)
const sheet = workbook.getActiveSheet(); sheet.getProtection().setProtection({ protection: true, password: '123456', // 可选,设密码后需调用 unprotect() 才能修改保护设置 ranges: [ { range: 'A1:B10', permission: 'readonly' }, // A1:B10 只读 { range: 'D1:D100', permission: 'edit' } // D1:D100 可编辑 ] });注意:此方式下,未在
ranges中声明的区域默认为readonly。如果想让大部分区域可编辑,仅锁定少数单元格,必须显式声明所有可编辑区域,否则会全部锁定。
方式二:单元格级属性(适合精细控制)
sheet.getCell(1, 0)?.setStyle({ // 设置第2行第1列(B2)的样式 protection: { locked: true, // 锁定单元格 hidden: false // 不隐藏 } }); // 或批量设置 sheet.getRange('C2:C10').setStyle({ protection: { locked: true } });实操心得:
locked: true仅在工作表启用保护时生效。它比方式一更灵活,可针对单个单元格设置,但无法设置“隐藏”行为(hidden 只在 formulas 中有效)。适合需要动态锁定的场景,比如根据用户选择的“产品型号”自动锁定对应参数列。
方式三:自定义编辑器(适合复杂交互)
当“只填数字”或“下拉选择”无法满足需求时,Univer 支持注入自定义编辑器:
import { CustomEditor } from '@univerjs/sheets'; class DateEditor extends CustomEditor { createEditor() { const input = document.createElement('input'); input.type = 'date'; return input; } getValue() { return this.editorElement?.value || ''; } } // 注册到 Univer univer.registerPlugin(new class extends Plugin { onMounted() { this.context.getPluginManager().registerPlugin(DateEditor); } }());踩过的坑:自定义编辑器必须继承
CustomEditor并实现createEditor和getValue。createEditor返回的 DOM 元素会被 Univer 自动挂载到编辑浮层中。我们曾因忘记在getValue中处理空值,导致日期清空后仍提交旧值,引发数据一致性问题。
3.3 数据绑定与业务逻辑注入:让表格“活”起来
Univer 的强大之处在于,它允许你在单元格公式之外,注入任意 JavaScript 逻辑。典型场景是“动态下拉列表”:
// 假设 B2 单元格是“产品线”,C2 是“型号”,需要根据 B2 值动态更新 C2 下拉选项 sheet.getCell(1, 1)?.setFormula('=GET_MODEL_LIST(B2)'); // 自定义函数 // 注册自定义函数 univer.registerFunction({ name: 'GET_MODEL_LIST', functionType: FunctionType.Normal, call: (ctx, values) => { const productLine = values[0]; // 调用你的业务 API 获取型号列表 return fetch(`/api/models?line=${productLine}`) .then(res => res.json()) .then(data => data.map(item => item.name)); } });关键原理:
registerFunction注册的函数会进入 Univer 的公式引擎,所有公式计算都在引擎内异步执行。GET_MODEL_LIST返回的是 Promise,Univer 会自动等待其 resolve 后再渲染结果。这比在 React 组件中监听 B2 变化、手动调用 API、再 setCell 更可靠——因为 Univer 保证了公式的执行时序和依赖关系。
4. 实操过程与核心环节实现:从本地开发到生产部署的全链路
4.1 本地开发环境搭建:避开 Windows 路径陷阱
Univer 的官方示例基于 Vite,但实际项目中常遇到路径问题。尤其在 Windows 系统下,_artifacts\winui_packages\sdk\build\native\microsoft.windowsappsdk.props这类路径中的反斜杠\会导致 Node.js 模块解析失败。解决方案是强制使用正斜杠:
// vite.config.ts import { defineConfig } from 'vite'; import react from '@vitejs/plugin-react'; export default defineConfig({ plugins: [react()], resolve: { alias: { // 显式映射,避免 Windows 路径问题 '@univerjs/core': require.resolve('@univerjs/core'), '@univerjs/sheets': require.resolve('@univerjs/sheets'), } } });实测心得:
require.resolve()能获取模块的绝对路径,且自动处理平台差异。我们曾因未加此配置,在 CI 流水线(Linux)和本地开发(Windows)间出现模块找不到的报错,排查耗时两天。
4.2 生产环境配置:体积优化与 CDN 加速
Univer 默认包体积较大(core + sheets + ui 约 2.1MB)。生产环境必须做三件事:
- 代码分割:利用 Webpack 或 Vite 的 dynamic import,按需加载文档类型:
// 只有用户点击“新建表格”时才加载 sheets 相关代码 const loadSheets = async () => { const { UniverSheetsPlugin, DefaultSheetPlugin } = await import('@univerjs/sheets'); const { UniverUIPlugin } = await import('@univerjs/ui'); univer.installPlugin(new UniverSheetsPlugin()); univer.installPlugin(new DefaultSheetPlugin()); univer.installPlugin(new UniverUIPlugin()); };- CDN 托管静态资源:Univer 的 UI 图标、字体等资源可托管到 CDN。在
index.html中添加:
<script> window.UNIVER_ASSET_BASE = 'https://cdn.example.com/univer-assets/'; </script>- Tree-shaking 配置:确保
package.json中"sideEffects": false,并在vite.config.ts中启用:
export default defineConfig({ build: { rollupOptions: { output: { manualChunks: { univer: ['@univerjs/core', '@univerjs/sheets'] } } } } });性能数据:某客户项目经上述优化后,首屏 JS 体积从 2.8MB 降至 1.2MB,Lighthouse 性能评分从 42 提升至 79。
4.3 权限同步实战:对接企业微信/钉钉的免登体系
Univer 本身不处理用户认证,但提供了IAuthManager接口供你注入。以企业微信为例:
import { IAuthManager } from '@univerjs/core'; class WeComAuthManager implements IAuthManager { async getAccessToken(): Promise<string> { // 调用企业微信 JS-SDK 获取 access_token return new Promise((resolve) => { wx.config({ /* config */ }); wx.ready(() => { wx.invoke('getCurExternalContact', {}, (res) => { resolve(res.accessToken); // 实际应从后端获取,此处简化 }); }); }); } async getUserInfo(): Promise<{ userId: string; userName: string }> { // 从企业微信获取当前用户 ID 和姓名 return { userId: 'zhangsan', userName: '张三' }; } } // 注入到 Univer univer.setAuthManager(new WeComAuthManager());关键逻辑:
getAccessToken()返回的 token 会被 Univer 用于后续所有 API 请求(如保存文档、获取协作用户列表)。getUserInfo()返回的userId会作为协同用户的唯一标识,确保不同设备登录的“张三”被视为同一人。我们曾因userId未统一(前端生成随机 ID),导致同一用户在手机和 PC 上编辑时被识别为两人,产生冲突。
4.4 文档持久化:从 localStorage 到云存储的平滑迁移
Univer 内置LocalStoragePreset,适合快速验证:
import { LocalStoragePreset } from '@univerjs/core'; univer.installPlugin(new LocalStoragePreset());但生产环境必须替换为云存储。我们推荐两种方案:
方案 A:自定义存储适配器(推荐)
import { IStorageAdapter } from '@univerjs/core'; class AliyunOSSAdapter implements IStorageAdapter { async save(unitId: string, data: string): Promise<void> { // 调用阿里云 OSS SDK 上传 await client.putObject(`univer/${unitId}.json`, data); } async load(unitId: string): Promise<string | null> { try { const result = await client.getObject(`univer/${unitId}.json`); return result.content.toString(); } catch (e) { return null; } } } univer.setStorageAdapter(new AliyunOSSAdapter());方案 B:后端代理(适合已有文件服务)
在vite.config.ts中配置代理:
export default defineConfig({ server: { proxy: { '/api/univer': { target: 'https://your-backend.com', changeOrigin: true } } } });然后 Univer 会自动将save/load请求发往/api/univer/save和/api/univer/load。
注意事项:无论哪种方案,都必须实现
load方法的容错。我们曾因 OSS 权限配置错误,导致load返回 403,Univer 会静默失败并创建空白文档,用户数据丢失。正确做法是在load中捕获异常并返回null,Univer 会自动回退到初始状态。
5. 常见问题与排查技巧实录:那些官方文档不会写的坑
5.1 公式计算不触发?检查这四个致命点
| 问题现象 | 根本原因 | 排查步骤 | 解决方案 |
|---|---|---|---|
=SUM(A1:A10)值不变,即使 A1-A10 已修改 | 单元格未启用“自动计算” | 1. 检查sheet.getCalculation().isAutoCalculate()是否为true2. 查看控制台是否有 CalculationEngine初始化日志 | 调用sheet.getCalculation().setAutoCalculate(true) |
自定义函数=MY_FUNC()显示#VALUE! | 函数未注册或参数类型错误 | 1. 在控制台执行univer.getFunctionRegistry().getFunction('MY_FUNC')2. 检查 call方法中values参数是否为数组 | 确保registerFunction在univer实例创建后、createWorkBook前调用;values是二维数组,需用values[0][0]取第一个参数 |
| 公式结果延迟 2 秒才更新 | OT 协同冲突导致重算队列堆积 | 1. 打开 Chrome DevTools → Network,过滤univer-sync2. 查看 op字段是否大量积压 | 降低协同频率:univer.getConfig().collaboration.syncInterval = 5000(默认 1000ms) |
=INDIRECT("A"&B1)返回#REF! | INDIRECT 不支持跨 sheet 引用 | 1. 查看公式引擎日志console.log(univer.getFormulaEngine())2. 检查 INDIRECT是否在白名单中 | 替换为=SHEET1!A1等硬编码引用,或改用=GET_CELL_VALUE("SHEET1", "A1")自定义函数 |
5.2 协同编辑卡顿?内存泄漏的三个信号
Univer 的协同基于 WebSocket,长时间运行易出现内存问题。以下信号出现任一,立即检查:
- 信号 1:Chrome 任务管理器中,页面内存占用持续 > 500MB
→ 原因:未及时销毁univer实例。每次路由离开文档页,必须调用univer.dispose()。我们曾因忘记此步,导致用户切换 10 个文档后内存飙升至 2GB。 - 信号 2:WebSocket 连接数指数增长
→ 原因:重复调用univer.installPlugin()。插件安装是幂等的,但某些 UI 插件会重复注册事件监听器。解决方案:在安装前加锁if (!univer.hasPlugin(MyPlugin)) univer.installPlugin(new MyPlugin())。 - 信号 3:编辑操作后,光标频繁跳转到 A1
→ 原因:setCell时传入了非法坐标(如行号为负数)。Univer 会尝试恢复到默认位置。解决方案:在setCell(row, col, value)前,添加断言if (row < 0 || col < 0) throw new Error('Invalid cell position')。
5.3 Android SDK 兼容性问题专项指南
虽然 Univer 是前端 SDK,但常被集成到 Cordova/React Native 的 Android App 中。以下是高频问题:
- 问题:WebView 中表格渲染空白,控制台无报错
→ 原因:Android 5.0+ WebView 默认禁用WebGL,而 Univer 的 Canvas 渲染依赖 WebGL 加速。
→ 解决:在MainActivity.java中添加:if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { WebView.setWebContentsDebuggingEnabled(true); WebSettings settings = webView.getSettings(); settings.setJavaScriptEnabled(true); settings.setDomStorageEnabled(true); settings.setAllowContentAccess(true); // 关键:启用 WebGL settings.setAllowFileAccess(true); settings.setDatabaseEnabled(true); settings.setAppCacheEnabled(true); } - 问题:触摸编辑时,长按弹出系统键盘,但输入不生效
→ 原因:Univer 的编辑浮层与系统键盘 Z-index 冲突。
→ 解决:在index.html中添加 CSS:.univer-editor-container { z-index: 2147483647 !important; /* 确保高于系统键盘 */ }
5.4 “Univer Online” 与自建 SDK 的选型决策表
| 维度 | Univer Online(SaaS) | 自建 Univer SDK |
|---|---|---|
| 上线速度 | 5 分钟内可开通,提供管理后台 | 需 1-2 周完成集成、测试、部署 |
| 定制深度 | 仅支持界面主题、菜单项增删 | 可修改任意源码,包括公式引擎、OT 算法 |
| 数据主权 | 数据存储于 Univer 云,需签 DPA 协议 | 数据完全自主可控,符合等保三级要求 |
| 成本结构 | 按活跃用户/月收费,年费约 ¥80,000 起 | 一次性开发投入约 ¥200,000,无后续许可费 |
| 适用场景 | 内部轻量级协作(如市场活动排期)、POC 快速验证 | 核心业务系统(如 ERP 工单、CRM 合同)、强合规要求场景 |
我的建议:永远从自建 SDK 开始。Univer Online 的价值在于“验证 MVP”,一旦业务跑通,立刻迁移到自建方案。我们服务的某银行客户,就是先用 Online 版 2 周内上线信贷审批表单,验证了业务价值后,再用 3 周完成 SDK 集成,最终将审批流程嵌入其核心信贷系统,数据不出内网。
6. 进阶能力延展:从“表格 SDK”到“企业级文档中台”
6.1 构建文档版本控制系统(Document Versioning)
Univer 本身不提供版本管理,但其unitId和snapshot机制可轻松构建。核心思路:每次保存时,生成文档快照并存入数据库:
// 保存时生成快照 const snapshot = univer.getSnapshot(); // 返回 JSON 序列化的完整文档状态 await api.saveVersion({ unitId: workbook.getUnitId(), version: Date.now(), // 或用 git-style commit hash snapshot, author: currentUser.id }); // 回滚到某版本 const oldSnapshot = await api.getVersion(unitId, versionId); univer.loadFromSnapshot(oldSnapshot); // SDK 原生支持实战效果:某制造业客户用此方案实现了“工艺参数表”的版本追溯。当某批次产品出现质量问题,质量工程师可一键回溯到该批次生产时使用的参数表版本,并对比前后变更,定位到某次误操作导致温度阈值被修改。
6.2 与 BI 工具打通:让 Univer 成为数据源
Univer 文档可导出为标准 CSV/Excel,但更高效的方式是暴露 REST API:
// 在后端封装 Univer 文档数据接口 app.get('/api/univer/:unitId/data', async (req, res) => { const workbook = await loadWorkbook(req.params.unitId); // 从存储加载 const sheet = workbook.getActiveSheet(); const data = sheet.getData(); // 获取二维数组数据 res.json({ headers: data[0], // 第一行作为列名 rows: data.slice(1) // 后续行为数据 }); });然后在 Tableau/Power BI 中,将此 API 设为数据源。我们曾帮一家零售公司实现:门店店长每天用 Univer 填写《每日销售快报》,系统自动将数据推送到 BI 平台,生成全国门店销售热力图。关键优势是:店长无需学习 BI 工具,只需填表;BI 工程师无需维护 ETL 脚本,API 自动同步。
6.3 AI 增强:在 Univer 中集成 LLM 能力
Univer 的公式引擎可无缝接入 LLM。例如,实现“智能摘要”:
univer.registerFunction({ name: 'AI_SUMMARIZE', call: async (ctx, values) => { const text = values[0]; // 调用你的 LLM API(如阿里云百炼) const response = await fetch('https://dashscope.aliyuncs.com/api/v1/services/aigc/text-generation/generation', { method: 'POST', headers: { 'Authorization': `Bearer ${apiKey}` }, body: JSON.stringify({ model: 'qwen-max', input: { messages: [{ role: 'user', content: `请用 50 字以内总结以下内容:${text}` }] } }) }); const result = await response.json(); return result.output.text; } });在表格中输入=AI_SUMMARIZE(A1),即可对 A1 单元格内容进行 AI 摘要。这已不是未来场景,而是我们已在三个客户项目中落地的能力——它让 Univer 从“数据录入工具”升级为“智能数据处理终端”。
7. 我的实际体会:为什么 Univer 正在改变企业软件的构建逻辑
我在一线做过 12 年企业软件交付,见过太多“为表格而表格”的失败案例。某次给电力公司做巡检系统,客户最初的需求是“工人用手机填一张 Excel 表”,我们按传统思路做了个 H5 表单,结果上线一周就被投诉:工人说“填完不知道提交成功没”“想改一个字要重新填整张表”“领导要看汇总数据还得我手动导出”。后来我们换成 Univer SDK,只改了三处:1)用setProtection锁定所有非必填字段;2)在表头加了个=NOW()实时显示填写时间;3)用registerFunction接入他们的 GIS 服务,自动填充“当前经纬度”。结果呢?工人反馈“像在手机上用 Excel,顺手”;领导打开后台,所有工单自动按区域聚合,实时生成热力图。这件事让我意识到:Univer 的本质,是把“用户对 Excel 的肌肉记忆”和“企业对数据治理的刚性要求”缝合在了一起。它不强迫用户学新工具,而是把新工具变得像旧工具一样自然;它不牺牲安全性去换易用性,而是用 SDK 的可编程性,让安全策略像公式一样写在文档里。现在每当我听到“我们要做个数据采集系统”,第一反应不再是画 ER 图、设计 API,而是打开 Univer 文档,用鼠标拖拽出一个原型——因为我知道,那个原型,就是最终上线的系统。这或许就是“spreadsheets are all you need”最朴实的注脚:当工具足够贴近人的直觉,复杂问题的答案,往往就藏在最熟悉的界面上。