首页
/ Web-Dev-For-Beginners 浏览器扩展实战:Carbon Trigger 从 webpack 构建、Edge 部署到 MV3 源码解析

Web-Dev-For-Beginners 浏览器扩展实战:Carbon Trigger 从 webpack 构建、Edge 部署到 MV3 源码解析

2026-09-06 17:17:30作者:裴麒琰

本文基于 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 扩展运行截图,绿色圆点表示当前区域碳排放较低

一、扩展要解决的问题

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 的说明文档,构建前需要:

  1. 本机已安装 npm(Node.js 环境);
  2. 将本仓库中 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/)之后,按 原文档 的安装步骤操作:

  1. 在 Edge 右上角点击「三个点」菜单,进入**扩展(Extensions)**面板;
  2. 选择 Load Unpacked(加载解压缩的扩展)
  3. 在弹出的文件选择框中定位并打开 dist/ 文件夹;
  4. Edge 会读取其中的 manifest.json 并完成加载,工具栏随即出现该扩展图标。

在 Edge 扩展面板中选择 Load Unpacked 安装解压缩扩展

几个实践要点:

  • 必须选 dist/ 而不是 solution/ 根目录——浏览器只认清单文件所在的文件夹,webpack 产物才是扩展真正的「根」;
  • 「Load Unpacked」加载的是本地文件而非商店包,因此每次修改源码后重新 npm run build,再回到扩展面板点击「重新加载」即可看到变化,这正是 npm run watch 脚本存在的意义:开发期间持续监听源码变化、自动重打包。

四、配置 API 密钥与区域代码

扩展加载后,点击工具栏图标会看到 index.html 弹窗。要让它工作,需要填两个值:

  1. CO2 Signal API 密钥(auth-token):向 CO2 Signal 的官方渠道申请,页面提供邮箱输入框,注册后通过邮件发放密钥;
  2. 区域代码(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 函数),弹窗关闭后再打开不必重新输入;
  • 每次初始化都会先向后台发一条消息,把图标重置为通用绿色,避免上一次会话残留的颜色误导用户;
  • 「清除」按钮只删除 regionreset 函数),保留密钥,降低切换区域时的摩擦。

5.2 调用 CO2 Signal API

核心取数函数 displayCarbonUsagesrc/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 中;
  • 对响应做了空值校验carbonIntensityfossilFuelPercentage 任一缺失都主动抛错并进入 catch 分支,避免在界面上渲染出 undefined
  • 错误路径统一把 loading 与结果区隐藏,仅显示友好文案,保证弹窗始终有确定状态。

5.3 「彩色圆点」的色阶映射算法

calculateColorsrc/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. 1-about-browsers —— 浏览器的工作原理与扩展部署机制;
  2. 2-forms-browsers-local-storage —— 表单、API 调用与 localStorage,对应本文 5.1/5.2 节;
  3. 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.js5-browser-extension/solution/dist/background.js5-browser-extension/solution/dist/manifest.json;日文版说明文档即本文所依据的 README.ja.md

登录后查看全文
热门项目推荐
相关项目推荐