Web-Dev-For-Beginners 浏览器扩展实战:Carbon Trigger 从 webpack 构建、Edge 部署到 MV3 源码解析
本文基于 Web-Dev-For-Beginners 仓库第五模块(Building a browser extension)的完整示例代码 solution/ 目录,讲解 Carbon Trigger 浏览器扩展的完整落地流程:如何用 npm 与 webpack 构建扩展产物、如何通过 Edge 的「Load Unpacked」机制安装未打包的扩展、如何申请 CO2 Signal API 密钥并选择 Electricity Map 区域代码,并深入 popup 主脚本 与 后台 service worker 的源码,讲清「彩色圆点」图标如何根据区域电网碳强度实时变色。读完后你将掌握 Manifest V3 扩展的构建、安装与调试全流程,并能读懂 popup 页面与 background 之间通过 chrome.runtime.sendMessage 通信的完整调用链。
一、扩展要解决的问题
Carbon Trigger 是一个「微型站点式」的浏览器扩展:它向 CO2 Signal API(tmrow 提供的碳强度数据接口)查询你所在电网区域的两个关键指标——
- 碳强度(carbon intensity):每千瓦时电力所排放的二氧化碳克数;
- 化石燃料占比(fossil fuel percentage):该区域发电中化石燃料所占的百分比。
查询结果会展示在扩展弹窗中,同时驱动浏览器工具栏上一个彩色圆点改变颜色,直观反映当前区域电网的「脏/净」程度。设计意图是「临场决策辅助」:比如在电网碳强度很高的时段,推迟运行烘干机、启动高耗能计算任务等耗电行为,从而让个人的用电决策与实时电网数据挂钩。
这一「圆点」设计借鉴了加州碳排放扩展 Energy Lollipop 的图标结构——用一个颜色编码的圆点表达排放水平,概念上被本模块直接沿用(见 background.js 中的注释:borrowed from energy lollipop extension, nice feature!)。
二、环境准备与构建流程
2.1 前置条件
按照 solution 的说明文档,构建前需要:
- 本机已安装 npm(Node.js 环境);
- 将本仓库中
solution/目录的代码拷贝到本地任意文件夹。
从 package.json 可以看到官方声明的运行环境要求,这是实际动手前值得核实的硬性前提:
"engines": {
"npm": ">=9.0.0",
"node": ">=18.0.0"
}
2.2 安装依赖
进入 solution/ 目录(本仓库中的路径为 5-browser-extension/solution/),执行:
npm install
依赖清单非常精简,全部声明在 package.json 中:
- 运行时依赖:
axios(^1.15.0),用于从 popup 页面发起对 CO2 Signal API 的 HTTPS 请求; - 开发依赖:
webpack(^5.105.4)与webpack-cli(^5.1.4),负责把 ES Module 源码打包为浏览器可直接加载的产物。
2.3 用 webpack 构建扩展
npm run build
npm run build 实际执行的是 package.json scripts 中定义的 webpack 命令,另外还提供了一个开发用脚本:
"scripts": {
"watch": "webpack --watch",
"build": "webpack"
}
构建完成后,产物落在 dist/ 目录中。本仓库已直接提供了构建好的 dist/ 产物,其结构就是最终加载进浏览器的完整扩展包:
| 文件 | 作用 |
|---|---|
| manifest.json | Manifest V3 清单文件,声明扩展身份与权限 |
| background.js | 后台 service worker,负责绘制工具栏图标 |
| index.html | 点击工具栏图标后弹出的 popup 页面 |
| main.js | popup 页面的打包后脚本(对应源码 src/index.js) |
| styles.css | popup 样式 |
| images/ | 弹窗内配图资源 |
2.4 读懂清单文件:这是一个 Manifest V3 扩展
manifest.json 的完整内容很短,但它揭示了扩展的骨架:
{
"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("service_worker": "background.js"),而不再是 MV2 时代的常驻 background page;host_permissions: ["<all_urls>"]—— 申请对任意站点的请求权限。本扩展需要跨域调用 CO2 Signal API,从清单结构看,这是为了让扩展侧发起的外部请求不被同源策略拦截;action.default_popup—— 点击浏览器工具栏图标时弹出index.html。也就是说,扩展的日常交互界面完全由这个 popup 页面承担。
三、在 Edge 中安装未打包扩展
构建(或直接使用仓库中现成的 dist/)之后,按 原文档 的安装步骤操作:
- 在 Edge 右上角点击「三个点」菜单,进入**扩展(Extensions)**面板;
- 选择 Load Unpacked(加载解压缩的扩展);
- 在弹出的文件选择框中定位并打开
dist/文件夹; - Edge 会读取其中的
manifest.json并完成加载,工具栏随即出现该扩展图标。
几个实践要点:
- 必须选
dist/而不是solution/根目录——浏览器只认清单文件所在的文件夹,webpack 产物才是扩展真正的「根」; - 「Load Unpacked」加载的是本地文件而非商店包,因此每次修改源码后重新
npm run build,再回到扩展面板点击「重新加载」即可看到变化,这正是npm run watch脚本存在的意义:开发期间持续监听源码变化、自动重打包。
四、配置 API 密钥与区域代码
扩展加载后,点击工具栏图标会看到 index.html 弹窗。要让它工作,需要填两个值:
- CO2 Signal API 密钥(auth-token):向 CO2 Signal 的官方渠道申请,页面提供邮箱输入框,注册后通过邮件发放密钥;
- 区域代码(region code):对应 Electricity Map 的电网分区编码(zone),官方提供 zones 查询接口。文档中给出的示例是波士顿使用
US-NEISO(新英格兰独立系统运营商辖区)。
选错区域代码时不会崩溃——popup 脚本中有容错分支,会展示 Sorry, data unavailable for the selected region. 的提示而非报错(见 src/index.js 的 catch 块),这对调试区域代码很有帮助。
五、源码解析:popup 页面如何取数并驱动图标
5.1 表单、localStorage 与页面状态
popup 脚本 src/index.js 按 DOM 选择器绑定了一组元素:
// form fields
const form = document.querySelector('.form-data');
const region = document.querySelector('.region-name');
const apiKey = document.querySelector('.api-key');
// results
const errors = document.querySelector('.errors');
const loading = document.querySelector('.loading');
const results = document.querySelector('.result-container');
const usage = document.querySelector('.carbon-usage');
const fossilfuel = document.querySelector('.fossil-fuel');
const myregion = document.querySelector('.my-region');
const clearBtn = document.querySelector('.clear-btn');
初始化逻辑(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';
// ... 其余 UI 复位
} else {
//if we have saved keys/regions in localStorage, show results when they load
displayCarbonUsage(storedApiKey, storedRegion);
// ...
}
};
要点有三:
- 密钥与区域通过
localStorage.setItem('apiKey'/'region', ...)持久化(setUpUser 函数),弹窗关闭后再打开不必重新输入; - 每次初始化都会先向后台发一条消息,把图标重置为通用绿色,避免上一次会话残留的颜色误导用户;
- 「清除」按钮只删除
region(reset 函数),保留密钥,降低切换区域时的摩擦。
5.2 调用 CO2 Signal API
核心取数函数 displayCarbonUsage(src/index.js)展示了 MV3 扩展中典型的 axios 请求写法:
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);
// 更新弹窗文本:克数与化石燃料百分比
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';
});
值得注意的实现细节:
- 认证放在 请求头
auth-token中,区域代码放在 查询参数countryCode中; - 对响应做了空值校验:
carbonIntensity或fossilFuelPercentage任一缺失都主动抛错并进入 catch 分支,避免在界面上渲染出undefined; - 错误路径统一把 loading 与结果区隐藏,仅显示友好文案,保证弹窗始终有确定状态。
5.3 「彩色圆点」的色阶映射算法
calculateColor(src/index.js)把连续的碳强度数值离散到五档色阶:
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];
let num = (element) => element > closestNum;
let scaleIndex = co2Scale.findIndex(num);
let closestColor = colors[scaleIndex];
chrome.runtime.sendMessage({ action: 'updateIcon', value: { color: closestColor } });
};
对照色阶表:
| 碳强度(g CO2/kWh) | 档位颜色 | 语义 |
|---|---|---|
| 0 以下附近 | #2AA364(绿) |
电网非常清洁 |
| 150 附近 | #F5EB4D(黄) |
偏低 |
| 600 附近 | #9E4229(褐红) |
偏高 |
| 750 及以上 | #381D02(深褐) |
很高 |
算法思路是:先按「离数值最近」对刻度数组排序取最近刻度,再找到第一个大于最近刻度的档位索引,从而把数值落入对应颜色区间,最后通过 chrome.runtime.sendMessage 把颜色值发给后台。从源码结构看,色阶阈值(0/150/600/750/800)是硬编码常量,若你的区域电网碳强度普遍更高(例如重度煤电区域),可以在这里调档——但注意仓库是只读参考,实际修改应在你自己的拷贝中进行。
5.4 background service worker:画出一个圆点
消息的另一端在 background.js:
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);
}
这里的工程细节很有教学价值:
- MV3 的 service worker 没有 DOM,所以不能创建普通
<canvas>,代码改用OffscreenCanvas离屏绘制——这是 MV3 背景下 Canvas 图形的标准做法; - 在 200×200 的画布上画一个半径 50 的实心圆,再截取中心 100×100 的
ImageData交给chrome.action.setIcon,工具栏圆点即完成变色; - 由于颜色值由 popup 端计算、图标由后台绘制,
popup → background的这条sendMessage消息链(action 名为updateIcon)就是整个扩展唯一的跨上下文通信,调用关系清晰可查:calculateColor()与init()两处发送,onMessage监听器一处接收。
六、学习路径与延伸阅读
本模块(5-browser-extension/)把扩展开发拆成三课,与本文的完整示例构成「分步练习 + 成品对照」的关系:
- 1-about-browsers —— 浏览器的工作原理与扩展部署机制;
- 2-forms-browsers-local-storage —— 表单、API 调用与 localStorage,对应本文 5.1/5.2 节;
- 3-background-tasks-and-performance —— 后台任务与性能度量,对应本文 5.4 节与 Profiler 工具的使用。
模块总览见 5-browser-extension/README.md:这个扩展的写法适用于 Edge、Chrome 与 Firefox(以 Chromium 系浏览器为主,Firefox 需按 Manifest V3 规范自行调整)。仓库中 start/ 目录提供了一份「挖空」的练习版(仅 182 字节的 start/src/index.js 起步文件),solution/ 则就是本文剖析的完整答案,两者 package.json 结构一致,可以按练习 → 对照 → 阅读源码的顺序使用。
七、小结
- 构建:
npm install+npm run build(webpack 5,要求 Node ≥ 18 / npm ≥ 9),产物在dist/; - 安装:Edge「扩展 → Load Unpacked」选择
dist/目录,Manifest V3 清单声明 service worker 与 popup; - 配置:CO2 Signal API 密钥(请求头
auth-token)+ Electricity Map 区域代码(如US-NEISO),二者存入 localStorage 免重复填写; - 原理:popup 请求 API → 按五档色阶(0/150/600/750/800)计算颜色 →
sendMessage通知 background →OffscreenCanvas绘制圆点 →chrome.action.setIcon更新工具栏图标。
完整源码入口:5-browser-extension/solution/src/index.js、5-browser-extension/solution/dist/background.js、5-browser-extension/solution/dist/manifest.json;日文版说明文档即本文所依据的 README.ja.md。
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 StartedRust0624
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

