Web-Dev-For-Beginners 浏览器扩展实战:Carbon Trigger 从 webpack 构建、Edge 部署到 MV3 源码解析
【免费下载链接】Web-Dev-For-Beginners24 Lessons, 12 Weeks, Get Started as a Web Developer项目地址: https://gitcode.com/GitHub_Trending/we/Web-Dev-For-Beginners
本文基于 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 buildnpm 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。
【免费下载链接】Web-Dev-For-Beginners24 Lessons, 12 Weeks, Get Started as a Web Developer项目地址: https://gitcode.com/GitHub_Trending/we/Web-Dev-For-Beginners
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考