Web-Dev-For-Beginners 浏览器扩展模块:从 CO2 Signal API 调用、LocalStorage 持久化到动态工具栏图标的三步实战
【免费下载链接】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 仓库的5-browser-extension模块,完整讲解如何从零构建一个可在 Edge、Chrome、Firefox 中运行的 "Carbon Trigger" 浏览器扩展:先用表单收集用户输入的地区代码与 CO2 Signal API Key,再通过 HTTP API 拉取该地区的实时碳强度与化石燃料占比,最后用chrome.runtime消息机制和OffscreenCanvas把结果绘制成工具栏上的彩色圆点。读完本篇,你将掌握 Manifest V3 扩展的项目结构、Webpack 构建流程、"Load Unpacked" 安装方式、LocalStorage 用户偏好持久化、API 鉴权调用与错误处理,以及基于浏览器 DevTools 的性能剖析方法。
模块总览:一个"迷你网站"式的扩展
5-browser-extension/README.md对这个模块的定位是:构建浏览器扩展是一种"以特定任务为导向的迷你网站"开发方式,在动手的同时训练你对 Web 应用性能的理解。该扩展的具体功能是:
- 调用 CO2 Signal API 查询指定地区(以 Electricity Map 的地区代码标识,如波士顿地区使用
US-NEISO)电网的电力消耗与碳强度; - 把结果以"碳足迹读数"的形式呈现给用户,并同步改变工具栏图标的颜色(即所谓的"dot"圆点系统,灵感来自面向加州排放数据的 Energy Lollipop 扩展);
- 用户可按需(ad hoc)打开扩展做决策参考——模块给出的典型场景是:在所在地区电力碳强度较高的时段,推迟烘干衣物这类高碳排活动。
模块由三节课程串联,对应5-browser-extension/README.md中的 Topics 列表:
- 浏览器原理与扩展工程搭建 —— 浏览器架构、扩展安装与开发流程、双屏 UI 的 HTML 结构;
- 表单、API 调用与 Local Storage —— DOM 引用、事件监听、表单提交拦截、本地持久化、
async/await异步数据获取; - 后台任务与性能优化 —— 动态图标、后台 Service Worker 消息处理、DevTools Performance 剖析。
每节课程都配有独立作业,如 为扩展重新设计样式、采用一个 API、分析一个站点的性能,可结合 start 目录 的练习代码与 solution 目录 的参考答案对照完成。
工程结构与构建工具链
仓库为该模块提供了两套代码:start目录是留给学生填空的练习脚手架,solution目录是完整可运行的参考实现。两者结构一致:
5-browser-extension/ ├── start/ # 练习用脚手架(含占位注释) │ ├── dist/ # 构建产物 + 静态资源(扩展真正加载的目录) │ │ ├── manifest.json # 扩展清单(MV3) │ │ ├── index.html # 弹出层 UI │ │ ├── background.js # 后台 Service Worker │ │ ├── main.js # Webpack 打包后的主脚本 │ │ ├── styles.css # 弹出层样式 │ │ └── images/ │ ├── src/ │ │ └── index.js # 待实现的主脚本(带编号注释占位) │ └── package.json └── solution/ # 完整参考实现,目录结构同上 ├── src/index.js └── dist/start/package.json 定义了两个与构建相关的 npm 脚本与依赖:
{ "name": "carbon-trigger-extension", "engines": { "npm": ">=9.0.0", "node": ">=18.0.0" }, "scripts": { "watch": "webpack --watch", "build": "webpack" }, "devDependencies": { "webpack": "^5.105.0", "webpack-cli": "^5.1.4" }, "dependencies": { "axios": "^1.15.0" } }要点有三:一是运行环境要求 Node.js 18 / npm 9 及以上;二是构建完全依赖 Webpack 的默认约定——入口为src/index.js,产物输出到dist/main.js(仓库中并未额外提交webpack.config.js,直接由npm run build触发默认打包);三是运行时代码引用了axios库发起 API 请求。
扩展的"身份证"是 dist/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" } }从这份清单可以读出扩展的完整骨架:host_permissions授予跨域请求权限(供调用 CO2 Signal API 使用);background.service_worker指向background.js,它负责不在弹出层中执行的后台工作(更新工具栏图标);action.default_popup指向index.html,即用户点击工具栏图标时弹出的界面。练习版 start/dist/background.js 只有两行注释占位("add listener here / draw the icon here"),而 solution/dist/background.js 则补全了完整实现,这正是第三节课的实操内容。
快速开始:安装、构建与加载
前置条件与操作步骤在 start/README.md 中给出:
# 1. 安装依赖(需要本机已安装 npm) npm install # 2. 用 Webpack 构建扩展 npm run build随后在 Edge 中通过右上角"三点"菜单进入扩展面板,选择 "Load Unpacked" 并打开dist文件夹,扩展即可加载。课程 1-about-browsers 补充了完整的开发循环:构建后若是首次安装选 "Load Unpacked",若是已有版本则点 "Reload";并强调测试自建扩展时要打开开发者模式。
使用扩展前需要准备两样凭据:
- API Key:向 CO2 Signal 服务方申请(在 co2signal.com 页面输入邮箱即可获得);
- 地区代码:对应 Electricity Map 的电网分区代码,例如波士顿地区为
US-NEISO。课程中提醒:不要把 API Key 提交进代码仓库,本模块将其保存在 LocalStorage 属于教学简化手段,生产环境应使用服务端安全存储。
弹出层 UI:配置表单与结果展示双屏
扩展采用"渐进披露"的两屏设计:首次使用显示配置表单,配置完成后显示数据结果。参考实现 solution/dist/index.html 中的核心结构如下:
<!--form area--> <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> <!--result area--> <div class="result"> <div class="loading">loading...</div> <div class="errors"></div> <div class="data"></div> <div class="result-container"> <p><strong>Region: </strong><span class="my-region"></span></p> <p><strong>Carbon Usage: </strong><span class="carbon-usage"></span></p> <p><strong>Fossil Fuel Percentage: </strong><span class="fossil-fuel"></span></p> </div> <button class="clear-btn">Change region</button> </div>这套结构的每个类名都有明确职责:.form-data/.region-name/.api-key是脚本要操作的输入区;.loading在请求期间显示加载态;.errors承载面向用户的错误文案;.result-container内的三个<span>(.my-region、.carbon-usage、.fossil-fuel)是 API 数据的落点;.clear-btn允许用户清除已存地区、重新配置。表单的required属性让浏览器在提交前自动校验两个字段均已填写,autocomplete="on"则开启自动补全改善输入体验。
弹出层脚本:DOM 引用、LocalStorage 与 API 调用
5-browser-extension/start/src/index.js 用//1~//6的编号注释把实现切成了六个填空段落(表单字段引用、事件监听、初始化检查、提交处理、用户配置、API 调用),solution/src/index.js 则是这些段落的完整答案。按仓库中的实际实现,其逻辑链条如下。
1. DOM 引用与事件绑定(solution/src/index.js第 4~15 行、第 125~129 行):
// 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'); form.addEventListener('submit', (e) => handleSubmit(e)); clearBtn.addEventListener('click', (e) => reset(e)); //start app init();2. 初始化与重置——用 LocalStorage 区分新用户与老用户(第 89~123 行):
//initial checks 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'; } }; const reset = async (e) => { e.preventDefault(); //clear local storage for region only localStorage.removeItem('region'); init(); };这里体现了 LocalStorage 的完整用法:键值对存取(getItem/setItem/removeItem)、跨会话持久化(关闭浏览器后数据仍在)、键不存在时返回null的判定语义。扩展的 LocalStorage 与普通网页相互隔离,可避免站点间数据冲突;在 DevTools 的 Application → Local Storage 面板中可以直接查看apiKey与region两个键。注意reset只清除region键,API Key 得以保留,再次进入表单时用户只需重新选择地区。
3. 提交处理与用户配置(handleSubmit/setUpUser,第 72~86 行):
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); }; // handle form submission const handleSubmit = async (e) => { e.preventDefault(); setUpUser(apiKey.value, region.value); };e.preventDefault()拦截了表单默认提交行为(否则会刷新弹出层页面、丢失全部 JS 状态),使扩展保持单页应用式的状态管理;随后一次性完成"持久化凭据 + 显示加载态 + 清除旧错误 + 触发首次 API 调用"四件事。
4. 异步 API 调用(displayCarbonUsage,第 34~68 行),这是把扩展从"静态工具"变成"实时数据工具"的核心:
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.'; } };这段代码覆盖了 API 集成的标准要素:以查询参数countryCode指定地区、以请求头auth-token携带 API Key 鉴权、用async/await保持扩展在等待网络响应时不冻结、对 HTTP 失败与响应体字段缺失分别设防(可选链 + 显式判空抛错),最终把carbonIntensity(克 CO₂/千瓦时)与fossilFuelPercentage格式化后写入对应<span>。catch分支统一负责"隐藏加载态 + 显示用户友好的错误提示"。第二节课的 README 还讲解了对应的纯fetch写法与try/catch、加载态、错误态的通用设计思路,可对照学习。
动态图标:消息传递 + OffscreenCanvas
第三节课的增量是把数值变成工具栏上的一枚彩点。solution/src/index.js#L17-L32 中的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]; let num = (element) => element > closestNum; let scaleIndex = co2Scale.findIndex(num); let closestColor = colors[scaleIndex]; chrome.runtime.sendMessage({ action: 'updateIcon', value: { color: closestColor } }); };颜色刻度把碳强度(克/千瓦时)分为四档:接近 0~150 的绿色#2AA364(清洁)、150~600 的黄色#F5EB4D(中等)、600~750 的橙色#9E4229(偏高)、750 以上的深棕#381D02(很高),与init()中默认的green占位色共同构成"红绿灯"式的直观反馈。
关键机制是扩展内部的消息传递:弹出层脚本与后台 Service Worker 处于不同的执行上下文,无法直接互调,chrome.runtime.sendMessage/chrome.runtime.onMessage就是二者间的"神经系统"。solution/dist/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); }这里有两个值得注意的实现选择:其一,图标不是预置图片而是运行时用 Canvas 2D 绘制——在 200×200 的OffscreenCanvas上画一个半径 50 的实心圆,再用getImageData(50, 50, 100, 100)裁出圆点区域的像素数据,直接喂给chrome.action.setIcon({ imageData });OffscreenCanvas不挂在任何 UI 上,不会阻塞界面渲染。其二,消息结构统一为{ action: 'updateIcon', value: { color } },这样无论是init()发默认绿色,还是calculateColor发数据驱动色,后台只需一个监听器即可覆盖(该绘制思路借鉴自 Energy Lollipop 扩展,课程也指出 Canvas API 的更多用法可在仓库的 Space Game 绘制章节 学到)。
需要说明的一点:从仓库源码结构看,drawIcon中getImageData返回的ImageData直接作为imageData参数传入,这是课程参考实现的原始写法;在部分 Chromium 版本中chrome.action.setIcon期望ImageBitmap/Blob 形式的图像数据,若遇到图标不更新,可在background.js中将其转换为createImageBitmap再传入——这属于课程未覆盖的跨版本兼容细节。
性能剖析:用 DevTools 给扩展做"体检"
第三节课的另一半内容是 Web 性能方法论。模块的核心观点是"网站性能 = 页面加载速度 + 页面上代码的执行速度",并给出了一套可复现的排查流程:
- 打开 DevTools(Windows 上
Ctrl+Shift+I,macOS 上Option+Command+I),进入 Performance 标签; - 点击 Record,刷新页面并触发目标交互,停止录制后查看时间线中脚本、渲染、绘制三个维度的分布;
- 选中时间线片段查看 summary 面板,在 Event Log 中定位耗时超过 15ms 的事件;
- 对照清单判断瓶颈类型:长任务(优化 JS)、大资源(压缩图片/按需加载)、渲染阻塞脚本(加
defer)、昂贵绘制(简化样式)。
课程同时给出了三类通用优化抓手:资源体积(图片压缩、按设备下发尺寸、CSS/JS 压缩、懒加载)、DOM 复杂度(精简标签与嵌套层级、清理无用 CSS)、JavaScript 执行(使用defer让脚本在 DOM 解析后加载、按需拆分代码)。对扩展开发还有一个专门提示:扩展本身是独立的浏览器上下文,应从扩展自身的 DevTools 入口进入剖析,才能获得扩展专属的性能指标。课程还列了一组感知阈值供建立直觉:约 100ms 的延迟用户可感知,1 秒级别开始失去注意力,3 秒以上会造成显著流失。
资料来源与延伸阅读
模块 README 的 Credits 部分说明了项目的思想来源:Web 碳排触发器的概念由 Microsoft Green Cloud Advocacy 团队的 Asim Hussain 提出(其 Green Principles 项目,最初是一个网站项目);扩展的结构参考了 Adebola Adeniran 的 COVID 扩展;圆点图标体系则受 Energy Lollipop 扩展启发;课程由 Jen Looper 编写。
想继续深入,可以沿仓库内这些路径走:start 目录的练习说明 带你走一遍构建与安装,solution 目录 提供逐文件对照的完整答案,1-about-browsers 的 "Restyle your extension" 作业适合练习 CSS 定制,而 3-background-tasks-and-performance 的 "Analyze a site for performance" 作业则把 DevTools 剖析方法应用到真实站点上。
【免费下载链接】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),仅供参考