项目地址:fthux/GitZipPro。本文继续从源码角度拆解 GitZip Pro 的下载器实现。
上一篇分析了content.js如何进入 GitHub 页面、扫描文件列表并维护选中项。页面层最终会把一个selectedItems传给下载器。这个selectedItems里保存的是 GitHub 文件或目录链接。
从这一篇开始,源码主角变成source/downloader.js。
downloader.js的任务可以分成两半:前半段负责理解“用户选中了什么”,也就是 URL 解析和 GitHub API 请求;后半段负责“把这些内容下载下来并打包”,也就是递归收集、并发下载和 zip 生成。本篇先讲前半段。
下载器的输入
在content.js中,选中项保存在一个Map里:
selectedItems.set(row,getPathFromRow(row));也就是说,下载器拿到的不是一个已经结构化的对象,而是一组 GitHub href,例如:
/fthux/GitZipPro/tree/main/source /fthux/GitZipPro/blob/main/README.md下载器第一步要做的事情,就是把这些路径解析成后续 API 请求需要的结构:
{owner,repo,branch,path,type:'file'|'dir'}对应函数就是parseGitHubUrl()。
parseGitHubUrl:从 GitHub 链接中提取下载目标
parseGitHubUrl(href)首先会把传入的 href 标准化成 URL:
url=newURL(href,'https://github.com');这里第二个参数很关键。因为从 GitHub 页面 DOM 中拿到的 href 可能是完整 URL,也可能只是相对路径。使用new URL(href, 'https://github.com')可以统一处理这两种情况。
接着函数会限制 host:
if(url.hostname!=='github.com')returnnull;这保证下载器只处理 GitHub 链接。
然后把 pathname 拆成数组:
constparts=url.pathname.replace(/^\//,'').split('/').filter(Boolean);典型文件路径会变成:
fthux/GitZipPro/blob/main/README.md拆开后:
parts[0] = owner parts[1] = repo parts[2] = blob 或 tree parts[3] = branch parts[4...] = path如果第三段是tree,说明这是目录;如果第三段是blob,说明这是文件。代码最终返回:
return{owner,repo,branch,path,type:'dir'};或:
return{owner,repo,branch,path,type:'file'};这个函数是下载器的入口转换器。它把 GitHub 页面链接转换成 GitHub API 可以理解的参数。
下载前的设置读取
下载入口start(selectedItems, callbacks)一开始会调用getSettings()。这个函数从chrome.storage.local里读取下载相关设置:
chrome.storage.local.get([STORAGE.NAMING_PRESET,STORAGE.NAMING_CUSTOM,STORAGE.NOTIFY_SHOW,STORAGE.NOTIFY_SOUND,STORAGE.NOTIFY_OPEN,STORAGE.IGNORE_LABELS,STORAGE.IGNORE_CUSTOM_VARS,STORAGE.GITHUB_TOKEN,STORAGE.TOKEN_ACCESS_MODE],...)这里读到的设置会影响后面的流程:
- 命名规则决定 zip 文件名。
- 通知设置决定下载完成后是否提醒。
- 忽略规则决定哪些文件不进入 zip。
- GitHub token 和访问模式决定 API 请求头。
也就是说,downloader.js不是一个纯粹的网络请求模块,它会把用户配置纳入下载流程。
buildHeaders:匿名模式与 token 模式
GitHub API 可以匿名访问,但匿名访问有更严格的 rate limit。GitZip Pro 支持用户配置 token,所以请求头构造被封装成了buildHeaders():
functionbuildHeaders(githubToken='',tokenAccessMode='anonymous'){constheaders={'Accept':'application/vnd.github.v3+json'};if(githubToken&&tokenAccessMode==='custom'){headers['Authorization']=`token${githubToken}`;}returnheaders;}这里的逻辑很明确:
- 所有请求都带
Accept: application/vnd.github.v3+json。 - 只有在
custom模式且 token 存在时,才添加Authorization。 - 匿名模式不会把 token 放进请求头。
这让下载器可以统一调用 GitHub API,而不必在每个请求处重复判断 token。
githubFetch:统一 API 请求与重试
真正发起 GitHub API 请求的是githubFetch(apiPath, signal, githubToken, tokenAccessMode, attempt)。
它把相对 API 路径拼到 GitHub API base URL 上:
constfullUrl=`${GITHUB_API_BASE}${apiPath}`;然后调用fetch:
resp=awaitfetch(fullUrl,{headers,signal});这里的signal来自AbortController,用于取消下载任务。比如用户取消下载时,后续请求可以被中断。
githubFetch还处理了限流重试。如果返回状态是429,或者是带有限流语义的403,并且重试次数还没超过 3 次,就会等待一段时间后再次请求:
constretryAfter=resp.headers.get('Retry-After');constwait=retryAfter?parseInt(retryAfter)*1000:(attempt+1)*2000;这个封装让上层函数不需要关心每次请求的错误处理细节。它们只要调用githubFetch(),拿到 JSON 数据即可。
listDir:请求目录内容
目录列表由listDir()完成:
constapiPath=`/repos/${owner}/${repo}/contents/${encodeURIFilePath(path)}?ref=${branch}`;constdata=awaitgithubFetch(apiPath,signal,githubToken,tokenAccessMode);GitHub Contents API 对目录返回数组,对文件返回对象。因此listDir()会检查返回值必须是数组:
if(!Array.isArray(data)){thrownewError(`Expected directory listing for:${path}`);}这说明listDir()只负责目录场景。文件内容的读取会交给fetchFile()。
fetchFile:请求文件内容
fetchFile()根据 owner、repo、branch、path 获取单个文件内容。它同样依赖 GitHub Contents API,但文件响应中会包含编码后的内容。下载器后续会把内容转换成二进制数据,放入 zip。
本篇不展开 base64 转换和 symlink 处理,先把主线看清楚:
这个流程是下载器前半段的核心。
URL 解析与 API 请求的分工
这一篇看到的几个函数边界非常清楚:
parseGitHubUrl()只负责把 GitHub 页面 URL 解析成结构化信息。buildHeaders()只负责根据 token 设置构造请求头。githubFetch()只负责统一请求、错误处理和限流重试。listDir()只负责目录列表。fetchFile()只负责单文件内容。
这些函数组合起来,完成了从“页面链接”到“GitHub API 数据”的转换。
本篇小结
downloader.js的前半段解决的是下载前的识别问题。它从 content script 传来的 href 出发,解析出仓库、分支、路径和类型,再通过 GitHub API 获取文件或目录数据。
下一篇继续沿着downloader.js往后走:当目录数据拿到之后,GitZip Pro 如何递归收集所有文件、控制并发下载、写入 JSZip,并最终把 zip 交给 background 触发浏览器下载。
你可以在 fthux/GitZipPro 查看完整项目源码,也欢迎 Star 支持这个 GitHub 文件/文件夹下载扩展。
如果你也好奇这个 Pro 版为什么会做起来,可以接着看这篇项目缘起:用了 GitZip 这么多年,我动手做了一个「Pro」版。