首页
/ Web-Dev-For-Beginners 实战:为你的浏览器扩展采用一个公开 API,并用 localStorage 赋予它记忆

Web-Dev-For-Beginners 实战:为你的浏览器扩展采用一个公开 API,并用 localStorage 赋予它记忆

2026-09-06 15:08:13作者:温玫谨Lighthearted

本文基于 Web-Dev-For-Beginners 课程浏览器扩展项目第五模块的作业文档 assignment.fr.md(“Adoptez une API”),并结合同目录英文版 assignment.md 与仓库中的完整示例代码 solution/src/index.js,完整讲解“选择公开 API → 规划 → 构建扩展 → 测试打磨”的端到端流程,以及评分标准中每一项能力在真实代码里的落点。读完后,你将能够独立交付一个能调用外部 API、持久化用户偏好并妥善处理错误的浏览器扩展。

作业背景:这是整个扩展项目的收官之作

5-browser-extension/2-forms-browsers-local-storage/README.md 是本项目第二部分“Call an API, use Local Storage”的教程,其知识地图覆盖了五大能力:DOM 操作(元素选择、事件处理、状态管理)、Local Storage(数据持久化、键值对、用户偏好)、API 集成(HTTP 请求、认证、JSON 解析、错误处理)、异步编程(Promise、async/await)与用户体验(加载状态、错误提示)。本作业正是对这一整章能力的综合验收:

  • 作业原文(法语版)要求:从免费公开 API 清单中选择一款 API,创建一个解决真实问题的浏览器扩展。它可以是一个简单问题(“手边没有足够的宠物照片”——原文建议此时尝试 Dog CEO 随机狗狗图片 API),也可以是更大的项目——“玩得开心就好”。
  • 英文版作业把要求展开为四个步骤:选择 API、规划扩展、构建扩展、测试打磨,并给出了完整评分表与提交规范,下文将完整继承。

仓库中贯穿本模块的示例项目是一个碳强度追踪扩展(在 solution/package.json 中命名为 carbon-trigger-extension):用户通过表单提交 API 密钥与地区代码,扩展调用 CO2 Signal 接口获取电网实时碳强度数据,并把设置持久化到浏览器本地存储。下面逐节对照作业要求,先看要求本身,再看示例代码如何满足它。

第一步:选择你的 API

作业要求从免费公开 API 清单(public-apis 列表)中挑选一个 API。英文版给出的入门友好候选方向包括:

  • 娱乐:Dog CEO API——随机狗狗图片
  • 天气:OpenWeatherMap——实时天气数据
  • 名言:Quotable——励志语录
  • 新闻:NewsAPI——当前头条
  • 趣味冷知识:Numbers API——关于数字的有趣事实

选择时的核心判断标准是:这个 API 的数据能否支撑一个可感知的小问题被解决——狗狗图片解决“不够看狗”,碳强度数据解决“我的电是否绿色”。作业同时隐含了一个工程约束:优先选择不需要复杂认证的 API(“Getting Started Tips”第 1 条),并在动手前彻底读懂所选 API 的端点与响应结构(第 2 条)。

第二步:规划扩展(动代码之前必须回答的四个问题)

规划问题 指向的工程决策
你的扩展解决什么问题? 明确数据需求,决定调用哪个端点
目标用户是谁? 决定 UI 文案与交互复杂度
你会把什么数据存进 local storage? 决定键名设计(如 API key、地区偏好)
如何处理 API 失败或限流? 决定错误处理与重试策略

这四个问题不是形式主义:示例代码中的每个结构都能对应到一个答案。例如“存什么”对应 apiKeyregion 两个 localStorage 键;“如何失败”对应 try/catch 与面向用户的错误文案(详见下文)。

第三步:构建扩展——必备功能与代码要求

必备功能(Required Features):

  1. 为所需 API 参数提供表单输入;
  2. 带恰当错误处理的 API 集成;
  3. 用 local storage 保存用户偏好或 API key;
  4. 干净、响应式的用户界面;
  5. 加载状态与用户反馈。

代码要求(Code Requirements):

  • 使用现代 JavaScript(ES6+)特性;
  • API 调用使用 async/await;
  • 用 try/catch 实现恰当的错误处理;
  • 添加解释性注释;
  • 保持一致的代码格式。

第四步:测试与打磨

  • 用多种输入测试扩展;
  • 处理边缘情况(无网络、无效的 API 响应);
  • 确保浏览器重启后扩展仍然可用——这正是 local storage 持久化的验收点;
  • 添加用户友好的错误消息。

