# 用 Nuxt 与 OPFS 搭出的本地优先 PDF 工具箱

> 一篇 dev.to 报告介绍 beePDF：一个把 PDF 处理留在浏览器里的纯客户端 PWA，文件字节交给 OPFS，元数据交给 IndexedDB。

2026-10-06，dev.to 账号 eoinmcg 发表了一篇关于 beePDF 的建设报告。报告把 beePDF 描述为一个
100% 纯客户端的 Progressive Web App，文档处理完全在浏览器中发生，文档从不被上传到远端服务器。
报告给出了支撑这一说法的架构：origin private file system 保存文档字节，IndexedDB 保存元数据
与缩略图，而处理工作跑在 Web Worker 里
([该报告](https://dev.to/eoinmcg/why-i-built-a-local-first-pdf-toolkit-using-nuxt-and-opfs-54k3))。

## 报告的出发点是什么问题

报告的出发点关乎信任，而不是吞吐。它写明主流工具（它点名 Smallpdf 与 ILovePDF）要求用户把敏感
文档上传到它们的远端服务器；并写明即使服务使用传输中加密、并承诺在限定期限后删除文件，把机密
文档上传到远端服务器仍然意味着把这些文档托付给一个第三方服务。它自己对这一区别的总结是：传输
中加密保护的是传输过程，并不消除去信任那个接收并处理该文档的系统的必要。它补充说，取决于文件
类型与使用场景（它给出的例子是法律与医疗），出于隐私、保密或监管方面的要求，把它们上传到第三方
服务可能根本不是一个选项
([该报告](https://dev.to/eoinmcg/why-i-built-a-local-first-pdf-toolkit-using-nuxt-and-opfs-54k3))。

### 报告列出的技术栈

报告以清单形式给出技术栈。框架是 Nuxt，它标注为 Vue 4、TypeScript、Vite。PWA 与离线支持来自
`@vite-pwa/nuxt` 加 Service Worker。内存中的文件处理使用 `pdf-lib`、`PDF.js` 以及原生 Canvas
API。本地持久化是 origin private file system，报告通过 `navigator.storage.getDirectory()`
访问它。样式与 UI 是 Pico CSS
([该报告](https://dev.to/eoinmcg/why-i-built-a-local-first-pdf-toolkit-using-nuxt-and-opfs-54k3))。

MDN 记录了这份清单点名的那个入口。MDN 把 `StorageManager.getDirectory()` 描述为用于取得一个
`FileSystemDirectoryHandle` 对象引用的方法，该对象允许访问存放在 origin private file system
中的某个目录及其内容，并写明它返回一个会以该 handle 兑现的 promise。MDN 还记录了该调用会以两
种方式失败：当浏览器无法把所请求的目录映射到本地 OPFS 时（它给出的例子是存储或内存约束）抛出
`SecurityError`，某些浏览器在隐私浏览模式下调用 `getDirectory()` 时也会抛出它；以及某些浏览器
在隐私浏览模式下抛出的 `UnknownError`
([MDN，StorageManager.getDirectory()](https://developer.mozilla.org/en-US/docs/Web/API/StorageManager/getDirectory))。

### 报告为什么否掉了其他存储选项

报告逐个排除了其他方案。报告写明 LocalStorage 被限制在约 5MB 到 10MB，是同步且仅限文本的存储，
它称之为对二进制 PDF 完全无用。它写明 IndexedDB 对结构化元数据与较小的二进制对象非常合适，但
在把大文档当作具备高效字节级访问的持久文件来对待这件事上不够自然。它写明 File System Access
API 在应用需要直接操作用户文件系统中的文件时很好用，但访问需要用户授权，并且受到浏览器安全
限制的约束
([该报告](https://dev.to/eoinmcg/why-i-built-a-local-first-pdf-toolkit-using-nuxt-and-opfs-54k3))。

MDN 自己关于 origin private file system 为何存在的叙述，与上述第三条反对意见贴得很近。MDN 把
File System Access API 描述为通过选择器方法提供文件访问，并写明由于这些改动是针对用户可见的
文件系统作出的，因此有大量安全检查在位（它给出的例子是 Chrome 中的 safe browsing），以防恶意
内容被写入该文件系统。MDN 写明这些写入不是就地写入，而是使用一个临时文件，原文件在通过全部
安全检查之前不会被修改；并写明其结果是这些操作相当慢，在作出更显著的大规模文件更新（例如
SQLite 数据库修改）时性能会受损
([MDN，Origin private file system](https://developer.mozilla.org/en-US/docs/Web/API/File_System_API/Origin_private_file_system))。

## 报告说 OPFS 给了它什么

报告把 origin private file system 描述为提供一个私有的、以源为作用域的文件系统，专为持久的
类文件数据设计，具备低层字节访问与经过优化的就地写入；并写明它还在 Web Worker 内部提供同步
访问 handle，面向对性能敏感的工作负载。它的代码示例用
`await navigator.storage.getDirectory()` 取到根目录，用
`root.getFileHandle("working-draft.pdf", { create: true })` 取到文件 handle，再通过
`createWritable()`、`write()` 与 `close()` 写入一个二进制缓冲区
([该报告](https://dev.to/eoinmcg/why-i-built-a-local-first-pdf-toolkit-using-nuxt-and-opfs-54k3))。

随后报告点名了它认为 PDF 处理为何重要的那些属性。第一是零权限提示：报告写明由于该目录
对 `https://beepdf.app` 是私有的，浏览器不会用权限弹窗轰炸用户。第二是磁盘读写速度：报告写明
OPFS 提供直接的字节级文件流，并写明在 Web Worker 中可以用 `createSyncAccessHandle()` 获得接近
原生的同步文件读写操作。第三是配额与清理：报告写明文件会跨浏览器重新加载而留存，所以不小心关掉
标签页也不会丢掉正在处理的文档，但它们会自动遵守浏览器的源存储配额
([该报告](https://dev.to/eoinmcg/why-i-built-a-local-first-pdf-toolkit-using-nuxt-and-opfs-54k3))。

MDN 用它自己的措辞为这些属性各自提供了依据。关于权限，MDN 写明访问 OPFS 中的文件不需要权限
提示与安全检查，并写明 OPFS 对页面的源是私有的，不像常规文件系统那样对用户可见。关于速度，MDN
写明 OPFS 提供低层的、逐字节的文件访问，它不需要同一串安全检查与权限授予，因此比 File System
Access API 的调用更快；并写明它提供一组同步调用（File System API 的其他调用是异步的），这些
调用只能在 web worker 内部运行，以免阻塞主线程。关于配额，MDN 写明 OPFS 与任何其他按源分区的
存储机制一样受浏览器存储配额限制，它占用的空间量可以通过 `navigator.storage.estimate()` 读取，
并写明清除该站点的存储数据会删除 OPFS
([MDN，Origin private file system](https://developer.mozilla.org/en-US/docs/Web/API/File_System_API/Origin_private_file_system))。

MDN 还就同步 handle 补上了一条报告措辞里留作隐含的约束。MDN 写明 `FileSystemSyncAccessHandle`
只能在专用（dedicated）Web Worker 内部访问，以使其方法不会阻塞主线程上的执行，并且只针对
origin private file system 内的文件；并写明其结果是它的方法不受运行在用户可见文件系统文件上的
方法所受的同一批安全检查约束，因此性能高得多。MDN 还提示：尽管名字里有 "Sync"，
`createSyncAccessHandle()` 本身是异步的；并写明 `close()` 会释放先前施加在该 handle 所关联文件
上的独占锁
([MDN，FileSystemSyncAccessHandle](https://developer.mozilla.org/en-US/docs/Web/API/FileSystemSyncAccessHandle))。

### 报告描述的处理流水线

报告把一个拖入的 PDF 走过它点名的各个阶段。本地摄入：浏览器把 `File` 对象读入内存中的一个
`ArrayBuffer`。OPFS：文档立即被存入本地 OPFS 工作区目录。元数据与一张很小的缩略图被存入
IndexedDB，报告说这也让搜索与打标签变得轻而易举。Web Worker 卸载：报告点名的几类任务（渲染页面
缩略图、提取纯文本或 Markdown、运行词级 diff）被卸载到 Web Worker，以让 UI 保持在 60 FPS。
客户端导出：修改或签署文档会在内存中生成一个新的 `Uint8Array`，并通过一个
`URL.createObjectURL` Blob 触发直接的客户端下载。报告还描述了一次飞行模式测试，写明可以先加载
beePDF，再把连接完全断开，然后在离线状态下合并、拆分、压缩或签署文档
([该报告](https://dev.to/eoinmcg/why-i-built-a-local-first-pdf-toolkit-using-nuxt-and-opfs-54k3))。

MDN 对报告分配给 IndexedDB 的那个角色的描述，与报告给出的分工吻合。MDN 把 IndexedDB 描述为一
套用于在客户端存储大量结构化数据（包括文件与 blob）的低层 API，写明它用索引来实现对这些数据的
高性能检索，并说 Web Storage 在存储较少量数据时有用，但在存储较大量结构化数据时就不那么有用。
MDN 写明使用 IndexedDB 执行的操作是异步完成的，以免阻塞应用；并写明 IndexedDB 在 web worker
中可用
([MDN，IndexedDB API](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API))。

### 报告报出的限制

报告点名了它的限制。它写明在浏览器 JS 内处理体量巨大的 500MB+ 扫描版 PDF，在低端设备上可能撞上
V8 堆内存上限。它写明重度的图像压缩或 canvas 操作必须卸载到 Web Worker，否则渲染高 DPI canvas
会冻结 UI 线程。它对两者的定调是：现代浏览器提供了一套相当丰富的能力，可以通过标准化的 Web
API 访问，但构建纯客户端的文件工具并非没有它的限制
([该报告](https://dev.to/eoinmcg/why-i-built-a-local-first-pdf-toolkit-using-nuxt-and-opfs-54k3))。

## 对 PWA 开发者为什么重要

报告的架构把一条隐私要求变成了一个存储决策，而这个决策是对照平台文档所写明的属性作出的，而不是
对照某种框架偏好。否掉 File System Access API 并不是出于人体工学：MDN 写明它的写入要针对用户
可见的文件系统过安全检查、使用临时文件而非就地写入，因此在大规模更新上相当慢。这正是 OPFS 反转
的同一组属性，MDN 把它们总结为逐字节访问、不需要权限提示与安全检查，以及仅限 web worker 的同步
调用
([MDN，Origin private file system](https://developer.mozilla.org/en-US/docs/Web/API/File_System_API/Origin_private_file_system))。

报告在两种客户端存储之间划出的分工，是其中可复用的部分。文档字节进 OPFS，MDN 在那里记录了字节
级读写以及一个可在专用 worker 中使用的同步 handle；元数据与缩略图进 IndexedDB，MDN 把它记录为
对结构化数据的索引式存储，并支持在其上的高性能检索。一个既需要大块不透明 blob、又需要关于它们的
可查询记录的应用，对这两件不同的事有两件分别有文档可依的工具，而报告给每一件分配的正是其文档所
描述的那件事
([MDN，IndexedDB API](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API))。

持久性是需要当心的那一部分。MDN 写明 OPFS 与任何其他按源分区的存储机制一样受浏览器存储配额限制，
并写明清除该站点的存储数据会删除 OPFS。
MDN 还写明浏览器会把 OPFS 的内容持久化到磁盘上的某处，但不能指望所创建的文件与之一一对应，因为
OPFS 本就不打算对用户可见
([MDN，Origin private file system](https://developer.mozilla.org/en-US/docs/Web/API/File_System_API/Origin_private_file_system))。

## 站内相关条目

- [Storage](/zh/reference/storage/)：报告的核心决策就是由哪一种客户端存储保存文档字节，而这一页
  正是 OpenPWA 记录以源为作用域的存储机制、它们的配额以及逐出行为的地方。
- [Capabilities](/zh/reference/capabilities/)：origin private file system 以及 MDN 限制在专用
  worker 内使用的同步访问 handle，都是各有自己支持情况的平台能力，而这一页是 OpenPWA 记录 PWA
  可以动用什么的地方。
- [Performance](/zh/reference/performance/)：报告写明的目标是把缩略图渲染、文本提取与 diff 移出
  主线程，从而把 UI 维持在 60 FPS，而这一页正是记录这笔预算的地方。

## 待核实与存疑之处

- beePDF 在哪些浏览器与版本上做过测试，报告并未说明，因此关于速度的说法与那次飞行模式测试都无法
  钉到某个引擎上。
- 报告里的同步访问 handle 是否运行在专用 worker 中，并未说明；MDN 把
  `FileSystemSyncAccessHandle` 限制在专用 Web Worker 内。
- beePDF 是否申请了持久化存储，以及由此在配额压力下一份 OPFS 工作文档会发生什么，报告未涉及。
- 报告点名的 V8 堆上限会在多大的文件、哪些设备上被触及，除了它写的 "500MB+ 扫描版 PDF" 与
  "低端设备" 之外没有更多说明。
- 报告为作对比而给出的约 5MB 到 10MB 的 LocalStorage 数字，在任何特定浏览器上是否成立，未经确认。
- 隐私浏览模式（MDN 写明某些浏览器在该模式下会让 `getDirectory()` 抛错）是否测过，未被涉及。

## 参考来源

- [Why I built a local-first PDF toolkit using Nuxt and OPFS](https://dev.to/eoinmcg/why-i-built-a-local-first-pdf-toolkit-using-nuxt-and-opfs-54k3)（dev.to）
- [Origin private file system](https://developer.mozilla.org/en-US/docs/Web/API/File_System_API/Origin_private_file_system)（developer.mozilla.org）
- [FileSystemSyncAccessHandle](https://developer.mozilla.org/en-US/docs/Web/API/FileSystemSyncAccessHandle)（developer.mozilla.org）
- [StorageManager: getDirectory() method](https://developer.mozilla.org/en-US/docs/Web/API/StorageManager/getDirectory)（developer.mozilla.org）
- [IndexedDB API](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API)（developer.mozilla.org）