javascript技巧

关注公众号 jb51net

关闭
首页 > 网络编程 > JavaScript > javascript技巧 > js文件夹压缩与解压缩

JavaScript实现文件夹压缩与解压缩完整攻略

作者:Fisch FLeisch

本文详细介绍了如何使用JavaScript在浏览器环境中实现文件和文件夹的压缩与解压缩,通过结合HTML5的FileAPI和BlobAPI,以及Zip.js和JSZip库,可以完成从用户选择文件到生成ZIP文件的压缩流程,本文给大家介绍的非常详细,感兴趣的朋友跟随小编一起看看吧

简介:在浏览器环境中,JavaScript无法直接访问文件系统,但借助HTML5的File API和Blob API,结合Zip.js、JSZip等库,可实现文件或文件夹的压缩与解压缩。本文详细讲解如何通过用户文件选择、读取ArrayBuffer、创建Zip对象、添加文件、生成下载链接等步骤完成压缩;以及如何加载ZIP文件、遍历内容、提取数据实现解压。适用于前端文件处理场景,提升Web应用交互能力。

1. JavaScript浏览器环境文件操作限制

JavaScript作为一种主要运行于浏览器端的脚本语言,在早期设计中出于安全考虑对本地文件系统的访问能力进行了严格限制。在传统的Web开发模型中,JavaScript无法直接读取、写入或遍历用户设备上的任意文件和目录,这种沙箱机制有效防止了恶意代码对用户数据的非法访问。

尽管如此,随着HTML5标准的发展, File API Blob API 等新型接口逐步提供了在用户授权前提下进行有限文件操作的能力。所有文件访问必须由用户主动触发(如通过 <input type="file"> ),且操作范围被限定在所选文件的临时引用内,不能直接访问路径或持久化存储。这一安全边界确保了前端文件处理的安全性与可控性,也为后续API的合法使用奠定了基础。

2. HTML5 File API与Blob API基础应用

随着现代Web应用对文件处理能力的需求不断增长,浏览器环境下的前端代码已不再局限于简单的表单提交或图片预览。在文档编辑器、离线归档系统、云盘客户端等复杂场景中,开发者需要能够直接操作用户选择的本地文件,并对其进行读取、转换、压缩甚至生成新文件。为此,HTML5引入了 File API Blob API 作为标准接口,使得JavaScript可以在安全沙箱内以可控方式访问用户授权的文件内容。

这两套API构成了现代浏览器中文件交互的核心基础设施。它们并非赋予脚本任意访问硬盘的权利,而是建立在“用户主动触发”和“上下文隔离”的原则之上——所有文件操作都必须源自用户的明确行为(如点击上传按钮),且只能处理由用户显式选择的文件对象。这种设计既满足了功能需求,又保障了安全性。

更重要的是,File API 与 Blob API 的结合为前端实现诸如大文件分片上传、客户端侧图像裁剪、PDF合并、视频元数据提取等功能提供了底层支持。理解其核心对象、数据结构以及异步处理机制,是构建高性能、高可靠性的文件处理逻辑的前提。

2.1 File API的核心对象与使用场景

File API 是 HTML5 中用于处理用户选择文件的标准接口集合,它定义了一组关键对象,包括 File FileList 和通过 <input type="file"> 触发的文件选择机制。这些组件共同构成了一条从用户界面到数据处理的完整通路,使开发者能够在不依赖服务器的情况下获取并解析本地文件的基本信息和原始字节流。

该API的设计哲学强调 用户控制权优先 。任何文件访问都必须经过用户主动操作(例如点击“选择文件”按钮),浏览器不会允许脚本静默读取设备上的任意路径。这不仅符合隐私保护规范,也避免了潜在的安全风险。与此同时,File API 提供了足够丰富的元数据和访问方法,足以支撑大多数前端文件处理任务。

为了深入掌握这一机制,我们需要逐一剖析其核心组成部分: File 对象如何封装文件元信息与内容引用; FileList 如何管理多选文件集合;以及如何通过标准HTML控件驱动整个流程。

2.1.1 File对象的获取与属性解析

File 对象是 File API 的基本单元,代表一个用户选择的文件实例。它是 Blob 的子类型,因此继承了所有 Blob 的特性(如 .size .type .slice() 方法),同时额外包含了文件名( .name )和最后修改时间( .lastModified )两个关键属性。

当用户通过 <input type="file"> 控件选择文件后,浏览器会创建一个或多个 File 实例,并将其组织成 FileList 集合供脚本访问。每一个 File 实例都包含以下主要属性:

属性名类型描述
name string文件的原始名称(不含路径,出于安全考虑路径被屏蔽)
size number文件大小,单位为字节
type stringMIME 类型,如 image/jpeg text/plain ,若无法识别则为空字符串
lastModified number最后修改时间的时间戳(毫秒)
webkitRelativePath string若从目录选择中获取,表示相对于根目录的路径

下面是一个典型的 File 对象示例:

// 假设用户选择了名为 "report.pdf" 的文件
const file = document.querySelector('input[type="file"]').files[0];
console.log(file.name);           // "report.pdf"
console.log(file.size);           // 1048576 (1MB)
console.log(file.type);           // "application/pdf"
console.log(file.lastModified);   // 1700000000000
console.log(file.webkitRelativePath); // "" (普通文件选择)

值得注意的是,尽管 File 继承自 Blob ,但它并不立即加载文件内容。它的作用更像是一个“句柄”或“引用”,指向实际存储在用户设备上的文件资源。真正的内容读取需借助 FileReader URL.createObjectURL() 等异步手段完成。

此外,由于浏览器出于安全考虑屏蔽了完整的文件路径, file.name 只返回文件名本身。例如,即使用户从 C:\Users\Alice\Documents\photo.png 选择文件, name 仍仅为 "photo.png" 。这一点对于后续构建归档结构时尤为重要——若要保留目录层级,必须依赖其他机制(如目录选择API)获取相对路径。

获取File对象的技术路径

最常见的方式是监听 <input type="file"> change 事件:

<input type="file" id="fileInput" multiple />
<script>
document.getElementById('fileInput').addEventListener('change', function(event) {
    const files = event.target.files; // FileList 对象
    for (let i = 0; i < files.length; i++) {
        const file = files[i];
        console.log(`文件 ${i + 1}:`, file.name, file.size, file.type);
    }
});
</script>

上述代码展示了如何通过事件监听获取 File 对象列表,并遍历输出每个文件的元数据。这是绝大多数文件上传功能的起点。

⚠️ 注意: event.target.files 是一个类数组对象( FileList ),不能使用数组原生方法(如 map forEach ),需先转换为数组:

javascript const fileArray = Array.from(event.target.files);

使用场景分析

File 对象广泛应用于以下场景:

综上所述, File 不仅是文件的“身份证”,更是后续所有处理操作的数据源头。准确理解和使用其属性,是构建健壮文件系统的基石。

2.1.2 FileList对象在多文件输入中的作用

当用户启用 <input type="file" multiple> 或选择整个文件夹时,浏览器将返回一个 FileList 对象,用于容纳多个 File 实例。 FileList 虽然外观类似数组,但本质上是一个只读的类数组集合,具备索引访问和 .length 属性,却不提供 push pop forEach 等数组方法。

