Web-Dev-For-Beginners 浏览器扩展实战(Part 2):表单、LocalStorage 持久化与 CO2Signal API 集成
本篇基于 Web-Dev-For-Beginners 仓库中"浏览器扩展项目"的第二课(马来文翻译版 README.ms.md),带你把一个静态的碳足迹查询扩展真正"跑起来":用 document.querySelector 绑定表单元素、用 localStorage 记住用户的 API 密钥与地区、用 async/await 异步调用 CO2Signal API 并把碳强度数据渲染回界面。读完后你将掌握一套可直接复制运行的扩展前端完整实现链路,以及 LocalStorage 的隔离性、安全性等关键注意事项。
项目背景与本课目标
本课是"Carbon Trigger Browser Extension"项目的第二部分。项目目标是构建一个浏览器扩展:用户填入 API 密钥和自己的电力区域代码(例如波士顿地区使用 US-NEISO),扩展调用 CO2Signal 接口获取当前区域每千瓦时电力的碳排放克数与化石燃料发电占比,用不同颜色的小圆点提示"何时适合跑电耗大的任务"。项目启动说明见 5-browser-extension/start/README.md。
上一课(1-about-browsers)已经搭好了扩展的 HTML 表单与结果区 <div>。本课起,所有代码都写在 /src/index.js 中,按照文件内编号注释分段逐步搭建。仓库中 start/src/index.js 就是留给学生填空的骨架,每个编号(//1~//6)对应本文的一个小节。
第一步:为扩展准备可操作的 DOM 引用
JavaScript 要操纵界面,必须先拿到具体元素的引用。在本课的 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');
所有字段都按上一课在 HTML 中设置的 class 名来引用。把引用存进 const 变量,后续各函数可以直接复用,而不必每次重新查询 DOM——这也是为什么这一节被放在文件最顶部。
第二步:注册事件监听并启动应用
接下来给表单和"清空"按钮挂上事件监听,让"提交"和"点击重置"这两个动作有响应;并在文件底部立即调用 init() 完成应用初始化:
form.addEventListener('submit', (e) => handleSubmit(e));
clearBtn.addEventListener('click', (e) => reset(e));
init();
submit监听器在用户按回车或点击提交时触发,事件对象e被透传给handleSubmit;click监听器绑定到清空按钮,用于重置表单状态;- 这里使用了箭头函数简写监听回调。你可以尝试把它改写为传统
function表达式的等价形式——两种写法功能完全一致,简写只是更紧凑。
一个值得自查的细节:如果表单提交时忘记在处理器中调用 e.preventDefault(),页面会执行默认行为——整页刷新,所有 JavaScript 状态丢失,扩展体验被彻底打断。这正是下一步 handleSubmit 里第一件事就是阻止默认行为的原因。
第三步:init() 与 reset()——基于 LocalStorage 的状态恢复
init() 的职责是判断"这是新用户还是回访用户",并据此调整界面:
function init() {
//jika ada di localStorage
const storedApiKey = localStorage.getItem('apiKey');
const storedRegion = localStorage.getItem('regionName');
//tetapkan ikon menjadi hijau generik
//todo
if (storedApiKey === null || storedRegion === null) {
//jika kami tidak mempunyai kunci, tunjukkan borangnya
form.style.display = 'block';
results.style.display = 'none';
loading.style.display = 'none';
clearBtn.style.display = 'none';
errors.textContent = '';
} else {
//jika kita telah menyimpan kunci / kawasan di localStorage, tunjukkan hasilnya ketika dimuat
displayCarbonUsage(storedApiKey, storedRegion);
results.style.display = 'none';
form.style.display = 'none';
clearBtn.style.display = 'block';
}
};
function reset(e) {
e.preventDefault();
//kosongkan simpanan tempatan untuk wilayah sahaja
localStorage.removeItem('regionName');
init();
}
其中包含几段关键逻辑:
- 两个
const用localStorage.getItem()检查用户是否已保存过apiKey和regionName; - 若任一为
null(首次使用),则把表单display设为'block'显示,同时隐藏结果区、loading 指示和清空按钮,并把错误文本重置为空字符串; - 若两者都存在,则直接调用
displayCarbonUsage(storedApiKey, storedRegion)拉取数据,隐藏表单、显示重置按钮; reset()只删除regionName这一个键(保留密钥),然后重新执行init(),让用户重新选择区域。
理解 LocalStorage:键值对、不过期、独立于页面
本课使用到 Web API 中的 LocalStorage,它有三个特性值得记住:
- 键值对存储:只保存字符串,用
getItem()/setItem()/removeItem()三个方法读写,浏览器支持度非常高; - 不随会话消失:LocalStorage 的数据没有有效期,关闭浏览器甚至重启电脑后仍在;另一类 Web 存储 SessionStorage 则会随浏览器关闭而被清除。二者各有适用场景,需按"数据是否要跨会话存活"来选型;
- 扩展拥有独立的存储空间:注意,浏览器扩展的 LocalStorage 与主窗口网页的是相互隔离的两份实例——扩展页面里的
window和主浏览器窗口的window行为互不影响,这既带来独立性也带来安全边界。
你可以在浏览器中验证存储内容:在扩展页面(或任意页面)右键选择"检查",打开开发者工具的 Application(应用) 标签页,展开 Local Storage 面板,就能看到自己 setItem 写入的 apiKey、regionName 等键值:
安全注意:把 API 密钥写进 LocalStorage 在生产应用中是个坏主意——任何同源的 JavaScript 都能读到这份数据。本课之所以这样设计,纯粹因为这是个教学项目、不会发布到应用商店。真实产品应将敏感凭据放在服务端安全存储中。
第四步:处理表单提交
用户提交表单后,需要阻止默认的页面刷新行为,并把两个输入框的值传递出去:
function handleSubmit(e) {
e.preventDefault();
setUpUser(apiKey.value, region.value);
}
两个输入框的 value 通过文件顶部定义的 const(apiKey、region)捕获。上一课写的 HTML 表单中两个字段都带了 required 属性,因此浏览器会在这一层自动拦截空值提交——用户不填,handleSubmit 根本不会执行。这种"HTML 校验在前、JS 逻辑在后"的分工是表单处理的常用模式。
第五步:setUpUser——持久化配置并发起首次请求
setUpUser 是连接"表单输入"与"API 调用"的枢纽,也是本课最重要的函数之一:
function setUpUser(apiKey, regionName) {
localStorage.setItem('apiKey', apiKey);
localStorage.setItem('regionName', regionName);
loading.style.display = 'block';
errors.textContent = '';
clearBtn.style.display = 'block';
//buat panggilan awal
displayCarbonUsage(apiKey, regionName);
}
它做了四件事:把 apiKey 和 regionName 写入 LocalStorage(下次打开扩展就能自动恢复)、显示 loading 提示、清空之前的错误信息、显示清空按钮,最后发起首次数据请求。数据持久化与界面状态更新被协调在同一次动作里完成,用户不会看到"表单提交后界面卡住"的空档。
第六步:displayCarbonUsage——异步调用 CO2Signal API
先认识 API 与 REST
API(Application Programming Interface)是程序之间交互的标准接口,是 Web 开发者工具盒里的基础元件。最流行的一类是 REST API:REST 是 "Representational State Transfer" 的缩写,其特点是把各种参数配置在 URL 上,用标准 HTTP 方法(GET/POST 等)取回通常以 JSON 形式组织的数据。
异步编程是调用 API 的另一半工具。async 关键字让函数以非阻塞方式等待数据返回:你无法控制 API 多快响应(它可能完全没响应!),所以必须用异步方式调用,否则整个扩展会冻结等待网络。
完整实现
用 axios 请求 CO2Signal 的 latest 端点,以 auth-token 请求头携带密钥、以 countryCode 查询参数指定区域:
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,内部用try/catch包裹,因为 axios 的get()返回 Promise,网络异常会在catch分支被接住; - 认证方式:CO2Signal 要求把密钥放进
headers的auth-token字段,而不是 URL 查询参数——这是本课"API 鉴权要放进请求头"的实践示范; - 响应字段:
response.data.data.carbonIntensity(碳强度,取整后显示"克 CO2/千瓦时")与response.data.data.fossilFuelPercentage(化石燃料发电占比,toFixed(2)保留两位小数)被分别写入usage、fossilfuel两个元素,区域名写入myregion; - 成功后隐藏 loading 与表单、显示结果容器;失败则隐藏 loading 与结果,显示"Sorry, we have no data for the region you have requested."的友好错误提示,而不是把堆栈信息甩给用户;
- 被注释掉的
calculateColor(CO2)是留给下一课的钩子:根据碳强度值给扩展图标上色的逻辑,本课先不做。
异步编程是工具盒里另一件非常趁手的工具,值得自行阅读 MDN 上关于 async_function 的各种配置方式(Promise 链、Promise.all 等),它们都能解决同一类"等待外部数据"的问题。
对照仓库最终实现:源码里多出来的细节
完成本课六步之后,可以把你的 index.js 与仓库的完成版 solution/src/index.js 对照,会发现几处值得学习的工程化改进:
- 更稳的数据解析:完成版在
.then()里先做const data = response?.data?.data,再显式校验carbonIntensity与fossilFuelPercentage是否为null,缺数据就主动throw,从而统一走catch分支(见 5-browser-extension/solution/src/index.js#L34-L68)。教学代码直接访问response.data.data.carbonIntensity,若 API 返回结构变化会抛更隐晦的错误; - 存储键名差异:教学文档使用
localStorage键regionName,而完成版统一使用region(见 init 函数)。两套键名不兼容,迁移旧扩展数据时需注意; - 图标联动:完成版在
init()中通过chrome.runtime.sendMessage({ action: 'updateIcon', value: { color: 'green' } })先把图标设为通用绿色,拿到数据后由calculateColor依据co2Scale = [0, 150, 600, 750, 800]五档阈值映射到['#2AA364', '#F5EB4D', '#9E4229', '#381D02', '#381D02']五色,再发消息更新图标(见 calculateColor 实现)——这正是本课//todo注释留出的扩展点; - 错误提示更克制:完成版用
console.warn('Data fetch failed:', error.message)代替console.log,界面提示改为"Sorry, data unavailable for the selected region."。
最终效果大致如下(扩展运行后的结果界面):
构建、加载与验证
构建链路在 start/package.json 中定义:项目名为 carbon-trigger-extension,使用 webpack 5 打包("build": "webpack",另有 "watch": "webpack --watch" 支持热构建),运行时依赖 axios(^1.15.0),要求 Node ≥ 18、npm ≥ 9。操作流程:
npm install
npm run build
然后在 Edge 中通过右上角"三点"菜单进入 Extensions 面板,选择 "Load Unpacked" 加载 dist 目录(安装过程见 start/README.md)。使用前需要准备两样东西:CO2Signal API 的密钥(注册邮箱获取),以及对应 Electricity Map 的区域代码(如 US-NEISO)。构建并刷新扩展后,如果表单提交、数据展示、刷新后自动恢复都正常,你就有了一个可用的版本——唯一还没修好的是图标颜色,那是下一课的内容。
挑战与延伸阅读
- 挑战:本课涉及的只是众多 API 中的一类。自选一个 Web API 深入研究它提供了什么,比如浏览器内置的 HTML Drag and Drop API 等,思考"什么构成一个好的 API":错误处理是否清晰、边界情况如何约定、跨浏览器支持如何、对开发者是否友好;
- 自学方向:LocalStorage 与 API 是两位专业 Web 开发者的常用工具,思考它们如何协同——比如设计一个会把待提交项先存入本地、再交给 API 处理的网页架构;
- 课后作业:从公开 API 清单中选一个,独立完成一个带表单输入、API 集成(含错误处理)、LocalStorage 持久化、loading 状态的扩展,要求使用 ES6+ 与
async/await,并附 README 说明选型理由与截图,评分标准详见 assignment.md。
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 StartedRust0625
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

