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 课程“浏览器扩展”模块的第二部分,目标是把上一课搭建的静态碳足迹表单扩展改造成一个真正动态的工具:通过调用 CO2 Signal API 获取指定地区电网的实时碳强度数据,并利用 LocalStorage 记住用户的 API 密钥与地区设置。读完本篇,你将掌握 DOM 元素引用、事件监听、
async/await异步网络请求、try/catch错误处理与浏览器本地存储的完整实战链路,最终得到一个可安装运行的碳足迹追踪浏览器扩展。
课程定位:从静态表单到动态扩展
在上一课中,你已经完成了浏览器扩展的界面部分——一个外观不错的配置表单,但它本质上还是“静态”的,无法与真实世界交互。本课要做两件核心的事:
- 接入真实数据:让扩展通过 HTTP 请求访问外部 API,获取某个国家/地区电网的实时碳强度数据(grams CO₂ / kWh)与化石燃料发电占比。
- 赋予记忆:用
localStorage把用户的 API 密钥和地区代码保存在浏览器中,让扩展在下次打开时能自动恢复用户设置,而不是每次重新输入。
这正是本课程项目“Carbon Trigger”(碳触发器)扩展的价值场景:用户输入 API 密钥和地区代码后,即可随时查看本地区当前电力碳强度,从而判断何时适合运行烘干机这类高耗电活动(例如在电力较清洁的时段使用)。
仓库中本课的代码骨架位于 5-browser-extension/start/src/index.js,以编号注释(//1到//6)标出了你需要在哪些位置补全代码;完整实现位于 5-browser-extension/solution/src/index.js,可作为对照参考。
捕获 DOM 元素:为 JavaScript 建立界面引用
在 JavaScript 能操控界面之前,必须先拿到具体 HTML 元素的引用。仓库中的表单结构定义了以下关键 CSS 类:.form-data(表单)、.region-name(地区输入框)、.api-key(API 密钥输入框),以及结果展示区域.result-container、.carbon-usage、.fossil-fuel、.my-region,外加交互辅助元素.loading(加载指示)、.errors(错误信息)、.clear-btn(清除按钮)。
在 5-browser-extension/start/src/index.js 的//1位置补全如下代码,用document.querySelector()按 CSS 类选择器逐一捕获引用:
// 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');这段代码做了四件事:
- 用
document.querySelector()配合 CSS 类选择器捕获表单与输入框元素; - 为地区名与 API 密钥创建输入框引用;
- 建立与碳数据结果展示元素的连接(碳强度、化石燃料占比、地区名);
- 将每个元素引用存入
const变量,供代码后续复用。
全部使用const声明,因为引用一经建立就不会重新赋值,这也符合现代 JavaScript 的最佳实践。解决方案源码 5-browser-extension/solution/src/index.js 中采用了完全一致的捕获方式,可见这是贯穿项目的标准模式。
事件驱动:添加 submit 与 click 监听器
拿到元素引用后,下一步是让扩展响应用户操作。在 5-browser-extension/start/src/index.js 的//2位置添加监听器并启动应用:
form.addEventListener('submit', (e) => handleSubmit(e)); clearBtn.addEventListener('click', (e) => reset(e)); init();要点解析:
- 为表单挂载
submit监听器,用户按下 Enter 或点击提交按钮时触发; - 为清除按钮挂载
click监听器,用于重置表单; - 事件对象
(e)被传递给处理函数,供其调用e.preventDefault()等方法做精细控制; - 立即调用
init(),在扩展启动时设置初始状态。
这里使用了 ES6 箭头函数简写语法,比传统的function表达式更简洁,两者功能等价。完整实现见 5-browser-extension/solution/src/index.js。
自测:如果忘了在表单提交处理中调用
e.preventDefault()会怎样?答案是页面会被重新加载,所有 JavaScript 状态全部丢失,用户输入也会丢失,体验被彻底打断。这正是现代单页式扩展必须拦截默认提交行为的原因。
记忆功能:理解浏览器 LocalStorage
本课的数据持久化核心是localStorage。它有以下关键特性:
| 特性 | 说明 |
|---|---|
| 持久性 | 数据在浏览器会话之间保留,关闭浏览器、重启电脑甚至浏览器崩溃后依然存在(区别于 sessionStorage) |
| 数据模型 | 以“键-值对”存储,通过getItem()/setItem()/removeItem()操作 |
| 缺省返回 | 键不存在时getItem()返回null |
| 访问速度 | 本地即时访问,无网络延迟 |
| 隔离性 | 每个浏览器扩展拥有独立的LocalStorage,与普通网页隔离,避免安全冲突 |
一个重要的隔离细节:扩展的 LocalStorage 与访问普通网站时使用的 LocalStorage 是彼此独立的存储空间,这保证了扩展数据的私密性与安全性。
你可以打开浏览器开发者工具(F12),切到Application(应用程序)标签页,展开Local Storage分支查看扩展保存的数据,界面如下图所示:
⚠️安全提醒:在生产级应用中,把 API 密钥存进 LocalStorage 存在安全风险——任何能执行 JavaScript 的脚本都有可能读到这些数据。本课为教学目的采用该方案是可行的,但真实应用应对敏感凭据使用安全的服务端存储。
初始状态管理:init() 与 reset()
init()是扩展的“导航系统”:启动时检查 LocalStorage 中是否已有上次保存的凭据,据此决定显示配置表单还是直接加载结果。在 5-browser-extension/start/src/index.js 的//3位置实现:
function init() { // Check if user has previously saved API credentials const storedApiKey = localStorage.getItem('apiKey'); const storedRegion = localStorage.getItem('regionName'); // Set extension icon to generic green (placeholder for future lesson) // TODO: Implement icon update in next lesson if (storedApiKey === null || storedRegion === null) { // First-time user: show the setup form form.style.display = 'block'; results.style.display = 'none'; loading.style.display = 'none'; clearBtn.style.display = 'none'; errors.textContent = ''; } else { // Returning user: load their saved data automatically displayCarbonUsage(storedApiKey, storedRegion); results.style.display = 'none'; form.style.display = 'none'; clearBtn.style.display = 'block'; } } function reset(e) { e.preventDefault(); // Clear stored region to allow user to choose a new location localStorage.removeItem('regionName'); // Restart the initialization process init(); }逻辑拆解:
- 从 LocalStorage 读取
apiKey与regionName两个键; - 任一为
null说明是新用户:显示配置表单,隐藏结果、加载指示与清除按钮,清空错误信息; - 两个都有值说明是回访用户:直接调用
displayCarbonUsage()自动拉取数据,隐藏表单并显示清除按钮; reset(e)先preventDefault(),再移除regionName键,最后重新执行init()回到首次配置状态。
这套“检查存储 → 分支渲染”的状态机也反映在课程文档的状态图中:扩展启动 → 检查存储 →(无数据)显示表单 → 用户输入 → 保存 → 拉取 API → 显示结果 →(有数据)直接加载 → 拉取 API → 显示结果 → 用户点清除 → 删除存储 → 回到配置。
仓库解决方案在 5-browser-extension/solution/src/index.js 中的init()/reset()实现了同样的状态管理,并额外通过chrome.runtime.sendMessage把扩展图标先设置为通用绿色(这是下一课“背景任务与性能”将完整实现的图标动态变色功能的前置动作)。
处理表单提交:handleSubmit 与 setUpUser
拦截提交:handleSubmit
浏览器默认会在表单提交后刷新页面,我们要拦截这个行为以获得流畅的单页体验。在 5-browser-extension/start/src/index.js 的//4位置实现:
function handleSubmit(e) { e.preventDefault(); setUpUser(apiKey.value, region.value); }e.preventDefault()阻止页面重载;- 从
apiKey.value与region.value提取用户输入; - 把表单数据交给
setUpUser()继续处理。
另外,表单输入框带有required属性,浏览器会在调用本函数前自动校验用户是否已填写 API 密钥和地区,未填写则不会触发提交。
保存偏好并首拉数据:setUpUser
setUpUser负责把用户凭据持久化并启动首次 API 调用,在 5-browser-extension/start/src/index.js 的//5位置实现:
function setUpUser(apiKey, regionName) { // Save user credentials for future sessions localStorage.setItem('apiKey', apiKey); localStorage.setItem('regionName', regionName); // Update UI to show loading state loading.style.display = 'block'; errors.textContent = ''; clearBtn.style.display = 'block'; // Fetch carbon usage data with user's credentials displayCarbonUsage(apiKey, regionName); }一步步看:
- 把 API 密钥与地区名写入 LocalStorage,供未来会话使用;
- 显示加载指示器,告知用户数据正在获取;
- 清空历史错误信息;
- 显示清除按钮,方便用户日后重置设置;
- 用用户凭据发起 API 调用获取真实碳数据。
该函数把“数据持久化”和“界面状态更新”协调在一个动作里,实现了从配置到结果的平滑过渡。仓库解决方案 5-browser-extension/solution/src/index.js 中该函数被声明为async(为后续调用链预留异步能力),核心逻辑与课程一致,只是存储键名简化为region。
接入真实数据:displayCarbonUsage 调用 API
REST API 基本概念
API(应用程序编程接口)是不同应用之间通信的通道。本课使用的是 CO2 Signal API,它提供全球电网的实时碳强度数据。REST 风格 API 的核心约定:
- REST 即 Representational State Transfer(表述性状态转移);
- 使用标准 HTTP 方法(GET、POST、PUT、DELETE)操作数据;
- 返回可预测的格式,通常是 JSON;
- 通过一致的 URL 端点提供不同类型的请求。
本课请求的参数构成是:HTTP 方法GET、请求头携带auth-token、查询参数携带countryCode(地区代码);响应体则包含碳强度、化石燃料百分比与时间戳。
理解异步 JavaScript
调用外部 API 属于网络 I/O,如果同步阻塞,整个扩展会像空管系统等待一架飞机应答一样“冻结”。async/await的价值正在于此:
- 数据加载期间扩展保持响应;
- 网络请求期间其他代码可继续执行;
- 相比传统回调写法可读性大幅提升;
- 能为网络错误提供优雅的捕获路径。
实现 fetch + async/await
在 5-browser-extension/start/src/index.js 的//6位置实现数据获取与展示函数:
// Modern fetch API approach (no external dependencies needed) async function displayCarbonUsage(apiKey, region) { try { // Fetch carbon intensity data from CO2 Signal API const response = await fetch('https://api.co2signal.com/v1/latest', { method: 'GET', headers: { 'auth-token': apiKey, 'Content-Type': 'application/json' }, // Add query parameters for the specific region ...new URLSearchParams({ countryCode: region }) && { url: `https://api.co2signal.com/v1/latest?countryCode=${region}` } }); // Check if the API request was successful if (!response.ok) { throw new Error(`API request failed: ${response.status}`); } const data = await response.json(); const carbonData = data.data; // Calculate rounded carbon intensity value const carbonIntensity = Math.round(carbonData.carbonIntensity); // Update the user interface with fetched data loading.style.display = 'none'; form.style.display = 'none'; myregion.textContent = region.toUpperCase(); usage.textContent = `${carbonIntensity} grams (grams CO₂ emitted per kilowatt hour)`; fossilfuel.textContent = `${carbonData.fossilFuelPercentage.toFixed(2)}% (percentage of fossil fuels used to generate electricity)`; results.style.display = 'block'; // TODO: calculateColor(carbonIntensity) - implement in next lesson } catch (error) { console.error('Error fetching carbon data:', error); // Show user-friendly error message loading.style.display = 'none'; results.style.display = 'none'; errors.textContent = 'Sorry, we couldn\'t fetch data for that region. Please check your API key and region code.'; } }这段代码展示了现代前端开发的多个核心实践:
- 使用原生
fetch()API,无需引入 Axios 等外部依赖,代码更轻量(仓库解决方案版本 5-browser-extension/solution/src/index.js 则演示了用 axios 实现同一请求的等价写法,两者可互相印证); - 通过
response.ok做早期错误检查,在解析前拦截失败的请求(HTTP 非 2xx 状态); - 用
async/await让异步流程读起来像同步代码; - 通过
auth-token请求头完成 CO2 Signal API 鉴权; - 解析 JSON 响应并提取
data.data.carbonIntensity、data.data.fossilFuelPercentage; - 用
Math.round()四舍五入碳强度,用toFixed(2)格式化百分比; - 用模板字面量(
${})拼接多语言友好文案,同时更新myregion、usage、fossilfuel多个 DOM 节点,最后显示结果容器; - 用
try/catch兜底:网络异常、API 返回错误或数据缺失时,隐藏加载指示与结果区,展示“抱歉,无法获取该地区数据,请检查 API 密钥与地区代码”的友好提示,并通过console.error输出诊断日志。
请求-响应的完整错误分支可概括为:发起 API 调用 → 网络失败则报网络错误 → 响应状态非 OK 则报 API 错误 → 解析 JSON → 数据无效则报数据错误 → 全部通过才更新 UI → 无论成败最后都要隐藏加载指示。
一句话总结本函数:它演示了专业开发者日常必备的一组基本功——与外部服务器通信、鉴权、数据处理、界面更新与优雅的错误管理。
从仓库源码看工程化细节
对照 5-browser-extension/solution/package.json,本扩展的工程配置包含:
- 包名
carbon-trigger-extension,依赖axios(^1.15.0),开发依赖webpack与webpack-cli; engines要求 npm >= 9.0.0、node >= 18.0.0;- 脚本
npm run build(webpack 打包)、npm run watch(监听模式)、npm test(占位)。
解决方案版 5-browser-extension/solution/src/index.js 相比课程文档的 fetch 示例还额外展示了三个细节:
- 数据完整性校验:
if (data?.carbonIntensity == null || data?.fossilFuelPercentage == null)使用可选链与空值检查,在渲染前验证响应数据完整,避免向用户展示未定义值; - 图标联动:
calculateColor()依据碳强度值(分段区间[0, 150, 600, 750, 800]对应绿/黄/红等色阶)通过chrome.runtime.sendMessage({ action: 'updateIcon', ... })通知后台脚本更新扩展栏图标颜色——这正是课程中// TODO: calculateColor(carbonIntensity)注释指向的下一课内容; - 存储键差异:解决方案使用
apiKey+region,课程教学代码使用apiKey+regionName,两者均可工作,注意保持一致即可。
构建、安装与验证
完成上述六个编号段的编码后,按以下步骤构建并验证你的扩展:
npm install # 安装依赖 npm run build # webpack 打包,生成 dist 目录安装到浏览器(以 Edge 为例):点击浏览器右上角“三点”菜单进入扩展管理页,开启“开发人员模式”,选择“加载解压缩的扩展”并指向打包输出的dist文件夹。首次使用需要准备:
- 一个 CO2 Signal API 密钥(在其官网注册邮箱获取);
- 一个地区代码(参照 Electricity Map 的区码,例如波士顿可用
US-NEISO)。
在扩展界面输入 API 密钥与地区代码并提交后,扩展会保存设置并立即拉取数据;刷新或重启浏览器后再次打开,扩展应自动恢复并直接展示上次地区的最新碳数据,点击清除按钮则可回到重新配置状态。
进阶挑战与学习路线
GitHub Copilot Agent 挑战
使用 Agent 模式增强displayCarbonUsage,目标包括:1) 对失败的 API 调用实现带指数退避的重试机制;2) 在发起请求前校验地区代码格式;3) 带进度指示器的加载动画;4) 在 LocalStorage 中缓存 API 响应并附 30 分钟过期时间戳;5) 展示历史调用数据;同时用 TypeScript 风格的 JSDoc 注释完整标注参数与返回类型。
浏览器 API 探索挑战
选一个浏览器内置 API 制作小演示,体会 API 设计质量:Geolocation API(获取位置)、Notification API(桌面通知)、HTML Drag and Drop API(拖放交互)、Web Storage API(本地存储进阶)、Fetch API(XMLHttpRequest 的现代替代)。研究时可重点关注:该 API 解决什么现实问题、如何处理错误与边界情况、使用时有哪些安全考量、跨浏览器支持情况如何。
单元作业
完成作业 5-browser-extension/2-forms-browsers-local-storage/assignment.md:“采用一个 API”——从公开 API 清单中任选一个(如随机狗狗图片、天气、名言、新闻头条、数字趣闻等),设计并构建一个解决真实问题的浏览器扩展,要求包含表单输入、带错误处理的 API 集成、LocalStorage 偏好存储、响应式界面、加载态与用户反馈,并使用 ES6+、async/await、try/catch编写规范注释的代码。评分维度覆盖 API 集成质量、代码质量、用户体验、LocalStorage 运用与文档完整性五个方面。
能力成长时间轴
按课程给出的节奏规划:DOM 基础(约 15 分钟)→ LocalStorage 持久化(约 20 分钟)→ 表单处理(约 25 分钟)→ API 集成(约 35 分钟)→ 异步编程(约 40 分钟)→ 错误处理(约 30 分钟)→ 进阶模式(缓存策略、速率限制、重试机制、性能优化,约 1 周)→ 生产技能(安全实践、API 版本化、监控日志、可扩展架构,约 1 个月)。
总结
本课让你完成了一个从静态表单到动态数据应用的完整跃迁,收获的能力清单包括:精准的 DOM 元素定位与操作、基于 localStorage 的持久数据管理、实时数据拉取与鉴权、不阻塞界面的异步编程、优雅的异常处理、以及加载态/校验/平滑交互的用户体验设计。这些模式同样适用于单页应用、API 驱动的移动应用、Electron 桌面软件、企业级系统乃至 React/Vue/Angular 的数据管理——你已经掌握了现代 Web 开发中“数据获取 + 本地持久化 + 界面响应”这条核心主线的标准解法。下一步,可以继续探索缓存策略、实时 WebSocket 连接与复杂状态管理等更进阶的主题。
【免费下载链接】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),仅供参考