如果你最近也在给公司的后台系统加一个“在线编辑 Excel / Word”的功能大概率会碰到 Univer、Jspreadsheet、OnlyOffice 这三个名字。它们都号称能做 Web 表格但实际用起来完全是三条路线。我花了差不多两周时间把这几个方案都跑了一遍从最简单的嵌入式表格到完整私有化文档服务都过了一遍踩了不少坑也把每个方案适合的场景摸清楚了。这文章不是官方文档复读是我作为开发者的实战整理。Univer 适合想在 React/Vue 项目里深度定制表格交互的人Jspreadsheet 适合只想快速嵌入一个不用太重依赖的表格控件的人OnlyOffice 则适合需要把“一套完整 Office”部署到自己服务器、还要支持多人协同编辑的团队。我会把三个项目都快速介绍一遍重点放在 OnlyOffice 的 Docker 安装、Java 后端集成、在线协同编辑和常见安装问题上最后给你一张可以直接抄作业的选型表。1. 先把三个项目看清谁解决什么问题1.1 Univer可嵌入的现代办公组件Univer 是近年起来的一个开源办公套件项目定位不是“一个部署好的网站”而是一套可以嵌进现有前端项目的组件库。它同时支持表格、文档、幻灯片三种类型底层用 Canvas 自绘渲染不是拿一个textarea模拟出来的假 Excel。这带来一个很实际的好处你可以在不切页面的情况下把表格做到自己的 Web 应用里还能自己定制工具栏、右键菜单、数值格式、公式计算这些细节。我第一次用 Univer 时最大的感受是它“现代感”很强。React 技术栈、TypeScript 类型导出、插件化架构几乎每个功能都是一个插件按需加载。比如你要做数据录入页面不需要加载幻灯片模块只需要引入 Sheet 相关的 preset 就行。它的公式引擎是自带实现的支持很多 Excel 常用函数所以做轻量级财务计算、排班表、项目计划这类场景非常顺手。还有一个容易被忽视的点Univer 的协议是 Apache 2.0社区版可以放心商用只要保留版权声明。对于喜欢定制 UI、且团队有一定前端能力的人来说Univer 是一个下限很低、上限很高的方案。但它的缺点是项目迭代非常快API 变化也很频繁我后面会专门说。1.2 Jspreadsheet轻量级在线表格控件Jspreadsheet 和 Univer 是另一个方向。它是一个纯粹的 JavaScript 表格控件没有厚重的插件体系也没有文档和幻灯片专注“表格”这一件事。它的加载方式很简单一个 JS 文件加一个 CSS然后在页面里初始化一个基于div的电子表格就能快速做出可编辑、可排序、可筛选的数据表格。Jspreadsheet 最吸引人的地方是轻。它的社区版运行起来几乎不挑浏览器也不要求你用什么前端框架原生 JS 就能跑jQuery 早就不是必要条件了。它非常适合做“表单里的表格组件”比如配置项列表、批量录入区、报表数据预览。你可以把服务端返回的 JSON 数组直接塞给它也可以在用户改完每个格子后触发事件自己把数据同步回后端。但轻量也意味着边界要自己守住。Jspreadsheet 不是完整 Office 产品它没有 Word、没有 PPT公式能力和 Excel 兼容度比 Univer / OnlyOffice 弱一档。它的社区版开源协议是 GPL 类如果你的产品需要完全闭源商用最好先核实当前版本协议或者买商业授权。这也是很多公司最后没有选它的原因。1.3 OnlyOffice开源、可私有化的完整 Office 套件OnlyOffice 跟前面两个完全不是一个量级。它是一套完整的办公套件包含文档、表格、演示文稿还提供配套的文档服务端Document Server。你把 Document Server 部署好之后浏览器里打开就是一个长得非常接近桌面 Office 的在线编辑器。更重要的是它能做到多人同时编辑同一个文件还能完整兼容.docx、.xlsx、.pptx这类 Microsoft Office 格式。我理解 OnlyOffice 的定位是这样的如果你需要给企业内部做一个“私有化在线 Office”不想用各种云端协作平台只想数据留在自己服务器里OnlyOffice 是开源社区里成熟度最高的选择。它不是一个简单的编辑器组件而是一个有服务端、有存储/缓存交互、有回调机制的产品级解决方案。OnlyOffice 社区版对个人和小团队是友好的官方 Docker 镜像一条命令就能启动但在生产环境里需要规划好 JWT 签名、回调地址、文件下载地址、WebSocket 长连接等问题否则会出现“文档打不开”“保存不回写”这类经典问题。这部分我会在第 3、4 节展开讲。1.4 三选一之前先看一张对比表维度UniverJspreadsheetOnlyOffice产品形态前端组件库前端表格控件独立服务端 前端编辑器能编辑什么表格/文档/幻灯片表格文档/表格/演示文稿部署成本低打包进前端项目最低引入 JS/CSS 即可高需部署独立服务协同编辑需按官方方案实现/依赖配套基本不支持原生支持多人协同与 Office 格式兼容中等较弱很高定制能力强插件化一般靠事件/配置一般前端代码不像开源组件那样随意改引用场景要做成产品内的“编辑模块”快速加个表格面板企业私有化在线办公这个表格不是绝对的比如 Univer 在文档和幻灯片方面还在快速迭代Jspreadsheet 的 Pro 版功能会更丰富但我认为选型的第一逻辑是你到底要一个“表格控件”还是要一套“文档系统”前者在 Jspreadsheet 和 Univer 里选后者基本就是 OnlyOffice。2. 快速跑通两个前端表格方案2.1 Univer 十分钟接入你的 React 项目Univer 官方文档一直强调“preset”概念我建议新手先不要手动拼一堆插件直接用现成的预设包。假设你已经有一个 Vite React 项目先安装依赖npm install univerjs/preset-sheets univerjs/preset-sheets-ui univerjs/core univerjs/design不同版本导入路径可能有差异以你安装的版本为准。初始化一个简单的表格import { LocaleType } from univerjs/core; import { UniverSheetsPreset } from univerjs/preset-sheets; import { UniverSheetsUIPreset } from univerjs/preset-sheets-ui; import { createRoot } from react-dom/client; const univer new Univer({ locale: LocaleType.ZH_CN }); univer.registerPlugin(UniverSheetsPreset, { container: app, // 这里可以配置默认工作表结构 sheets: [ { name: Sheet1, rowCount: 100, columnCount: 20, cellData: { 0: { 0: { v: 项目名称 }, 1: { v: 负责人 }, 2: { v: 进度 }, }, 1: { 0: { v: 在线表格预研 }, 1: { v: 张三 }, 2: { v: 80% }, }, }, }, ], });把这段代码放进 React 的useEffect里或者直接放在组件挂载后执行页面上就会渲染出一个可以编辑的表格。别被“注册插件”这个词吓到实际体验就是配置一个对象。核心思路是Univer 负责渲染和计算你负责往cellData里喂数据。Univer 里读取表格数据也类似通过univer.getCurrentUniverSheet()等 API 获取当前实例再调用getSheetData()之类的方法。这里的 API 名在版本迭代中改过几次所以我不写死你照着官方示例跑一遍即可。新手最容易遇到的问题是“渲染出来了但公式不算”这时候先检查公式单元格是否设置了f属性比如{ v: 1, f: SUM(A1:A5) }有些版本需要显式配置。2.2 Jspreadsheet 五步嵌入现有页面Jspreadsheet 的上手路径比 Univer 短得多。先引样式和脚本我用的是社区版 CDNlink relstylesheet hrefhttps://cdn.jsdelivr.net/npm/jspreadsheet-ce4/dist/jspreadsheet.min.css / script srchttps://cdn.jsdelivr.net/npm/jspreadsheet-ce4/dist/jspreadsheet.min.js/script然后在页面里放一个容器div idspreadsheet/div初始化jspreadsheet(document.getElementById(spreadsheet), { data: [ [产品A, 已上线, 2024-03-01], [产品B, 开发中, 2024-05-20], ], columns: [ { title: 产品名称, width: 160 }, { title: 状态, width: 120, type: dropdown, source: [已上线, 开发中] }, { title: 计划时间, width: 140, type: calendar }, ], onchange: function (el, cell, value, row, col) { console.log(单元格变更, row, col, value); }, });如果你想用 JSON 数据填充只需要把data换成对象数组再在columns里配置name字段即可。比如后端返回[{name: 产品A, status: 已上线}]初始化时可以用data: data.map(row [row.name, row.status])或者直接使用官方支持的对象数据映射模式。Jspreadsheet 的license配置项在社区版 4.x 里需要注意有些版本不填会弹出版权提示你需要去官网申请一个免费的社区版密钥。这一步经常被人忽略实际初始化时看到提示会很懵。2.3 这两个方案最容易踩的坑先说 Univer。它最大的坑是版本碎片化。0.x 到 1.x 之间API 和插件注册方式都有变化你搜索到的很多教程是旧版写法直接复制过来在最新版上跑不通。我的建议是打开官方文档的“Playground”先用它提供的模板跑通再往你的项目里迁移不要自己从零拼 API。其次是“样式和主题容易失控”。Univer 默认样式已经不错但企业接入时往往想调整主色、字体、行高这需要了解它的设计令牌Design Token。如果你只是临时集成建议先用默认主题不要一上来就套自定义 CSS否则会花大量时间在样式微调上。Jspreadsheet 的问题更多集中在边界能力上。它面对超大表格时性能不如 Canvas 渲染方案一次塞几万行数据会卡。另外中文输入法下有些老版本单元格在拼音组词过程中会丢失焦点实测在 Chrome 和微软拼音里偶尔会出现需要升级到最新版并测试。如果你们产品里用户大量使用中文输入一定要在真机上用中文输入法完整打一遍字再决定是否采用。3. OnlyOffice 安装部署从 Docker 到避开常见安装问题3.1 一条 Docker 命令跑起 OnlyOfficeOnlyOffice 的官方 Docker 镜像把整套 Document Server 打包好了最简安装其实就一条命令sudo docker run -i -t -d \ --name onlyoffice_document_server \ -p 8080:80 \ -e JWT_ENABLEDtrue \ -e JWT_SECRETyour-strong-secret-key \ --restartalways \ onlyoffice/documentserver启动后访问http://你的服务器IP:8080/welcome能看到 OnlyOffice 欢迎页说明服务已经起来了。这里的端口映射我用了8080:80把宿主机的 8080 映射到容器内的 80。官方默认文档里给的是-p 80:80但实际部署时 80 端口经常被 Nginx、Apache 占用换成 8080 能减少冲突后面用 Nginx 反代到对外域名即可。JWT_SECRET是这段命令里最重要的东西。从 OnlyOffice 7.2 开始JWT 默认启用后端集成时必须跟这个密钥保持一致否则在编辑器里打开文档会报签名错误。可以用openssl rand -hex 32生成一个随机密钥不要用123456这种弱口令。3.2 安装阶段最容易翻车的三个细节第一个坑容器一直重启日志里报内存不足。OnlyOffice 的 Docker 容器至少需要 2GB 内存我刚开始在一台 1GB 的小机器上测试启动几分钟后进程就没了。解决办法是先确保宿主机内存充足也可以用 Docker 限制容器最小内存有时需要清理残留缓存。如果你是在本地虚拟机里试建议直接分 4GB 内存给虚拟机。第二个坑端口映射改了之后编辑器加载不出来。有些教程直接用http://localhost:8080访问但在集成到前端页面时OnlyOffice 内部的加载地址可能还会引用容器里的服务地址。如果出现编辑器一直在加载、控制台报跨域或文件找不到优先检查是不是后端生成document.url和编辑器 API 地址不一致。生产环境强烈建议用域名 HTTPS而不是直接暴露 IP 端口。第三个坑镜像下载很慢或者卡在某个中间层。OnlyOffice 官方镜像体积比较大几百 MB 到 1GB 都有受网络环境影响明显。如果你网络不稳定可以在 Docker 配置里换一个国内镜像加速器或者在半夜网络好的时候先手动拉一遍。拉取过程中不要中断中断后重新拉取可能产生残层导致容器启动后行为诡异。3.3 用 docker-compose 直接落地一套可维护环境生产环境我习惯用 docker-compose 来管理而不是裸docker run。下面是一个社区版可用的最小 compose 文件version: 3 services: onlyoffice: image: onlyoffice/documentserver:latest container_name: onlyoffice_document_server restart: always ports: - 127.0.0.1:8080:80 environment: - JWT_ENABLEDtrue - JWT_SECRET${JWT_SECRET} volumes: - ./data/logs:/var/log/onlyoffice - ./data/data:/var/www/onlyoffice/Data - ./data/lib:/var/lib/onlyoffice - ./data/db:/var/lib/postgresql这里我把端口绑定限制在127.0.0.1:8080意味着宿主机外部不能直接访问 8080只能通过 Nginx 等反向代理访问。这个做法可以避免文件服务端口裸奔也算是一层安全防护。后面用 Nginx 在 443 端口做 TLS 终结server { listen 443 ssl; server_name doc.example.com; ssl_certificate /etc/nginx/ssl/doc.example.com.pem; ssl_certificate_key /etc/nginx/ssl/doc.example.com.key; client_max_body_size 100m; location / { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection upgrade; } }Nginx 配置里最后两行升级 WebSocket 是 OnlyOffice 协同编辑的关键如果没有这两行多人同时打开文档时会出现连接不断中断、光标不同步的现象。很多 OnlyOffice 在线协同出问题根源不是 OnlyOffice 本身而是反向代理把 WebSocket 掐断了。4. OnlyOffice Java 开发集成与在线协同编辑4.1 在线协同编辑到底是怎么协同的OnlyOffice 的协同编辑模型可以这样理解你有一个自研系统负责文件存储和业务权限OnlyOffice Document Server 负责真正的编辑渲染和多人同步。当用户打开一个文档时浏览器通过 iframe 加载 OnlyOffice 编辑器编辑器再去 Document Server 拉取文件内容。Document Server 会为每个文档维护一个实时状态多人之间的光标、修改、保存都由它来处理。这个模型的好处是你的系统不需要实现复杂的 CRDT 或 OT 算法只需要在文件保存时接住 Document Server 的回调把最新内容写回你自己的存储里。听起来很简单但实际集成的难点在于Document Server 需要能访问到你系统提供的文件下载地址你的系统也需要能访问到 Document Server 下载新文件的地址。如果两边都在内网就需要提前规划好网络互通如果有公网域名则要保证回调地址不是localhost。4.2 后端生成签名配置文件Java 示例如果你是 Java 后端集成 OnlyOffice 的基本流程是先建一个接口根据文件 ID 查询文件真实路径构造 OnlyOffice 编辑器配置对象然后用 JWT 签名最后返回给前端。前端拿到这个配置初始化编辑器。先引入 JJWT 依赖也可以换成你项目里已有的 JWT 库dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt-api/artifactId version0.11.5/version /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt-impl/artifactId version0.11.5/version scoperuntime/scope /dependency然后写一个生成配置的 Serviceimport io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import javax.crypto.spec.SecretKeySpec; import java.nio.charset.StandardCharsets; import java.security.Key; import java.util.HashMap; import java.util.Map; public class OnlyOfficeConfigBuilder { private final String jwtSecret; private final String documentServerUrl; public OnlyOfficeConfigBuilder(String jwtSecret, String documentServerUrl) { this.jwtSecret jwtSecret; this.documentServerUrl documentServerUrl; } public MapString, Object buildConfig(String fileId, String fileName, String fileType, String downloadUrl, String callbackUrl, String userName) { MapString, Object document new HashMap(); document.put(fileType, fileType); // xlsx, docx, pptx document.put(key, fileId -v1); // 每次内容变化最好更新 key document.put(title, fileName); document.put(url, downloadUrl); // OnlyOffice 能访问到的下载地址 document.put(permissions, Map.of( edit, true, download, true, print, true )); MapString, Object editorConfig new HashMap(); editorConfig.put(mode, edit); editorConfig.put(lang, zh-CN); // 多语言 editorConfig.put(callbackUrl, callbackUrl); editorConfig.put(user, Map.of( id, user-001, name, userName )); // 可选自定义展示 MapString, Object customization new HashMap(); customization.put(autosave, true); customization.put(chat, false); editorConfig.put(customization, customization); MapString, Object config new HashMap(); config.put(document, document); config.put(editorConfig, editorConfig); // 生成 JWT Key key new SecretKeySpec(jwtSecret.getBytes(StandardCharsets.UTF_8), SignatureAlgorithm.HS256.getJcaName()); String token Jwts.builder() .setClaims(config) .signWith(key, SignatureAlgorithm.HS256) .compact(); config.put(token, token); return config; } }这里有两个关键设计。第一是key它不是文件 ID 本身而是文档内容的版本标识。OnlyOffice 用这个 key 判断文档是否变化如果你每次都固定成同一个 key多用户打开时可能看到旧缓存如果每次编辑都变又会导致协作者被踢。比较合理的做法是“文件 ID 版本号”保存成功后再更新版本号。第二是downloadUrl这个 URL 必须能让 OnlyOffice 服务器直接访问到不能像浏览器里那样用localhost。如果你在 Docker 容器里跑后端容器名或者内网 IP 反而比localhost可靠。4.3 前端嵌入 OnlyOffice 编辑器前端部分反而简单。页面里放一个占位div引入 Document Server 提供的api.js请求后端接口拿到配置然后初始化div idoffice-editor stylewidth: 100%; height: 100vh;/div script srchttps://doc.example.com/web-apps/apps/api/documents/api.js/script script fetch(/api/onlyoffice/config?fileId123) .then((res) res.json()) .then((config) { const docEditor new DocsAPI.DocEditor(office-editor, config); // 监听文档加载完成 docEditor.on(onDocumentReady, () { console.log(文档已就绪); }); // 监听保存状态 docEditor.on(onRequestSave, () { console.log(用户手动触发了保存); }); }); /script前端这一步踩坑主要集中在跨域和 HTTPS。如果 Document Server 用了域名和证书而你的业务系统是http://localhost浏览器会因为混合内容或 iframe 跨域限制导致编辑器加载不出来。我建议本地联调时用一个临时 HTTPS 证书给业务系统也套上一层或者用同一个域名下的不同路径反向代理。关于多语言editorConfig.lang在 OnlyOffice 里支持很多值比如zh-CN、en、ru、ja、de等。这个配置直接影响编辑器界面语言、右键菜单语言和部分多语言文本。如果你要面向国际用户可以在后端根据用户偏好动态传入不需要写死。4.4 保存回调与协同状态处理OnlyOffice 会在文档保存时向前端页面里配置的callbackUrl发送一个 POST 请求。你的后端需要实现这个回调接口否则用户编辑完发现数据没有落库很致命。回调请求的body里包含status字段常用取值如下status含义处理建议1正在编辑仅通知记录在线用户数或心跳不落库2需要保存从 body 里的url下载最新文件覆盖存储3保存出错记录日志并告警4关闭无变更不需要处理6正在保存可做轻量日志不要重复下载7强制保存和 status2 一样处理一个标准的回调接口长这样PostMapping(/api/onlyoffice/callback) public ResponseEntityMapString, Object callback(RequestParam(fileId) String fileId, RequestBody MapString, Object body) { Integer status (Integer) body.get(status); if (status ! null (status 2 || status 6 || status 7)) { String downloadUrl (String) body.get(url); byte[] fileContent restTemplate.getForObject(downloadUrl, byte[].class); if (fileContent ! null) { fileService.saveFile(fileId, fileContent); } } MapString, Object response new HashMap(); response.put(error, 0); return ResponseEntity.ok(response); }注意OnlyOffice 要求回调接口最终返回的 JSON 里必须包含error: 0否则它会认为保存失败并反复重试。我见过有人在这里顺手把业务数据也返回了结果 OnlyOffice 一直重试把日志刷爆了。协同编辑是否生效除了回调之外还要看editorConfig.mode是不是edit。如果文档被其他人以只读方式打开permissions.edit会被覆盖新打开的用户看到的工具栏就没有保存按钮。另外社区版对同时连接人数有限制一般是 20 个连接左右真实企业场景如果超过这个规模要么拆分文档服务要么升级商业版本。5. 常见问题速查与场景选型建议5.1 高频问题解决速查表整理一下这段时间踩到的高频问题方便你排错现象可能原因解决办法OnlyOffice 容器反复重启宿主机内存不足确保至少 2GB 可用内存用free -h确认打开文档提示“文档下载失败”document.url指向了前端访问不到的地址把 URL 改成 OnlyOffice 服务器能访问到的内网地址JWT 签名报错编辑器无法加载JWT_SECRET 不一致或算法不是 HS256后端和 Document Server 设置为同一个 secret多人打开文档光标不同步反向代理没配置 WebSocket Upgrade在 Nginx location 里加proxy_set_header Upgrade $http_upgrade编辑器一直打转控制台报跨域业务系统和 Office 域名不同统一用域名 HTTPS或使用同域名反代文档能打开但保存不生效回调接口没返回{error:0}检查回调 Controller 返回格式Univer 打开大数据量表格卡顿一次性渲染了太多行/列分页加载数据或先评估是否真的需要一次展示十万行Jspreadsheet 中文输入丢字老版本 IME 兼容问题升级到最新社区版并在中文浏览器实测5.2 什么场景选什么方案含多语言和“好不好用”如果你现在还在犹豫“OnlyOffice 到底好不好用”我的看法是对比的基准很重要。如果你拿它跟一个普通的 Web 表格控件比会觉得它太重、部署麻烦、回调逻辑复杂但如果你拿它跟“自研一个在线 Word”比OnlyOffice 就是省了几百人日的最优解。它好不好用取决于你愿不愿意按它的规则来容器部署、JWT 签名、回调地址这三点理顺之后后面就很顺。选型最后再给一次建议如果你只是要在后台管理页面里嵌入一个“类 Excel”的录入表格用户不需要导入导出.xlsx也不在乎公式有多全用Jspreadsheet就够了半天接入。如果你的产品是面向用户的一套系统表格是你这个产品的重要模块需要自定义 UI、权限、数据处理而且团队前端比较强优先考虑Univer。如果你要做企业内部文档协作或者说你的系统里需要在线预览和编辑.docx、.xlsx这些真实 Office 文件那直接OnlyOffice别考虑用前两者硬撑。在实际操作中我自己最终落地方案是“OnlyOffice 出正式文件编辑Jspreadsheet 做轻量数据录入”两者不冲突完全可以同时存在。我给 OnlyOffice 配了中英文两种lang前端根据用户语言动态传参Jspreadsheet 那边则固定中文因为只服务内部运营。最后分享一个小经验无论选哪个方案第一天上手时不要急着加权限、加自定义功能先把最小的 Demo 跑通。OnlyOffice 就先用官方 Docker 镜像启动再用一个简单页面把编辑器打开把保存回调打通。基础链路稳定之后再叠加登录、权限、多语言、版本管理这些外围功能。这个顺序反过来的话你会在定位问题时非常痛苦。