项目地址:fthux/GitZipPro。这是 GitZip Pro 源码解析系列最后一篇,收束到国际化、Popup 和多浏览器构建。

前六篇已经把 GitZip Pro 的主流程串起来了:页面注入、URL 解析、GitHub API、zip 打包、background 下载、设置页和历史统计。最后一篇收束三个辅助但很关键的部分:popup.jsi18n.jsbuild.js

它们不直接构成下载核心,但决定了扩展的完整体验:用户点击扩展图标看到什么,不同语言如何切换,以及源码如何构建成 Chrome、Firefox、Edge 可发布的扩展包。

Popup:扩展图标弹窗

popup 由 manifest.jsonaction 指定:

"action": {
  "default_popup": "popup.html"
}

对应源码是:

source/popup.html
source/popup.js

popup.js 文件不长,主要做五件事:

popup 打开

读取主题并 applyTheme()

初始化 GZP_I18N

显示 manifest 版本号

绑定设置、Star、Issue、评分按钮

检测当前 tab 是否为 GitHub 仓库页

它首先读取主题:

const result = await chrome.storage.local.get([STORAGE.THEME]);
const theme = result[STORAGE.THEME] || DEFAULTS.THEME;
applyTheme(theme);

然后初始化国际化:

await GZP_I18N.init();
GZP_I18N.applyTranslations();

再通过 chrome.runtime.getManifest().version 显示当前版本号。

Popup 如何判断当前页面状态

popup 会查询当前活动标签页:

GitZIp Pro

const [tab] = await chrome.tabs.query({
  active: true,
  currentWindow: true
});

如果当前 tab 不是 GitHub,显示“不在 GitHub 页面”。如果是 GitHub,但不是仓库文件页,则显示“不在仓库页面”。如果路径符合仓库页结构,就把状态点设置为 active,并显示当前 owner/repo。

判断逻辑和 content script 的目标一致,但 popup 并不注入页面,也不下载文件。它只是给用户一个快速状态反馈,并提供进入 options 页、项目仓库、issue 页面和商店评分页的入口。

国际化系统:i18n.js

GitZip Pro 有两套国际化资源:

source/_locales/
source/locales/

_locales 是浏览器扩展标准目录,用于扩展名称、描述等 manifest 相关文案。source/locales 则是应用内部 UI 使用的 JSON 语言包,例如 popup、options、content、background 中的文案。

真正的应用内国际化逻辑在 i18n.js

它通过立即执行函数暴露全局 API:

global.GZP_I18N = {
  init,
  t,
  setLocale,
  getCurrentLocale,
  applyTranslations,
  getTranslatedMessage,
  loadLocale,
  reloadLocale,
  SUPPORTED_LOCALES,
  DETECTED
};

支持语言定义为:

const SUPPORTED_LOCALES = ['en', 'zh-CN'];
const DEFAULT_LOCALE = 'en';

语言检测与加载

首次初始化时,initI18n() 会从 storage 中读取 gzpLocale。如果没有保存过语言,就调用 detectBrowserLocale()

detectBrowserLocale() 会把中文浏览器识别为 zh-CN,其他情况回退到 en

语言文件通过扩展资源 URL 加载:

fetch(chrome.runtime.getURL(`locales/${normalized}.json`))

如果目标语言加载失败,再回退到英文。这样即使某个语言包缺失,界面也不会完全失去文案。

t() 与 applyTranslations()

t(key, vars) 支持点路径读取:

GZP_I18N.t('popup.active_on', { owner, repo })

内部会把 key 按 . 拆开,在语言 JSON 中逐层查找。如果文案里有变量,例如 {owner},则用传入的 vars 替换。

applyTranslations() 则负责扫描 DOM:

[data-i18n]
[data-i18n-html]
[data-i18n-placeholder]
[data-i18n-title]
[data-i18n-value]

这解释了为什么 popup 和 options 的 HTML 中可以只写 data-i18n,然后在 JS 初始化后统一替换文案。

语言切换时,setLocale(locale) 会保存 storage、重新加载语言包、调用 applyTranslations(),并派发:

document.dispatchEvent(new CustomEvent('gzp-locale-changed', {
  detail: { locale }
}));

这样其他脚本也可以响应语言变化。

background 中的国际化

background 没有普通页面 DOM,所以它不会使用 applyTranslations()。它会调用 GZP_I18N.loadLocale(locale) 取得语言对象,然后自己实现一个 t(key, vars)backgroundTranslations 中取值。

