前端页面在线预览压缩包

351 阅读2分钟

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;
};