【W(wǎng)eb技術(shù)】959- JavaScript 如何在線解壓 ZIP 文件?
相信大家對(duì) ZIP 文件都不會(huì)陌生,當(dāng)你要打開(kāi)本地的 ZIP 文件時(shí),你就需要先安裝支持解壓 ZIP 文件的解壓軟件。但如果預(yù)解壓的 ZIP 文件在服務(wù)器上,我們應(yīng)該如何處理呢?
最簡(jiǎn)單的一種方案就是把文件下載到本地,然后使用支持 ZIP 格式的解壓軟件進(jìn)行解壓。那么能不能在線解壓 ZIP 文件呢?答案是可以的,接下來(lái)阿寶哥將介紹瀏覽器解壓和服務(wù)器解壓兩種在線解壓 ZIP 文件的方案。
在介紹在線解壓 ZIP 文件的兩種方案前,我們先來(lái)簡(jiǎn)單了解一下 ZIP 文件格式。
一、ZIP 格式簡(jiǎn)介
ZIP 文件格式是一種數(shù)據(jù)壓縮和文檔儲(chǔ)存的文件格式,原名 Deflate,發(fā)明者為菲爾·卡茨(Phil Katz),他于 1989 年 1 月公布了該格式的資料。ZIP 通常使用后綴名 “.zip”,它的 MIME 格式為 “application/zip”。目前,ZIP 格式屬于幾種主流的壓縮格式之一,其競(jìng)爭(zhēng)者包括RAR 格式以及開(kāi)放源碼的 7z 格式。
ZIP 是一種相當(dāng)簡(jiǎn)單的分別壓縮每個(gè)文件的存檔格式,分別壓縮文件允許不必讀取另外的數(shù)據(jù)而檢索獨(dú)立的文件。理論上,這種格式允許對(duì)不同的文件使用不同的算法。然而,在實(shí)際上,ZIP 大多數(shù)都是在使用卡茨(Katz)的 DEFLATE 算法。
簡(jiǎn)單介紹完 ZIP 格式,接下來(lái)阿寶哥先來(lái)介紹基于 JSZip 這個(gè)庫(kù)的瀏覽器解壓方案。
二、瀏覽器解壓方案
JSZip 是一個(gè)用于創(chuàng)建、讀取和編輯 .zip 文件的 JavaScript 庫(kù),該庫(kù)支持大多數(shù)瀏覽器,具體的兼容性如下圖所示:

其實(shí)有了 JSZip 這個(gè)庫(kù)的幫助,要實(shí)現(xiàn)瀏覽器端在線解壓 ZIP 文件的功能并不難。因?yàn)楣俜揭呀?jīng)為我們提供了 解壓本地文件、解壓遠(yuǎn)程文件和生成 ZIP 文件 的完整示例。好的,廢話不多說(shuō),下面我們來(lái)一步步實(shí)現(xiàn)在線解壓 ZIP 文件的功能。
2.1 定義工具類
瀏覽器端在線解壓 ZIP 文件的功能,可以拆分為 下載 ZIP 文件、解析 ZIP 文件和展示 ZIP 文件 3 個(gè)小功能。考慮到功能復(fù)用性,阿寶哥把下載 ZIP 文件和解析 ZIP 文件的邏輯封裝在 ExeJSZip 類中:
class ExeJSZip {
// 用于獲取url地址對(duì)應(yīng)的文件內(nèi)容
getBinaryContent(url, progressFn = () => {}) {
return new Promise((resolve, reject) => {
if (typeof url !== "string" || !/https?:/.test(url))
reject(new Error("url 參數(shù)不合法"));
JSZipUtils.getBinaryContent(url, { // JSZipUtils來(lái)自于jszip-utils這個(gè)庫(kù)
progress: progressFn,
callback: (err, data) => {
if (err) {
reject(err);
} else {
resolve(data);
}
},
});
});
}
// 遍歷Zip文件
async iterateZipFile(data, iterationFn) {
if (typeof iterationFn !== "function") {
throw new Error("iterationFn 不是函數(shù)類型");
}
let zip;
try {
zip = await JSZip.loadAsync(data); // JSZip來(lái)自于jszip這個(gè)庫(kù)
zip.forEach(iterationFn);
return zip;
} catch (error) {
throw new error();
}
}
}
2.2 在線解壓 ZIP 文件
利用 ExeJSZip 類的實(shí)例,我們就可以很容易實(shí)現(xiàn)在線解壓 ZIP 文件的功能:
html 代碼
<p>
<label>請(qǐng)輸入ZIP文件的線上地址:</label>
<input type="text" id="zipUrl" />
</p>
<button id="unzipBtn" onclick="unzipOnline()">在線解壓</button>
<p id="status"></p>
<ul id="fileList"></ul>
JS 代碼
const zipUrlEle = document.querySelector("#zipUrl");
const statusEle = document.querySelector("#status");
const fileList = document.querySelector("#fileList");
const exeJSZip = new ExeJSZip();
// 執(zhí)行在線解壓操作
async function unzipOnline() {
fileList.innerHTML = "";
statusEle.innerText = "開(kāi)始下載文件...";
const data = await exeJSZip.getBinaryContent(
zipUrlEle.value,
handleProgress
);
let items = "";
await exeJSZip.iterateZipFile(data, (relativePath, zipEntry) => {
items += `<li class=${zipEntry.dir ? "caret" : "indent"}>
${zipEntry.name}</li>`;
});
statusEle.innerText = "ZIP文件解壓成功";
fileList.innerHTML = items;
}
// 處理下載進(jìn)度
function handleProgress(progressData) {
const { percent, loaded, total } = progressData;
if (loaded === total) {
statusEle.innerText = "文件已下載,努力解壓中";
}
}
好了,在瀏覽器端如何通過(guò) JSZip 這個(gè)庫(kù)來(lái)實(shí)現(xiàn)在線解壓 ZIP 文件的功能已經(jīng)介紹完了,我們來(lái)看一下以上示例的運(yùn)行結(jié)果:

