一、ZIP 格式簡介
ZIP 文件格式是一種數據壓縮和文檔儲存的文件格式,原名 Deflate,發明者為菲爾·卡茨(Phil Katz),他於 1989 年 1 月公布了該格式的資料。ZIP 通常使用后綴名 “.zip”,它的 MIME 格式為 “application/zip”。目前,ZIP 格式屬於幾種主流的壓縮格式之一,其競爭者包括RAR 格式以及開放源碼的 7z 格式。
ZIP 是一種相當簡單的分別壓縮每個文件的存檔格式,分別壓縮文件允許不必讀取另外的數據而檢索獨立的文件。理論上,這種格式允許對不同的文件使用不同的算法。然而,在實際上,ZIP 大多數都是在使用卡茨(Katz)的 DEFLATE 算法。
簡單介紹完 ZIP 格式,接下來阿寶哥先來介紹基於jsZip這個庫的瀏覽器解壓方案。
二、瀏覽器解壓方案
jsZip 是一個用於創建、讀取和編輯.zip文件的 JavaScript 庫,該庫支持大多數瀏覽器,具體的兼容性如下圖所示:
其實有了 JSZip 這個庫的幫助,要實現瀏覽器端在線解壓 ZIP 文件的功能並不難。因為官方已經為我們提供了解壓本地文件、解壓遠程文件和生成 ZIP 文件的完整示例。好的,廢話不多說,下面我們來一步步實現在線解壓 ZIP 文件的功能。
2.1 定義工具類
瀏覽器端在線解壓 ZIP 文件的功能,可以拆分為下載 ZIP 文件、解析 ZIP 文件和展示 ZIP 文件3 個小功能。考慮到功能復用性,阿寶哥把下載 ZIP 文件和解析 ZIP 文件的邏輯封裝在ExeJSZip類中:
classExeJSZip{
//用於獲取url地址對應的文件內容
getBinaryContent(url,progressFn=()=>{}){
returnnewPromise((resolve,reject)=>{
if(typeofurl!=="string"||!/https?:/.test(url))
reject(newError("url參數不合法"));
JSZipUtils.getBinaryContent(url,{//JSZipUtils來自於jszip-utils這個庫
progress:progressFn,
callback:(err,data)=>{
if(err){
reject(err);
}else{
resolve(data);
}
},
});
});
}
//遍歷Zip文件
asynciterateZipFile(data,iterationFn){
if(typeofiterationFn!=="function"){
thrownewError("iterationFn不是函數類型");
}
letzip;
try{
zip=awaitJSZip.loadAsync(data);//JSZip來自於jszip這個庫
zip.forEach(iterationFn);
returnzip;
}catch(error){
thrownewerror();
}
}
}
2.2 在線解壓 ZIP 文件
利用ExeJSZip類的實例,我們就可以很容易實現在線解壓 ZIP 文件的功能:
html 代碼
<p>
<label>請輸入ZIP文件的線上地址:</label>
<inputtype="text"id="zipUrl"/>
</p>
<buttonid="unzipBtn"onclick="unzipOnline()">在線解壓</button>
<pid="status"></p>
<ulid="fileList"></ul>
JS 代碼
constzipUrlEle=document.querySelector("#zipUrl");
conststatusEle=document.querySelector("#status");
constfileList=document.querySelector("#fileList");
constexeJSZip=newExeJSZip();
//執行在線解壓操作
asyncfunctionunzipOnline(){
fileList.innerhtml="";
statusEle.innerText="開始下載文件...";
constdata=awaitexeJSZip.getBinaryContent(
zipUrlEle.value,
handleProgress
);
letitems="";
awaitexeJSZip.iterateZipFile(data,(relativePath,zipEntry)=>{
items+=`<liclass=${zipEntry.dir?"caret":"indent"}>
${zipEntry.name}</li>`;
});
statusEle.innerText="ZIP文件解壓成功";
fileList.innerHTML=items;
}
//處理下載進度
functionhandleProgress(progressData){
const{percent,loaded,total}=progressData;
if(loaded===total){
statusEle.innerText="文件已下載,努力解壓中";
}
}
好了,在瀏覽器端如何通過 JSZip 這個庫來實現在線解壓 ZIP 文件的功能已經介紹完了,我們來看一下以上示例的運行結果:
現在我們已經可以在線解壓 ZIP 文件了,這時有的小伙伴可能會問,能否預覽解壓后的文件呢?答案是可以的,因為 JSZip 這個庫為我們提供了fileAPI,通過這個 API 我們就可以讀取指定文件中的內容。比如這樣使用zip.file("amount.txt").async("arraybuffer"),之后我們就可以執行對應的操作來實現文件預覽的功能。
需要注意的是,基於 JSZip 的方案並不是完美的,它存在一些限制。比如它不支持解壓加密的 ZIP 文件,當解壓較大的文件時,在 IE 10 以下的瀏覽器可能會出現閃退問題。此外,它還有一些其它的限制,這里阿寶哥就不詳細說明了。感興趣的小伙伴,可以閱讀Limitations of JSZip文章中的相關內容。
既然瀏覽器解壓方案存在一些弊端,特別是在線解壓大文件的情形,要解決該問題,我們可以考慮使用服務器解壓方案。
https://www.98891.com/article-76-1.html
三、服務器解壓方案
服務器解壓方案就是允許用戶通過文件 ID 或文件名進行在線解壓,接下來阿寶哥將基於 koa 和 node-stream-zip 這兩個庫來介紹如何實現服務器在線解壓 ZIP 文件的功能。如果你對 koa 還不了解的話,建議你先大致閱讀一下 koa 的官方文檔。
constpath=require("path");
constKoa=require("koa");
constcors=require("@koa/cors");
constRouter=require("@koa/router");
constStreamZip=require("node-stream-zip");
constapp=newKoa();
constrouter=newRouter();
constZIP_HOME=path.join(__dirname,"zip");//ZIP文件的根目錄
constUnzipCaches=newMap();//保存已解壓的文件信息
router.get("/",async(ctx)=>{
ctx.body="服務端在線解壓ZIP文件示例(阿寶哥)";
});
//注冊中間件
app.use(cors());
app.use(router.routes()).use(router.allowedMethods());
app.listen(3000,()=>{
console.log("appstartingatport3000");
});
在以上代碼中,我們使用了@koa/cors和@koa/router兩個中間件並創建了一個簡單的 Koa 應用程序。基於上述的代碼,我們來注冊一個用於處理在線解壓指定文件名的路由。
3.1 根據文件名解壓指定 ZIP 文件
app.js
router.get("/unzip/:name",async(ctx)=>{
constfileName=ctx.params.name;
letfilteredEntries;
try{
if(UnzipCaches.has(fileName)){//優先從緩存中獲取
filteredEntries=UnzipCaches.get(fileName);
}else{
constzip=newStreamZip.async({file:path.join(ZIP_HOME,fileName)});
constentries=awaitzip.entries();
filteredEntries=Object.values(entries).map((entry)=>{
return{
name:entry.name,
size:entry.size,
dir:entry.isDirectory,
};
});
awaitzip.close();
UnzipCaches.set(fileName,filteredEntries);
}
ctx.body={
status:"success",
entries:filteredEntries,
};
}catch(error){
ctx.body={
status:"error",
msg:`在線解壓${fileName}文件失敗`,
};
}
});
在以上代碼中,我們通過ZIP_HOME和fileName獲得文件的最終路徑,然后使用StreamZip對象來執行解壓操作。為了避免重復執行解壓操作,阿寶哥定義了一個UnzipCaches緩存對象,用來保存已解壓的文件信息。定義好上述路由,下面我們來驗證一下對應的功能。
3.2 在線解壓 ZIP 文件
html 代碼
<p>
<label>請輸入ZIP文件名:</label>
<inputtype="text"id="fileName"value="kl_161828427993677"/>
</p>
<buttonid="unzipBtn"onclick="unzipOnline()">在線解壓</button>
<pid="status"></p>
<ulid="fileList"></ul>
JS 代碼
constfileList=document.querySelector("#fileList");
constfileNameEle=document.querySelector("#fileName");
constrequest=axios.create({
baseURL:"http://localhost:3000/",
timeout:10000,
});
asyncfunctionunzipOnline(){
constfileName=fileNameEle.value;
if(!fileName)return;
constresponse=awaitrequest.get(`unzip/${fileName}`);
if(response.data&&response.data.status==="success"){
constentries=response.data.entries;
letitems="";
entries.forEach((zipEntry)=>{
items+=`<liclass=${zipEntry.dir?"caret":"indent"}>${
zipEntry.name
}</li>`;
});
fileList.innerHTML=items;
}
}
以上示例成功運行后的結果如下圖所示:
現在我們已經實現根據文件名解壓指定 ZIP 文件,那么我們可以預覽壓縮文件中指定路徑的文件么?答案也是可以的,利用zip對象提供的entryData(entry: string | ZipEntry): Promise<Buffer>方法就可以讀取指定路徑下文件的內容。
3.3 預覽 ZIP 文件中指定路徑的文件
app.js
router.get("/unzip/:name/entry",async(ctx)=>{
constfileName=ctx.params.name;//ZIP壓縮文件名
constentryPath=ctx.query.path;//文件的路徑
try{
constzip=newStreamZip.async({file:path.join(ZIP_HOME,fileName)});
constentryData=awaitzip.entryData(entryPath);
awaitzip.close();
ctx.body={
status:"success",
entryData:entryData,
};
}catch(error){
ctx.body={
status:"error",
msg:`讀取${fileName}中${entryPath}文件失敗`,
};
}
});
在以上代碼中,我們通過zip.entryData方法來讀取指定路徑的文件內容,它返回的是一個Buffer對象。當前端接收到該數據時,還需要把接收到的Buffer對象轉換為ArrayBuffer對象,對應的處理方式如下所示:
functiontoArrayBuffer(buf){
letab=newArrayBuffer(buf.length);
letview=newUint8Array(ab);
for(leti=0;i<buf.length;++i){
view[i]=buf[i];
}
returnab;
}
定義完toArrayBuffer函數之后,我們就可以通過調用app.js定義的 API 來實現預覽功能,具體的代碼如下所示:
asyncfunctionpreviewZipFile(path){
constfileName=fileNameEle.value;//獲取文件名
constresponse=awaitrequest.get(
`unzip/${fileName}/entry?path=${path}`
);
if(response.data&&response.data.status==="success"){
const{entryData}=response.data;
constentryBuffer=toArrayBuffer(entryData.data);
constblob=newBlob([entryBuffer]);
//使用URL.createObjectURL或blob.text()讀取文件信息
}
}