首页
/ Web-Dev-For-Beginners 浏览器扩展实战二:表单提交、API 调用与 Local Storage 持久化

Web-Dev-For-Beginners 浏览器扩展实战二:表单提交、API 调用与 Local Storage 持久化

2026-09-06 14:47:39作者:晏闻田Solitary

本文基于 Web-Dev-For-Beginners 仓库中「浏览器扩展项目」第 2 课(日文版课程文档),带你把第 1 课搭好的静态扩展页面变成真正“有记忆、会联网”的动态应用:通过 CSS 类选择器定位 DOM 元素、注册事件监听器、用 Local Storage 持久化用户设置、以 async/await 异步调用 CO2 Signal REST API 并渲染结果。读完并动手实现后,你将掌握一个可运行的碳足迹查询扩展的完整前端逻辑,理解 Web 存储、REST 认证与异步错误处理这三个职业 Web 开发的核心技能如何串联成一条数据流。

Carbon Trigger 浏览器扩展最终界面,左侧为植物与人物配图,右侧展示扩展标题与查询结果

本课在整个扩展项目中的位置

浏览器扩展项目分三课推进:第 1 课 Browser Extension Part 1 讲解浏览器与扩展的基本概念、项目脚手架(webpack 构建)以及表单 HTML 与结果 <div> 的搭建;本课在第 1 课成果之上补齐 JavaScript 逻辑;第 3 课再做后台任务与性能优化。课程文档约定:编写代码时对照相应文件内的“编号段落”放置代码,学习者起点是一份带编号占位注释的骨架文件 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

可以看到编号与课程小节一一对应:1 是 DOM 引用,2 是监听器与启动,3 是 init(),4 是表单提交,5 是用户设置,6 是 API 调用。完成构建后运行 npm run build(webpack),并在扩展页刷新即可看到效果。项目的构建配置见 solution/package.json:要求 Node ≥ 18、npm ≥ 9,依赖 axios(^1.15.0)与 webpack(^5.105.4),脚本提供 buildwatch 两条命令。

第一步:准备要操作的 DOM 引用

在 JavaScript 能操作界面之前,需要先拿到具体 HTML 元素的引用。课程要求在 src/index.js 顶部用 const 变量保存每个关键元素的引用,全部通过 document.querySelector() 配合 CSS 类选择器获取:

// フォームフィールド
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');

这些类名与第 1 课在 HTML 中设置的结构一一对应:.form-data 是表单容器,.region-name.api-key 是两个输入框,其余为加载指示、错误区、结果区(.carbon-usage.fossil-fuel.my-region 分别是碳强度、化石燃料占比、区域名的展示位)以及清除按钮。把元素引用集中存为 const 后,后文所有函数都可以直接复用,而不必重复查询 DOM。

第二步:注册事件监听器并启动应用

接下来让扩展响应用户动作:为表单挂 submit 监听、为清除按钮挂 click 监听,并在文件末尾立即调用一次 init() 完成初始化:

form.addEventListener('submit', (e) => handleSubmit(e));
clearBtn.addEventListener('click', (e) => reset(e));
init();

注意这里的箭头函数速记法:事件对象 (e) 被直接转发给 handleSubmit / reset,等价于写一个匿名 function 表达式再调用。课程还提示一个自查点:如果忘了在表单提交处理里调用 e.preventDefault(),浏览器会执行默认提交行为(整页刷新),所有 JavaScript 状态都会丢失。

第三步:构建 init() 与 reset(),理解 Local Storage

init() 是扩展的“导航系统”:扩展每次打开时先检查用户是否保存过设置,再决定展示哪个界面:

function init() {
	//何かがローカルストレージにある場合は、それをピックアップします。
	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 先检查用户是否已在 Local Storage 保存了 API 密钥与区域代码;
  • 任一为 null(首次使用)时,把 form 显示为 block,同时隐藏结果区、加载区、清除按钮,并把错误文本清空;
  • 两者都存在(回访用户)时,直接用存储值调用 displayCarbonUsage() 发起 API 查询,隐藏表单、显示清除按钮。

reset() 只删除区域相关的键(regionName)再重新 init(),让用户换一个区域而不必从头配置。

Local Storage 的关键特性与安全边界

课程在此引入了一个对扩展开发极其重要的概念:Local Storage。它以 key-value 字符串对的形式把数据保存在浏览器中,特点是:

  • 无有效期——关闭浏览器、重启电脑后数据依然存在;另一种 Web 存储 session storage 则会在浏览器关闭时清空;
  • 通过 getItem()setItem()removeItem() 三个 Web API 方法读写,被所有现代浏览器广泛支持;
  • 扩展拥有独立的 Local Storage 实例,与主浏览器窗口(普通网页)的存储相互隔离,各自独立工作。

你可以在浏览器里验证:给 API 键存入一个字符串值后(如 Edge 中右键页面选“检查”),切到 DevTools 的 Application 标签页查看 Local Storage 面板:

DevTools Application 标签页中的 Local Storage 面板,展示扩展保存的 apiKey 等键值对

课程同时给出了明确的安全警告:把 API 密钥放进 Local Storage 并不是好习惯,因为任何能运行 JavaScript 的代码都能读取它。本课之所以这样做,纯粹是因为这是一个不会发布到应用商店的学习项目。真实产品应在服务端安全地保管敏感凭据。

第四步:处理表单提交 handleSubmit

handleSubmit 接收事件参数 (e),阻止默认行为(避免页面刷新),然后取出两个输入框的值,交给 setUpUser

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

文档提醒回忆第 1 课的 HTML:两个输入字段都带了 required 属性,所以浏览器会在提交前强制用户填值,这里拿到的不会是空值。

第五步:保存用户设置 setUpUser

setUpUser 负责把凭据写入 Local Storage,并布置好“API 正在调用中”的界面状态:

function setUpUser(apiKey, regionName) {
	localStorage.setItem('apiKey', apiKey);
	localStorage.setItem('regionName', regionName);
	loading.style.display = 'block';
	errors.textContent = '';
	clearBtn.style.display = 'block';
	//初期化の呼び出し
	displayCarbonUsage(apiKey, regionName);
}

它做了五件事:写入两个存储键、显示 loading 提示、清空历史错误、显示清除按钮、发起首次 API 调用。数据持久化与界面状态更新在同一次协调动作中完成,这是“无刷新体验”的关键。

第六步:调用 CO2 Signal API 并渲染结果

课程先补了 API 背景:API(Application Programming Interface)是程序之间交互的标准方式,其中最流行的一类是 REST API(Representational State Transfer),特点是使用不同形态的 URL 端点配合标准 HTTP 方法来取数。本扩展查询的是 CO2 Signal 的 https://api.co2signal.com/v1/latest 端点,返回某区域电网实时的碳强度与化石燃料占比。

异步是这段代码的核心:async 关键字让函数“先发起请求、等数据返回后再继续”,避免扩展整体卡死在等待网络上。完整实现如下(课程文档版本,axios 的导入路径以最终源码为准,见下文对比):

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.';
	}
}

