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

项目地址:fthux/GitZipPro。这一篇进入 GitZip Pro 最核心的下载、并发和 ZIP 打包流程。
上一篇讲到 downloader.js 如何解析 GitHub URL,并通过 GitHub API 获取文件或目录数据。这一篇继续看下载器的后半段:当它知道用户选中了哪些文件或文件夹之后,如何递归收集文件、并发下载内容、生成 zip,并把结果交给浏览器下载。
这一篇的主函数是:
async function start(selectedItems, callbacks = {})
它是 window.GZPDownloader.start() 背后的核心入口。
start 的整体流程
start() 可以拆成七个阶段:
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')));
解析成功后,下载器会从第一个选中项中拿到 owner、repo、branch,并构造 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 文件名:

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 会在下载完成后保存它。
完整闭环如下:
本篇小结
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」版。
真正精彩的部分来了:一个文件夹可能有几十上百个文件,GitZip Pro 要递归遍历、过滤忽略项、控制并发、逐个下载,再塞进 JSZip。一个看似简单的 zip,背后是一条完整的下载流水线。
浙公网安备 33010602011771号