首页
/ Web-Dev-For-Beginners 浏览器扩展实战:表单处理、LocalStorage 持久化与 CO2 Signal API 调用

Web-Dev-For-Beginners 浏览器扩展实战:表单处理、LocalStorage 持久化与 CO2 Signal API 调用

2026-09-06 14:33:22作者:蔡怀权

本篇基于 Web-Dev-For-Beginners 仓库浏览器扩展模块的第 2 课(5-browser-extension/2-forms-browsers-local-storage),讲解如何把你已经搭好的静态表单"激活":注册事件监听、用 LocalStorage 记住用户配置、通过 async/await 调用 CO2 Signal API 获取某地区电网的碳排放强度,并把结果渲染回扩展界面。读完并按仓库源码逐段实现后,你将拥有一个能"带记忆"的可用扩展——用户下次打开时自动加载上次保存的 API Key 与地区数据。

浏览器开发者工具 Application 面板中的 Local Storage 存储项

本课时的项目背景与代码骨架

按照 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 即执行 webpacknpm 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 状态、打断用户体验——这正是 handleSubmitreset 里第一件事就是 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();
}

逻辑分解如下:

  1. 用两个 const 从 LocalStorage 读取 apiKeyregionName
  2. 任一值为 null(首次使用或未保存),把表单 display 设为 block,同时隐藏结果区、加载提示与清除按钮,并把错误文本清空;
  3. 两个值都存在,立即调用 displayCarbonUsage(storedApiKey, storedRegion) 拉取数据,期间先隐藏结果区、隐藏表单、显示清除按钮;
  4. reset() 只删除 regionName 一项(保留 API Key,用户重新选择地区即可),然后重新执行 init() 回到表单状态。

LocalStorage 核心概念

文档在此强调了一个浏览器端非常重要的概念:LocalStorage 是以"键值对"形式在浏览器中存储字符串的方式,可由 JavaScript 直接读写,用于管理浏览器内的数据。它的关键特性:

  • 不随会话过期:LocalStorage 不会自动清除;与之对比,SessionStorage 在浏览器关闭时会被清除。不同存储方式各有取舍,需要按场景选择;
  • API 统一且兼容性好:通过 getItem()setItem()removeItem() 三个方法即可完成读写删,在所有现代浏览器中广泛可用;
  • 按源隔离:文档特别提示——浏览器扩展拥有自己独立的 LocalStorage,与浏览器主窗口中打开的普通网页是不同的实例、互不干扰,这提供了隔离与安全边界。

你可以在开发者工具中直观验证:右键页面选择"检查"(或按 F12),切到 Application 面板的 Local Storage 区域,就能看到以键值对形式保存的 apiKeyregionName(即文首配图所示界面)。

⚠️ 安全提醒(原文档 ✅ 思考题):一般生产应用中把 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.jsdisplayCarbonUsage 在渲染前增加了空值校验——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 面板观察表单实际提交了什么。

登录后查看全文
热门项目推荐
相关项目推荐