在线压缩和在线解压听起来像服务端任务:上传文件、服务器打包、再下载结果。但如果目标格式是 ZIP,现代浏览器已经足够完成大部分日常场景。文件可以留在用户设备上,页面只负责读取本地文件、运行压缩算法、生成下载链接。
ToolGarden 的 ZIP 压缩和 ZIP 解压工具就是这个思路:压缩页把多个 File 对象转成 ZIP Blob;解压页读取 ZIP 字节,解析出条目,按路径还原目录树,并给每个文件生成单独下载。整个过程没有 API route 接收文件内容。
整体数据流
压缩:
File input / drag drop
-> File API 读取 Blob
-> 规范化 ZIP 内路径
-> fflate zipSync
-> Blob(application/zip)
-> URL.createObjectURL 下载
解压:
ZIP File
-> arrayBuffer
-> 读取中央目录修正文件名编码
-> fflate unzipSync
-> 过滤不安全路径
-> 构建目录树
-> 每个文件生成 Blob 下载这个数据流里最关键的边界是:页面可以读取用户通过文件选择器授权的本地文件,但不会把这些字节发送给服务器。服务端只提供 JavaScript、CSS 和静态资源,真正的处理发生在浏览器进程中。
为什么选择 ZIP,而不是 RAR 或 7z?
ZIP 的创建和解压都有成熟的 JavaScript 实现,系统兼容性也最好。用户下载后的 .zip 可以被 Windows、macOS、Linux 和常见手机系统直接打开。对在线工具来说,这是最稳的默认格式。
RAR 和 7z 的情况不同。它们的解压生态可以评估,但创建端并不像 ZIP 那样开放、轻量、浏览器友好。为了避免功能看似支持、实际兼容性脆弱,压缩工具只生成 ZIP,解压工具也先聚焦 ZIP。
压缩:从多个 File 生成 ZIP Blob
压缩端页面只做 UI 状态:用户选择文件、设置输出文件名、选择压缩等级、点击按钮。真正的处理放在 lib/utils/zip.ts 中,函数接收一组 filename + blob,返回判别联合类型,而不是直接操作 React state。
type ZipCompressionOutcome =
| {
ok: true;
blob: Blob;
filename: string;
fileCount: number;
originalSize: number;
outputSize: number;
durationMs: number;
}
| { ok: false; code: 'empty_selection' | 'empty_file' | 'zip_failed' };压缩前需要处理 ZIP 内路径。拖入文件夹时,浏览器可能提供 webkitRelativePath;普通多文件选择只有 file.name。路径要统一替换反斜杠、过滤空片段、过滤 . 和 ..,重复文件名要加后缀,避免后一个文件覆盖前一个文件。
压缩算法使用 fflate 的 zipSync。压缩等级 0 到 9,0 更像“只打包不压缩”,9 通常体积更小但耗时更长。默认 6 是比较合理的速度和体积平衡。
解压:先得到文件条目,再构建目录树
解压端不是把所有文件平铺成列表,而是把 ZIP 内部路径拆成目录树。例如 docs/readme.txt 和 docs/assets/logo.png 会共享 docs 目录,assets 是 docs 的子目录。UI 里目录行可以展开折叠,文件行显示大小和下载按钮。
interface ZipExtractedEntry {
path: string;
name: string;
size: number;
blob: Blob;
}
interface TreeNode {
name: string;
path: string;
children: Map<string, TreeNode>;
entry?: ZipExtractedEntry;
}目录树最好在页面层用纯数据结构生成,避免把展示状态和解压逻辑搅在一起。解压函数只返回 entries;React 组件再根据 entries 构建 TreeNode,并维护 expanded 这类交互状态。
中文文件名为什么会乱码?
很多 ZIP 文件名是 UTF-8,但并不是所有 ZIP 都会设置 UTF-8 标志。国内常见压缩软件生成的旧 ZIP,文件名可能实际是 GBK 或 GB18030 字节。如果解压库按 Latin-1 或错误编码解释这些字节,中文名就会变成乱码。
解决办法是读取 ZIP 中央目录里的原始文件名字节:如果条目没有 UTF-8 filename flag,就把这些字节用 GB18030 解码,再映射回解压库返回的 Latin-1 名称。这样既不破坏标准 UTF-8 ZIP,也能兼容中文 Windows 环境里常见的旧压缩包。
下载:用 Blob URL,而不是服务器文件
无论压缩还是解压,下载都可以用 Blob URL。压缩结果是 application/zip Blob;解压后的每个文件是 application/octet-stream Blob。页面调用 URL.createObjectURL,放到 a 标签的 href 上,再设置 download 文件名即可。
Blob URL 是临时资源,页面关闭后会消失。长期运行的工具页还要在清空结果或组件卸载时调用 URL.revokeObjectURL,避免用户连续处理多个大文件时堆积内存。
必须处理的安全和稳定性边界
- Zip Slip:ZIP 条目路径里可能包含 ../,展示和下载前必须过滤上级目录片段。
- zip bomb:压缩包体积很小但解压后极大,浏览器端应提示大文件风险,并在后续版本加入数量和总大小上限。
- 加密 ZIP:密码交互和兼容性复杂,当前工具应明确提示不支持,而不是吞掉错误。
- 内存压力:几百 MB 的 ZIP 或大量文件会占用浏览器内存,应优先保持 UI 可恢复、错误可理解。
- 文件名冲突:压缩时同名文件必须自动重命名,解压下载时至少要保留基础文件名。
工程分层怎么放?
在 ToolGarden 里,ZIP 能力遵循 Harness Engineering:工具元数据进 registry,文案进 messages,ZIP 算法进 lib/utils/zip.ts,页面组件只处理 useState、文件选择、目录展开和下载点击。这样首页、Other tools 菜单、面包屑、SEO、sitemap 和 llms 文件都能从同一套元数据自动派生。