GitZip Pro 源码解析:一个 GitHub 文件/文件夹下载扩展是如何工作的(四)递归下载、并发控制与 ZIP 打包
项目地址fthux/GitZipPro。这一篇进入 GitZip Pro 最核心的下载、并发和 ZIP 打包流程。上一篇讲到downloader.js如何解析 GitHub URL并通过 GitHub API 获取文件或目录数据。这一篇继续看下载器的后半段当它知道用户选中了哪些文件或文件夹之后如何递归收集文件、并发下载内容、生成 zip并把结果交给浏览器下载。这一篇的主函数是asyncfunctionstart(selectedItems,callbacks{})它是window.GZPDownloader.start()背后的核心入口。start 的整体流程start()可以拆成七个阶段start(selectedItems, callbacks)读取设置 getSettings()编译忽略规则 compileIgnoreRules()解析选中链接 parseGitHubUrl()收集文件列表 collectFiles() / fetchFile()并发下载 withConcurrency()写入 JSZip生成 base64 zip发送 GZP_DOWNLOAD_FILE 给 backgroundchrome.downloads.download()content.js只负责把选中项交给下载器而start()才是真正把这些选中项变成 zip 文件的地方。读取下载设置start()开始后会先创建AbortControllerconstabortCtrlnewAbortController();const{signal}abortCtrl;这个signal会被传给后续 GitHub API 请求用来支持取消下载。随后它调用getSettings()读取配置包括命名规则、通知开关、忽略规则、GitHub token 和 token 访问模式。如果读取失败会使用DEFAULTS里的默认值。这一步让下载器具有完整上下文。它不只是“下载文件”还要知道怎么命名、跳过哪些文件、是否播放提示音、是否保存历史记录。编译忽略规则GitZip Pro 支持忽略规则设置页中可以选择预设规则也可以添加自定义规则。下载器里通过constcompiledIgnoreRulescompileIgnoreRules(ignoreLabels||[],ignoreCustomVars||[]);把用户设置转换成真正可匹配的规则。后续不管是用户直接选中了某个文件还是递归目录时遇到了某个文件都会调用isIgnored(path, type, rules)判断是否跳过。忽略规则在两个位置生效顶层选中项本身可能被忽略。目录递归过程中遇到的子文件或子目录可能被忽略。这意味着忽略逻辑不是只过滤最终列表而是参与了递归收集过程。解析所有选中项start()会遍历selectedItemsfor(const[,href]ofselectedItems){constinfoparseGitHubUrl(href);if(info)parsed.push(info);}这里忽略了 Map 的 key只取 href。因为 key 是页面 DOM 行下载器只关心 GitHub 路径。如果所有 href 都无法解析就调用错误回调onErroronError(newError(t(downloader.no_valid_items)));解析成功后下载器会从第一个选中项中拿到owner、repo、branch并构造 zip 根目录constzipRoot${repo}-${branch};最终写入 zip 的路径会像这样GitZipPro-main/source/content.js GitZipPro-main/source/downloader.js递归收集目录文件当选中项是文件时下载器直接把它加入fileListfileList.push({path:item.path,sizeBytes:null,fetch:()fetchFile(...)});当选中项是目录时下载器调用collectFiles()awaitcollectFiles(item.owner,item.repo,item.branch,item.path,fileList,signal,0,compiledIgnoreRules,stats,githubToken,tokenAccessMode);collectFiles()会先调用listDir()获取目录列表。遇到文件或 symlink 时把它加入fileList遇到子目录时继续递归调用自己。这里有两个保护条件if(depth20)thrownewError(...)if(fileList.lengthMAX_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 报告进度。当收集完成后代码会得到consttotalfileList.length;这个total会用于进度回调onProgressonProgress(0,total,...)并发控制 withConcurrency如果一次性对所有文件发起请求很容易触发 GitHub API 限流也会让浏览器承受过大压力。所以下载器实现了一个简单的并发限制器asyncfunctionwithConcurrency(limit,tasks){constresults[];letindex0;asyncfunctionworker(){while(indextasks.length){constcurrentindex;results[current]awaittasks[current]();}}constworkersArray.from({length:Math.min(limit,tasks.length)},()worker());awaitPromise.all(workers);returnresults;}CONCURRENCY_LIMIT来自constants.js当前是 5。下载器会把每个文件包装成一个 taskconsttasksfileList.map(itemasync(){constbytesawaititem.fetch();zip.file(${zipRoot}/${item.path},bytes);completed;onProgressonProgress(completed,total,...);});然后调用awaitwithConcurrency(CONCURRENCY_LIMIT,tasks);所以下载阶段同时最多跑 5 个文件请求。每完成一个文件就写入 zip并更新进度。JSZip 打包下载器创建 zipconstzipnewJSZip();每个文件下载完成后写入zip.file(${zipRoot}/${item.path},bytes);所有文件写入后生成 base64constbase64awaitzip.generateAsync({type:base64,compression:DEFLATE,compressionOptions:{level:6}});这里没有直接生成 Blob 并下载而是把 base64 发给 background。原因是浏览器扩展里下载动作由background.js统一处理可以同时挂接通知、历史记录和下载完成监听。zip 文件命名下载完成后代码会根据设置生成 zip 文件名consttemplatenamingCustom.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 会在下载完成后保存它。完整闭环如下chrome.downloadsbackground.jsJSZipdownloader.jscontent.jschrome.downloadsbackground.jsJSZipdownloader.jscontent.jsstart(selectedItems, callbacks)parseGitHubUrl / collectFileswithConcurrency 下载文件zip.file(path, bytes)base64 zipGZP_DOWNLOAD_FILEdownloads.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」版。