GitZip Pro 源码解析:一个 GitHub 文件/文件夹下载扩展是如何工作的(三)URL 解析与 GitHub API 请求

发布时间:2026/7/14 4:32:50
GitZip Pro 源码解析:一个 GitHub 文件/文件夹下载扩展是如何工作的(三)URL 解析与 GitHub API 请求 项目地址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 标准化成 URLurlnewURL(href,https://github.com);这里第二个参数很关键。因为从 GitHub 页面 DOM 中拿到的 href 可能是完整 URL也可能只是相对路径。使用new URL(href, https://github.com)可以统一处理这两种情况。接着函数会限制 hostif(url.hostname!github.com)returnnull;这保证下载器只处理 GitHub 链接。然后把 pathname 拆成数组constpartsurl.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,tokenAccessModeanonymous){constheaders{Accept:application/vnd.github.v3json};if(githubTokentokenAccessModecustom){headers[Authorization]token${githubToken};}returnheaders;}这里的逻辑很明确所有请求都带Accept: application/vnd.github.v3json。只有在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};然后调用fetchrespawaitfetch(fullUrl,{headers,signal});这里的signal来自AbortController用于取消下载任务。比如用户取消下载时后续请求可以被中断。githubFetch还处理了限流重试。如果返回状态是429或者是带有限流语义的403并且重试次数还没超过 3 次就会等待一段时间后再次请求constretryAfterresp.headers.get(Retry-After);constwaitretryAfter?parseInt(retryAfter)*1000:(attempt1)*2000;这个封装让上层函数不需要关心每次请求的错误处理细节。它们只要调用githubFetch()拿到 JSON 数据即可。listDir请求目录内容目录列表由listDir()完成constapiPath/repos/${owner}/${repo}/contents/${encodeURIFilePath(path)}?ref${branch};constdataawaitgithubFetch(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 处理先把主线看清楚dirfileselectedItems 中的 GitHub hrefparseGitHubUrl()owner / repo / branch / path / typetypelistDir()fetchFile()githubFetch()buildHeaders()GitHub API这个流程是下载器前半段的核心。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」版。