Web-Dev-For-Beginners 的 Carbon Trigger 浏览器扩展:完整代码构建、Edge 加载与源码运行原理剖析
本文基于本课程仓库 5-browser-extension 模块中已完成的 Carbon Trigger 浏览器扩展工程(对应文档为 5-browser-extension/solution/README.md 及其多语言翻译版本),完整讲解如何从源码构建一个可用的碳强度提示扩展:你既能按「npm 构建 → Edge 加载 → 填写 API Key」的路径立即跑通它,也能通过阅读 src/index.js、manifest.json 与 background.js 理解它背后的 Manifest V3 架构、CO2 Signal API 调用链与动态图标机制。读完你将掌握浏览器扩展「界面表单 + 本地存储 + 后台 Service Worker」的完整实现范式。
一、扩展解决的问题:把区域电网的碳强度带进浏览器
Carbon Trigger 使用 tmrow 提供的 CO2 Signal API 追踪电力消耗数据,制作成一个运行在浏览器中的「碳提醒器」:它会实时告诉你当前所在区域电网的电力使用有多「重」,从而帮助你基于数据对高耗电活动做出判断。例如,当区域用电处于高峰、碳强度较高时,可以推迟运行烘干机这类高碳排活动。
其核心交互是一套「彩色圆点」图标系统——圆点颜色随区域碳强度变化,提示当前时段是否适合执行高能耗任务。这一设计概念源自面向加州排放量的 Energy Lollipop 浏览器扩展(这一点在 background.js 源码注释与模块 5-browser-extension/README.md 的致谢部分均有明确说明)。
在更宏观的课程层面,该扩展属于本仓库「Web-Dev-For-Beginners」课程的第 5 个模块「Building a browser extension」,整套课程的定位是 24 Lessons / 12 Weeks 的 Web 开发者入门路径。模块配套的三节理论课分别是:1-about-browsers、2-forms-browsers-local-storage 与 3-background-tasks-and-performance,依次覆盖浏览器原理、表单与本地存储、后台任务与性能评估——这些正是 Carbon Trigger 实现中用到的全部关键技术。
二、工程结构与运行环境
2.1 需要的前置条件
仓库的 package.json 明确声明了运行环境要求:
| 环境 | 最低版本 |
|---|---|
| Node.js | >= 18.0.0 |
| npm | >= 9.0.0 |
工程名为 carbon-trigger-extension,许可协议为 MIT。代码采用 ES Module 编写,由 webpack 负责打包。
2.2 依赖与脚本
依赖分为两类:运行时依赖 axios(用于发起 CO2 Signal API 请求),开发依赖 webpack 与 webpack-cli(负责打包)。脚本定义如下:
| 脚本 | 底层命令 | 用途 |
|---|---|---|
build |
webpack |
一次性的正式构建 |
watch |
webpack --watch |
监听源码变更并持续重建(开发调试用) |
2.3 工程目录清单
solution 目录下源码与构建产物的组织方式为:
src/index.js—— 弹窗页面(popup)的全部业务逻辑源码,是 webpack 的打包入口;package.json/package-lock.json—— npm 依赖与脚本声明;translation/—— 各语言的 README 翻译(如README.ms.md、README.ja.md等);dist/—— 构建产物目录,这是最终加载进浏览器扩展管理页的目录,内含manifest.json、background.js(Service Worker)、main.js(打包后的业务脚本)、index.html(弹窗页面)、styles.css与images/资源;- 同模块还提供了未完成的练习起点工程 5-browser-extension/start,供读者对照本完整实现动手补全。
三、三步构建:install → build → Load Unpacked
3.1 安装依赖
先把本仓库的 solution 目录代码复制到本地计算机的一个文件夹中,然后在其中执行:
npm install
该命令根据 package.json 安装 axios、webpack 等全部依赖包。
3.2 用 webpack 构建扩展
npm run build
脚本等价于直接调用 webpack,将 src/index.js 等源码打包输出到 dist 目录。开发过程中若希望源码改动即时反映到产物,可改用 npm run watch 让 webpack 进入监听模式持续重建。
3.3 加载到 Edge
安装扩展时,点击浏览器右上角的「三点」菜单,找到扩展面板;在其中选择「Load Unpacked」(加载解压缩的扩展),然后在弹出的文件选择框中打开刚才构建生成的 dist 文件夹,扩展即被加载。从本仓库的 package.json 的 keywords(chrome extension、edge extension)和模块 README 看,该扩展基于 Chromium 体系开发,上述方式同样适用于 Chrome 的开发者模式。
3.4 加载后扩展在浏览器中的「身份」
构建产物中的 manifest.json 定义了扩展的运行时元信息,采用 Manifest V3 规范:
{
"manifest_version": 3,
"name": "My Carbon Trigger",
"version": "0.1.0",
"host_permissions": ["<all_urls>"],
"background": {
"service_worker": "background.js"
},
"action": {
"default_popup": "index.html"
}
}
关键字段的工程含义:
manifest_version: 3:使用新一代 Manifest V3 规范,后台逻辑以 Service Worker 形式运行;action.default_popup:点击工具栏图标时弹出的页面为index.html;background.service_worker:后台脚本background.js,负责接收弹窗发来的消息并动态绘制工具栏图标;host_permissions: ["<all_urls>"]:声明对任意站点的访问权限,使扩展能够跨域请求 CO2 Signal API。
四、使用前必备:API Key 与区域代码
要让扩展真正返回数据,需要准备两项凭证:
- CO2 Signal API 的 API Key:前往 CO2 Signal 官网页面,在输入框中填写你的邮箱,官方会通过邮件把 Key 发给你;
- 区域(Zone)代码:需要与你所在地对应的电力区域编码。区域列表可参考 electricityMap 开放平台提供的 zones 接口数据,并与地图页面上标注的电网区域相互对应。以美国波士顿为例,文档中使用的是
US-NEISO。
这两项凭证并非硬编码在源码中,而是在扩展弹窗表单里由用户首次填写。打开扩展后,弹窗页面(对应 dist/index.html)会显示一个表单,包含 Region Name 与 Your API Key from tmrow 两个输入框和一个 Submit 按钮:
<form class="form-data" autocomplete="on">
<div>
<h2>New? Add your Information</h2>
</div>
<div>
<label for="region">Region Name</label>
<input type="text" id="region" required class="region-name" />
</div>
<div>
<label for="api">Your API Key from tmrow</label>
<input type="text" id="api" required class="api-key" />
</div>
<button class="search-btn">Submit</button>
</form>
两个输入框均带 required 约束;若填写合法,扩展会通过 Web Storage 把它们持久化下来(详见下文源码解析),这样下次打开浏览器时无需重复输入即可直接读取上次的区域数据。
五、源码级解析:从表单提交到结果展示的完整链路
src/index.js 是弹窗的全部逻辑。它的运行可拆成四条相互咬合的链路,与表单、结果容器等 DOM 元素(对应 dist/index.html 中的 form-data、loading、errors、result-container 区块)一一对应。
5.1 启动自检 init():决定「展示表单」还是「直接出数据」
const init = async () => {
//if anything is in localStorage, pick it up
const storedApiKey = localStorage.getItem('apiKey');
const storedRegion = localStorage.getItem('region');
//set icon to be generic green
chrome.runtime.sendMessage({
action: 'updateIcon',
value: {
color: 'green',
},
});
if (storedApiKey === null || storedRegion === null) {
//if we don't have the keys, show the form
form.style.display = 'block';
results.style.display = 'none';
loading.style.display = 'none';
clearBtn.style.display = 'none';
errors.textContent = '';
} else {
//if we have saved keys/regions in localStorage, show results when they load
results.style.display = 'none';
form.style.display = 'none';
displayCarbonUsage(storedApiKey, storedRegion);
clearBtn.style.display = 'block';
}
};
启动时首先把图标恢复为默认的绿色,然后检查 localStorage 中是否已有 apiKey 与 region:两者缺任一就显示配置表单;两者齐备则跳过表单,直接用历史值发起一次数据请求(这是「记忆上次配置」的用户体验来源)。
5.2 提交表单 setUpUser():写入本地存储并立即请求
const setUpUser = async (apiKey, region) => {
localStorage.setItem('apiKey', apiKey);
localStorage.setItem('region', region);
loading.style.display = 'block';
errors.textContent = '';
clearBtn.style.display = 'block';
//make initial call
displayCarbonUsage(apiKey, region);
};
表单 submit 事件(由 handleSubmit 阻止默认刷新行为后触发)调用 setUpUser:把 Key 与区域写入 localStorage,显示 loading 状态,紧接着发起首次数据请求。
5.3 请求数据 displayCarbonUsage():CO2 Signal API 的真实调用
这是整个扩展的数据核心,通过 axios 携带认证头与区域参数请求 CO2 Signal 接口:
const displayCarbonUsage = async (apiKey, region) => {
try {
await axios
.get('https://api.co2signal.com/v1/latest', {
params: { countryCode: region },
headers: { 'auth-token': apiKey },
})
.then((response) => {
const data = response?.data?.data;
// ✅ Validate required data before using
if (data?.carbonIntensity == null || data?.fossilFuelPercentage == null) {
throw new Error('Missing carbon intensity or fossil fuel data');
}
let CO2 = Math.floor(data.carbonIntensity);
calculateColor(CO2);
loading.style.display = 'none';
form.style.display = 'none';
myregion.textContent = region;
usage.textContent =
Math.round(data.carbonIntensity) + ' grams (grams C02 emitted per kilowatt hour)';
fossilfuel.textContent =
data.fossilFuelPercentage.toFixed(2) +
'% (percentage of fossil fuels used to generate electricity)';
results.style.display = 'block';
});
} catch (error) {
console.warn('Data fetch failed:', error.message);
loading.style.display = 'none';
results.style.display = 'none';
errors.textContent = 'Sorry, data unavailable for the selected region.';
}
};
值得注意的实现细节:
- 请求参数:区域通过
params.countryCode传递,API Key 通过请求头auth-token传递,即上文提到的US-NEISO一类区域代码直接作为countryCode使用; - 响应校验:取
response.data.data后,先校验carbonIntensity(碳强度)与fossilFuelPercentage(化石燃料占比)是否缺失,任一缺失立即抛出异常,避免后续对空值做运算; - 单位说明:碳强度展示为「每千瓦时发电产生的 CO2 克数」,如
173 grams (grams C02 emitted per kilowatt hour);化石燃料占比保留两位小数展示为百分比; - 失败兜底:网络失败或区域无数据时,隐藏 loading 与结果区,并在
errors区块显示「Sorry, data unavailable for the selected region.」。
5.4 换区域 reset():只清区域、保留 Key
结果区块下方有一个 Change region 按钮,点击后 reset 只删除 region 这一个键并重新执行 init(),从而让表单重新出现但不必再次填写 API Key——对需要跨区域对比数据的用户来说相当贴心。
5.5 彩色圆点如何映射碳强度
成功取数后 displayCarbonUsage 会调用 calculateColor,把整数化后的碳强度值映射为一种颜色并通知后台换图标:
calculateColor = async (value) => {
let co2Scale = [0, 150, 600, 750, 800];
let colors = ['#2AA364', '#F5EB4D', '#9E4229', '#381D02', '#381D02'];
let closestNum = co2Scale.sort((a, b) => {
return Math.abs(a - value) - Math.abs(b - value);
})[0];
//console.log(value + ' is closest to ' + closestNum);
let num = (element) => element > closestNum;
let scaleIndex = co2Scale.findIndex(num);
let closestColor = colors[scaleIndex];
//console.log(scaleIndex, closestColor);
chrome.runtime.sendMessage({ action: 'updateIcon', value: { color: closestColor } });
};
可以理解为一套分档调色板,用五档阈值锚点(0、150、600、750、800 gCO₂/kWh 量级)与五档颜色构成近似程度匹配,最终把图标调成对应的绿(#2AA364)、黄(#F5EB4D)或深棕(#9E4229/#381D02)等颜色,并以 chrome.runtime.sendMessage({ action: 'updateIcon', ... }) 发送消息交给后台 Service Worker 处理。绿→黄→棕的递进直观对应「当前电网清洁 → 中度 → 重度高碳」,也就是用户判断是否适合执行高耗电任务的依据。
六、后台 Service Worker:动态绘制工具栏圆点图标
弹窗向后台发送 updateIcon 消息后,由 background.js 完成图标的实际绘制。它用 OffscreenCanvas 画一个纯色圆形,再通过 chrome.action.setIcon 设置为工具栏图标,其代码开头的注释「borrowed from energy lollipop extension, nice feature!」也印证了文档中 Energy Lollipop 的设计借鉴关系:
chrome.runtime.onMessage.addListener(function (msg, sender, sendResponse) {
if (msg.action === 'updateIcon') {
chrome.action.setIcon({ imageData: drawIcon(msg.value) });
}
});
//borrowed from energy lollipop extension, nice feature!
function drawIcon(value) {
let canvas = new OffscreenCanvas(200, 200);
let context = canvas.getContext('2d');
context.beginPath();
context.fillStyle = value.color;
context.arc(100, 100, 50, 0, 2 * Math.PI);
context.fill();
return context.getImageData(50, 50, 100, 100);
}
该机制的价值在于:图标不用预生成一组静态 PNG,而是按需在后台用 Canvas 动态绘制任意颜色,代码量与资源占用都极小——这正呼应了模块第三课 3-background-tasks-and-performance 关于后台任务与性能的主题。整个链路可概括为:
- 弹窗
src/index.js完成 API 请求与碳强度计算; - 通过
chrome.runtime.sendMessage把目标颜色发给 Service Worker; - Service Worker 用
OffscreenCanvas画出圆点并调用chrome.action.setIcon更新工具栏图标; - 用户无需打开弹窗,仅看工具栏圆点颜色即可感知当前区域碳强度。
七、交互状态机与用户体验设计小结
综合 dist/index.html 与 src/index.js 的可见逻辑,弹窗存在三种明确的状态:未配置(显示表单)、加载中(loading 提示)、已出结果(显示区域、碳强度、化石燃料占比 + Change region 按钮),以及一种异常状态(错误提示文案)。各状态通过 style.display 在 form、loading、errors、result-container、clear-btn 五个元素间切换,代码在 src/index.js 末尾以两个事件监听收束:
form.addEventListener('submit', (e) => handleSubmit(e));
clearBtn.addEventListener('click', (e) => reset(e));
//start app
init();
八、实践建议与进一步学习
若想在真实环境中运行本扩展验证上述全部机制,只需按文档路径操作:将本仓库的 solution 目录下载到本地后依次执行 npm install 与 npm run build,在 Edge(或 Chrome)扩展管理页启用开发者模式并「Load Unpacked」选择 dist 文件夹,最后用邮箱领取 CO2 Signal API Key 并填入与你所在电网区域匹配的 Zone 编码即可。仓库只读,以上操作均在你本地副本中完成,不会改动仓库内容。
若想亲手从零实现而非直接使用完整代码,可对照同模块的练习起点工程 5-browser-extension/start,并依次学习模块配套课程:
- 浏览器基础:理解扩展运行的宿主环境与页面生命周期;
- 表单与本地存储:对应本扩展的
setUpUser/reset的 localStorage 读写实践; - 后台任务与性能:对应 Service Worker 绘制图标与接口调用的效率考量。
这套「小型专用网站式扩展」的架构——表单采集配置、本地持久化、HTTP 拉取第三方数据、消息驱动后台换肤——是浏览器扩展开发中可复用的标准范式,Carbon Trigger 的完整代码即为该范式最直观的落地样板。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0627
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00

