JavaScript 如何在線解壓 ZIP 文件?
本文轉載自微信公眾號「全棧修仙之路」,作者阿寶哥。轉載本文請聯(lián)系全棧修仙之路公眾號。
相信大家對 ZIP 文件都不會陌生,當你要打開本地的 ZIP 文件時,你就需要先安裝支持解壓 ZIP 文件的解壓軟件。但如果預解壓的 ZIP 文件在服務器上,我們應該如何處理呢?
最簡單的一種方案就是把文件下載到本地,然后使用支持 ZIP 格式的解壓軟件進行解壓。那么能不能在線解壓 ZIP 文件呢?答案是可以的,接下來阿寶哥將介紹瀏覽器解壓和服務器解壓兩種在線解壓 ZIP 文件的方案。
在介紹在線解壓 ZIP 文件的兩種方案前,我們先來簡單了解一下 ZIP 文件格式。
一、ZIP 格式簡介
ZIP 文件格式是一種數(shù)據(jù)壓縮和文檔儲存的文件格式,原名 Deflate,發(fā)明者為菲爾·卡茨(Phil Katz),他于 1989 年 1 月公布了該格式的資料。ZIP 通常使用后綴名 “.zip”,它的 MIME 格式為 “application/zip”。目前,ZIP 格式屬于幾種主流的壓縮格式之一,其競爭者包括RAR 格式以及開放源碼的 7z 格式。
ZIP 是一種相當簡單的分別壓縮每個文件的存檔格式,分別壓縮文件允許不必讀取另外的數(shù)據(jù)而檢索獨立的文件。理論上,這種格式允許對不同的文件使用不同的算法。然而,在實際上,ZIP 大多數(shù)都是在使用卡茨(Katz)的 DEFLATE 算法。
簡單介紹完 ZIP 格式,接下來阿寶哥先來介紹基于 JSZip 這個庫的瀏覽器解壓方案。
二、瀏覽器解壓方案
JSZip 是一個用于創(chuàng)建、讀取和編輯 .zip 文件的 JavaScript 庫,該庫支持大多數(shù)瀏覽器,具體的兼容性如下圖所示:
其實有了 JSZip 這個庫的幫助,要實現(xiàn)瀏覽器端在線解壓 ZIP 文件的功能并不難。因為官方已經為我們提供了 解壓本地文件、解壓遠程文件和生成 ZIP 文件 的完整示例。好的,廢話不多說,下面我們來一步步實現(xiàn)在線解壓 ZIP 文件的功能。
2.1 定義工具類
瀏覽器端在線解壓 ZIP 文件的功能,可以拆分為 下載 ZIP 文件、解析 ZIP 文件和展示 ZIP 文件 3 個小功能。考慮到功能復用性,阿寶哥把下載 ZIP 文件和解析 ZIP 文件的邏輯封裝在 ExeJSZip 類中:
- class ExeJSZip {
- // 用于獲取url地址對應的文件內容
- getBinaryContent(url, progressFn = () => {}) {
- return new Promise((resolve, reject) => {
- if (typeof url !== "string" || !/https?:/.test(url))
- reject(new Error("url 參數(shù)不合法"));
- JSZipUtils.getBinaryContent(url, { // JSZipUtils來自于jszip-utils這個庫
- 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來自于jszip這個庫
- zip.forEach(iterationFn);
- return zip;
- } catch (error) {
- throw new error();
- }
- }
- }
2.2 在線解壓 ZIP 文件
利用 ExeJSZip 類的實例,我們就可以很容易實現(xiàn)在線解壓 ZIP 文件的功能:
html 代碼
- <p>
- <label>請輸入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 = "開始下載文件...";
- 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;
- }
- // 處理下載進度
- function handleProgress(progressData) {
- const { percent, loaded, total } = progressData;
- if (loaded === total) {
- statusEle.innerText = "文件已下載,努力解壓中";
- }
- }
好了,在瀏覽器端如何通過 JSZip 這個庫來實現(xiàn)在線解壓 ZIP 文件的功能已經介紹完了,我們來看一下以上示例的運行結果:
現(xiàn)在我們已經可以在線解壓 ZIP 文件了,這時有的小伙伴可能會問,能否預覽解壓后的文件呢?答案是可以的,因為 JSZip 這個庫為我們提供了 file API,通過這個 API 我們就可以讀取指定文件中的內容。比如這樣使用 zip.file("amount.txt").async("arraybuffer") ,之后我們就可以執(zhí)行對應的操作來實現(xiàn)文件預覽的功能。
需要注意的是,基于 JSZip 的方案并不是完美的,它存在一些限制。比如它不支持解壓加密的 ZIP 文件,當解壓較大的文件時,在 IE 10 以下的瀏覽器可能會出現(xiàn)閃退問題。此外,它還有一些其它的限制,這里阿寶哥就不詳細說明了。感興趣的小伙伴,可以閱讀 Limitations of JSZip 文章中的相關內容。
既然瀏覽器解壓方案存在一些弊端,特別是在線解壓大文件的情形,要解決該問題,我們可以考慮使用服務器解壓方案。
三、服務器解壓方案
服務器解壓方案就是允許用戶通過文件 ID 或文件名進行在線解壓,接下來阿寶哥將基于 koa 和 node-stream-zip 這兩個庫來介紹如何實現(xiàn)服務器在線解壓 ZIP 文件的功能。如果你對 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 = "服務端在線解壓ZIP文件示例(阿寶哥)";
- });
- // 注冊中間件
- app.use(cors());
- app.use(router.routes()).use(router.allowedMethods());
- app.listen(3000, () => {
- console.log("app starting at port 3000");
- });
在以上代碼中,我們使用了 @koa/cors 和 @koa/router 兩個中間件并創(chuàng)建了一個簡單的 Koa 應用程序。基于上述的代碼,我們來注冊一個用于處理在線解壓指定文件名的路由。
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}文件失敗`,
- };
- }
- });
在以上代碼中,我們通過 ZIP_HOME 和 fileName 獲得文件的最終路徑,然后使用 StreamZip 對象來執(zhí)行解壓操作。為了避免重復執(zhí)行解壓操作,阿寶哥定義了一個 UnzipCaches 緩存對象,用來保存已解壓的文件信息。定義好上述路由,下面我們來驗證一下對應的功能。
3.2 在線解壓 ZIP 文件
html 代碼
- <p>
- <label>請輸入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;
- }
- }
以上示例成功運行后的結果如下圖所示:
現(xiàn)在我們已經實現(xiàn)根據(jù)文件名解壓指定 ZIP 文件,那么我們可以預覽壓縮文件中指定路徑的文件么?答案也是可以的,利用 zip 對象提供的 entryData(entry: string | ZipEntry): Promise
3.3 預覽 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}文件失敗`,
- };
- }
- });
在以上代碼中,我們通過 zip.entryData 方法來讀取指定路徑的文件內容,它返回的是一個 Buffer 對象。當前端接收到該數(shù)據(jù)時,還需要把接收到的 Buffer 對象轉換為 ArrayBuffer 對象,對應的處理方式如下所示:
- 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ù)之后,我們就可以通過調用 app.js 定義的 API 來實現(xiàn)預覽功能,具體的代碼如下所示:
- 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()讀取文件信息
- }
- }
由于完整的示例代碼內容比較多,阿寶哥就不放具體的代碼了。感興趣的小伙伴,可以訪問以下地址瀏覽示例代碼。
https://gist.github.com/semlinker/3bb9634f4e4ec7b6ab4008a688583115
注意:以上代碼僅供參考,請根據(jù)實際業(yè)務進行調整。
四、總結
本文阿寶哥介紹了在線解壓 ZIP 文件的兩種方案,在實際項目中,建議使用服務器解壓的方案。這樣不僅可以解決瀏覽器的兼容性問題,而且也可以解決大文件在線解壓的問題,同時也方便后期擴展支持其它的壓縮格式。
五、參考資源
維基百科 ZIP 格式
Limitations of JSZip