Web-Dev-For-Beginners 浏览器扩展实战(Part 2):在 Extension 中调用 CO2Signal API 并用 Local Storage 持久化用户偏好
本文基于 Web-Dev-For-Beginners 仓库的「Browser Extension Project Part 2」课程文档,讲解如何给浏览器扩展的表单接入真实数据:通过 document.querySelector 建立 DOM 引用、用 localStorage 保存 API Key 与区域偏好、以 async/await 异步调用 CO2Signal API 并把碳排放数据渲染到扩展界面。读完并按步骤操作后,你将拥有一个能跨会话记住用户设置、实时查询碳强度并优雅处理错误的可用扩展,并能用仓库中 start 骨架 和 solution 参考实现 对照验证自己的代码。
项目背景与前置条件
本扩展项目是仓库的 5-browser-extension 课程的第二个部分。到这一步,你已经完成了扩展的表单 HTML 与 <div> 结构(见 Part 1 课程文档),接下来的工作全部发生在 /src/index.js 文件中,通过 npm run build 逐步构建你的扩展。
仓库中 start/package.json 明确了运行环境要求:
- Node.js >= 18.0.0、npm >= 9.0.0
- 构建工具为 webpack ^5(
build与watch脚本分别对应webpack和webpack --watch) - 运行时依赖 axios ^1.15.0,用于发送 HTTP 请求
仓库同时提供了两个可直接对照的代码位置:start/src/index.js 是按课程步骤标注了编号注释的骨架文件(//1 DOM 变量、//2 事件监听、//3 初始检查、//4 表单提交、//5 用户设置、//6 API 调用),而 solution/src/index.js 是完成态参考实现。下文所有代码均可在 solution 中找到对应落点。
建立可操纵元素的 DOM 引用
JavaScript 要操纵界面,必须先拿到 HTML 元素的引用。在 index.js 顶部,为每个重要的表单字段和结果区域创建 const 变量:
// 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');
这些引用全部通过 CSS 类选择器完成,与你在 Part 1 中写好的 HTML 类名一一对应。这样做的收益是:
- 用
document.querySelector()一次性捕获表单、输入框、加载指示器、错误提示和结果容器; - 每个元素引用存进
const,后续各函数直接复用,避免重复查询 DOM; - 表单域(
form、region、apiKey)与结果域(errors、loading、results等)分组声明,与 solution/src/index.js#L3-L15 中的组织方式完全一致。
添加事件监听器并启动应用
让扩展响应用户动作的下一步是给表单和清除按钮挂上事件监听,并在文件末尾调用 init() 启动应用:
form.addEventListener('submit', (e) => handleSubmit(e));
clearBtn.addEventListener('click', (e) => reset(e));
init();
这里有三点值得注意:
- 监听器分别覆盖两种用户动作:
submit事件在用户按下回车或点击提交时触发,click事件覆盖清除按钮; - 事件对象
(e)被透传给处理函数,供e.preventDefault()等控制手段使用; - 这里用了箭头函数简写形式。等价的长写法是
form.addEventListener('submit', function(e) { handleSubmit(e); });——两种写法功能等价,简写更符合现代 JavaScript 风格。
一个常见的自我检验问题:如果 handleSubmit 中忘了 e.preventDefault(),表单会走浏览器默认的提交行为——页面刷新、JavaScript 状态全部丢失,用户体验被直接打断。
构建 init() 与 reset():用 Local Storage 记忆用户
init() 是扩展的“导航系统”:它检查用户是否曾用过扩展,并据此决定展示表单还是直接加载数据。reset() 则给用户一个重新开始的入口:
function init() {
//if anything is localStorage, pick it up
const storedApiKey = localStorage.getItem('apiKey');
const storedRegion = localStorage.getItem('regionName');
//set icon to be generic green
//todo
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
displayCarbonUsage(storedApiKey, storedRegion);
results.style.display = 'none';
form.style.display = 'none';
clearBtn.style.display = 'block';
}
};
function reset(e) {
e.preventDefault();
//clear local storage for region only
localStorage.removeItem('regionName');
init();
}
这段逻辑的工作方式是:
- 先读取
localStorage中保存的apiKey和regionName两个键; - 任一为
null(首次用户):显示表单,隐藏结果、加载指示器和清除按钮,并清空错误文本; - 两者都存在(回访用户):跳过表单,直接用存储的凭据调用
displayCarbonUsage()拉取数据,并显示清除按钮; reset()只清除regionName(保留 API Key),然后重新调用init(),让扩展回到设置流程。
Local Storage 的核心特性与安全边界
LocalStorage 是浏览器中以“键值对”字符串形式存储数据的一种 Web 存储 API,具有以下关键特性:
- 键值对模型:通过
localStorage.getItem(key)、setItem(key, value)、removeItem(key)操作; - 持久不失效:与在浏览器关闭时即清空的会话级存储不同,LocalStorage 的数据在浏览器关闭、甚至重启后依然存在,直到被显式删除;
- 扩展独立存储:浏览器扩展拥有自己隔离的 localStorage 实例,与主浏览器窗口中的网页页面相互独立、互不干扰;
- 可检查性:可以在扩展面板上右键选择“检查”打开 DevTools,进入 Application(或对应的 Application/Storage)标签查看存储内容,效果与本文开头的 Local Storage 面板截图一致。
课程原文在此明确提示了一个安全考量:在生产环境中把 API Key 放进 LocalStorage 是坏主意——任何运行在该上下文的 JavaScript 都能读到这些明文值。本课程之所以这样做,纯粹是因为这是一个用于学习、不会上架应用商店的示例。真实产品应把敏感凭据放到服务端安全存储中。
处理表单提交:拦截默认行为并提取输入
function handleSubmit(e) {
e.preventDefault();
setUpUser(apiKey.value, region.value);
}
函数接收事件参数 (e),首先阻止事件冒泡导致的默认刷新行为(我们要保持单页应用式的体验),然后把两个输入框的 value 交给 setUpUser。这里能成立,依赖于 Part 1 中 HTML 表单的两个输入字段都带 required 属性——浏览器会在用户提交空值前就拦下来,所以 handleSubmit 拿到的必然是非空输入。
保存用户设置并发起首次调用
setUpUser 是连接“表单”与“API”的桥梁:它写入 localStorage、更新 UI 状态、发起首次请求:
function setUpUser(apiKey, regionName) {
localStorage.setItem('apiKey', apiKey);
localStorage.setItem('regionName', regionName);
loading.style.display = 'block';
errors.textContent = '';
clearBtn.style.display = 'block';
//make initial call
displayCarbonUsage(apiKey, regionName);
}
逐步拆解:
- 持久化凭据:
apiKey与regionName写入 localStorage,供下次打开扩展时init()读取; - 进入加载态:显示 loading 指示器;
- 清理历史错误:把错误区域重置为空字符串;
- 暴露清除按钮:让用户稍后随时重置;
- 发起首次 API 调用:把两个值传入
displayCarbonUsage()。
调用 CO2Signal API:异步请求、鉴权与错误处理
API 与 REST 基础
API(Application Programming Interface)为程序之间的交互提供了标准接口。REST(Representational State Transfer)是其中最流行的一类,其特点是:使用标准 HTTP 方法、以配置的 URL 端点定位数据、通常返回 JSON 格式的可预测响应。本扩展使用的 CO2Signal API 提供全球电网的实时碳强度数据,端点为 https://api.co2signal.com/v1/latest,通过查询参数 countryCode 指定区域,通过请求头 auth-token 传递 API Key 完成鉴权。
用 async/await + axios 查询并渲染结果
displayCarbonUsage 是整个扩展的核心函数,它用 async 关键字声明——意味着函数可以 await 网络请求返回,而不阻塞扩展的其余执行:
import axios from '../node_modules/axios';
async function displayCarbonUsage(apiKey, region) {
try {
await axios
.get('https://api.co2signal.com/v1/latest', {
params: {
countryCode: region,
},
headers: {
'auth-token': apiKey,
},
})
.then((response) => {
let CO2 = Math.floor(response.data.data.carbonIntensity);
//calculateColor(CO2);
loading.style.display = 'none';
form.style.display = 'none';
myregion.textContent = region;
usage.textContent =
Math.round(response.data.data.carbonIntensity) + ' grams (grams C02 emitted per kilowatt hour)';
fossilfuel.textContent =
response.data.data.fossilFuelPercentage.toFixed(2) +
'% (percentage of fossil fuels used to generate electricity)';
results.style.display = 'block';
});
} catch (error) {
console.log(error);
loading.style.display = 'none';
results.style.display = 'none';
errors.textContent = 'Sorry, we have no data for the region you have requested.';
}
}
这个函数值得逐点理解:
- 为什么用
async:网络请求的响应速度不受你控制(甚至可能完全不应答),所以必须以异步方式调用;await让代码在等待期间保持响应,扩展界面不会冻结; - 为什么用
try/catch:axios.get返回 Promise,网络错误、鉴权失败、区域无数据都会走到catch,在那里隐藏 loading 与结果区、显示友好的错误提示,而不是让扩展崩溃; - 鉴权方式:API Key 通过
headers中的auth-token字段传递,而不是放在 URL 上; - 数据解析:响应体结构是
response.data.data,其中carbonIntensity(每千瓦时电网排放的 CO2 克数)取整后展示,fossilFuelPercentage用toFixed(2)保留两位小数展示; - 注释掉的
calculateColor(CO2)是留给下一课(图标动态变色)的钩子。
对照仓库参考实现:从课程代码到 solution 的演进
课程文档中的代码与仓库最终版 solution/src/index.js 高度一致,但 solution 在几处做了工程化增强,值得留意:
- 存储键名的差异:课程版使用键
regionName,而 solution 中setUpUser写入的是'region'(见 solution/src/index.js#L72-L80),init()读取的也是'region'(#L89-L92)。如果你在自己的练习中混用两个键名,会出现“保存了区域但刷新后不生效”的现象; - 响应数据校验:solution 在渲染前显式检查
data?.carbonIntensity与data?.fossilFuelPercentage是否存在,缺失时主动抛错(#L42-L47),比课程版更健壮; - 图标联动:solution 已经实现了
calculateColor(),它把 CO2 值映射到 5 档色阶([0, 150, 600, 750, 800]对应绿色到深棕色),并通过chrome.runtime.sendMessage({ action: 'updateIcon', value: { color } })通知后台脚本更新扩展图标(#L17-L32)——这正是本课结尾提到的“唯一还没工作的图标”在下一课的解决方案; - 初始化时即发送绿色图标消息:solution 的
init()在读取存储后就会sendMessage一个color: 'green'的默认图标(#L94-L100),对应课程代码中//set icon to be generic green //todo那行注释的落地。
而 start/src/index.js 则保留了 //1 到 //6 的编号注释骨架,与本课文档的六个步骤(DOM 引用 → 事件监听 → init/reset → 表单提交 → 用户设置 → API 调用)严格对应,适合作为跟练时的落点清单。
构建、验证与延伸练习
完成 src/index.js 后,执行:
npm run build
然后在扩展面板中刷新扩展。此时你应该得到一个完全可用的扩展:表单提交后显示所查区域的碳强度与化石燃料占比,设置被存入 localStorage,刷新后自动加载;唯一暂未生效的是图标颜色,课程将其留给了 Part 3(后台任务与性能优化)。开发过程中可以用 start/package.json 中的 npm run watch 脚本保持 webpack 监听,改代码即自动重新构建。
课程的挑战环节建议:挑选一个 Web API 做深入研究(例如浏览器内置的 HTML 拖放 API 或 Fetch API),思考什么样的 API 对开发者最友好、最可靠。配套作业 Adopt an API(印地语版) 则要求你自选一个外部 API,独立构建一个包含表单输入、localStorage 持久化、async/await 调用、错误处理与加载状态的完整扩展,并按评分表检验 API 集成、代码质量、用户体验、存储使用与文档五个维度。
小结:LocalStorage 与 API 如何协同
本课程的两大主题在架构上是互补的:LocalStorage 提供“记忆”,API 提供“实时性”。init() 从存储恢复状态 → 表单提交经 handleSubmit/setUpUser 把新偏好写回存储并触发请求 → displayCarbonUsage 异步取数并渲染结果 → reset() 允许用户随时清空重来。这条“存储—请求—渲染—重置”的闭环,正是专业 Web 开发者构建单页应用与浏览器扩展时的通用骨架,也是你接下来在 Part 3 中继续添加动态图标与性能调优的基础。
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