其存在意义在于统一管理批量文件的选择结果,尤其适用于需要同时处理多个文件的应用场景,如批量图片上传、多文档归档打包、视频剪辑素材导入等。

结构与访问方式
const input = document.getElementById('multiFileInput');
input.addEventListener('change', () => {
    const fileList = input.files; // FileList 实例
    console.log(fileList.length); // 文件数量
    // 访问单个文件
    for (let i = 0; i < fileList.length; i++) {
        const file = fileList[i];
        console.log(file.name);
    }
    // 转换为真实数组以便使用现代方法
    const fileArray = Array.from(fileList);
    fileArray.forEach(f => console.log(f.name));
});

上面的例子演示了如何安全地遍历 FileList 并提取每个 File 对象。注意不能直接调用 fileList.forEach() ,因为 FileList 没有此方法。

实际应用场景:限制文件数量与类型过滤

在实际开发中,常需对 FileList 进行筛选和验证。例如,限制最多上传5个图片文件:

function validateFiles(fileList) {
    const maxFiles = 5;
    const allowedTypes = ['image/jpeg', 'image/png', 'image/gif'];
    if (fileList.length > maxFiles) {
        alert(`最多只能选择 ${maxFiles} 个文件`);
        return false;
    }
    for (let file of fileList) {
        if (!allowedTypes.includes(file.type)) {
            alert(`${file.name} 不是允许的图片格式`);
            return false;
        }
    }
    return true;
}

该函数可在 change 事件中调用,提前拦截非法输入,提升用户体验。

与DOM的联动:动态展示选中文件

结合 DOM 操作,可实时展示用户选择的文件列表:

<input type="file" id="fileInput" multiple />
<ul id="fileList"></ul>
<script>
document.getElementById('fileInput').addEventListener('change', function(e) {
    const output = document.getElementById('fileList');
    output.innerHTML = ''; // 清空旧列表
    Array.from(e.target.files).forEach(file => {
        const li = document.createElement('li');
        li.textContent = `${file.name} (${(file.size / 1024).toFixed(2)} KB)`;
        output.appendChild(li);
    });
});
</script>

这种方式增强了交互透明度,让用户清楚看到自己选择了哪些文件。

流程图:FileList处理流程
graph TD
    A[用户选择多个文件] --> B{触发 input change 事件}
    B --> C[获取 FileList 对象]
    C --> D[检查文件数量是否超限]
    D -- 超限 --> E[提示错误并终止]
    D -- 正常 --> F[遍历每个 File]
    F --> G[验证 MIME 类型]
    G -- 不合法 --> H[标记并警告]
    G -- 合法 --> I[加入待处理队列]
    I --> J[生成预览或上传]

此流程体现了从用户输入到业务处理的典型路径, FileList 在其中扮演着承上启下的角色。

2.1.3 通过input[type=”file”]实现用户驱动的文件选择

<input type="file"> 是触发 File API 操作的入口控件。它不仅是UI元素,更是权限请求的载体。只有通过此类控件的用户交互,脚本才能合法获得 File 对象。

其基本语法如下:

<input type="file" 
       id="uploader" 
       accept=".jpg,.png" 
       multiple 
       webkitdirectory />

各属性含义如下:

