我是 File Viewer 的作者。这个项目做的事情很单一:在浏览器里直接打开各种附件,不经过服务端转码。目前格式目录里注册了 275 个扩展名、47 条预览链路,最新版本是 3.2.2(2026-10-18 发布)。
这篇不讲"怎么用"为主,主要聊聊这么多格式在纯前端是怎么组织起来的,哪些地方比较难,以及它目前做不到什么。
一、为什么要自己做
做后台、OA、工单系统时,"附件点一下就能看"几乎是必选需求。我之前常见的几种做法:
- 服务端转码:kkFileView、LibreOffice 转 PDF/HTML。效果稳定,但要多维护一套服务,文件要先上传到服务器,大文件转码还要排队。
- Office Online 等 iframe 方案:需要文件能被公网访问,内网和私有化项目基本用不了。
- 每种格式找一个库:pdf.js 看 PDF,docx-preview 看 Word,SheetJS 看 Excel……每个库的 API、样式、工具栏都不一样,搜索、缩放、打印要各写一遍,PPT、CAD 这类格式还经常找不到合适的库。
我想要的是第三种方案的"纯前端",加上一个统一的外壳:同一套文件源、生命周期、搜索、缩放、打印、导出和下载接口,格式只是可插拔的实现细节。
二、整体架构:core + renderer + preset
项目是一个 monorepo,分四层:
@file-viewer/core:格式识别、文件源归一化(URL / File / Blob)、渲染器调度、生命周期和统一工具栏能力。它本身不包含任何重型解析器。@file-viewer/renderer-*:每类格式一个独立包,比如renderer-word、renderer-pdf、renderer-cad、renderer-3d、renderer-archive。@file-viewer/preset-*:把 renderer 组合起来,有lite、standard、office、engineering、all几档。- 组件包:Vue 3 / Vue 2.7 / Vue 2.6、React、Svelte、jQuery、Web Component,每个都有轻量版和
-full版。
renderer 的契约大致是这样(节选自 core 的类型定义):
export interface RendererDefinition {
id: string
label: string
category: FileViewerRendererCategory
extensions: readonly string[]
/** 可选的内容探测:扩展名不可信时,按内容决定交给谁 */
resolveSourceType?: RendererSourceTypeProbe
/** 尽量把原始 URL 交给渲染器,而不是先整体下载 */
sourceAccess?: 'buffer' | 'stream-preferred'
supportLevel?: 'high-fidelity' | 'structured' | 'basic' | 'experimental'
status?: 'stable' | 'experimental'
load?: RendererLoader
}
关键在 load:它是一个异步函数,只有命中这个格式时才会去 import() 真正的解析代码、Worker 和 WASM。所以只装 preset-office 的项目不会把 CAD、3D 的代码打进来,用了 preset-all 首屏也不会因为 3D 引擎变慢。
格式数量不是手写在文档里的。仓库里有一份 ecosystem/format-catalog.json,作为唯一的格式事实源,记录每条链路的扩展名、所属包、支持级别(高还原 / 结构化 / 基础 / 实验)和已知限制。运行时注册表、文档里的格式矩阵和统计数字都由它生成。275 这个数就是从这里数出来的:其中 225 个稳定、50 个实验。
resolveSourceType 是后来加的。真实业务里经常遇到扩展名是 .docx、内容其实是老版 OLE 二进制 Word 的文件,只看扩展名会直接报错。现在渲染器可以做有上限的内容嗅探,再决定交给哪条链路。
三、几个难点
Office:自己写 DOCX 引擎
Word 这部分我最后决定自研,单独发布为 @file-viewer/docx。DOCX 默认在 Worker 里解析,主线程只负责异步分批渲染,长文档滚动时不会一次性塞进几千个节点。PPTX 也是 Worker 解析、按页渐进输出。Excel 基于 styled-exceljs 和 e-virt-table,表格用虚拟渲染;超过 1MB 的文件会自动尝试走 Worker。CSV 会自动识别 UTF-8 / GBK / GB18030,这是国内用户提得最多的问题之一。
老版二进制 .ppt 是单独的可选包,解析跑在 WASM 里,不进默认配置。
PDF 和 OFD
PDF 基于 PDF.js(当前锁定 5.4 稳定线)。服务端支持 Range 时会自动分片加载,大 PDF 不需要下载完才显示第一页。比较费事的是中文字体:很多 PDF 引用了微软雅黑、宋体却没有嵌入字体,渲染器会先找本机字体,找不到再用离线回退字体。
OFD 是国内电子发票和公文常用的版式格式,这里基于 DLTech21/ofd.js 的纯前端解析链路,按需加载。
CAD:DWG 是二进制格式
DWG 没有公开规范,好在有 LibreDWG。CAD 渲染器把 LibreDWG 编译成 WASM,放在 Worker 里解析,避免卡主线程;DXF 用 JavaScript parser;DWF / DWFx / XPS 走另一个 native renderer 的 WASM。这几部分归一化成统一的 CAD 文档模型,再交给同一个视图层。
3D:从 glTF 到 STEP
网格类格式(GLB / glTF、OBJ、STL、FBX、3MF、USD 等)用 Three.js 加按格式异步加载的 loader。STEP、IGES、BREP 这类工业格式是精确几何,Three.js 不能直接读,所以单独起了一个 OCCT 的 Worker,把 B-Rep 在本地三角化后再渲染。IFC 和 GIS 类 3D 数据做成了显式子入口,不装就不会加载。
压缩包:递归预览
压缩包用 libarchive.js 的 Worker + WASM 读取 RAR、7z、ZIP、TAR 等。打开时只读目录,点击内部某个文件才解压这一个条目,然后交回 core 再走一遍格式识别。所以压缩包里的 docx、pdf、dwg 都能直接打开。这里加了总体积上限、单文件上限、Worker 超时和 IndexedDB 缓存,防止把整个包展开到内存。老 ZIP 里的 GBK 中文文件名也做了兼容。
二进制检查器
这是给开发者用的可选包:在 Worker 里解析 ELF、PE/COFF、Mach-O、PNG、ZIP、WASM、Java class 的文件头,展示偏移量、十六进制、ASCII 列和字段树,字段和字节可以双向选中。默认整文件上限 16 MiB,解析时间、节点数、层级都有上限,UI 只渲染可见的行。
四、性能上的几个原则
总结下来就三条:
- 重活放 Worker:DOCX、PPTX、大表格、DWG、压缩包、STEP、二进制解析都在 Worker 里。
- 按需加载:renderer 命中才加载,Worker、WASM、字体是独立的静态资源。
- 能流式就不整读:PDF 走 Range,表格和 hex 视图虚拟滚动,压缩包按条目解压。
这些静态资源默认全部自托管,不依赖公网 CDN,内网、Docker、离线环境都能部署。代价是接入时要多一步"复制资源",下面会讲。
五、接入示例
Vue 3(Full 包):
import { createApp } from 'vue'
import App from './App.vue'
import FileViewer from '@file-viewer/vue3-full'
createApp(App).use(FileViewer).mount('#app')
<file-viewer url="/files/demo.pdf" :options="{ theme: 'light' }" />
React:
import FileViewer from '@file-viewer/react-full'
export function Preview() {
return (
<div style={{ height: '100vh' }}>
<FileViewer url="/files/demo.pdf" options={{ theme: 'light' }} />
</div>
)
}
Svelte:
<script>
import FileViewer from '@file-viewer/svelte-full'
</script>
<FileViewer url="/files/report.pdf" containerStyle="height: 720px;" />
Web Component(不依赖框架):
import { defineFileViewerElement } from '@file-viewer/web-full'
defineFileViewerElement()
<flyfish-file-viewer src="/files/demo.pdf" style="display:block;height:720px"></flyfish-file-viewer>
别漏掉资源复制。 -full 包带有同版本的 Worker、WASM 和字体,需要和应用一起发布:
# Vite 项目
npm i -D @file-viewer/vite-plugin
# vite.config.ts 中:plugins: [fileViewerRenderers({ copyAssets: true })]
# Webpack / Rspack / Vue CLI 等
npx --no-install file-viewer-copy-assets ./public/file-viewer
新项目也可以直接用 CLI 生成示例:npm create file-viewer@latest my-viewer。
如果只需要 Office,用轻量组件加 @file-viewer/preset-office 即可,不必上 Full 包。
不想在项目里装 npm 包的话,Release 里还有一个离线 iframe 交付包,部署到静态目录后直接嵌入;父页面可以用 postMessage(Blob) 把文件传进去,适合老系统或者非前端工程的场景。
六、局限
这部分我想说清楚,免得大家踩坑:
- 只读预览,不是编辑器,也不打算替代专业的 Office、CAD 软件。
- 注册不等于高还原。 275 个扩展名里有 50 个是实验性的,比如 Adobe 设计文件、DICOM、签名容器。即使是稳定格式,字体缺失、厂商私有特性、复杂图表也会影响效果。比如 PPTX 图表在特定字体下的标签、图例位置,还有一个问题没解决(#385)。上线前请对照格式矩阵,用自己业务里的真实文件测一遍。
- 体积不小。
@file-viewer/web-full@3.2.2解压后约 145 MB,大头是 Typst 编译器 WASM 和中文字体。实际只会按需加载,但部署目录和安装体积确实大,在意的话请用轻量包按需组合。 - 大文件受浏览器内存限制。 纯前端意味着解析发生在用户的设备上,几百 MB 的文件在低端手机 WebView 里可能打不开,这一点服务端转码方案更有优势。
- CAD 运行时协议不同。 项目源码是 Apache-2.0,但 DWG / DWF / DWFX 运行时(
@flyfish-dev/cad-viewer、dwf-viewer)是 AGPL-3.0-only,商用启用 CAD 前请评估。 - 出于安全考虑,文档里的宏和 VBA 一律不执行,DOCX 内嵌的 HTML 部件也在沙箱里渲染。
七、最后
项目从 2.x 一路做到 3.2.2,现在 GitHub 上有 2.5k 左右的 Star,@file-viewer/core 在 npm 上每周有两万次左右的下载。
- 在线体验(文件只在浏览器里处理,不会上传):demo.file-viewer.app
- 官网:file-viewer.app
- 文档:doc.file-viewer.app
- GitHub:github.com/flyfish-dev…
比起 Star,我更希望收到真实场景下的兼容性反馈:哪个文件打不开、哪里还原得不对,附上脱敏样例提个 issue,对我帮助最大。如果这个项目对你有用,也欢迎顺手点个 Star,谢谢。
也想听听大家:你们项目里的文件预览现在是怎么做的?