课程对这段“大功能”的要点解读值得完整保留:

  • 因为 API 会返回 Promise,所以函数写成 async 并包裹在 try/catch 中——我们无法控制 API 响应速度(它甚至可能完全不响应),必须用异步来消化这种不确定性;
  • 通过请求头的 auth-token 参数完成认证,查询参数 countryCode 指定区域;
  • 响应到达后,把 response.data.data 里的 carbonIntensity(碳强度,克 CO₂/千瓦时)与 fossilFuelPercentage(发电中化石燃料占比,保留两位小数)分别渲染到 usagefossilfuel,并显示结果容器;
  • 出错时隐藏加载与结果区,向用户显示友好错误消息,而不是抛出原始异常。

代码里的 //calculateColor(CO2) 占位注释是留给第 3 课“动态图标”的内容,本课完成时扩展主体已可运行,只有图标颜色还是静态的。

对照最终源码:课程的落地形态

把课程文档与仓库中的完成版 solution/src/index.js 对照,可以验证整条调用链与几处演进:

  • 调用链一致:最终版同样是 init()(L89-L116)读取存储 → displayCarbonUsage()(L34-L68)用 axios 请求 v1/latest 端点并携带 auth-token 头 → 成功后更新 UI、失败时写入错误提示;监听器注册与 init() 启动(L125-L129)与课程描述完全一致。
  • 导入方式修正:最终版使用标准的 import axios from 'axios'(L1),而非课程讲义里示意性的 ../node_modules 路径写法。
  • 存储键名变化:最终版把区域键命名为 'region'(L74、L92、L121),课程讲义中为 'regionName',两者语义相同,读代码时注意区分。
  • 防御性增强:最终版在解析响应前先做可选链取值与空值校验(L42-L47),数据缺失时主动抛错走 catch 分支,比讲义版本更健壮。
  • calculateColor 的落地:讲义中的占位注释在最终版实现了为 calculateColor(L17-L32)——按 [0, 150, 600, 750, 800] 的 CO₂ 刻度把数值映射到五个渐深颜色,再通过 chrome.runtime.sendMessage({ action: 'updateIcon', value: { color } }) 通知后台脚本更新扩展图标;init() 里也补发了一个“通用绿色”的初始图标消息(L95-L100)。这正是课程所说“图标在下一课修复”的后续。

构建产物位于 dist/ 目录(如 start/dist),包含 manifest.jsonmain.jsbackground.jsindex.htmlstyles.cssimages/——main.js 就是 src/index.js 经 webpack 打包的结果。完成本课并 npm run build、刷新扩展页后,碳足迹查询即可用:首次使用显示表单,提交后显示加载态并拉取实时数据,重开扩展则自动用存储的设置恢复结果,清除按钮随时可以回到初始状态。

挑战与课后作业

课程的延伸练习是:挑选一个浏览器内置的 Web API(如 HTML Drag and Drop API)深入研究,思考“什么样的 API 称得上优秀”——它解决什么真实问题、如何处理错误与边界情况、有哪些安全考量、浏览器兼容如何。

配套作业 API を採用する(Adopt an API) 要求你自选一个免费公共 API 并构建一个完整的浏览器扩展,英文版 assignment.md 给出了更细的验收标准:表单输入 API 所需参数、带正确错误处理的 API 集成、用 Local Storage 保存用户偏好或密钥、加载态与用户反馈、async/await + try/catch 的现代 JS 实现;加分项包括多端点、缓存、键盘快捷键、数据导入导出等。评分维度覆盖 API 集成、代码质量、用户体验、Local Storage 使用与 README 文档质量。

课程最后的复习建议值得展开:Local Storage 与 API 是职业 Web 开发者的两件常用工具,想一想它们如何协同——例如设计一个网站,把用户授权信息缓存在 Web 存储中,页面每次加载时先读缓存再决定是否重新向 API 请求,这正是本课 init() 分支逻辑的放大版。

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