Web-Dev-For-Beginners 浏览器扩展实战:表单处理、LocalStorage 持久化与 CO2 Signal API 调用
本篇基于 Web-Dev-For-Beginners 仓库浏览器扩展模块的第 2 课(5-browser-extension/2-forms-browsers-local-storage),讲解如何把你已经搭好的静态表单"激活":注册事件监听、用 LocalStorage 记住用户配置、通过 async/await 调用 CO2 Signal API 获取某地区电网的碳排放强度,并把结果渲染回扩展界面。读完并按仓库源码逐段实现后,你将拥有一个能"带记忆"的可用扩展——用户下次打开时自动加载上次保存的 API Key 与地区数据。
本课时的项目背景与代码骨架
按照 5-browser-extension/README.md 的说明,整个模块的目标是构建一个可在 Edge、Chrome 和 Firefox 中运行的 "My Carbon Trigger" 扩展:它针对某个地区代码查询 CO2 Signal API,返回该地区的电力使用与碳强度数据,从而给出当地碳足迹读数,帮助用户决定例如"是否延迟烘干衣物"这类用电行为。第 1 课(1-about-browsers)已完成扩展项目搭建、manifest 配置与 HTML 表单部分,本课从这里接续。
仓库为学习者提供了带编号注释的骨架文件 start/src/index.js,注释块按 //1 到 //6 标出每个代码段应填入的位置——文档反复提示的"✅ 按照对应文件中的编号段落放置代码"即指此处:
//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
从源码结构看,扩展采用 Manifest V3:start/dist/manifest.json 中声明了 "service_worker": "background.js" 与 "default_popup": "index.html"。构建环境要求见 solution/package.json:Node >= 18、npm >= 9,依赖 axios(用于发请求)、开发依赖 webpack(npm run build 即执行 webpack,npm run watch 可开启监听模式)。
第 1 步:配置要操作的 DOM 元素
在第 1 课中,你已经创建了表单与结果 <div> 的 HTML。从本课开始,工作重心转移到 /src/index.js。首先在文件顶部创建一组 const 变量,通过 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');
这 9 个引用分成两组:
- 表单字段(
.form-data、.region-name、.api-key):对应上节课 HTML 中的表单容器与两个输入框; - 结果区域(
.errors、.loading、.result-container、.carbon-usage、.fossil-fuel、.my-region、.clear-btn):错误提示、加载状态、结果容器、碳强度读数、化石燃料占比、地区名与"清除"按钮。
这些类名与第 1 课 HTML 中设置的一一对应。把所有引用集中在文件顶部存为 const,后续各函数都直接复用这些变量,避免在多处重复执行 querySelector 查询。参考实现见 solution/src/index.js。
第 2 步:添加事件监听器
接下来给表单和"清除"按钮注册事件监听器,并在文件底部调用初始化函数,让扩展打开时立即进入正确状态:
form.addEventListener('submit', (e) => handleSubmit(e));
clearBtn.addEventListener('click', (e) => reset(e));
init();
这里值得留意两点:
- 监听器使用箭头函数简写
(e) => handleSubmit(e),其长格式等价写法是(e) => { handleSubmit(e); };两种写法功能完全等价,可按个人偏好选择; - 事件对象
e被显式传给处理函数,后续函数中依赖它调用e.preventDefault()。若忘记调用,表单提交将触发整页刷新,丢失全部 JavaScript 状态、打断用户体验——这正是handleSubmit与reset里第一件事就是e.preventDefault()的原因。
第 3 步:init() 与 reset() —— LocalStorage 驱动的初始化逻辑
init() 决定扩展打开时展示什么:如果 LocalStorage 里已有上次保存的配置,就直接带数据进入"老用户"模式;否则展示设置表单。
function init() {
//si hay algo en localStorage, recójalo(如果 localStorage 中有数据就取出来)
const storedApiKey = localStorage.getItem('apiKey');
const storedRegion = localStorage.getItem('regionName');
//establecer el icono en verde genérico(把图标设为通用绿色)
//todo(留待后续课时实现动态图标)
if (storedApiKey === null || storedRegion === null) {
//si no tenemos las claves, mostrar el formulario(没有保存配置则显示表单)
form.style.display = 'block';
results.style.display = 'none';
loading.style.display = 'none';
clearBtn.style.display = 'none';
errors.textContent = '';
} else {
//si hemos guardado claves / regiones, mostrar los resultados(有保存配置则直接拉取数据)
displayCarbonUsage(storedApiKey, storedRegion);
results.style.display = 'none';
form.style.display = 'none';
clearBtn.style.display = 'block';
}
};
function reset(e) {
e.preventDefault();
//borrar almacenamiento local solo para la región(只清除地区项的本地存储)
localStorage.removeItem('regionName');
init();
}
逻辑分解如下:
- 用两个
const从 LocalStorage 读取apiKey与regionName; - 任一值为
null(首次使用或未保存),把表单display设为block,同时隐藏结果区、加载提示与清除按钮,并把错误文本清空; - 两个值都存在,立即调用
displayCarbonUsage(storedApiKey, storedRegion)拉取数据,期间先隐藏结果区、隐藏表单、显示清除按钮; reset()只删除regionName一项(保留 API Key,用户重新选择地区即可),然后重新执行init()回到表单状态。
LocalStorage 核心概念
文档在此强调了一个浏览器端非常重要的概念:LocalStorage 是以"键值对"形式在浏览器中存储字符串的方式,可由 JavaScript 直接读写,用于管理浏览器内的数据。它的关键特性:
- 不随会话过期:LocalStorage 不会自动清除;与之对比,SessionStorage 在浏览器关闭时会被清除。不同存储方式各有取舍,需要按场景选择;
- API 统一且兼容性好:通过
getItem()、setItem()、removeItem()三个方法即可完成读写删,在所有现代浏览器中广泛可用; - 按源隔离:文档特别提示——浏览器扩展拥有自己独立的 LocalStorage,与浏览器主窗口中打开的普通网页是不同的实例、互不干扰,这提供了隔离与安全边界。
你可以在开发者工具中直观验证:右键页面选择"检查"(或按 F12),切到 Application 面板的 Local Storage 区域,就能看到以键值对形式保存的 apiKey、regionName(即文首配图所示界面)。
⚠️ 安全提醒(原文档 ✅ 思考题):一般生产应用中把 API Key 放进 LocalStorage 是坏主意——因为同一来源内的任何 JS 都能读到它。本课之所以这样写,仅因为该应用纯粹用于学习、不会发布到应用商店。
注意一个实现细节:本课讲义代码使用的键名是 regionName,而仓库参考答案 solution/src/index.js 中实际使用的键名是 region。两者只要自洽(getItem/setItem/removeItem 使用同一键名)即可正常工作;如果你把讲义代码与 solution 混用,务必统一键名,否则会出现"初始化读不到数据"的隐蔽 bug。
第 4 步:处理表单提交
在写 displayCarbonUsage() 之前,先搭好首次提交的处理函数。handleSubmit 接收事件参数 (e),阻止默认行为(我们不希望浏览器提交后刷新页面),再把两个输入框的当前值传给 setUpUser:
function handleSubmit(e) {
e.preventDefault();
setUpUser(apiKey.value, region.value);
}
✅ 回顾上节课的 HTML:表单有两个输入字段,其 value 正是通过文件顶部的两个 const 引用捕获;且两个字段都带 required 属性,浏览器会阻止用户以空值提交,天然做了一层非空校验。
第 5 步:setUpUser() 保存用户配置
setUpUser 负责把用户凭据写入 LocalStorage,并为界面切换到"加载中"状态,随后触发首次 API 调用:
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);
}
执行顺序有讲究:先落盘,再改 UI,最后发请求。这样即使请求失败,init() 重入时依然能从 LocalStorage 恢复到一致的界面状态;而 loading 的显示让用户在等待网络响应期间有明确的反馈。
第 6 步:displayCarbonUsage() 调用 CO2 Signal API
终于轮到核心函数。先补上文档中的 API 背景知识:API(Application Programming Interface,应用程序编程接口)是程序之间相互通信的标准方式——比如你要查数据库,可能已有别人封装好的 API 供你调用。众多 API 类型中最流行的之一是 REST API(REST 即 "Representational State Transfer",表现层状态转移),它通过以不同方式构造 URL 来获取数据,配合标准 HTTP 方法与可预测的响应格式(通常是 JSON)。
本课使用 async 关键字将函数声明为异步。异步执行意味着:函数发起网络请求后会交出控制权,而不是让扩展界面卡死等待。由于你无法控制 API 响应快慢(甚至可能完全不响应),必须用异步方式处理这种不确定性——这也是 try/catch 必须包裹请求的原因。
完整实现如下(axios 在文件顶部以 import axios from 'axios' 引入;讲义示例中写作 import axios from '../node_modules/axios',webpack 打包时两种写法均可解析,但常规写法是前者,参考 solution/src/index.js 第 1 行):
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.';
}
}
逐点拆解这个"大函数"在做什么:
- 认证方式:CO2 Signal API 使用
auth-token请求头传递 API Key(注意放在headers里,而不是查询参数); - 查询参数:通过 axios 的
params对象传入countryCode: region,axios 会将其序列化为 URL 查询串,从而按地区过滤数据; - 数据映射:响应到达后,把
response.data.data中的carbonIntensity(碳强度,单位:克 CO2/千瓦时)四舍五入后写入.carbon-usage,把fossilFuelPercentage保留两位小数写入.fossil-fuel,同时更新.my-region、隐藏 loading 与表单、显示结果容器; - 错误路径:请求失败(网络错误、Key 无效、该地区无数据等任何抛错)都会落入
catch:隐藏 loading 与结果区,并在.errors中给出"抱歉,没有您请求地区的数据"提示。
对照仓库参考答案可以看到工程化上的增量:solution/src/index.js 的 displayCarbonUsage 在渲染前增加了空值校验——if (data?.carbonIntensity == null || data?.fossilFuelPercentage == null) throw ...,并在 catch 中改用 console.warn 记录 error.message。从源码结构看,这提示了实战中"先校验响应结构再取字段"的健壮性习惯:API 可能返回 200 但载荷不完整,仅靠 try/catch 捕获不到这类"静默坏数据"。
构建与验证
按文档要求,在扩展目录执行构建并在扩展面板中刷新即可看到成果:
npm run build
构建后,到浏览器的扩展管理页刷新该扩展,再点开 popup:首次打开显示 API Key 与地区表单;提交后看到"加载中";数据返回后看到地区名、克 CO2/千瓦时的碳强度与化石燃料百分比。此时唯一尚未生效的是动态图标——init() 与 displayCarbonUsage() 中被注释掉的 calculateColor/图标逻辑正是下一课时的内容。solution 中对应的完整实现(co2Scale 色阶 + chrome.runtime.sendMessage({ action: 'updateIcon', ... }))可参考 solution/src/index.js,它印证了 popup 页面与 MV3 service worker 之间通过消息机制联动更新图标的设计。
挑战题与课后练习
- 研究挑战(原文档 🚀 题):选一个浏览器原生 API 深入调研,例如 HTML 拖放 API(HTML Drag and Drop API)、Geolocation API 等,思考"什么样的 API 称得上优秀":文档是否清晰、错误处理如何、跨浏览器支持度如何;
- 正式作业:Adopt an API。作业要求你自选一个外部 API 构建浏览器扩展,硬性功能包括:用于 API 参数的表单输入、带错误处理的 API 集成、用 LocalStorage 保存用户偏好、加载状态与用户反馈;代码要求使用 ES6+ 与 async/await、
try/catch错误处理,并处理无网络、非法响应等边界情况,验证扩展在浏览器重启后仍能工作。
复习与自测
本课掌握了 LocalStorage 与 API 调用这两项对职业 Web 开发者都非常有用的能力。自检时问自己:这两者如何协同工作?你会如何设计一个"把本地存储的数据作为 API 输入"的网站?另外可用 5 分钟快速验证:在任意网站的 DevTools Application 面板查看其 localStorage;在控制台里手动 localStorage.setItem/getItem 一轮;用 Network 面板观察表单实际提交了什么。
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
