GitZip Pro 源码解析:一个 GitHub 文件/文件夹下载扩展是如何工作的(四)递归下载、并发控制与 ZIP 打包

1785066287472

项目地址:fthux/GitZipPro。这一篇进入 GitZip Pro 最核心的下载、并发和 ZIP 打包流程。

上一篇讲到 downloader.js 如何解析 GitHub URL,并通过 GitHub API 获取文件或目录数据。这一篇继续看下载器的后半段:当它知道用户选中了哪些文件或文件夹之后,如何递归收集文件、并发下载内容、生成 zip,并把结果交给浏览器下载。

这一篇的主函数是:

async function start(selectedItems, callbacks = {})

它是 window.GZPDownloader.start() 背后的核心入口。

start 的整体流程

start() 可以拆成七个阶段:

flowchart TD A["start(selectedItems, callbacks)"] --> B["读取设置 getSettings()"] B --> C["编译忽略规则 compileIgnoreRules()"] C --> D["解析选中链接 parseGitHubUrl()"] D --> E["收集文件列表 collectFiles() / fetchFile()"] E --> F["并发下载 withConcurrency()"] F --> G["写入 JSZip"] G --> H["生成 base64 zip"] H --> I["发送 GZP_DOWNLOAD_FILE 给 background"] I --> J["chrome.downloads.download()"]

content.js 只负责把选中项交给下载器,而 start() 才是真正把这些选中项变成 zip 文件的地方。

读取下载设置

start() 开始后会先创建 AbortController

const abortCtrl = new AbortController();
const { signal } = abortCtrl;

这个 signal 会被传给后续 GitHub API 请求,用来支持取消下载。

随后它调用 getSettings() 读取配置,包括命名规则、通知开关、忽略规则、GitHub token 和 token 访问模式。如果读取失败,会使用 DEFAULTS 里的默认值。

这一步让下载器具有完整上下文。它不只是“下载文件”,还要知道怎么命名、跳过哪些文件、是否播放提示音、是否保存历史记录。

编译忽略规则

GitZip Pro 支持忽略规则,设置页中可以选择预设规则,也可以添加自定义规则。下载器里通过:

const compiledIgnoreRules = compileIgnoreRules(ignoreLabels || [], ignoreCustomVars || []);

把用户设置转换成真正可匹配的规则。

后续不管是用户直接选中了某个文件,还是递归目录时遇到了某个文件,都会调用 isIgnored(path, type, rules) 判断是否跳过。

忽略规则在两个位置生效:

  • 顶层选中项本身可能被忽略。
  • 目录递归过程中遇到的子文件或子目录可能被忽略。

这意味着忽略逻辑不是只过滤最终列表,而是参与了递归收集过程。

解析所有选中项

start() 会遍历 selectedItems

for (const [, href] of selectedItems) {
  const info = parseGitHubUrl(href);
  if (info) parsed.push(info);
}

这里忽略了 Map 的 key,只取 href。因为 key 是页面 DOM 行,下载器只关心 GitHub 路径。

如果所有 href 都无法解析,就调用错误回调:

onError && onError(new Error(t('downloader.no_valid_items')));

解析成功后,下载器会从第一个选中项中拿到 ownerrepobranch,并构造 zip 根目录:

const zipRoot = `${repo}-${branch}`;

最终写入 zip 的路径会像这样:

GitZipPro-main/source/content.js
GitZipPro-main/source/downloader.js

递归收集目录文件

当选中项是文件时,下载器直接把它加入 fileList

fileList.push({
  path: item.path,
  sizeBytes: null,
  fetch: () => fetchFile(...)
});

当选中项是目录时,下载器调用 collectFiles()

await collectFiles(
  item.owner,
  item.repo,
  item.branch,
  item.path,
  fileList,
  signal,
  0,
  compiledIgnoreRules,
  stats,
  githubToken,
  tokenAccessMode
);

collectFiles() 会先调用 listDir() 获取目录列表。遇到文件或 symlink 时,把它加入 fileList;遇到子目录时,继续递归调用自己。

这里有两个保护条件:

if (depth > 20) throw new Error(...)
if (fileList.length >= MAX_FILE_COUNT) return;

depth > 20 防止过深目录导致无限递归或过度请求;MAX_FILE_COUNT 来自 constants.js,默认最多收集 500 个文件。

fileList 的设计

fileList 里的每个元素并不马上保存文件内容,而是保存一个 fetch 函数:

