内网项目里做文档在线预览,第一反应基本都是"前端找个库渲染一下不就行了",结果真上手才发现,docx 用 docx-preview 能糊出来但一遇到复杂排版、页眉页脚、文本框就散架,Excel 用 SheetJS 转成表格之后样式全丢,PPT 更是几乎找不到能打的前端方案。我最近接的这版系统跑在完全隔离的内网里,服务器是 CentOS 7,浏览器是客户统一发的版本,用户要看的恰好是 Word、Excel、PPT 这三类最麻烦的办公文档。折腾了一圈之后,我把方案落到了 KKFileView 上——服务端转换、前端只负责展示,实测在内网这种"没有外网、不能随便装东西、运维只给你一台机器"的环境下,反而是最省心的路子。下面就把我这一路的选择逻辑、部署细节、前端接入写法和踩过的坑完整摊开讲,尤其是编码、字体、跨域、并发这几块,属于不踩一遍不会知道、但提前知道能省两天的内容。
1. 为什么纯前端方案在真实项目里经常会顶不住
1.1 docx-preview、SheetJS、pptxjs 各自的能力边界
先说清楚我为什么放弃纯前端。这三个库我都实际试过,它们的问题不在"能不能显示",而在"显示得像不像原件"。
docx-preview的思路是把 docx 这个 zip 拆开,解析里面的 XML,再用 HTML 和 CSS 去还原。它的优点是不依赖后端、纯浏览器跑、体积可控。但它的还原度天花板很明显:分页是靠计算模拟的,页眉页脚、文本框、艺术字、嵌入的 Excel 表格、复杂的表格边框和单元格合并,经常出现错位或者直接丢掉。客户拿来预览的是正式的合同和报告,第一页的表格线歪了半行,验收就直接卡住了。而且它要读取 ArrayBuffer,文件大了之后主线程解析会明显卡顿,页面直接白屏两三秒。
SheetJS更典型。它是一个数据解析库,本质是把 xlsx 还原成二维数组和单元格样式对象,但它不做排版。也就是说公式的显示值、单元格合并、条件格式、图表、数据透视表这些,它要么不支持要么支持得很勉强。你要自己做一套表格渲染层去还原列宽、行高、边框、数字格式,工作量瞬间从"引个库"变成"做一个精简版 Excel"。真要这么干,还不如直接买商业方案。
pptxjs这个方向就更尴尬了。PPT 的核心是绝对定位的元素堆叠、主题字体、动画和母版,用 DOM 去模拟本身就是逆着来的。这个库已经很久没有实质性更新,对新版 pptx 的兼容性堪忧,而且不能读取软换行、渐变填充这类细节。
1.2 三种方案的代价对比
我把当时的评估整理成了下面这张表,这也是我最后做决策的依据。
| 方案 | 前端改动量 | 还原度 | 内网适配 | 主要风险 |
|---|---|---|---|---|
| docx-preview 等纯前端库 | 中到大,需要各格式分别处理 | 中低,复杂排版易崩 | 好,无外部依赖 | 验收时被排版问题反复打回 |
| 转 PDF 后前端渲染 | 小 | 高 | 好 | 需要后端转换服务 |
| KKFileView | 极小,基本只拼 URL | 高 | 好,可离线部署 | 需要维护一个 Java 服务 |
看这张表其实结论已经出来了。纯前端方案的隐形成本是"无穷无尽的排版兼容问题",这些问题不会在开发阶段暴露,只会集中爆发在验收和上线之后。而"服务端转成 PDF,前端用现成的 PDF 渲染器显示"这条路径,是把最难的那部分交给专业的排版引擎去做——LibreOffice 本身就有一套成熟的 Office 文档渲染实现,它转出来的 PDF 自然带分页、带页眉页脚、带字体替换规则,浏览器只需要显示 PDF 就行。
那为什么不自己写个 Java 程序调 LibreOffice 呢?因为除了转换,还有一堆脏活:文件下载、编码处理、临时文件管理、缓存、水印、截图、日志、限流。KKFileView 把这些都做完了,而且它同时支持图片、文本、压缩包、音视频的预览,后面业务再提需求时不用重新选型。它本质上就是一个开箱即用的文档预览中间件,对我这种"一个人扛前后端加部署"的场景性价比最高。
2. 一次预览请求在 KKFileView 内部走了哪几步
2.1 从 onlinePreview 接口到本地临时文件
KKFileView 对外暴露的核心接口就是onlinePreview,你在浏览器里请求http://你的服务地址:8012/onlinePreview?url=xxx,服务端就开始干活了。第一步是把url参数解出来,这是一个 Base64 编码过的文件地址(这点很多人第一次看不懂,我在第 4 节会详细说)。解出来之后,服务端会去下载这个文件,落到本地临时目录里。
这里有两个非常关键的机制。第一是trust.host白名单,服务端不会去下载任意地址,只允许下载你配置过的域名下的文件,这是为了防止这个接口被当成内网探测工具用(也就是常说的 SSRF)。第二是文件后缀的判定,服务端需要知道这个文件到底是 docx 还是 xlsx,才能决定用哪条转换链路。
早期的版本靠 URL 的后缀去猜,但如果你的文件下载地址长这样http://files/api/download?id=123,后面根本没有后缀,服务端就懵了,只能按默认类型处理,结果就是转出来一堆乱码或者直接报不支持。所以老版本要求你在 URL 后面额外拼一个&fullfilename=测试文档.docx来把真实文件名告诉它。新版对这个的处理有所改进,但我在实际部署时依然习惯性把文件名带上,属于花一秒钟换取确定性。
2.2 LibreOffice 无头转换为什么是整个链路的瓶颈
拿到本地文件之后,如果是 Office 系列,KKFileView 会通过 jodconverter 调用 LibreOffice 的 headless 模式做转换。流程大致是:启动一个 soffice 进程,把文件加载进去,导出成 PDF(或者按配置导出成图片),再把结果写回临时目录。
这一环是整个系统里唯一真正"重"的地方,原因有三个。其一,LibreOffice 启动本身就要吃内存和时间,第一次冷启动往往要几秒。其二,转换是 CPU 密集型的,一个 20 页带图表的 PPT,转换耗时可能到十几秒。其三,也是最要命的,单个 soffice 实例在处理时基本是串行的,你并发十个请求,后面的就得排队,排队超时用户那边看到的就是一直转圈。
明白了这一点,后面所有的优化方向就都清楚了:要么加缓存让重复请求不再二次转换,要么控制并发数不让请求把服务压死,要么加机器做多实例分流。这跟我后面第 6 节讲的内容是呼应的,不是拍脑袋想出来的招,而是从这条链路上推导出来的。
提示:如果你的服务器内存只有 2G 以内,我会强烈建议把 LibreOffice 的转换和主服务分开部署,或者至少给 JVM 和服务预留足够的内存,否则 OOM 会来得比你想象中早。
2.3 转换结果是怎么还给浏览器的
转换完成之后,结果落在本地缓存目录里,服务端返回给浏览器的其实是一个预览页面地址,这个页面里内嵌了 PDF 渲染器(较新版本用的是 PDF.js 那一路的实现)或者图片的展示逻辑。前端拿到的本质上还是一个可 iframe 嵌入的页面。
这里有个细节值得说:如果你是用图片模式预览(把每页转成一张图),页数多的文档会生成很多图片,加载体验是逐页出现的,缩放的清晰度也不如 PDF;如果是 PDF 模式,则是单个文件流式加载,体感更接近本地打开 PDF。不同版本的默认行为不一样,配置项的名字也在演进,我这里踩过的坑是——不要照抄网上某篇文章里的配置键,因为它对应的可能是两年前的版本。最靠谱的做法是打开你手上那个版本压缩包里的application.properties,看看里面实际写了哪些键、默认值是什么,再改。
还有一点,转换结果的目录结构是按文件特征分层的,同一个文件第二次请求时会直接命中缓存返回,不再走 LibreOffice。这意味着同一个文档第一次打开慢、后面就快。这个特性在演示的时候很有用,你可以提前把客户最常看的那几个文件点一遍预热。
3. CentOS 7 内网离线部署:依赖是怎样一点点搬进去的
3.1 JDK、LibreOffice、字体这三件套一个都不能少
内网部署最大的特点就是没有 yum 源、没有网络、所有依赖都得手工搬。我先把必须的东西列出来,这是整个部署清单的骨架。
- JDK:KKFileView 是 Spring Boot 应用,跑在 Java 上,主流版本对 JDK 8 兼容性最好。先把 tar.gz 版本(不是 rpm 版本,tar 版解压即用,不用装)拷进去,配好
JAVA_HOME。 - LibreOffice:这是核心依赖,必须装。它的 rpm 依赖树比较长,直接一个个
rpm -ivh会疯狂报依赖缺失。我的做法是在一台能连外网的 CentOS 7 机器上用yumdownloader --resolve --destdir=/tmp/pkgs libreoffice*把主包和全部依赖一次性拉下来,打包拷进内网,再rpm -ivh *.rpm批量安装。前提是两台机器的系统版本和架构完全一致,否则依赖匹配会对不上。 - 中文字体:这是最容易被忽略、但影响最大的一项,下一节单独说。
- KKFileView 本体:直接用官方发布的打包好的 jar 或者 tar.gz,不需要在内网重新编译。
安装完之后一定做一次验证,不要等到前端联调才发现问题。验证命令很简单:
# 确认 LibreOffice 能被找到 which soffice libreoffice --version # 手工转一个文件试试,这一步能通,说明转换引擎没问题 soffice --headless --convert-to pdf --outdir /tmp /tmp/test.docx如果这行命令能生成 PDF 且打开之后排版基本正确,说明底层引擎是通的,剩下的都是 KKFileView 的配置问题。
3.2 中文字体不装,转出来的 PDF 就是一排方框
这是我在这个项目里踩得最深的一个坑,值得单独拎出来讲。
LibreOffice 在 Linux 上做 Word 转 PDF 时,需要靠系统字体来渲染文字。CentOS 7 最小化安装默认几乎没有中文字体,于是它遇到"黑体""宋体"这些字体名时找不到对应文件,就会做字体替换,替换的目标如果也不存在,最终结果就是正文变成一个个空心方框,或者字符全部挤在一起、行距异常。我第一版转出来的 PDF,标题是方框,正文是方框,只有英文和数字正常,看上去像是编码坏了,实际上就是字体缺失。
解决办法是把字体装进系统。操作本身不复杂:
# 建立中文字体目录 mkdir -p /usr/share/fonts/chinese # 把 Windows 上的字体文件(黑体 simhei.ttf、宋体 simsun.ttc、微软雅黑 msyh.ttc 等)拷进来 # 然后重建字体缓存 fc-cache -fv # 验证字体是否被系统识别 fc-list :lang=zh最后那条fc-list :lang=zh能列出一串中文字体名,才算装成功。装完之后必须重启 KKFileView 服务,因为 LibreOffice 是在启动时加载字体配置的,不重启不生效。另外还有一件事要做:把之前生成的缓存全部删掉重新转。因为缓存目录里存的是旧字体渲染出来的结果,你只重启服务不清缓存,页面依然显示旧的方框,会让你误以为字体没装上,白折腾半小时。
关于字体本身,我只能说一句:请确认你手上的字体文件是合法可用的,企业内部通常有统一的字体授权来源,从公司正版渠道获取。
3.3 用 systemd 托管服务和目录规划
内网环境通常没人给你配自动重启,所以我习惯把 KKFileView 交给 systemd 管起来,这样服务挂了能自动拉起来,机器重启也能自动起。
一个最小可用的 unit 文件大概长这样:
[Unit] Description=kkfileview preview service After=network.target [Service] Type=simple User=root WorkingDirectory=/opt/kkfileview ExecStart=/usr/bin/java -jar /opt/kkfileview/kkfileview.jar Restart=always RestartSec=10 StandardOutput=append:/opt/kkfileview/logs/stdout.log StandardError=append:/opt/kkfileview/logs/stderr.log [Install] WantedBy=multi-user.target配好之后systemctl daemon-reload、systemctl enable --now kkfileview就完事了。目录规划上我会建议把三类东西分开放在不同挂载点:程序目录(放 jar 和配置)、日志目录、缓存目录。缓存目录单独放的目的是它增长最快,万一磁盘满了,只影响预览,不至于把系统盘写爆。
注意:日志目录一定要提前建好,否则 systemd 的 append 输出会因为目录不存在而启动失败,这个报错信息很不直观,很容易让人以为是 jar 坏了。
4. 前端接入的姿势与参数拼装的坑
4.1 iframe 配合 Base64 URL 的标准写法
前端接入这块,KKFileView 的设计其实非常简单粗暴:你把文件的可访问地址告诉它,它返回一个预览页,你用 iframe 嵌进去就行。核心逻辑只有三步——拿到文件的真实下载地址、Base64 编码、作为url参数拼到预览地址上。
// 后端或前端拿到文件真实地址,例如: // http://192.168.1.10:8080/api/file/download?fileId=abc123&fileName=季度报告.docx function buildPreviewUrl(fileUrl) { // 关键:先处理中文,再 Base64,最后再 URL 编码 const base64 = window.btoa(unescape(encodeURIComponent(fileUrl))); const encoded = encodeURIComponent(base64); return `http://192.168.1.20:8012/onlinePreview?url=${encoded}`; }然后组件里就一个 iframe:
<iframe :src="previewUrl" frameborder="0" style="width:100%;height:100%" ></iframe>就这么多。真正麻烦的从来不是这段代码,而是下面几个看起来不起眼、但能让整个下午报废的细节。
4.2 中文文件名编码:btoa 报错的根因和正确修法
先讲第一层坑。window.btoa()这个函数只接受"每个字符码点都在 0-255 范围内"的字符串,中文的码点远超这个范围,所以你直接把一个带中文的 URL 扔进去,浏览器会直接抛InvalidCharacterError。很多人在这一步会误以为是自己传参方式错了,其实是 btoa 本身的限制。
正确姿势就是上面代码里那层unescape(encodeURIComponent(str))。encodeURIComponent先把中文字符转成%E5%AD%A3这样的 UTF-8 百分号编码,此时字符串里全是 ASCII 字符,unescape再把它还原成对应的字节(相当于让每个字节变成一个字符),这时传给btoa就不会报错了。
第二层坑是 Base64 结果里的特殊字符。Base64 的字母表里包含+、/、=这三个字符,其中+在 URL 查询串里会被解析成空格,=也容易出问题。所以生成 Base64 之后必须再套一层encodeURIComponent,也就是代码里的encoded那步。漏了这一步的典型症状是:英文文件名一切正常,中文文件名偶尔预览失败,而且失败的规则看起来很随机——实际上就是取决于文件名编码之后有没有生成+。
第三层坑是服务端解码的字符集。如果你的文件名中文显示成问号或者乱码,而前端编码逻辑已经写对了,那就要确认 KKFileView 那边的解码用的是 UTF-8。较新版本默认没问题,但如果你用的是比较旧的版本,可能会遇到默认字符集不是 UTF-8 的情况。这类问题的排查方法是:先在前端console.log出编码前的 URL 和编码后的 Base64,再拿 Base64 到网上的解码工具里解一下,看解出来的原文对不对。这一步能立刻区分是"前端编码错"还是"服务端解码错",省掉大量瞎猜。
4.3 弹窗组件里的 iframe 生命周期管理
在企业后台里,预览一般不是单独开页面,而是点一下列表行的"预览"按钮,弹个 Modal 出来看。这里有几个真金白银的经验。
第一,切换文件时一定要让 iframe 重新加载。因为 iframe 的src变化本身会触发新请求,但如果两个文件的预览 URL 碰巧长得一样(比如某些实现里 URL 中间部分相同),浏览器可能复用旧页面。稳妥做法是给 iframe 加一个:key,值用文件 ID,让它强制重建。
第二,关闭弹窗时要把 iframe 的src置空。因为 PDF 渲染器在 iframe 里是会持续占内存的,尤其是几十页的文档,反复打开关闭几次之后页面内存会明显上涨,浏览器标签页开始发卡。置空src让浏览器回收掉内部文档,是成本最低的释放手段。
第三,也是最容易被忽略的一点:转换是耗时的。十几兆的 PPT 第一次转换可能要十几秒,这期间 iframe 里是白屏或者一个 loading,用户会以为坏了,可能反复点击。所以我在 UI 上一定会加一个蒙层提示"文档转换中,请稍候",同时把按钮禁用掉。这不是体验优化,是防止并发雪崩的必要手段——用户点五次,就真的有五个请求同时在等 LibreOffice。
5. trust.host、跨域与 https 混合内容这三座大山
5.1 "不安全的 url"报错到底从哪来
部署完第一次测试,十有八九会遇到这一条报错:不安全的url,请检查trust.host配置。很多人第一反应是"我访问的地址很安全啊",其实这个安全不是指网络安全,而是指白名单。
KKFileView 的url参数允许传入任意地址,服务端会主动去请求它。这就意味着如果这个接口暴露给不特定的人,别人可以拿它去探测内网服务、读取内网接口返回的内容。所以它加了trust.host这个白名单配置,只有配置在里面的主机名,服务端才会去下载。
解决方式就是在配置文件里把你的文件服务域名或者 IP 加进去:
# 多个 host 用逗号分隔,支持域名和 IP trust.host=192.168.1.10,file.internal.com配置完重启服务。这里有个细节:白名单匹配的是 URL 里的 host 部分,如果你用的是 IP 加端口,端口一般不用写进去,只写 host 就行。另外如果你后面加了 Nginx 反向代理,别忘记把代理后的域名也一起配进去,否则线上某个入口会突然开始报这个错。
从安全角度我得说一句,这个白名单千万别图省事配成通配符或者写个宽松的规则,它本质上是一道防线,配松了等于这道防线不存在。
5.2 Nginx 同域反代是最省事的解法
接下来是跨域和混合内容。这两个问题的根因其实是同一个:浏览器出于安全策略,对 iframe 的跨域访问和 https 页面里嵌 http 资源都有严格限制。而你的主系统大概率是 https 的,KKFileView 大概率是 http 加 8012 端口的,这两者一撞,就会出现"接口能请求,但 iframe 里一片空白,控制台报 Mixed Content"。
我试过三种方案,最后选了第三种。
| 方案 | 做法 | 问题 |
|---|---|---|
| 给 KKFileView 单独配证书 | 服务本身启用 https | 需要额外证书,内网可能是自签,浏览器要装信任 |
| 放宽浏览器策略 | 引导用户设置 | 完全不现实,内网机器不归我们控制 |
| Nginx 同域反代 | 主站路径下代理预览服务 | 需要维护一段 Nginx 配置,但一劳永逸 |
反代的配置核心就是加一个 location,把预览路径转发到本地端口:
location /preview/ { proxy_pass http://127.0.0.1:8012/; 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_read_timeout 300s; proxy_send_timeout 300s; }这样一来前端拼出来的预览地址就变成/preview/onlinePreview?url=xxx,和主站同源同协议,跨域和混合内容两个问题一起消失。反代之后要注意两件事:一是trust.host里要把反代后的域名加进去;二是 KKFileView 如果有base.url这类表示自身访问地址的配置,要改成反代后的对外地址,否则预览页面里引用的静态资源可能还是旧的内部地址,导致样式加载不出来。
5.3 大文件与超时参数的联动调整
反代会让一个原本隐藏的问题浮现出来:超时。KKFileView 转换一个几十兆的 PPT 可能要半分钟以上,而 Nginx 默认的proxy_read_timeout只有 60 秒,中间还可能被其他超时参数截断。如果文件更大或者服务正忙,就会看到 504。
所以我在做反代的时候一定会同步调整这几处超时:Nginx 的读写超时、KKFileView 侧的转换超时(配置文件里通常有对应的项,不同版本名字不同,去配置文件里搜 timeout 关键字最快)、以及如果前面还有一层网关的话,那层的超时也要一起放宽。这三处只要有一个短,整个链路就按最短的那个算。
顺便说一句,超时放宽只是让长耗时任务能跑完,不等于解决性能问题。真正治本的办法还是缓存和并发控制,见下一节。
6. 缓存、并发、水印这些上线前必须定的事
6.1 缓存目录膨胀与清理策略
前面提过,KKFileView 会把转换结果缓存到本地,命中缓存就不再走 LibreOffice,这是它性能上最大的一个杠杆。但反过来说,缓存目录会持续增长,而且增长速度超出很多人的预期——一个 30 页的 PPT 转出来可能就占几十兆。
上线前必须定的事就是清理策略。我一般按三个层次来考虑。第一,设置一个最大的缓存容量上限,超过之后按时间淘汰最旧的文件,多数版本都有类似配置,去配置文件里找 cache 相关的那几项。第二,配一个定时清理任务,只清理超过 N 天没被访问过的产物,这个 N 根据业务来定,如果文档内容更新频繁,N 就设小一点,比如 3 天,避免用户看到的是旧版本。第三,把缓存目录放在独立的挂载点上,并且配上磁盘告警,别等写满了才发现。
这里有一个我踩过的坑:清理缓存的时候不要粗暴地rm -rf整个目录。因为这个目录在服务运行期间是被引用的,直接删掉正在被使用的文件,可能导致部分请求报文件不存在。稳妥做法是停服务后清理,或者只删超过一定时间没被访问的文件。
6.2 并发转换排队与多实例分流
关于并发,我的建议很直接:先在配置里限制同时转换的任务数量,把它设成一个你能接受的数字(比如 2 到 4,取决于 CPU 核数),超出的请求直接排队或者快速失败,而不是让它们全都涌进 LibreOffice。因为转换任务堆太多,结果是所有任务都变慢,整体响应时间反而更长,还容易把内存吃爆。
如果单实例确实扛不住,就上多实例加负载均衡。做法是同一台机器起两个服务实例、监听不同端口,或者干脆多台机器;然后在前面用 Nginx 做分流。要注意的是多实例之间缓存是不共享的,同一个文档可能在两个实例里各转一次,磁盘占用翻倍。如果要解决这个问题,可以考虑把缓存目录做成共享存储,但这条路会增加复杂度,除非真的有必要,我一般不上。
还有一个很实用的经验:把常用文档做预热。在系统刚启动完、用户还没大量访问的时候,用一个脚本把这些文档的预览接口挨个请求一遍,让缓存提前建好。用户真正点进去的时候就是秒开。这个技巧在给客户演示之前特别有用,能避免演示现场尴尬地转圈等半分钟。
6.3 水印、禁止下载和权限边界
最后是权限相关的一圈。企业内网系统对文档预览通常有额外要求,最常见的是加动态水印和禁止下载。
水印这块,KKFileView 支持在预览页面上叠加文字水印,配置一般涉及一个水印文本文件和一个开关项。能不能做到"每个用户看到的水印不一样"取决于版本,如果需要动态水印(比如带当前用户名和工号),通常得在反代层或者自己包一层页面来实现,把用户信息透传进去。这块我建议在需求阶段就和业务方确认清楚,因为动态水印的实现成本比静态水印高一个量级。
禁止下载的话,需要理解一个前提:只要文件内容能被渲染到用户屏幕上,就不可能做到严格意义上的防泄漏,截图、抓包、另存为,手段太多。KKFileView 提供的禁用下载主要是把页面上的下载按钮、右键菜单这些入口关掉,属于提高门槛而不是物理阻断。我在跟客户沟通的时候都会明确说清楚这一点,避免上线之后被质疑"不是说不让下载吗,怎么还能截图"。把预期管理好,比事后解释容易得多。
另外,如果预览的文件涉及不同密级,建议在业务层做过滤,也就是只有用户有权限的文件才允许拼出预览 URL。不要依赖预览服务本身做鉴权,因为它收到的只是一个文件地址,它并不知道谁在请求。
7. 一次"转圈转到天荒地老"的完整排查过程
讲一个真实案例,整个过程我觉得比我上面讲的所有配置都更有价值,因为排查思路是可以复用的。
现象是:系统上线后第三天,有用户反馈打开某个 Excel 一直转圈,等了两分钟也没出来,刷新之后依然如此。但同一个文件我本地点开是好的,其他文件基本正常。
第一步,先区分是链路问题还是内容问题。我让用户换成另一个 Excel 试,结果能打开。这说明服务本身活着,网络通,问题出在这个特定文件上。这一步的关键是不要一上来就重启服务,那会把现场破坏掉。
第二步,去看 KKFileView 的日志。日志里能看到这次请求进来了,下载文件成功,然后调用了转换,之后就没有下文了,也没有明显的异常堆栈。这种"走到转换就没声了"的状态,通常意味着转换进程卡住或者耗时极长。
第三步,确认是不是真的在转换。我登到服务器上执行top,看到一个 soffice 进程 CPU 占得很高,说明它确实在干活,只是干不完。这基本排除了"卡死"的可能,指向"文件本身让转换变慢"。
第四步,找到这个文件的特殊性。我把这个 Excel 下载下来,发现它有大概四十个工作表,里面有大量的公式引用、条件格式和若干外部链接。LibreOffice 在转换这类文件时需要重新计算和渲染,耗时呈指数上升。而且它里面有指向外部文件的数据链接,LibreOffice 可能会去尝试解析这些链接,进一步拖慢时间。
第五步,验证假设。我把文件里的一大半工作表删掉另存一份再传上去,转换几秒就完成了。假设成立。
第六步,给出可落地的方案。对这类文件我做了两件事:一是适当放宽转换超时时间,让它有机会跑完;二是给这个接口加了前端提示和超时兜底,超过一定时间就提示用户"文档较复杂,可下载后查看",同时后台继续转换,缓存建好之后下次就快了。另外提醒业务方,这类超大 Excel 更推荐用分页导出或者改成在线查询的方式呈现,而不是硬走预览。预览适合的是常规文档,不是把所有数据都塞在一个文件里的怪物。
这次排查给我留下的最大教训是:在线预览的性能问题,八成不在服务,在文件本身。所以在做压力测试的时候,别只用几个干净的样例文档,一定要把业务上最复杂、最脏的那几个真实文件拿出来跑一遍,那才是你系统真实的服务水平。
至于日常运维上,我现在养成了一个习惯,就是每周扫一遍缓存目录里体积最大的那些产物,看看是哪些文件。如果某个文件反复出现而且体积异常,往往就预示着下一次的性能问题,提前跟业务方打个招呼,比事后被人找上门舒服得多。