現(xiàn)在我們已經(jīng)可以在線解壓 ZIP 文件了,這時(shí)有的小伙伴可能會(huì)問(wèn),能否預(yù)覽解壓后的文件呢?答案是可以的,因?yàn)?JSZip 這個(gè)庫(kù)為我們提供了 file API,通過(guò)這個(gè) API 我們就可以讀取指定文件中的內(nèi)容。比如這樣使用 zip.file("amount.txt").async("arraybuffer") ,之后我們就可以執(zhí)行對(duì)應(yīng)的操作來(lái)實(shí)現(xiàn)文件預(yù)覽的功能。
需要注意的是,基于 JSZip 的方案并不是完美的,它存在一些限制。比如它不支持解壓加密的 ZIP 文件,當(dāng)解壓較大的文件時(shí),在 IE 10 以下的瀏覽器可能會(huì)出現(xiàn)閃退問(wèn)題。此外,它還有一些其它的限制,這里阿寶哥就不詳細(xì)說(shuō)明了。感興趣的小伙伴,可以閱讀 Limitations of JSZip 文章中的相關(guān)內(nèi)容。
既然瀏覽器解壓方案存在一些弊端,特別是在線解壓大文件的情形,要解決該問(wèn)題,我們可以考慮使用服務(wù)器解壓方案。
三、服務(wù)器解壓方案
服務(wù)器解壓方案就是允許用戶通過(guò)文件 ID 或文件名進(jìn)行在線解壓,接下來(lái)阿寶哥將基于 koa 和 node-stream-zip 這兩個(gè)庫(kù)來(lái)介紹如何實(shí)現(xiàn)服務(wù)器在線解壓 ZIP 文件的功能。如果你對(duì) koa 還不了解的話,建議你先大致閱讀一下 koa 的官方文檔。
const path = require("path");
const Koa = require("koa");
const cors = require("@koa/cors");
const Router = require("@koa/router");
const StreamZip = require("node-stream-zip");
const app = new Koa();
const router = new Router();
const ZIP_HOME = path.join(__dirname, "zip"); // ZIP文件的根目錄
const UnzipCaches = new Map(); // 保存已解壓的文件信息
router.get("/", async (ctx) => {
ctx.body = "服務(wù)端在線解壓ZIP文件示例(阿寶哥)";
});
// 注冊(cè)中間件
app.use(cors());
app.use(router.routes()).use(router.allowedMethods());
app.listen(3000, () => {
console.log("app starting at port 3000");
});
在以上代碼中,我們使用了 @koa/cors 和 @koa/router 兩個(gè)中間件并創(chuàng)建了一個(gè)簡(jiǎn)單的 Koa 應(yīng)用程序。基于上述的代碼,我們來(lái)注冊(cè)一個(gè)用于處理在線解壓指定文件名的路由。
3.1 根據(jù)文件名解壓指定 ZIP 文件
app.js
router.get("/unzip/:name", async (ctx) => {
const fileName = ctx.params.name;
let filteredEntries;
try {
if (UnzipCaches.has(fileName)) { // 優(yōu)先從緩存中獲取
filteredEntries = UnzipCaches.get(fileName);
} else {
const zip = new StreamZip.async({ file: path.join(ZIP_HOME, fileName) });
const entries = await zip.entries();
filteredEntries = Object.values(entries).map((entry) => {
return {
name: entry.name,
size: entry.size,
dir: entry.isDirectory,
};
});
await zip.close();
UnzipCaches.set(fileName, filteredEntries);
}
ctx.body = {
status: "success",
entries: filteredEntries,
};
} catch (error) {
ctx.body = {
status: "error",
msg: `在線解壓${fileName}文件失敗`,
};
}
});
在以上代碼中,我們通過(guò) ZIP_HOME 和 fileName 獲得文件的最終路徑,然后使用 StreamZip 對(duì)象來(lái)執(zhí)行解壓操作。為了避免重復(fù)執(zhí)行解壓操作,阿寶哥定義了一個(gè) UnzipCaches 緩存對(duì)象,用來(lái)保存已解壓的文件信息。定義好上述路由,下面我們來(lái)驗(yàn)證一下對(duì)應(yīng)的功能。
3.2 在線解壓 ZIP 文件
html 代碼
<p>
<label>請(qǐng)輸入ZIP文件名:</label>
<input type="text" id="fileName" value="kl_161828427993677" />
</p>
<button id="unzipBtn" onclick="unzipOnline()">在線解壓</button>
<p id="status"></p>
<ul id="fileList"></ul>
JS 代碼
const fileList = document.querySelector("#fileList");
const fileNameEle = document.querySelector("#fileName");
const request = axios.create({
baseURL: "http://localhost:3000/",
timeout: 10000,
});
async function unzipOnline() {
const fileName = fileNameEle.value;
if(!fileName) return;
const response = await request.get(`unzip/${fileName}`);
if (response.data && response.data.status === "success") {
const entries = response.data.entries;
let items = "";
entries.forEach((zipEntry) => {
items += `<li class=${zipEntry.dir ? "caret" : "indent"}>${
zipEntry.name
}</li>`;
});
fileList.innerHTML = items;
}
}
以上示例成功運(yùn)行后的結(jié)果如下圖所示:

