Web-Dev-For-Beginners 浏览器扩展第 2 课:调用 CO2 Signal API 并用 localStorage 持久化用户设置
本篇基于 Web-Dev-For-Beginners 课程中“构建浏览器扩展”模块的第 2 课(5-browser-extension/2-forms-browsers-local-storage 目录)展开。第 1 课中你已经完成了表单 HTML 与页面结构,本课的任务是把静态表单“激活”:通过 JavaScript 调用 CO2 Signal API 获取某地区的电网碳强度数据,并用浏览器 Local Storage 把 API 密钥与地区代码持久化下来,使扩展在下次打开时自动恢复状态。读完后,你将掌握 DOM 引用、事件监听、async/await 异步请求、localStorage 键值存储以及错误处理这一整套浏览器扩展的核心数据流。
课程定位与前置准备
本课是整个浏览器扩展项目(Carbon Trigger,用于查询某地区发电的碳强度与化石燃料占比)的第二步。扩展的工作流是:用户在弹窗表单中输入 API 密钥和地区代码 → 扩展把凭据存入本地存储 → 调用 CO2 Signal API 查询实时数据 → 把碳强度(克 CO2/千瓦时)和化石燃料百分比渲染到结果区。
按照课程提示,你需要“跟随文件中编号的片段(numbered segments)来确定代码放置位置”。仓库为学员提供的初始模板 start/src/index.js 就是一个只有编号占位注释的骨架:
//1
// form fields
// results divs
//6
//call the API
//5
//set up user's api key and region
//4
// handle form submission
//3 initial checks
//2
// set listeners and start app
这些编号注释对应本课要依次实现的六块代码:DOM 引用(1)、事件监听与启动(2)、init() 初始检查(3)、表单提交处理(4)、用户设置(5)、API 调用(6)。
构建环境以仓库参考实现 solution/package.json 为准:
- 要求
node >= 18.0.0、npm >= 9.0.0(见engines字段); npm run build执行一次 webpack 打包,npm run watch开启增量编译,方便边改边刷新扩展;- 依赖中包含
axios ^1.15.0(package.json),本课的 API 请求正是用它完成。
项目搭建与编译流程详见前一课 关于浏览器(意译版)。
第一步:建立 DOM 元素引用
JavaScript 要操作界面,先得拿到 HTML 元素的引用。所有字段都通过 CSS 类名选择(类名在第 1 课的 HTML 中已定义好),用 document.querySelector 一次性取出并保存到 const 变量:
// 表单字段
const form = document.querySelector('.form-data');
const region = document.querySelector('.region-name');
const apiKey = document.querySelector('.api-key');
// 结果区
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');
这组引用覆盖了三类元素:两个输入字段(.region-name、.api-key,外加表单容器 .form-data)、结果展示元素(.carbon-usage、.fossil-fuel、.my-region、.result-container),以及交互控件(.loading 加载提示、.errors 错误消息、.clear-btn 清除按钮)。集中声明后,后续 init()、handleSubmit、displayCarbonUsage 等函数都能直接复用这些引用,不必每次重新查询 DOM。
第二步:添加事件监听并启动应用
接下来给表单的 submit 事件和清除按钮的 click 事件挂上监听器,并在文件末尾立即调用一次 init() 来初始化扩展状态:
form.addEventListener('submit', (e) => handleSubmit(e));
clearBtn.addEventListener('click', (e) => reset(e));
init();
注意这里用的是箭头函数简写:事件对象 e 被直接转发给处理函数。等价的长写法是 form.addEventListener('submit', function (e) { handleSubmit(e); })——两种写法功能相同,课程建议体会这种缩写并思考自己更偏好哪种。init() 在脚本加载时同步执行一次,它负责判断“新用户显示表单 / 老用户直接加载数据”的分支(下节详述)。
第三步:init() 与 reset()——围绕 Local Storage 的状态机
function init() {
// 如果 localStorage 里有东西就取出来
const storedApiKey = localStorage.getItem('apiKey');
const storedRegion = localStorage.getItem('regionName');
// 把图标设为通用绿色(占位,下一课实现)
// TODO
if (storedApiKey === null || storedRegion === null) {
// 没有存过凭据:显示表单
form.style.display = 'block';
results.style.display = 'none';
loading.style.display = 'none';
clearBtn.style.display = 'none';
errors.textContent = '';
} else {
// 已保存过密钥/地区:加载时直接展示结果
displayCarbonUsage(storedApiKey, storedRegion);
results.style.display = 'none';
form.style.display = 'none';
clearBtn.style.display = 'block';
}
}
function reset(e) {
e.preventDefault();
// 只清除地区,不删密钥
localStorage.removeItem('regionName');
init();
}
这段逻辑值得逐条读:
- 两个
const用localStorage.getItem检查用户是否已经保存过 API 密钥和地区代码; - 任一项为
null(首次使用):把表单显示为block,同时隐藏结果区、加载提示和清除按钮,并把错误文本置空; - 两者都存在(回访用户):立即调用
displayCarbonUsage(storedApiKey, storedRegion)重新拉取数据,隐藏表单、显示清除按钮; reset(e)先e.preventDefault()阻止默认行为,然后只removeItem('regionName')——删掉地区后init()重新走“新用户分支”,用户得以重新选择地区,但已保存的 API 密钥保留下来。
深入理解 Local Storage API
Local Storage 是浏览器提供的键值(key-value)字符串存储,JavaScript 通过 getItem()、setItem()、removeItem() 三个方法读写,各类现代浏览器都广泛支持。它的两个关键特性:
- 不过期:内容持久保存;而另一种存储
SessionStorage在浏览器关闭后会被清除。两种存储各有适用场景; - 按源隔离:浏览器扩展拥有自己独立的本地存储,与浏览器主窗口的页面是相互隔离的实例。也就是说,扩展里的
localStorage与你在普通网页上看到的不是同一份数据。
验证数据是否写入:打开开发者工具(F12),切到 Application(应用程序) 标签页,展开 Local Storage 即可看到 apiKey、regionName 两个键——正如本文开头那张截图所示。
课程同时强调了一个重要的安全提醒:把 API 密钥放进 Local Storage 在生产环境中是坏主意,因为页面上的任何 JavaScript 都能读到它。本扩展之所以这样设计,仅仅是因为它是一个仅供学习、不会上架应用商店的练习项目。思考题:什么情况下你绝不应该把数据存入 Local Storage?
第四步:处理表单提交 handleSubmit
默认情况下,表单提交会触发页面刷新(整页重载)。handleSubmit 拦截这一行为,从两个输入字段取出值并交给 setUpUser:
function handleSubmit(e) {
e.preventDefault();
setUpUser(apiKey.value, region.value);
}
这里用到了两个 const 引用捕获的 .value 属性,即用户在第 1 课 HTML 中填写的内容。别忘了:那两个 <input> 都带 required 属性,浏览器会在调用这段代码之前先阻止空值提交——表单校验是浏览器内置能力,JS 无需重复实现。
第五步:setUpUser 保存设置并发起首次请求
function setUpUser(apiKey, regionName) {
localStorage.setItem('apiKey', apiKey);
localStorage.setItem('regionName', regionName);
loading.style.display = 'block';
errors.textContent = '';
clearBtn.style.display = 'block';
// 执行首次 API 调用
displayCarbonUsage(apiKey, regionName);
}
这个函数一次完成了四件事:写入 Local Storage(持久化凭据)、显示加载指示、清空历史错误、显示清除按钮,最后发起首次 API 调用。数据持久化与界面状态更新在同一个函数里协调完成,用户从“设置”到“看结果”的过渡没有空档。
第六步:调用 CO2 Signal API 展示碳消耗
先补一点背景:API(Application Programming Interface)是程序之间交互的标准接口。要查询数据库、获取天气或——如本课——获取电网碳数据,都可以依赖别人建好的 API。其中最流行的一类是 REST API(Representational State Transfer),它用 URL 定位资源,用标准 HTTP 方法(GET/POST 等)操作数据,通常返回 JSON。
异步是这一课的另一核心。请求外部 API 时无法控制响应速度(甚至可能不应答),如果同步等待会卡死整个扩展界面。async 关键字让函数以异步方式执行:遇到 await 时挂起,数据返回后再继续,期间界面保持响应。
下面是本课的“重头戏”——查询 CO2 Signal API 的函数:
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) + ' grammi (grammi di C02 emessi per kilowatt/ora)';
fossilfuel.textContent =
response.data.data.fossilFuelPercentage.toFixed(2) +
'% (percentuale di combustibile fossile usato per generare elettricità)';
results.style.display = 'block';
});
} catch (error) {
console.log(error);
loading.style.display = 'none';
results.style.display = 'none';
errors.textContent = 'Spiacente, non ci sono dati per la regione richiesta.';
}
}
逐点拆解:
async+try/catch:axios 的get返回 Promise,API 可能成功、可能失败(网络错误、密钥无效、地区无数据),必须用 try/catch 兜住;- 请求参数:
params: { countryCode: region }把地区代码作为查询参数;headers: { 'auth-token': apiKey }用请求头做身份认证——CO2 Signal 要求把密钥放在auth-token头里; - 成功后:取
response.data.data.carbonIntensity(碳强度,单位克/千瓦时)取整后写入.carbon-usage,fossilFuelPercentage.toFixed(2)保留两位小数写入.fossil-fuel,地区名写入.my-region,然后隐藏加载与表单、显示结果区; - 失败时:隐藏加载与结果区,在
.errors中显示“抱歉,没有所请求地区的数据”这类友好提示,而不是让页面直接报错; - 被注释的
calculateColor(CO2)是留给第 3 课(后台任务)的钩子——届时扩展图标颜色会随碳强度变化。
对照仓库参考实现:教学代码与 solution 的差异
课程文档是讲解脉络,仓库里的参考实现 solution/src/index.js 是可运行的完整答案。对照两者能更清楚地看到各函数的最终形态,也暴露了几个值得注意的细节:
- 导入方式更规范。教学代码写的是
import axios from '../node_modules/axios',而参考实现直接用包名import axios from 'axios'(index.js#L1)。webpack 会自动解析包路径,后者是更标准的写法。 - 存储键名不同。参考实现把地区存在键
'region'下(index.js#L72-L80 的setItem('region', region),init#L89-L116 中读getItem('region')),而本课教学代码使用'regionName'。如果你对照 solution 调试时数据“读不出来”,先检查这两个键名是否一致。 - 防御性校验。参考实现在解析响应前先检查数据完整性(index.js#L44-L47):
carbonIntensity或fossilFuelPercentage缺失就主动抛错,走 catch 分支显示错误消息,比教学版更健壮。 - 图标逻辑已落地。教学代码中被注释掉的
calculateColor,在参考实现中已是完整函数(index.js#L17-L32):它维护[0, 150, 600, 750, 800]的碳强度刻度与对应颜色数组,找到最接近的刻度后通过chrome.runtime.sendMessage({ action: 'updateIcon', value: { color } })把颜色发给后台脚本。这正是第 3 课“后台任务”的主题,本课的init()里对应位置只留了“设为通用绿色”的占位。 - 函数风格。参考实现把
handleSubmit、setUpUser、init、reset都写成const ... = async (...) => {}的箭头函数并统一加async(index.js#L83-L123),事件绑定与init()启动调用在文件末尾(index.js#L125-L129),与教学代码的“文件末尾三件套”(两个addEventListener+init())一一对应。
构建与验证
完成六块代码后,执行 npm run build(或常驻 npm run watch),然后在浏览器的扩展面板里刷新该扩展。此时扩展应当已经可用:输入密钥与地区能查到碳数据,刷新后自动恢复结果。唯一还不工作的是动态图标——这正是下一课 后台任务与性能(意译版) 要解决的问题。
验证时的两个观察点:
- 用 DevTools 的 Application → Local Storage 确认
apiKey与regionName已写入(见文首截图); - 用 DevTools 的 Network 面板观察发往
api.co2signal.com/v1/latest的 GET 请求,检查countryCode查询参数与auth-token请求头是否符合预期,响应 JSON 中data.carbonIntensity与data.fossilFuelPercentage两个字段就是界面上渲染的数据来源。
挑战与课后任务
探索挑战:浏览内置了大量 API 的浏览器本身。任选一个(如 Geolocation API、Notification API、HTML Drag and Drop API、Web Storage API、Fetch API),调研它解决什么问题、如何处理边界情况、有哪些安全考量与浏览器兼容范围,并总结“什么样的 API 对开发者友好、可靠”。
正式作业:Adopt an API(意译版作业)——从公开 API 清单中选一个,写一个解决具体小问题(哪怕只是“宠物照片不够看”这种级别)的浏览器扩展。评分标准:提交了完整可用的扩展(优秀)/ 部分完成(合格)/ 存在 bug(需改进)。
复习自测:LocalStorage 与 API 如何配合工作?如果你要设计一个“先保存若干条目、再交给 API 使用”的网站,会怎么安排存储与请求的时序?结合本课的 init() → 读存储 → 调 API → 渲染 链路作答。
相关文件索引
| 文件 | 用途 |
|---|---|
| 本课意译版原文 | 本文依据的教学文档 |
| 本课英文版原文 | 同课英文主文档 |
| 初始代码模板 | 带编号占位的待填写骨架 |
| 参考实现 | 完整可运行的 solution 代码 |
| 构建与依赖配置 | webpack 脚本、Node 版本要求、axios 依赖 |
| LocalStorage 检查截图 | DevTools Application 面板中查看存储数据 |
| 模块总览 | 浏览器扩展三课的整体说明 |
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 StartedRust0627
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