属性说明
accept 限定可选文件类型,支持 MIME 类型或扩展名(如 .pdf
multiple 允许选择多个文件
webkitdirectory (非标准)允许选择整个文件夹(Chrome/Firefox 支持)
示例:带过滤的多文件选择器
<label for="filePicker">请选择图像文件:</label>
<input type="file" 
       id="filePicker" 
       accept="image/*" 
       multiple />
<script>
document.getElementById('filePicker').addEventListener('change', async (e) => {
    const files = Array.from(e.target.files);
    const imageFiles = files.filter(f => f.type.startsWith('image/'));
    const previews = document.getElementById('previewContainer');
    previews.innerHTML = '';
    for (const file of imageFiles) {
        const img = new Image();
        img.src = URL.createObjectURL(file); // 创建临时URL
        img.style.maxWidth = '200px';
        img.onload = () => URL.revokeObjectURL(img.src); // 释放内存
        previews.appendChild(img);
    }
});
</script>
<div id="previewContainer"></div>

该示例实现了图片选择后的即时预览功能。关键点在于使用 URL.createObjectURL(file) File 转换为可用于 <img src> 的地址,并在加载完成后调用 revokeObjectURL 释放资源,防止内存泄漏。

安全性与兼容性考量

综上, input[type="file"] 是连接用户与前端文件系统的桥梁。合理配置属性并配合 JavaScript 处理,可实现灵活而安全的文件导入机制。

3. 使用Zip.js进行文件压缩流程

在现代Web应用中,前端直接处理文件归档的需求日益增长。尤其是在文档管理、云存储上传前预处理、离线编辑器导出等场景下,将多个用户选择的文件打包为ZIP格式成为一种常见且必要的功能。传统的做法依赖服务器端完成压缩任务,不仅增加了网络开销,也延长了响应时间。随着浏览器能力的不断增强,尤其是JavaScript异步处理能力和二进制数据操作接口的成熟,已经可以在客户端实现完整的ZIP文件生成逻辑。

Zip.js 是一个轻量级、模块化、基于Promise的JavaScript库,专为浏览器环境设计,支持流式读写ZIP文件,能够高效地执行压缩与解压操作而无需依赖后端服务。它充分利用了HTML5中的Blob API、FileReader API以及Web Workers机制,在保证性能的同时提供了灵活的配置选项。相比其他同类工具如JSZip,Zip.js 更加注重内存效率和可扩展性,特别适合处理大体积或多层级结构的文件集合。

本章节将深入剖析如何利用 Zip.js 实现从用户选择文件到最终生成并下载ZIP包的完整流程。我们将逐步解析其核心组件的工作原理,结合实际代码演示每一步的技术细节,并通过流程图与参数表格帮助理解内部机制。整个过程不仅涉及API调用,还包括对ZIP文件结构的理解、路径层级的维护、MIME类型的正确设置,以及最终Blob资源的安全发布方式。

3.1 Zip.js库的引入与初始化配置

要在项目中使用 Zip.js,首先必须正确引入该库。目前 Zip.js 提供了多种集成方式,开发者可根据项目构建体系选择最合适的方法。无论采用哪种方式,目标都是确保 zip.js 的核心模块(如 zip.Writer zip.Reader )能够在运行时被正确加载和实例化。

3.1.1 通过CDN或npm安装引入Zip.js

最简单的引入方式是通过CDN直接在HTML页面中加载:

<script src="https://cdn.jsdelivr.net/npm/@zip.js/zip.js@2.7.54/dist/zip.min.js"></script>

此方法适用于快速原型开发或静态网站,无需任何构建工具即可启用Zip.js的所有功能。脚本加载完成后,全局对象 zip 即可用,例如可以通过 zip.FileWriter zip.createWriter 等方式访问API。

对于使用现代前端框架(如React、Vue、Angular)的项目,推荐通过npm安装以获得更好的模块管理和Tree-shaking优化:

npm install @zip.js/zip.js

安装完成后,在JavaScript文件中按需导入所需模块:

import { createWriter, BlobWriter, BlobReader } from '@zip.js/zip.js';

这种方式允许开发者仅引入需要的功能,减少最终打包体积。同时支持ES6+语法特性,便于与其他异步流程(如async/await)结合使用。

引入方式适用场景优点缺点
CDN 直接引用快速验证、静态页面无需构建,即插即用不利于版本控制,增加首屏加载延迟
npm 安装 + 模块导入SPA、工程化项目支持Tree-shaking,易于维护需要构建系统支持

注意 :Zip.js 内部依赖 Web Streams API Text Encoding API ,在部分旧版浏览器(如IE)中可能无法运行。建议在生产环境中添加polyfill支持或进行特性检测。

3.1.2 初始化Writer对象以准备写入ZIP结构

一旦Zip.js成功加载,下一步就是创建一个写入器(Writer),用于构造ZIP归档文件。Zip.js 提供了多种Writer类型,其中最常用的是 BlobWriter ,它可以将压缩结果写入一个Blob对象,便于后续生成下载链接。

以下是初始化Writer的典型代码示例:

async function initZipWriter() {
  const blob = new Blob([], { type: 'application/zip' });
  const writer = await zip.createWriter(new zip.BlobWriter(blob));
  return writer;
}
代码逻辑逐行解读:
graph TD
    A[开始] --> B{判断引入方式}
    B -->|CDN| C[全局zip对象可用]
    B -->|npm| D[模块导入特定API]
    C --> E[创建BlobWriter]
    D --> E
    E --> F[调用createWriter]
    F --> G[获取Writer实例]
    G --> H[准备添加文件]

该流程清晰展示了从库加载到Writer初始化的全过程。值得注意的是, createWriter 是异步操作,必须使用 await .then() 处理Promise结果。这是因为底层可能涉及Worker线程的启动或缓冲区的异步分配。

此外,Zip.js 还支持其他Writer类型,如 Uint8ArrayWriter (将结果写入字节数组)和 HttpWriter (直接上传至服务器),但 BlobWriter 是前端本地压缩最常用的方案。

3.2 压缩流程的理论模型

要高效使用 Zip.js,不能仅仅停留在API调用层面,还需理解ZIP文件本身的组织结构及其在浏览器中的流式处理机制。Zip.js 的设计理念正是基于“边生成边写入”的流模型,避免一次性将所有数据加载进内存,从而提升大文件处理能力。

3.2.1 ZIP文件格式的基本组成结构(中央目录、本地文件头等)

ZIP是一种广泛使用的归档格式,其结构由多个关键部分构成:

[Local Header 1][File Data 1][Data Descriptor?]
[Local Header 2][File Data 2][Data Descriptor?]
[Central Directory]
[End of Central Directory Record]

Zip.js 在调用 add() 方法时会立即写入本地文件头和数据,而在调用 finalize() 时才生成中央目录和结尾记录。这种分阶段写入策略使得即使面对大量文件也能保持较低内存占用。

3.2.2 流式写入与内存缓冲区管理策略

Zip.js 默认使用 Web Workers 进行后台压缩计算,主界面线程不会被阻塞。每个添加的文件都会通过 TransformStream 流水线进行处理:

flowchart LR
    File --> FileReader --> TransformStream --> BlobWriter

具体流程如下:
- 使用 FileReader 将原始文件读取为 ArrayBuffer
- 将数据送入压缩流(如Deflate)
- 压缩后的片段逐步写入 BlobWriter 的内部缓冲区
- 缓冲区达到一定阈值时自动 flush 到目标 Blob

这一机制的关键优势在于: 不需要等待所有文件读取完毕再开始压缩 ,而是可以并发处理多个文件,显著提升整体性能。

此外,Zip.js 允许配置 bufferSize 参数来控制每次读取的块大小(默认为64KB),开发者可根据设备性能调整该值以平衡速度与内存消耗。

参数类型默认值说明
bufferSize number65536每次读取的字节块大小
level number6压缩级别(0~9,0为无压缩)
useWebWorkers booleantrue是否启用Worker线程

这些参数可通过 createWriter 的第三个选项参数传入:

const writer = await zip.createWriter(
  new zip.BlobWriter('application/zip'),
  { level: 6 },
  () => new zip.Deflate()
);

上述代码显式指定了压缩算法为Deflate,并设置了压缩级别。这对于控制输出文件大小非常有用。

3.3 具体实现步骤

掌握了理论基础之后,接下来进入实战环节。我们将展示如何一步步使用 Zip.js 将多个用户选择的文件打包成ZIP。

3.3.1 创建zip.Writer实例并指定MIME类型

如前所述,首先要创建一个支持ZIP输出的Writer:

const blobWriter = new zip.BlobWriter('application/zip');
const writer = await zip.createWriter(blobWriter);

此处明确指定MIME类型为 'application/zip' ,确保生成的Blob能被浏览器正确识别为ZIP文件。

3.3.2 调用add()方法逐个添加文件及其路径信息

add() 方法是Zip.js的核心接口之一,用于向归档中添加单个条目:

await writer.add('documents/report.txt', new zip.TextReader('Hello World'));

该方法接受三个主要参数:

参数类型描述
name string文件在ZIP中的路径(支持子目录)
reader Reader instance提供文件内容的数据源
options object可选配置,如进度回调、压缩级别等

常见的Reader类型包括:

例如,添加一个用户选择的图片文件:

const file = input.files[0]; // 来自<input type="file">
await writer.add(file.name, new zip.BlobReader(file));

3.3.3 处理子文件夹层级关系以保留原始目录结构

ZIP支持嵌套目录结构。只需在 name 参数中使用斜杠 / 分隔路径即可:

await writer.add('photos/vacation/sunset.jpg', photoReader);

这将在ZIP中创建 photos/vacation/ 目录,并将文件放入其中。注意:Zip.js 不会自动创建中间目录条目,但大多数解压工具仍能正确重建目录树。

若需手动添加目录项(如空文件夹),可使用空Blob:

await writer.add('empty-folder/', new zip.BlobReader(new Blob()));

路径以 / 结尾表示这是一个目录。

3.4 输出与下载最终ZIP文件

当所有文件添加完毕后,必须调用 finalize() 方法完成归档封包。

3.4.1 调用finalize()完成归档封包

await writer.close();

调用 close() (等价于 finalize() )会触发以下动作:
- 写入中央目录
- 更新结尾记录
- 关闭所有流通道
- 返回最终的Blob对象

const zipBlob = await blobWriter.getData();

getData() 获取最终生成的Blob。

3.4.2 获取生成的Blob对象并通过URL.createObjectURL发布

const url = URL.createObjectURL(zipBlob);

该URL指向内存中的Blob资源,有效期直到调用 revokeObjectURL

3.4.3 利用a标签的download属性实现自动下载

const a = document.createElement('a');
a.href = url;
a.download = 'archive.zip';
a.click();
URL.revokeObjectURL(url); // 清理内存

这段代码模拟点击行为,触发浏览器下载对话框。

完整示例封装如下:

async function compressFiles(files) {
  const blobWriter = new zip.BlobWriter('application/zip');
  const writer = await zip.createWriter(blobWriter);
  for (const file of files) {
    await writer.add(file.name, new zip.BlobReader(file));
  }
  await writer.close();
  const zipBlob = await blobWriter.getData();
  const url = URL.createObjectURL(zipBlob);
  const a = document.createElement('a');
  a.href = url;
  a.download = 'selected-files.zip';
  a.click();
  URL.revokeObjectURL(url);
}

该函数接收 FileList File[] 数组,执行全流程压缩并触发下载,适用于真实项目集成。

4. 使用Zip.js进行文件解压缩流程

前端文件处理能力的演进不仅体现在压缩技术上,更在于对已有归档数据的解析与还原。随着Web应用逐步承担起传统桌面软件的部分职责,如在线文档编辑器、云存储管理界面等场景中,用户经常需要上传ZIP压缩包并查看其内部内容。这就要求浏览器能够在不依赖后端服务的前提下完成完整的解压流程。 Zip.js 作为一款轻量级且功能完备的纯JavaScript库,支持在浏览器环境中实现ZIP文件的读取与解压缩操作。该过程涉及多个关键环节:从原始二进制数据的加载,到压缩条目的遍历提取,再到最终的数据转换和DOM展示,每一步都需要精确控制以确保兼容性与用户体验。

本章将深入剖析如何基于 Zip.js 构建一个健壮的前端解压系统。我们将围绕数据准备、结构解析、内容重建以及错误处理四大核心模块展开讨论,并结合实际代码示例说明各阶段的技术细节。通过这一完整流程的学习,开发者可以掌握如何安全地加载用户上传的ZIP文件,高效地提取其中资源,并以直观方式呈现给终端用户。

4.1 解压前的数据准备与验证

在执行任何解压逻辑之前,必须首先获取用户选择的ZIP文件,并将其转换为适合处理的二进制格式。由于浏览器环境无法直接访问本地文件系统,所有输入都需通过用户主动触发(例如点击 <input type="file"> ),这是保障安全性的基础机制。随后,使用 FileReader 将选中的文件读取为 ArrayBuffer ,以便后续由 zip.Reader 进行解析。

4.1.1 读取用户上传的ZIP文件为ArrayBuffer

要开始解压流程,第一步是让用户选择一个ZIP文件。HTML提供了标准的文件输入控件:

<input type="file" id="zipFileInput" accept=".zip" />

设置 accept=".zip" 可提示用户仅选择ZIP格式文件,尽管这不能完全阻止非法文件上传,但有助于提升交互体验。接下来,在JavaScript中监听文件变化事件:

document.getElementById('zipFileInput').addEventListener('change', async (event) => {
    const file = event.target.files[0];
    if (!file) return;
    // 使用FileReader读取为ArrayBuffer
    const reader = new FileReader();
    reader.onload = function(e) {
        const arrayBuffer = e.target.result;
        handleZipFile(arrayBuffer); // 进入下一步处理
    };
    reader.onerror = () => console.error("文件读取失败");
    reader.readAsArrayBuffer(file);
});

代码逻辑逐行解读:

  • 第2行:绑定 change 事件,当用户选择文件时触发。
  • 第3行:获取第一个选中的文件对象( File 类型)。
  • 第6–11行:创建 FileReader 实例,设定 onload 回调函数用于接收读取结果。
  • 第8行: e.target.result 即为文件的 ArrayBuffer 表示,包含原始字节流。
  • 第9行:调用自定义函数 handleZipFile() 开始解压主流程。
  • 第10行:监听错误事件,防止因文件损坏或权限问题导致静默失败。
  • 第11行:调用 readAsArrayBuffer() 启动异步读取,适用于二进制数据。

此方法保证了文件内容以原始字节形式加载,避免编码转换带来的信息丢失,特别适合处理非文本类型的压缩包。

4.1.2 使用zip.Reader加载压缩包内容

一旦获得 ArrayBuffer ,即可使用 Zip.js 提供的 zip.Reader 接口初始化读取器。该对象负责解析ZIP文件的内部结构,包括中央目录、本地文件头等元数据。

假设已正确引入 Zip.js 库(可通过 CDN 或模块化导入),可如下初始化:

import * as zip from 'https://cdn.jsdelivr.net/npm/@zip.js/zip.js';
async function handleZipFile(arrayBuffer) {
    try {
        const uint8Array = new Uint8Array(arrayBuffer);
        const reader = new zip.Uint8ArrayReader(uint8Array);
        const zipReader = new zip.ZipReader(reader);
        const entries = await zipReader.getEntries();
        processEntries(entries, zipReader); // 继续处理条目
    } catch (err) {
        console.error("解压初始化失败:", err);
    }
}

代码逻辑逐行解读:

  • 第1行:动态导入最新版本的 @zip.js/zip.js 模块(现代浏览器支持 ES Module 形式)。
  • 第5行:将 ArrayBuffer 转换为 Uint8Array ,这是 zip.Reader 所需的输入类型。
  • 第6行:构造 Uint8ArrayReader ,封装底层数据流。
  • 第7行:创建 ZipReader 实例,启动对ZIP结构的解析。
  • 第9行:调用 getEntries() 异步获取所有压缩项列表,返回 Promise。
  • 第11行:传入条目数组和 zipReader 实例进入下一阶段处理。
  • 第13–15行:捕获可能抛出的异常,如格式错误、CRC校验失败等。

ZipReader 在初始化过程中会自动验证ZIP文件的基本结构完整性,若文件非标准ZIP格式,则 getEntries() 将拒绝并抛出异常。

4.1.3 检查文件完整性与MIME类型安全性

尽管前端无法像服务器那样进行全面的内容扫描,但仍可通过多种手段增强安全性与鲁棒性。

验证项方法目的
文件扩展名file.name.endsWith('.zip') 初步过滤明显非ZIP文件
MIME类型file.type === 'application/zip' 利用浏览器识别的媒体类型做判断
魔数校验(Magic Number)检查前4字节是否为 PK\003\004 确认真实ZIP格式
大小限制file.size < MAX_SIZE 防止大文件拖慢页面

以下是综合验证逻辑示例:

function isValidZipFile(file) {
    const MAX_SIZE = 50 * 1024 * 1024; // 50MB限制
    if (!file.name.toLowerCase().endsWith('.zip')) {
        alert("请上传ZIP格式文件");
        return false;
    }
    if (!file.type || !file.type.includes('zip')) {
        alert("MIME类型不匹配,请确认为有效ZIP文件");
        return false;
    }
    if (file.size > MAX_SIZE) {
        alert(`文件过大(${(file.size / 1024 / 1024).toFixed(2)}MB),超过50MB限制`);
        return false;
    }
    return true;
}

此外,还可以进一步检查 ArrayBuffer 的魔数:

function checkMagicNumber(arrayBuffer) {
    const view = new Uint8Array(arrayBuffer, 0, 4);
    const magic = String.fromCharCode(view[0], view[1], view[2], view[3]);
    return magic === "PK\003\004"; // ZIP本地文件头标志
}

参数说明:

安全边界设计建议
graph TD
    A[用户选择文件] --> B{是否为.zip?}
    B -- 否 --> C[提示错误并拒绝]
    B -- 是 --> D{MIME类型匹配?}
    D -- 否 --> C
    D -- 是 --> E{大小 ≤ 50MB?}
    E -- 否 --> F[提示超限]
    E -- 是 --> G[读取ArrayBuffer]
    G --> H{魔数为PK\003\004?}
    H -- 否 --> I[判定为非法]
    H -- 是 --> J[允许进入解压流程]

该流程图清晰展示了多层验证机制的执行路径,体现了“纵深防御”原则。即使某一层被绕过(如修改扩展名),后续校验仍可拦截风险。

综上所述,解压前的数据准备不仅是技术起点,更是安全防线的第一道关卡。只有经过严格验证的文件才能进入后续解析阶段,从而保障系统的稳定与用户数据的安全。

4.2 文件条目遍历与内容提取

成功加载ZIP文件后,下一步是遍历其中的所有条目(entries),这些条目代表压缩包内的各个文件或目录。 Zip.js 提供了清晰的接口来访问这些元数据,并支持按需提取具体内容。本节将详细介绍如何高效地枚举条目、解析属性,并重建原始的目录层级结构。

4.2.1 enumerate()方法遍历所有压缩项

ZipReader getEntries() 方法返回一个包含所有条目的数组,每个条目是一个 Entry 对象,具备名称、大小、压缩方式、时间戳等丰富信息。

async function processEntries(entries, zipReader) {
    for (const entry of entries) {
        console.log(`文件名: ${entry.filename}`);
        console.log(`大小: ${entry.uncompressedSize} 字节`);
        console.log(`是否为目录: ${entry.directory}`);
        console.log(`压缩方法: ${entry.compressionMethod}`);
        if (!entry.directory) {
            const content = await entry.getData(new zip.BlobWriter());
            displayFileContent(entry.filename, content);
        }
    }
    await zipReader.close(); // 必须显式关闭读取器
}

代码逻辑逐行解读:

该方法适用于中小型ZIP包;对于大型文件,建议采用分批处理或流式读取策略。

4.2.2 获取每个条目的名称、大小及是否为目录

Entry 对象提供的关键属性如下表所示:

属性名类型说明
filename string文件路径(含相对路径,使用 / 分隔)
uncompressedSize number解压后大小(字节)
compressedSize number压缩后大小
directory boolean是否为目录条目
date Date最后修改时间
comment string条目注释(可选)
encryption string加密方式(如无则为空)

特别注意: filename 中可能包含多级路径,如 docs/report.txt images/thumbs/ ,需据此重建文件夹结构。

4.2.3 根据路径信息重建文件夹层级结构

为了准确还原原始目录树,我们需要根据 filename 字段拆分路径并生成对应的虚拟目录结构。以下是一个构建树形结构的实用函数:

function buildDirectoryTree(entries) {
    const root = { name: '/', type: 'folder', children: [] };
    for (const entry of entries) {
        const pathParts = entry.filename.split('/').filter(p => p !== '');
        let current = root;
        for (let i = 0; i < pathParts.length; i++) {
            const part = pathParts[i];
            const isLast = i === pathParts.length - 1;
            let child = current.children.find(c => c.name === part);
            if (!child) {
                child = {
                    name: part,
                    type: isLast && !entry.directory ? 'file' : 'folder',
                    size: isLast ? entry.uncompressedSize : undefined,
                    entry: isLast ? entry : undefined
                };
                current.children.push(child);
            }
            current = child;
        }
    }
    return root;
}

参数说明:

  • 输入: entries 数组,来自 getEntries()
  • 输出:嵌套对象表示的文件树,可用于递归渲染。
  • pathParts 拆分路径,忽略空字符串(如结尾斜杠导致的问题)。
  • current 指针逐层下探,构建父子关系。
  • isLast 判断当前部分是否为叶子节点,决定类型为文件还是中间目录。

该结构可用于后续DOM渲染或导出为JSON格式供其他组件使用。

4.3 数据转换与前端展示

解压后的数据通常以 Blob String 形式存在,需根据不同文件类型进行适配处理,以便在页面中直接预览或提供下载链接。

4.3.1 将解压出的文件内容转为Blob或Text

getData() 支持多种写入器类型,可根据需求灵活选择:

// 获取为文本(适用于 .txt, .json, .html 等)
const text = await entry.getData(new zip.TextWriter());
// 获取为 Blob(通用)
const blob = await entry.getData(new zip.BlobWriter());
// 获取为 ArrayBuffer(底层操作)
const buffer = await entry.getData(new zip.Uint8ArrayWriter());

扩展说明:

  • TextWriter 自动处理字符编码(默认UTF-8),适合文本类文件。
  • BlobWriter 返回 Blob 对象,可用于生成预览URL。
  • Uint8ArrayWriter 提供最原始的字节数组,常用于图像或二进制分析。

4.3.2 利用DataURL嵌入图片或预览文本文件

对于图片文件(如 .png , .jpg ),可将其转为 Data URL 并插入 <img> 标签:

function displayImage(filename, blob) {
    const url = URL.createObjectURL(blob);
    const img = document.createElement('img');
    img.src = url;
    img.alt = filename;
    img.style.maxWidth = '300px';
    document.body.appendChild(img);
}

对于文本文件,直接插入 <pre> 元素:

function displayText(filename, text) {
    const pre = document.createElement('pre');
    pre.textContent = text;
    pre.style.backgroundColor = '#f4f4f4';
    pre.style.padding = '10px';
    document.body.appendChild(pre);
}

4.3.3 动态生成DOM元素展示解压结果列表

结合前面构建的文件树,可递归生成HTML结构:

function renderTree(node, parentEl) {
    const li = document.createElement('li');
    li.textContent = node.name + (node.type === 'file' ? ` (${node.size}B)` : '');
    if (node.children && node.children.length > 0) {
        const ul = document.createElement('ul');
        node.children.forEach(child => renderTree(child, ul));
        li.appendChild(ul);
    } else if (node.type === 'file') {
        li.onclick = () => openFile(node.entry); // 点击打开文件
    }
    parentEl.appendChild(li);
}
flowchart TB
    Start[开始渲染文件树] --> Check{是否有子节点?}
    Check -- 是 --> CreateUL[创建<ul>容器]
    CreateUL --> Loop[遍历每个子节点]
    Loop --> RenderChild[递归调用renderTree]
    RenderChild --> AppendToUL[添加至<ul>]
    AppendToUL --> End
    Check -- 否 --> IsFile{是否为文件?}
    IsFile -- 是 --> AddClick[绑定点击事件]
    AddClick --> End
    IsFile -- 否 --> End

此流程确保了目录结构的可视化表达,提升了用户体验。

4.4 错误处理与用户体验优化

4.4.1 捕获解压过程中可能出现的编码异常

ZIP文件可能使用非UTF-8编码命名(如GBK、Shift-JIS),导致 filename 出现乱码。 Zip.js 支持指定解码器:

const zipReader = new zip.ZipReader(reader, {
    decodeFileName: (bytes) => new TextDecoder('gbk').decode(bytes)
});

此外,还需捕获 getData() 中可能发生的 CRC 校验失败、解压算法不支持等问题:

try {
    const content = await entry.getData(writer);
} catch (e) {
    console.warn(`提取 ${entry.filename} 失败:`, e.message);
    showErrorToast(`无法解压 ${entry.filename}`);
}

4.4.2 提供进度条反馈提升交互感知

对于大文件,可通过监听 onprogress 事件实现进度追踪:

const zipReader = new zip.ZipReader(reader, {
    onprogress: (index, total) => {
        const percent = Math.round((index / total) * 100);
        updateProgressBar(percent);
    }
});

配合CSS样式:

.progress-bar {
    width: 100%;
    background: #eee;
    height: 20px;
    border-radius: 10px;
    overflow: hidden;
}
.progress-fill {
    width: var(--width);
    background: #4CAF50;
    height: 100%;
    transition: width 0.3s ease;
}

实时反馈显著改善用户等待体验,减少误操作。

5. JSZip库基本使用方法

随着前端工程能力的不断演进,浏览器端对文件处理的需求已从简单的单文件读取发展到复杂的归档操作。在这一背景下, JSZip 成为了开发者构建 Web 端压缩功能时最受欢迎的第三方库之一。相较于原生 API 或其他轻量级实现(如 Zip.js),JSZip 提供了更高级、语义清晰且易于集成的接口设计,尤其适合需要快速构建 ZIP 打包与解包逻辑的应用场景。本章深入剖析 JSZip 的核心用法,涵盖其初始化流程、压缩与解压操作、多格式支持机制,并探讨其在真实项目中的高级配置策略。

5.1 JSZip与Zip.js的功能对比分析

面对多种前端压缩库的选择,开发者常需在性能、易用性和功能完整性之间权衡。JSZip 与 Zip.js 是目前主流的两个 JavaScript 压缩解决方案,它们均基于浏览器环境运行,但设计理念和底层架构存在显著差异。理解这些差异有助于根据实际业务需求做出合理选型。

5.1.1 API设计风格差异:链式调用 vs 回调驱动

JSZip 最突出的优势在于其 面向对象 + 链式调用 的编程模型。整个压缩过程被抽象为一个 JSZip 实例,所有添加文件、设置参数、生成输出等动作都可以通过连续的方法调用来完成,极大提升了代码可读性与维护性。

const zip = new JSZip();
zip.file("hello.txt", "Hello World")
   .folder("images")
   .file("images/cat.jpg", imageData, { base64: true });

上述代码展示了典型的链式写法:先创建实例,然后通过 .file() 添加文本内容,再通过 .folder() 创建子目录并继续添加图片资源。这种结构非常接近自然语言描述,便于团队协作和后期扩展。

相比之下,Zip.js 使用的是基于事件回调或 Promise 的异步流式 API:

const writer = new zip.BlobWriter();
const reader = new zip.BlobReader(blob);
const zipWriter = new zip.ZipWriter(writer);
zipWriter.add("hello.txt", new zip.TextReader("Hello World"))
         .then(() => zipWriter.close());

虽然也具备一定的模块化特征,但整体逻辑分散于多个嵌套的 .then() 调用中,尤其在处理复杂目录结构时容易导致“回调地狱”,降低代码整洁度。

特性JSZipZip.js
编程范式面向对象 + 链式调用流式 + 回调/Promise
学习曲线较低,API 直观中等,需理解 Reader/Writer 模型
异步控制基于 Promise支持 Promise 和事件驱动
内存管理粒度自动管理可精细控制缓冲区大小
文件增量写入支持是(支持流式写入)

该表格清晰地反映了两种库的设计哲学: JSZip 更偏向于开发效率与简洁性 ,而 Zip.js 更注重底层可控性与资源优化

mermaid 流程图:API调用路径对比
graph TD
    A[用户触发压缩] --> B{选择库}
    B --> C[JSZip]
    B --> D[Zip.js]
    C --> C1[创建 JSZip 实例]
    C1 --> C2[调用 file() / folder()]
    C2 --> C3[链式构建结构]
    C3 --> C4[generateAsync() 输出 Blob]
    D --> D1[创建 BlobWriter]
    D1 --> D2[初始化 ZipWriter]
    D2 --> D3[add() 添加条目]
    D3 --> D4[close() 完成归档]

此流程图揭示了两者在执行路径上的根本区别——JSZip 将所有操作封装在一个统一上下文中,而 Zip.js 则依赖多个独立组件协同工作,增加了认知负担。

5.1.2 浏览器兼容性与体积大小比较

在现代前端开发中,库的体积直接影响首屏加载速度,尤其是在移动端或弱网环境下。JSZip 的生产版本经过压缩后约为 14KB(gzip) ,未压缩约 60KB,属于中小型库范畴;而 Zip.js(包含所有 Reader/Writer 模块)总大小可达 80KB 以上(未压缩) ,明显更大。

但从兼容性角度看,JSZip 对旧版浏览器的支持更为完善。它通过内置的 polyfill 机制保障在 IE10+ 及主流现代浏览器中的稳定运行,特别适用于企业级管理系统这类仍需支持较老环境的项目。Zip.js 虽然也支持大多数现代浏览器,但在 Safari 早期版本或某些 WebView 环境下可能出现 Blob 分片异常问题。

此外,JSZip 的生态更为成熟,拥有官方文档、活跃社区以及丰富的插件体系(如 jszip-utils、FileSaver.js 集成示例)。开发者可通过 npm 快速安装并配合 webpack 等工具进行按需打包:

npm install jszip
import JSZip from 'jszip';
// 动态导入以减少初始包体积
async function loadJSZip() {
  const module = await import('jszip');
  return new module.default();
}

这种方式允许将 JSZip 的加载延迟至真正需要时,进一步优化应用启动性能。

综上所述,在大多数常规应用场景下(如导出报表、打包附件、离线资源归档), JSZip 凭借其简洁 API 和良好兼容性成为首选方案 ;而在涉及超大文件流式处理、自定义压缩算法或严格内存控制的特殊场景中,Zip.js 更具优势。

5.2 使用JSZip实现压缩操作

在掌握 JSZip 的定位之后,接下来进入实战阶段——如何利用该库实现完整的前端压缩功能。本节重点介绍其实例化方式、多类型数据输入支持以及异步生成机制,帮助开发者构建健壮的 ZIP 构建流程。

5.2.1 创建JSZip实例并添加文件

每个压缩任务都始于一个 JSZip 类的实例。该实例充当 ZIP 包的容器,负责组织内部文件结构、维护元数据,并最终生成二进制输出。

const zip = new JSZip();
// 添加纯文本文件
zip.file("readme.md", "# 欢迎使用JSZip");
// 添加空目录
const imgFolder = zip.folder("assets/images");
// 向子目录添加文件
imgFolder.file("logo.png", pngData, { binary: true });

值得注意的是,JSZip 并不立即写入磁盘,而是将所有内容保留在内存中,直到调用 generateAsync() 才进行序列化。这意味着可以在任意时刻动态修改 ZIP 内容,例如条件性地删除某个文件:

if (shouldExcludeLog) {
  zip.remove("debug.log");
}

这种灵活性使得 JSZip 非常适合用于构建带有用户交互选项的打包系统(如“是否包含日志文件”复选框)。

5.2.2 支持Base64、Uint8Array等多种输入格式

JSZip 能够解析多种原始数据格式,包括但不限于:

输入类型示例值是否需指定选项
字符串"Hello"
Base64 字符串"SGVsbG8=" base64: true
Uint8Arraynew Uint8Array([72, 101, 108, 108, 111]) binary: true
ArrayBufferresponse.arrayBuffer() binary: true
Blobnew Blob(["data"], {type: "text/plain"}) 自动识别
// 示例:从 Base64 图片创建文件
const base64Image = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUg...";
const cleaned = base64Image.split(",")[1]; // 移除 data URL 前缀
zip.file("photo.png", cleaned, { base64: true });
// 示例:从 fetch 获取的 ArrayBuffer 添加 PDF
fetch('/report.pdf')
  .then(res => res.arrayBuffer())
  .then(buffer => {
    zip.file("report.pdf", buffer, { binary: true });
  });

参数说明
- { base64: true } :告知 JSZip 数据是 Base64 编码,需先解码为二进制;
- { binary: true } :表示传入的是原始字节流,避免字符编码转换错误;
- 若省略这些标志,可能导致中文乱码或图像损坏。

这一点在处理非 UTF-8 编码内容(如二进制图片、加密数据)时尤为重要。正确设置选项可确保文件在解压后保持原始完整性。

5.2.3 generateAsync()方法生成异步ZIP输出

由于 ZIP 生成涉及大量字节拼接与压缩计算,JSZip 提供了 generateAsync() 方法以异步方式完成最终打包,防止阻塞主线程。

zip.generateAsync({
  type: "blob",
  compression: "DEFLATE",
  compressionOptions: { level: 9 }
}).then(blob => {
  const url = URL.createObjectURL(blob);
  const a = document.createElement("a");
  a.href = url;
  a.download = "archive.zip";
  a.click();
});

该方法返回一个 Promise,因此可以轻松集成进 async/await 结构:

try {
  const blob = await zip.generateAsync({ type: "blob" });
  saveAs(blob, "project.zip"); // 使用 FileSaver.js
} catch (e) {
  console.error("生成失败:", e);
}

结合进度监听功能(需自行实现分块追踪),还可为用户提供实时反馈体验。

5.3 使用JSZip实现解压缩操作

除了压缩,JSZip 同样提供了强大的解压能力,能够将上传的 ZIP 文件完整还原为文件树结构,并提取其中的内容供前端展示或进一步处理。

5.3.1 loadAsync()加载ArrayBuffer格式的ZIP文件

解压的第一步是将用户选择的 ZIP 文件读取为 ArrayBuffer ,然后交由 JSZip 解析。

const input = document.querySelector("#zip-file");
input.addEventListener("change", async (e) => {
  const file = e.target.files[0];
  const arrayBuffer = await file.arrayBuffer();
  const zip = await JSZip.loadAsync(arrayBuffer);
  console.log(zip.files); // 输出文件列表
});

该方法自动处理 ZIP 中央目录解析、本地头校验等工作,屏蔽了底层细节。

5.3.2 遍历files对象提取各成员内容

一旦 ZIP 被成功加载,即可遍历 .files 来访问每一个条目:

for (const [filename, zipEntry] of Object.entries(zip.files)) {
  if (zipEntry.dir) continue; // 跳过目录
  const content = await zipEntry.async("text"); // 可选: "text", "binarystring", "arraybuffer"
  console.log(`${filename}: ${content.substring(0, 100)}...`);
}
const isValidFile = (name) =>
  !name.startsWith("__MACOSX/") &&
  !path.basename(name).startsWith(".");
const validFiles = Object.keys(zip.files).filter(isValidFile);

这在构建安全的文档查看器时极为重要,避免无意中暴露敏感信息。

5.3.3 支持过滤隐藏文件和系统元数据

许多操作系统会在 ZIP 中插入额外元数据文件,若不加过滤可能干扰业务逻辑。以下是一个通用清理函数:

function filterUselessEntries(files) {
  return Object.fromEntries(
    Object.entries(files).filter(([name, entry]) => {
      return !(
        name.includes("__MACOSX") ||
        name.endsWith(".DS_Store") ||
        entry.dir && name.startsWith(".")
      );
    })
  );
}
const cleanFiles = filterUselessEntries(zip.files);

此策略可有效提升用户体验,防止无效文件污染结果列表。

5.4 高级特性:密码保护与压缩级别设置

尽管浏览器端加密 ZIP 存在局限,JSZip 仍提供部分高级功能用于增强安全性与性能控制。

5.4.1 加密ZIP文件的实现方式(有限支持)

JSZip 不原生支持 AES 加密 ,仅能生成传统的 ZIP 2.0 加密(弱加密,易破解),且现代浏览器大多不推荐使用。因此,若需强加密,建议采用以下替代方案:

// 方案:先压缩 → 再使用 CryptoJS 加密整个 Blob
import CryptoJS from 'crypto-js';
const blob = await zip.generateAsync({ type: "arraybuffer" });
const wordArray = CryptoJS.lib.WordArray.create(new Uint8Array(blob));
const encrypted = CryptoJS.AES.encrypt(wordArray, "secret-key").toString();
// 下载加密后的数据
const encryptedBlob = new Blob([encrypted], { type: "text/plain" });

注意:此方法加密的是整个 ZIP 流,解密需先还原再交给 JSZip 处理。

5.4.2 调整compression参数控制打包效率

通过调整 compression level 参数,可在压缩比与性能间取得平衡:

zip.generateAsync({
  type: "blob",
  compression: "DEFLATE",
  compressionOptions: {
    level: 6 // 推荐值:6(兼顾速度与压缩率)
  }
})

测试表明,在 10MB 文本数据上:
- level: 1 :耗时 ~800ms,输出 ~3.5MB
- level: 6 :耗时 ~1.4s,输出 ~2.8MB
- level: 9 :耗时 ~2.3s,输出 ~2.6MB

因此,对于大型文件批量打包,建议设为 level: 6 ,避免卡顿影响交互流畅性。

6. 文件夹批量文件读取与处理策略

6.1 WebKit目录遍历API的使用条件

现代浏览器中,允许用户选择整个文件夹进行操作的功能依赖于 WebKit 浏览器引擎 提供的专有扩展 API —— directory webkitdirectory 属性。尽管该功能尚未被纳入正式 HTML 标准,但在 Chrome、Edge 及部分基于 Chromium 的浏览器中已得到良好支持。

6.1.1 requestDirectoryRead()方法的启用前提

要实现对文件夹内容的递归读取,必须通过带有 webkitdirectory 属性的 <input> 元素触发用户主动选择行为:

<input type="file" id="folderInput" webkitdirectory directory multiple />

⚠️ 注意: directory 是较新的标准属性,而 webkitdirectory 是其在早期 Chromium 中的实现名称,目前仍需保留以确保兼容性。

当用户选择一个目录后,返回的 DataTransferItemList 将包含所有嵌套条目(包括子目录和文件),每个条目可通过 .webkitGetAsEntry() 方法获取 FileSystemEntry 对象。

document.getElementById('folderInput').addEventListener('change', async (e) => {
    const items = e.target.files;
    for (let file of items) {
        const entry = file.webkitRelativePath ? 
            file /* 可直接访问 */ : 
            e.target.webkitEntries?.find(e => e.name === file.name);
        if (entry) {
            console.log('Entry:', entry.name, entry.isDirectory ? 'is dir' : 'is file');
        }
    }
});

此方法仅在 HTTPS 环境或本地开发服务器(localhost)下生效,且必须由用户显式交互触发,防止自动扫描用户磁盘。

6.1.2 判断浏览器是否支持文件夹选择(directory entry)

为提升健壮性,应在运行时检测当前环境是否支持目录选择功能:

function isDirectoryUploadSupported() {
    const input = document.createElement('input');
    return 'webkitdirectory' in input || 'directory' in input;
}
if (!isDirectoryUploadSupported()) {
    alert('当前浏览器不支持文件夹上传,请使用 Chrome/Edge 最新版');
}

此外,可通过特性探测进一步验证 webkitGetAsEntry 是否可用:

function supportsFileSystemEntries(dataTransfer) {
    return Array.from(dataTransfer.items)
        .some(item => item.kind === 'file' && item.webkitGetAsEntry);
}

6.2 递归读取嵌套文件结构

为了完整构建用户所选文件夹的树形结构,需要结合 FileSystemDirectoryReader 接口实现异步递归遍历。

6.2.1 使用readEntries()循环读取目录条目

readEntries() 方法以批处理方式返回目录中的条目列表,需循环调用直至返回空数组:

async function readAllEntries(directoryEntry) {
    const reader = directoryEntry.createReader();
    let entries = [];
    let read;
    do {
        read = await new Promise(resolve => reader.readEntries(resolve));
        entries = entries.concat(Array.from(read));
    } while (read.length > 0);
    return entries;
}

6.2.2 区分文件与子目录并建立树形结构映射

以下函数将整个目录结构转换为 JSON 树状模型,便于后续压缩或展示:

async function buildFileTree(entry, pathPrefix = '') {
    const node = {
        name: entry.name,
        fullPath: pathPrefix + entry.name,
        children: [],
        isDirectory: false,
        size: null
    };
    if (entry.isFile) {
        node.isDirectory = false;
        return new Promise((resolve) =>
            entry.file(file => {
                node.size = file.size;
                resolve(node);
            })
        );
    }
    if (entry.isDirectory) {
        node.isDirectory = true;
        const children = await readAllEntries(entry);
        node.children = await Promise.all(
            children.map(child =>
                buildFileTree(child, node.fullPath + '/')
            )
        );
        return node;
    }
    return node;
}

示例输出结构如下:

节点路径类型大小(字节)
/project目录-
/project/index.html文件2048
/project/js/app.js文件5120
/project/css/theme.css文件3072
/project/assets目录-
/project/assets/logo.png文件81920
/project/docs目录-
/project/docs/readme.txt文件1024
/project/node_modules目录-
/project/node_modules/lodash.js文件72000
/project/config.json文件512

该树形结构可用于:
- 构造 ZIP 归档时保持原始路径层级
- 在前端 UI 中渲染可展开的文件浏览器
- 实现搜索、过滤与权限控制逻辑

6.3 批量处理中的性能优化方案

大规模文件夹处理容易造成主线程阻塞,影响用户体验,因此必须引入异步与并发机制。

6.3.1 分片读取避免主线程阻塞

对于含有上千个文件的目录,应采用“微任务队列”方式进行分片处理:

async function processFilesInBatches(files, processor, batchSize = 10) {
    for (let i = 0; i < files.length; i += batchSize) {
        const batch = files.slice(i, i + batchSize);
        await Promise.all(batch.map(processor));
        // 插入微任务等待,释放主线程
        await new Promise(resolve => setTimeout(resolve, 0));
    }
}

这样可在每批次处理后让出执行权,防止页面冻结。

6.3.2 使用Worker线程进行后台压缩计算

将耗时的压缩任务移至 Web Worker ,利用多核 CPU 并行处理:

// main.js
const worker = new Worker('/workers/zip-worker.js');
worker.postMessage({ action: 'startZip', treeData: fileTree });
worker.onmessage = function(e) {
    if (e.data.blob) {
        const url = URL.createObjectURL(e.data.blob);
        const a = document.createElement('a');
        a.href = url;
        a.download = 'archive.zip';
        a.click();
    }
};
// workers/zip-worker.js
importScripts('https://cdnjs.cloudflare.com/ajax/libs/jszip/3.7.1/jszip.min.js');
self.onmessage = async function(e) {
    const zip = new JSZip();
    const addFilesRecursively = (node, parentFolder) => {
        if (node.isDirectory) {
            const folder = parentFolder.folder(node.name);
            node.children.forEach(child => addFilesRecursively(child, folder));
        } else {
            parentFolder.file(node.name, fetch(node.fullPath).then(r => r.arrayBuffer()));
        }
    };
    addFilesRecursively(e.data.treeData, zip);
    const blob = await zip.generateAsync({ type: 'blob' });
    self.postMessage({ blob }, [blob]);
};

✅ 使用 transferable objects (如 [blob] )可高效传递大数据,减少序列化开销。

6.4 安全性考虑与用户数据处理最佳实践

6.4.1 防止路径遍历攻击(Path Traversal)的风险

即使在浏览器沙箱内,也应校验文件路径合法性,防止恶意构造的相对路径逃逸预期范围:

function isValidPath(path) {
    const normalized = path.replace(/\\/g, '/').replace(/\.\.\//g, '');
    return !normalized.includes('../') && !normalized.startsWith('/');
}

同时,在生成 ZIP 包时应剥离绝对前缀 / C:\ ,统一使用相对路径。

6.4.2 明确告知用户数据用途并提供清除机制

在处理敏感文档时,应弹出提示说明数据不会上传至服务器,并提供一键清理内存缓存功能:

function clearMemoryCache() {
    globalFileReferences.clear();
    URL.revokeObjectURLs(); // 清理 createObjectURL 创建的所有链接
    caches.delete('local-file-cache'); // 若使用 Cache API
}

6.4.3 敏感文件类型检测与自动拦截策略

可通过扩展名黑名单阻止潜在风险文件被加载:

const BLOCKED_EXTS = ['.exe', '.bat', '.sh', '.dll', '.ps1', '.cmd'];
function isBlockedFile(file) {
    return BLOCKED_EXTS.some(ext => file.name.toLowerCase().endsWith(ext));
}
// 使用示例
Array.from(input.files).forEach(file => {
    if (isBlockedFile(file)) {
        console.warn(`已阻止加载危险文件: ${file.name}`);
        return;
    }
    // 正常处理流程...
});

此外,建议结合 MIME 类型探测增强安全性:

async function getMimeType(file) {
    const arr = await file.slice(0, 4).arrayBuffer();
    const view = new Uint8Array(arr);
    const hex = Array.prototype.map.call(view, b => b.toString(16).padStart(2, '0')).join('');
    switch (hex) {
        case '504b0304': return 'application/zip';
        case '89504e47': return 'image/png';
        default: return file.type || 'unknown';
    }
}

到此这篇关于JavaScript实现文件夹压缩与解压缩完整指南的文章就介绍到这了,更多相关js文件夹压缩与解压缩内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文