用户只问了一句:
“这个合同,能不能在线预览?”
产品经理听见的是一个按钮。
开发听见的是一个接口。
但架构图会在接下来的几天里,悄悄长出一整条链路:
文件先被上传或转发到转换服务,进入任务队列,变成 PDF 或图片,再写入缓存,最后回到浏览器。
用户只是想看一眼。
系统却先让文件绕了一大圈。
更麻烦的是,这个文件可能是一份未公开的合同、一张工程图纸、一份内部报价,或者一个根本不该离开内网的附件。
于是,一个看起来无害的“预览”按钮,突然多出了权限、并发、缓存、清理和数据边界。
预览最容易被忽略的成本,不是页面上多了多少代码,而是文件为了被看见,到底经过了多少地方。
2022 年,我正是因为这种别扭开始做 File Viewer。
当时我以为,这只是一个普通的文件组件。
四年后我才知道,我真正面对的从来不是“怎么显示一个 Word”,而是另一个更难的问题:
能不能在不轻易改变文件归属和可信边界的前提下,让用户直接看见它?
一个预览按钮,背后有四张账单
服务端转码当然不是错误答案。
很多时候,它甚至是最稳妥的答案。
但在决定使用它之前,至少应该看见这四张账单。
第一张,是文件副本
合同原本只存在于业务系统。
预览之后,它可能又出现在转换节点、对象存储、缓存目录和临时文件夹里。
每多一份副本,就多一套需要回答的问题:
- 谁可以访问?
- 保存多久?
- 删除是否真的生效?
- 日志里是否留下了可追踪信息?
- 转换失败后,临时文件还在不在?
很多系统有严密的原文件权限,却从来没有认真设计过“预览副本”的生命周期。
我们以为自己只增加了一个功能,实际上增加了一条新的数据流。
第二张,是运行时
Word、PDF、CAD 和压缩包,不是换几个扩展名那么简单。
它们背后可能是完全不同的解析器、字体、原生库、沙箱和失败方式。
新增一种文件,往往意味着新增一套依赖。
新增一套依赖,往往意味着新增一次版本升级、漏洞修复和生产事故的可能。
最难维护的从来不是那段调用转换接口的代码,而是接口背后的东西永远不能不管。
第三张,是等待
文件上传、排队、转换、写回。
其中任何一步变慢,用户看到的都只有同一句话:
“文件还在加载。”
他不知道队列堵了,不知道字体丢了,也不知道转换进程刚刚崩过一次。
他只知道,自己明明只是想看一眼合同,却等得比下载再打开还久。
第四张,是内网交付
很多 OA、合同系统和工程资料平台不能访问公网。
在线预览 SaaS 再方便,只要文件不能离开内网,它就失去了前提。
自建转换服务可以解决边界问题,但团队也要从此维护转换节点、队列、缓存、字体和并发。
所以,真正的问题从来不是:
“上传到底好不好?”
而是:
为了得到需要的还原度,这份文件最少要经过哪些边界?
我做了四年,才敢说:三条路线都可能是对的
文件预览大致有三条路线。
| 路线 | 得到什么 | 付出什么 | 更适合 |
|---|---|---|---|
| 第三方在线预览 | 最快接入,少维护转换运行时 | 文件进入第三方边界,能力和价格受服务约束 | 公开或低敏文件、快速验证 |
| 自建服务端转码 | 输出集中可控,适合统一生成 PDF、图片和缩略图 | 维护队列、缓存、字体、并发和转换服务 | 文件本就在服务器、批处理或高一致性输出 |
| 浏览器本地解析 | 本地 File/Blob 可以不上传第三方,交互直接 | 计算和内存转移到客户端,仍要交付 Worker/WASM | 内网、敏感附件、交互式预览 |
在线 SaaS 的价值,是用服务费换时间。
自建转码的价值,是用运维成本换集中控制。
浏览器本地解析的价值,是用客户端计算换一条更短的文件路径。
它们没有道德高地,也没有天然高下。
技术路线真正的优劣,只存在于具体约束里。
如果必须生成可归档、可签章、打印结果高度一致的 PDF,我会优先考虑服务端。
如果只是快速预览公开材料,成熟的在线服务往往最省事。
如果文件敏感、网络隔离,或者团队不想为每种文件长期养一套转换服务,浏览器解析才更有意义。
现实中还有很多系统采用混合路线:
常见文件直接在浏览器预览,特殊文件和批量任务回退到自建服务。
这不够“纯粹”。
但通常足够可靠。
而可靠,比技术口号重要。
我最后只坚持了三个决定
File Viewer 没有试图证明纯前端在任何场景都更好。
我只是把四年里反复出现的问题,收敛成了三个决定。
1. 不让业务层认识二十多套预览器
PDF、Word、DWG 和压缩包可以有不同的解析器,但不应该让业务代码各自维护一套文件来源、加载状态、销毁逻辑和操作权限。
URL、本地 File、Blob 或 ArrayBuffer 先进入同一套文件源契约,再由能力矩阵选择预览链路。
在 Vue 里,本地文件可以直接交给组件:
<file-viewer :file="contractFile" />
重点不是少写了几行代码。
重点是合同可以继续作为浏览器里的 File 被解析,不必为了调用第三方转换接口再上传一次。
2. 重型解析不能堵住页面
复杂 Office、CAD、压缩包和工程模型都可能吃掉大量 CPU 与内存。
把它们直接塞进主线程,只会让“能预览”变成“页面先卡死”。
所以,重型能力通过 Worker、WASM 或独立运行时按需加载。
只有真正命中文件时,对应的成本才进入页面。
这不会让计算凭空消失。
它只是让计算发生在更合适的位置。
3. 纯前端也必须完整交付
这是我踩过最多坑的一件事。
很多人听到“纯前端”,会下意识理解成:
装一个 npm 包,结束。
但 PDF、PPT、CAD 和工程模型可能依赖 Worker、WASM、字体与 vendor 文件。
漏掉其中任何一项,都可能出现同一种尴尬:
本地 Demo 正常,部署到 /oa/app/ 后一片空白。
所以这些资源必须和代码保持同版本,并能部署到自己的静态域、内网网关或 Docker 环境。
纯前端不是零部署,而是把部署对象从转换服务变成了可版本化的静态运行资源。
208 个扩展名,并不是我最想炫耀的数字
File Viewer v2.2.3 当前把 208 个已注册扩展名映射到 25 条预览链路。
矩阵条目不等于彼此独立的格式,更不代表完美还原。
每个扩展名都必须命中明确的解析器,并说明搜索、缩放、打印、导出和下载边界。
公开仓库目前接近 1800 Star。
但比 Star 更能让我兴奋的,是那些“不好看”的问题:
- 一个复杂 DOCX 为什么突然错位;
- 一份 PPTX 为什么少了图片和最后几页;
- DWG 的运行时为什么本地正常、内网失效;
- 上传后的 PDF 为什么预览正常,下载却变成 0 字节;
- 一个 Worker 为什么在站点根路径能跑,换到业务子路径就 404。
每一个问题,都来自一份真实文件或一次真实部署。
它们让我慢慢明白:
格式数量给人进步的错觉,失败样本才能逼出真正的进步。
能打开第一页,只能说明 Demo 成功了。
文件来源、资源版本、内存释放、失败降级和隐私边界都能说清楚,才接近生产可用。
纯前端不是银弹,我甚至经常劝人别用
有些场景,我不会建议为了“纯前端”而纯前端:
- 需要集中生成可归档、可签章或高度一致的 PDF;
- 客户端性能不可控,却要处理超大文件和高复杂度模型;
- 必须在服务端完成病毒扫描、内容审查或统一水印;
- 需要编辑能力,而不只是只读预览;
- 文件包含浏览器解析器尚未覆盖的厂商扩展或特殊字体。
File Viewer 是预览工具,不是 Office,也不是 CAD 编辑器。
复杂字体、特殊排版、厂商扩展和极端大文件,依然可能存在差异。
而且“浏览器解析”不等于文件从未经过网络。
如果输入是业务 URL,浏览器仍然要从你的业务系统下载文件;如果输入是本地 File 或已经拿到的 Blob,它才可以不为了预览再次上传。
边界讲得越清楚,方案才越可信。
一项技术真正成熟的标志,不是它敢说自己无所不能,而是它知道什么时候应该退出。
最后
四年过去了,用户的问题没有变:
“这个合同,能不能在线预览?”
但我的答案已经从一个简单的“能”,变成了三个问题:
- 文件能不能离开现在的可信边界?
- 业务到底需要多高的还原度?
- 计算和运维成本,应该由客户端还是服务器承担?
回答完这三个问题,技术路线通常就不会太离谱。
我做 File Viewer,不是为了证明所有文件都应该留在浏览器。
我只是希望,在默认上传、默认转码、默认多复制一份文件之前,我们还有一个可交付的选择。
如果你也在设计文件预览链路,不妨先拿一份不涉密或已经脱敏的真实文件,把上传、缓存、运行时和失败降级逐项画出来。很多架构争论,到这一步就会变得具体。
文中提到的实现、格式矩阵和部署边界记录在开源仓库中:flyfish-dev/file-viewer。