首页
/ Web-Dev-For-Beginners 浏览器扩展实战(Part 2):表单、LocalStorage 持久化与 CO2Signal API 集成

Web-Dev-For-Beginners 浏览器扩展实战(Part 2):表单、LocalStorage 持久化与 CO2Signal API 集成

2026-09-06 14:55:25作者:傅爽业Veleda

本篇基于 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();
}

其中包含几段关键逻辑:

  • 两个 constlocalStorage.getItem() 检查用户是否已保存过 apiKeyregionName
  • 若任一为 null(首次使用),则把表单 display 设为 'block' 显示,同时隐藏结果区、loading 指示和清空按钮,并把错误文本重置为空字符串;
  • 若两者都存在,则直接调用 displayCarbonUsage(storedApiKey, storedRegion) 拉取数据,隐藏表单、显示重置按钮;
  • reset() 只删除 regionName 这一个键(保留密钥),然后重新执行 init(),让用户重新选择区域。

理解 LocalStorage:键值对、不过期、独立于页面

本课使用到 Web API 中的 LocalStorage,它有三个特性值得记住:

  1. 键值对存储:只保存字符串,用 getItem() / setItem() / removeItem() 三个方法读写,浏览器支持度非常高;
  2. 不随会话消失:LocalStorage 的数据没有有效期,关闭浏览器甚至重启电脑后仍在;另一类 Web 存储 SessionStorage 则会随浏览器关闭而被清除。二者各有适用场景,需按"数据是否要跨会话存活"来选型;
  3. 扩展拥有独立的存储空间:注意,浏览器扩展的 LocalStorage 与主窗口网页的是相互隔离的两份实例——扩展页面里的 window 和主浏览器窗口的 window 行为互不影响,这既带来独立性也带来安全边界。

你可以在浏览器中验证存储内容:在扩展页面(或任意页面)右键选择"检查",打开开发者工具的 Application(应用) 标签页,展开 Local Storage 面板,就能看到自己 setItem 写入的 apiKeyregionName 等键值:

扩展开发者工具 Application 面板中的 Local Storage 存储视图

安全注意:把 API 密钥写进 LocalStorage 在生产应用中是个坏主意——任何同源的 JavaScript 都能读到这份数据。本课之所以这样设计,纯粹因为这是个教学项目、不会发布到应用商店。真实产品应将敏感凭据放在服务端安全存储中。

第四步:处理表单提交

用户提交表单后,需要阻止默认的页面刷新行为,并把两个输入框的值传递出去:

function handleSubmit(e) {
	e.preventDefault();
	setUpUser(apiKey.value, region.value);
}

两个输入框的 value 通过文件顶部定义的 constapiKeyregion)捕获。上一课写的 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);
}

它做了四件事:把 apiKeyregionName 写入 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 要求把密钥放进 headersauth-token 字段,而不是 URL 查询参数——这是本课"API 鉴权要放进请求头"的实践示范;
  • 响应字段:response.data.data.carbonIntensity(碳强度,取整后显示"克 CO2/千瓦时")与 response.data.data.fossilFuelPercentage(化石燃料发电占比,toFixed(2) 保留两位小数)被分别写入 usagefossilfuel 两个元素,区域名写入 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,再显式校验 carbonIntensityfossilFuelPercentage 是否为 null,缺数据就主动 throw,从而统一走 catch 分支(见 5-browser-extension/solution/src/index.js#L34-L68)。教学代码直接访问 response.data.data.carbonIntensity,若 API 返回结构变化会抛更隐晦的错误;
  • 存储键名差异:教学文档使用 localStorageregionName,而完成版统一使用 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."。

最终效果大致如下(扩展运行后的结果界面):

Carbon Trigger 扩展运行后的结果界面截图

构建、加载与验证

构建链路在 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
登录后查看全文
热门项目推荐
相关项目推荐