現(xiàn)在我們已經(jīng)實(shí)現(xiàn)根據(jù)文件名解壓指定 ZIP 文件,那么我們可以預(yù)覽壓縮文件中指定路徑的文件么?答案也是可以的,利用 zip 對(duì)象提供的 entryData(entry: string | ZipEntry): Promise<Buffer> 方法就可以讀取指定路徑下文件的內(nèi)容。
3.3 預(yù)覽 ZIP 文件中指定路徑的文件
app.js
router.get("/unzip/:name/entry", async (ctx) => {
const fileName = ctx.params.name; // ZIP壓縮文件名
const entryPath = ctx.query.path; // 文件的路徑
try {
const zip = new StreamZip.async({ file: path.join(ZIP_HOME, fileName) });
const entryData = await zip.entryData(entryPath);
await zip.close();
ctx.body = {
status: "success",
entryData: entryData,
};
} catch (error) {
ctx.body = {
status: "error",
msg: `讀取${fileName}中${entryPath}文件失敗`,
};
}
});
在以上代碼中,我們通過(guò) zip.entryData 方法來(lái)讀取指定路徑的文件內(nèi)容,它返回的是一個(gè) Buffer 對(duì)象。當(dāng)前端接收到該數(shù)據(jù)時(shí),還需要把接收到的 Buffer 對(duì)象轉(zhuǎn)換為 ArrayBuffer 對(duì)象,對(duì)應(yīng)的處理方式如下所示:
function toArrayBuffer(buf) {
let ab = new ArrayBuffer(buf.length);
let view = new Uint8Array(ab);
for (let i = 0; i < buf.length; ++i) {
view[i] = buf[i];
}
return ab;
}
定義完 toArrayBuffer 函數(shù)之后,我們就可以通過(guò)調(diào)用 app.js 定義的 API 來(lái)實(shí)現(xiàn)預(yù)覽功能,具體的代碼如下所示:
async function previewZipFile(path) {
const fileName = fileNameEle.value; // 獲取文件名
const response = await request.get(
`unzip/${fileName}/entry?path=${path}`
);
if (response.data && response.data.status === "success") {
const { entryData } = response.data;
const entryBuffer = toArrayBuffer(entryData.data);
const blob = new Blob([entryBuffer]);
// 使用URL.createObjectURL或blob.text()讀取文件信息
}
}
由于完整的示例代碼內(nèi)容比較多,阿寶哥就不放具體的代碼了。感興趣的小伙伴,可以訪問(wèn)以下地址瀏覽示例代碼。
https://gist.github.com/semlinker/3bb9634f4e4ec7b6ab4008a688583115
注意:以上代碼僅供參考,請(qǐng)根據(jù)實(shí)際業(yè)務(wù)進(jìn)行調(diào)整。
四、總結(jié)
本文阿寶哥介紹了在線解壓 ZIP 文件的兩種方案,在實(shí)際項(xiàng)目中,建議使用服務(wù)器解壓的方案。這樣不僅可以解決瀏覽器的兼容性問(wèn)題,而且也可以解決大文件在線解壓的問(wèn)題,同時(shí)也方便后期擴(kuò)展支持其它的壓縮格式。
五、參考資源
維基百科 ZIP 格式 Limitations of JSZip