这个设计让右键菜单和系统通知也能使用同一套语言 JSON。

整体国际化关系如下:

source/locales/en.json
source/locales/zh-CN.json

i18n.js

popup.js
applyTranslations()

options_init.js
applyTranslations()

content.js
GZP_I18N.t()

background.js
loadLocale() + 自己的 t()

chrome.storage.local: gzpLocale

构建入口:package.json

GitZip Pro 的构建命令定义在 package.json

"scripts": {
  "build": "node build.js",
  "build:chrome": "node build.js --channel=chrome",
  "build:firefox": "node build.js --channel=firefox",
  "build:edge": "node build.js --channel=edge"
}

这说明项目支持按渠道构建。默认渠道是 Chrome,也可以显式指定 Firefox 或 Edge。

构建依赖包括:

terser
clean-css
html-minifier

分别用于 JS、CSS、HTML 压缩。

build.js 的处理流程

build.js 开头定义了构建目录和渠道:

const SOURCE_DIR = 'source';
const BUILD_ROOT_DIR = 'build';
const VALID_STORE_CHANNELS = new Set(['chrome', 'firefox', 'edge']);
const BUILD_DIR = path.join(BUILD_ROOT_DIR, STORE_CHANNEL);

resolveStoreChannel() 会从命令行参数 --channel= 或环境变量 GZP_STORE_CHANNEL 中读取渠道。如果没有传入,则默认 chrome

构建开始时,脚本会清空当前渠道的 build 目录:

fs.rmSync(BUILD_DIR, { recursive: true, force: true });

然后复制 icons,再遍历 source

walkDir(SOURCE_DIR, (filePath, relativePath) => {
  tasks.push(processFile(filePath, relativePath));
});

不同文件类型进入不同处理函数:

.js    -> processJavaScript()
.css   -> processCSS()
.html  -> processHTML()
.json  -> processJSON()
其他   -> 直接复制

JS、CSS、HTML 与 JSON 处理

JavaScript 通过 Terser 压缩。jszip.min.js 已经是压缩文件,所以直接复制。

constants.js 有一个特殊处理:

code = code.replaceAll('__GZP_STORE_CHANNEL__', STORE_CHANNEL);

这会把源码中的商店渠道占位符替换成当前构建渠道,从而让扩展在运行时知道自己是 Chrome、Firefox 还是 Edge 版本。

CSS 使用 CleanCSS 压缩,HTML 使用 html-minifier 压缩。

JSON 处理主要针对 manifest.json。对于 Chrome 和 Edge 这种 Chrome-like 渠道,如果 manifest 是 MV3 且存在 background 配置,构建脚本会删除:

delete json.background.scripts;

这是为了适配不同浏览器对 background 字段的要求。

多渠道构建图

chrome

firefox

edge

source/

build.js

STORE_CHANNEL

build/chrome

build/firefox

build/edge

压缩 JS/CSS/HTML

处理 manifest.json

替换 constants.js 渠道占位符

复制 icons / locales / 静态资源

构建完成后,build/{channel} 目录就是对应商店可以加载或打包的扩展源码。

系列总结

到这里,GitZip Pro 的源码主线已经完整闭环:

manifest.json
  -> 注入 constants / i18n / jszip / downloader / content
  -> content.js 改造 GitHub 文件列表
  -> downloader.js 解析 URL、请求 API、生成 zip
  -> background.js 调用 downloads API 并保存历史
  -> options.js 管理配置、token、历史和统计
  -> popup.js 提供轻量状态入口
  -> build.js 构建多浏览器扩展包

如果把 GitZip Pro 抽象成一句话,它的源码结构就是:

浏览器扩展装配层 + GitHub 页面适配层 + GitHub API 下载层 + ZIP 打包层 + 后台下载层 + 设置与历史数据层

这一系列从入口到运行链路再到构建流程,已经覆盖了 GitZip Pro 的主要源码结构。后续如果继续深挖,可以单独挑 content.js 的 GitHub DOM 适配、downloader.js 的 symlink 和忽略规则、options.js 的 OAuth 流程做更细粒度的函数级解析。

系列文章对应的完整源码在 fthux/GitZipPro。如果 GitZip Pro 对你有帮助,欢迎 Star、试用或继续基于源码学习浏览器扩展开发。

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

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