加分挑战(Bonus Challenges)

  • 接入多个 API 端点以获得更丰富的功能;
  • 实现数据缓存以减少 API 调用;
  • 为常用操作添加键盘快捷键;
  • 添加数据导出/导入功能;
  • 提供用户自定义选项。

提交要求与评分标准

提交要求(Submission Requirements):

  1. 一个能成功对接所选 API 的可用浏览器扩展
  2. 一份 README,说明:选择了哪个 API 及原因、如何安装和使用、需要什么 API key 或前置配置、扩展运行截图;
  3. 遵循现代 JavaScript 实践的干净、有注释的代码

评分标准:法语版作业给出的三级标准为:

标准 Exemplaire(优秀) Adéquat(达标) Besoin d'amélioration(需改进)
完整性 使用上述清单中的 API 提交了完整的浏览器扩展 提交了部分完成的扩展 提交存在 bug

英文版进一步细化为五个维度(API 集成、代码质量、用户体验、Local Storage、文档),按 90-100 / 80-89 / 70-79 / 60-69 四档评分,其中对 Local Storage 一维的要求从“基础实现”到“带数据校验与管理的成熟用法”逐级递进——这提示你在实现 localStorage 时不应只写 setItem,还应考虑键的清理(removeItem)与空值判断(getItem 返回 null 的分支)。

入门提示(Getting Started Tips):从简单的 API 起步;通读 API 文档;编码前先草拟 UI;增量构建、频繁测试;始终假设 API 调用可能失败并预先规划。

仓库示例代码如何逐条满足作业要求

