1. 使用JSZip解压缩zip文件
2. 使用await zip.loadAsync(blob) 把zip包下的所有文件放到key为文件名value为文件本身的对象zipContentFiles
3. 过滤出zipContentFiles的html、js、css文件,放到相应数组
4. 先判断html文件数组是否有值(目前规则压缩包首层只有一个html根文件),若无,则结束程序,并提示用户压缩包内容不正确
5. 接下来循环zipContentFiles对象,根据指定的一些文件类型(如html=>text/html,mp4=>video/mp4等等)使用new Blob([await fileValue.async("blob")], { type: getFileType(fileKey), })生成对应格式的blob,再通过URL.createObjectURL(blob)生成虚拟blobURL
6. 使用正则替换引入css、js文件的标签中的url、href属性值为对应资源的虚拟URL
7. await html?.async("string"); 解析index.html根文件为字符串
8. 从根文件递归找出所有的src、href、data属性进行依次使用setAttribute替换文件中的引用资源
9. new XMLSerializer().serializeToString(doc)更新index.html根文件内容
10. 把根文件生成新的虚拟blob并进行return,使用iframe进行展示
import JSZip from "jszip";
import { ElMessage } from "@element-ui";
// 获取资源虚拟url
export const getHtmlSrcFromZip = async (blob: any) => {
const zip = new JSZip();
const zipContentFiles = (await zip.loadAsync(blob))?.files;
const resourceObj = await getRescources(zipContentFiles);
const blobs = await getBlobs(zipContentFiles);
const folderName = resourceObj.htmlFile[0].name.includes("/")
? resourceObj.htmlFile[0].name.split("/")[0] + "/"
: "";
await handleCssResources(resourceObj.cssRescources, blobs, folderName);
await handleJsResources(
resourceObj.jsRescources,
blobs,
zipContentFiles,
folderName
);
const previewSrc = await transformHtml(
resourceObj.htmlFile[0],
blobs,
zipContentFiles,
folderName
);
return previewSrc;
};
const getBlobs = async (zipContentFiles: any) => {
// 为每个解压缩的资源创建一个 Blob URL
const blobs: any = {};
const entriesZipContentFiles: any = Object.entries(zipContentFiles);
for (const [fileKey, fileValue] of entriesZipContentFiles) {
if (!fileValue.dir) {
const blob = new Blob([await fileValue.async("blob")], {
type: getFileType(fileKey),
});
blobs[fileKey] = URL.createObjectURL(blob);
}
}
return blobs;
};
const getRescources = (zipContentFiles: any) => {
const resourceObj: any = {};
[
{ keyName: "htmlFile", matchContent: "index.html" },
{ keyName: "jsRescources", matchContent: ".js" },
{ keyName: "cssRescources", matchContent: ".css" },
].forEach((item: any) => {
resourceObj[item.keyName] = Object.values(zipContentFiles)?.filter(
(file: any) => file.name?.endsWith(item.matchContent)
);
});
if (!resourceObj.htmlFile) {
return ElMessage.warning("未在zip文件中找到index.html文件!");
}
return resourceObj;
};
// 获取资源类型
const getFileType = (fileKey: string) => {
const matchType = fileKey.split(".")[1];
const fileMap: any = {
html: "text/html",
css: "text/css",
js: "text/js",
png: "image/png",
svg: "image/svg+xml",
mp4: "video/mp4",
};
return fileMap[matchType] || "";
};
const handleJsResources = async (
jsResources: any,
blobs: any,
zipContentFiles: any,
folderName: string
) => {
for (const item of jsResources) {
let jsString = await item.async("string");
const regex = /href:["'](.*?)["']/g;
let match;
const replacements = [];
while ((match = regex.exec(jsString)) !== null) {
const key = folderName + match[1];
if (!key) break;
const htmlFile = zipContentFiles[key];
const blobUrl = await transformHtml(
htmlFile,
blobs,
zipContentFiles,
folderName
);
const replacedUrl = blobUrl || blobs[key];
if (replacedUrl) {
replacements.push({
original: match[0],
replacement: `href:"${replacedUrl}"`,
});
}
}
replacements.forEach(({ original, replacement }) => {
jsString = jsString.replace(original, replacement);
});
const blob = new Blob([jsString], { type: "text/javascript" });
blobs[item.name] = URL.createObjectURL(blob);
}
};
// 处理css资源
const handleCssResources = async (
cssRescources: any,
blobs: any,
folderName: string
) => {
for (const item of cssRescources) {
let cssString = await item.async("string");
// 匹配CSS中的url()函数
const regex = /url\(["']?(.*?)["']?\)/g;
cssString = cssString.replace(regex, (match: any, p1: any) => {
const key = folderName + p1.slice(6);
const replacedUrl = blobs[key];
return `url("${replacedUrl}")`;
});
const blob = new Blob([cssString], { type: "text/css" });
blobs[item.name] = URL.createObjectURL(blob);
}
};
// 递归去给html中的资源路径转换为虚拟路径
const transformHtml = async (
html: any,
blobs: any,
zipContentFiles: any,
folderName: string,
parentHtmlSrc?: string
) => {
// 读取index.html文件内容
const htmlContent = await html?.async("string");
// 使用 Blob URL 替换 index.html 文件中的资源引用
const parser = new DOMParser();
const doc = parser.parseFromString(htmlContent, "text/html");
const elements: any = doc.querySelectorAll("[src], [href], [data]");
for (const element of elements) {
const attr = ["src", "data", "href"].find((type: any) => {
return element.hasAttribute(type);
});
const originalUrl = element.getAttribute(attr);
const link = originalUrl.startsWith("./")
? originalUrl.slice(2)
: originalUrl;
if (parentHtmlSrc?.includes(link)) break;
if (link.endsWith(".html")) {
const file = zipContentFiles[folderName + link];
const parentHtmlSrc = htmlContent.includes(link) ? html?.name : "";
const newUrl = await transformHtml(
file,
blobs,
zipContentFiles,
folderName,
parentHtmlSrc
);
blobs[folderName + link] = newUrl;
element.setAttribute(attr, newUrl);
}
const blob = blobs[folderName + link];
if (blob) {
element.setAttribute(attr, blob);
}
}
// 更新 index.html 文件内容
const indexHtmlContent = new XMLSerializer().serializeToString(doc);
// 创建一个 Blob URL,包含替换后的 index.html 文件内容
const indexHtmlBlob = new Blob([indexHtmlContent], { type: "text/html" });
const indexHtmlUrl = URL.createObjectURL(indexHtmlBlob);
return indexHtmlUrl;
};