{
  path: entryPath,
  sizeBytes,
  fetch: () => fetchFile(owner, repo, branch, entryPath, signal, githubToken, tokenAccessMode)
}

这个设计有一个好处:收集阶段只确定“需要下载哪些文件”,真正下载发生在后面的并发阶段。这样下载器可以先完整计算总数,再向 UI 报告进度。

当收集完成后,代码会得到:

const total = fileList.length;

这个 total 会用于进度回调:

onProgress && onProgress(0, total, ...)

并发控制 withConcurrency

如果一次性对所有文件发起请求,很容易触发 GitHub API 限流,也会让浏览器承受过大压力。所以下载器实现了一个简单的并发限制器:

async function withConcurrency(limit, tasks) {
  const results = [];
  let index = 0;

  async function worker() {
    while (index < tasks.length) {
      const current = index++;
      results[current] = await tasks[current]();
    }
  }

  const workers = Array.from({ length: Math.min(limit, tasks.length) }, () => worker());
  await Promise.all(workers);
  return results;
}

CONCURRENCY_LIMIT 来自 constants.js,当前是 5。下载器会把每个文件包装成一个 task:

const tasks = fileList.map(item => async () => {
  const bytes = await item.fetch();
  zip.file(`${zipRoot}/${item.path}`, bytes);
  completed++;
  onProgress && onProgress(completed, total, ...);
});

然后调用:

await withConcurrency(CONCURRENCY_LIMIT, tasks);

所以下载阶段同时最多跑 5 个文件请求。每完成一个文件,就写入 zip,并更新进度。

JSZip 打包

下载器创建 zip:

const zip = new JSZip();

每个文件下载完成后写入:

zip.file(`${zipRoot}/${item.path}`, bytes);

所有文件写入后,生成 base64:

const base64 = await zip.generateAsync({
  type: 'base64',
  compression: 'DEFLATE',
  compressionOptions: { level: 6 }
});

这里没有直接生成 Blob 并下载,而是把 base64 发给 background。原因是浏览器扩展里,下载动作由 background.js 统一处理,可以同时挂接通知、历史记录和下载完成监听。

zip 文件命名

下载完成后,代码会根据设置生成 zip 文件名:

下载设置中的 ZIP 命名规则与忽略规则

const template = namingCustom.trim() !== '' ? namingCustom.trim() : namingPreset;

模板支持变量:

{owner}
{repo}
{branch}
{path}
{date}
{datetime}
{ts}

例如默认规则 {repo}-{branch}-{path}_{ts} 会被替换成类似:

GitZipPro-main-source_20260710.zip

如果最终名称没有 .zip 后缀,代码会自动补上。

发送给 background 下载

zip 生成后,downloader.js 不直接调用 chrome.downloads.download(),而是发送消息:

chrome.runtime.sendMessage({
  type: 'GZP_DOWNLOAD_FILE',
  filename: zipName,
  base64,
  mimeType: 'application/zip',
  notifyShow,
  notifyOpen,
  historyRecord
});

historyRecord 里包含仓库、分支、路径、文件列表、文件数量、被忽略数量等信息。后续 background 会在下载完成后保存它。

完整闭环如下:

sequenceDiagram participant C as content.js participant D as downloader.js participant Z as JSZip participant B as background.js participant Browser as chrome.downloads C->>D: start(selectedItems, callbacks) D->>D: parseGitHubUrl / collectFiles D->>D: withConcurrency 下载文件 D->>Z: zip.file(path, bytes) Z-->>D: base64 zip D->>B: GZP_DOWNLOAD_FILE B->>Browser: downloads.download(dataUrl)

本篇小结

downloader.js 的后半段完成了 GitZip Pro 最核心的功能:从选中项生成 zip。它的关键路径是:

start()
  -> getSettings()
  -> parseGitHubUrl()
  -> collectFiles()
  -> withConcurrency()
  -> JSZip.generateAsync()
  -> chrome.runtime.sendMessage(GZP_DOWNLOAD_FILE)

下一篇转到 background.js,看 background 如何接收下载消息、调用浏览器下载 API、维护右键菜单、监听下载完成,并把历史记录写入 storage。

完整源码和扩展说明见 fthux/GitZipPro。如果你觉得这个实现有参考价值,欢迎 Star GitZip Pro。

如果你也好奇这个 Pro 版为什么会做起来,可以接着看这篇项目缘起:用了 GitZip 这么多年,我动手做了一个「Pro」版

posted on 2026-08-04 22:36  fthux  阅读(0)  评论(0)    收藏  举报