以下对照 solution/src/index.js(学生用骨架在 start/src/index.js,以 //1//6 的编号注释标出待填写位置)逐项印证上述要求。

表单元素引用与事件监听

// 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');

元素引用集中在文件头部(对应 solution/src/index.js#L4-L15),对应作业中“表单输入 + 加载状态 + 反馈”的功能项。监听器与启动逻辑只有三行:

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

//start app
init();

submit 事件同时覆盖点击提交与按 Enter 提交;箭头函数把事件对象 e 透传给处理器,供 e.preventDefault() 阻止页面刷新——这是扩展保持单页行为、不丢失 JavaScript 状态的关键。

localStorage:init / reset / setUpUser 三函数闭环

init() 是扩展启动时的状态判断器:

const init = async () => {
	//if anything is localStorage, pick it up
	const storedApiKey = localStorage.getItem('apiKey');
	const storedRegion = localStorage.getItem('region');

	//set icon to be generic green
	chrome.runtime.sendMessage({
		action: 'updateIcon',
		value: { color: 'green' },
	});

	if (storedApiKey === null || storedRegion === null) {
		//if we don't have the keys, show the form
		form.style.display = 'block';
		results.style.display = 'none';
		loading.style.display = 'none';
		clearBtn.style.display = 'none';
		errors.textContent = '';
	} else {
		//if we have saved keys/regions in localStorage, show results when they load
		results.style.display = 'none';
		form.style.display = 'none';
		displayCarbonUsage(storedApiKey, storedRegion);
		clearBtn.style.display = 'block';
	}
};

(代码见 solution/src/index.js#L89-L116)它的行为精确实现了“浏览器重启后仍可用”的验收点:getItem 在无值时返回 null,据此区分首次用户(显示表单)与回头用户(直接自动拉取数据),无需任何额外逻辑。提交表单后,setUpUser 完成持久化并发起首次请求:

const setUpUser = async (apiKey, region) => {
	localStorage.setItem('apiKey', apiKey);
	localStorage.setItem('region', region);
	loading.style.display = 'block';
	errors.textContent = '';
	clearBtn.style.display = 'block';
	//make initial call
	displayCarbonUsage(apiKey, region);
};

reset() 则只清除地区键并重新初始化,把选择权交还用户:

const reset = async (e) => {
	e.preventDefault();
	//clear local storage for region only
	localStorage.removeItem('region');
	init();
};

教程强调:浏览器扩展拥有独立于普通网页的隔离 local storage,这既是安全隔离也意味着调试时要打开扩展页面的 DevTools(F12)→ Application → Local Storage 才能看到数据,如下图所示。

浏览器 DevTools Application 面板中的 Local Storage 视图

安全提示(继承自教程原文):在 LocalStorage 中存 API key 适合学习场景,但生产环境中 JavaScript 可读取该数据,敏感凭据应放在服务端安全存储中。

API 集成:fetch 教学版与 axios 最终版

教程 README 中演示的是原生 fetch 写法(无外部依赖):

async function displayCarbonUsage(apiKey, region) {
	try {
		const response = await fetch('https://api.co2signal.com/v1/latest', {
			method: 'GET',
			headers: {
				'auth-token': apiKey,
				'Content-Type': 'application/json'
			}
		});
		if (!response.ok) {
			throw new Error(`API request failed: ${response.status}`);
		}
		const data = await response.json();
		const carbonData = data.data;
		// ...更新 UI
	} catch (error) {
		console.error('Error fetching carbon data:', error);
		loading.style.display = 'none';
		results.style.display = 'none';
		errors.textContent = 'Sorry, we couldn\'t fetch data for that region. Please check your API key and region code.';
	}
}

而仓库的最终解决方案 solution/src/index.js#L34-L68 改用 axios(在 solution/package.json 中声明为 axios ^1.15.0 依赖),并把 countryCode 作为查询参数显式传入:

const displayCarbonUsage = async (apiKey, region) => {
	try {
		await axios
			.get('https://api.co2signal.com/v1/latest', {
				params: { countryCode: region },
				headers: { 'auth-token': apiKey },
			})
			.then((response) => {
				const data = response?.data?.data;

				// ✅ Validate required data before using
				if (data?.carbonIntensity == null || data?.fossilFuelPercentage == null) {
					throw new Error('Missing carbon intensity or fossil fuel data');
				}

				let CO2 = Math.floor(data.carbonIntensity);
				calculateColor(CO2);

				loading.style.display = 'none';
				form.style.display = 'none';
				myregion.textContent = region;
				usage.textContent =
					Math.round(data.carbonIntensity) + ' grams (grams C02 emitted per kilowatt hour)';
				fossilfuel.textContent =
					data.fossilFuelPercentage.toFixed(2) +
					'% (percentage of fossil fuels used to generate electricity)';
				results.style.display = 'block';
			});
	} catch (error) {
		console.warn('Data fetch failed:', error.message);
		loading.style.display = 'none';
		results.style.display = 'none';
		errors.textContent = 'Sorry, data unavailable for the selected region.';
	}
};

从仓库最终解决方案源码看,它对作业“代码要求”与“边缘情况处理”的落实体现在四处:

  1. 认证方式:通过 auth-token 请求头携带用户填写的 API key,正是作业“API key 存 local storage、随请求认证”要求的最小闭环;
  2. 响应校验:在渲染前用可选链判断 carbonIntensity / fossilFuelPercentage 是否缺失,缺失即抛错进入 catch——这覆盖了作业中“无效 API 响应”这一边缘情况;
  3. 错误分级:网络失败、HTTP 非 2xx、数据缺字段三条路径都汇入同一个 catch,统一显示面向用户的错误文案(而非把堆栈暴露给用户),并在控制台保留 console.warn 便于调试;
  4. 异步全程非阻塞:所有涉及请求的函数(displayCarbonUsagesetUpUserhandleSubmitinitreset)均声明为 async,符合“async/await 处理 API 调用”的要求。

另外,calculateColor 函数(solution/src/index.js#L17-L32)按碳强度分档(阈值 0/150/600/750/800)选出代表色,并通过 chrome.runtime.sendMessage({ action: 'updateIcon', ... }) 让后台脚本更新扩展图标——这一消息通道属于下一课的预告,但已展示了扩展页面脚本与后台脚本通信的典型模式。

构建与运行

两个项目(startsolution)的 package.json 声明了相同的环境约束与脚本:

  • 运行环境:node >= 18npm >= 9
  • npm run build:用 webpack 一次性构建(教程结尾要求“运行 npm run build 后在浏览器中刷新扩展来验证”);
  • npm run watch:webpack 监听模式,便于增量开发——对应作业“增量构建、频繁测试”的建议。

交作业前的自检清单

按评分标准逐项核对,可以压缩为五个可验证动作:

  1. 重启验收:关闭浏览器后重新打开扩展,确认凭表单逻辑能自动拉取并显示数据(init() 的回头用户分支被走到);
  2. 断网验收:拔掉网络提交表单,确认看到的是友好错误文案而不是空白或报错堆栈;
  3. 无效输入验收:故意填错地区代码或 API key,确认 response.ok / 数据校验分支触发并回退到可重试状态;
  4. 存储验收:在 DevTools 的 Application → Local Storage 中确认键名与值符合预期,并验证“清除”按钮能 removeItem 后回到初始表单;
  5. 文档验收:README 覆盖“选了哪个 API、为什么、如何安装、需要什么 key、运行截图”五项,代码注释解释关键函数意图。

参考路径

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