Web-Dev-For-Beginners 浏览器扩展作业实战:选一个 API,用表单 + LocalStorage 打造真正有用的插件
这篇指南围绕 Web-Dev-For-Beginners 课程「5-browser-extension」模块的第二部分作业「Adopt an API」展开:你需要自选一个外部 API,亲手构建一个能解决真实问题的浏览器扩展,完整覆盖表单输入、API 集成、LocalStorage 持久化与错误处理四大能力。读完本文后,你将掌握从 API 选型、扩展规划、编码实现到测试打磨的完整作业流程,并能参照仓库中的 Carbon Trigger 参考实现(start 骨架代码 与 solution 完整实现)逐项对照自己的代码,确保满足作业评分量表中的每一项要求。
一、作业背景:它在整个浏览器扩展项目中的位置
在 Web-Dev-For-Beginners 中,浏览器扩展模块(5-browser-extension/README.md)以「Carbon Trigger」为主线项目:一个可在 Edge、Chrome、Firefox 上运行的扩展,调用 CO2 Signal API 查询指定地区的用电碳强度,帮助用户判断当前是否适合进行高耗电动作(比如在碳强度高时暂缓烘干衣物)。整个模块分三课:
- 了解浏览器如何工作、如何部署扩展;
- 本课的作业上下文——表单、API 调用与 LocalStorage(见 2-forms-browsers-local-storage/README.md);
- 后台任务与性能分析。
本课正课教你把「静态表单」变成「有真实数据、有记忆的动态工具」;而 assignment.md 则要求你脱离课程指定的 CO2 Signal API,自选一个 API 完成一个独立扩展。作业原文的四步流程是:选择 API → 规划扩展 → 构建扩展 → 测试打磨。下文逐步展开,并在每一步给出仓库中的源码级参照。
二、Step 1:选择你的 API
作业要求从公开的免费公共 API 列表(即业界知名的 public-apis 清单仓库)中挑选一个。对初学者友好的类别与推荐如下(来自作业原文):
| 类别 | 推荐 API | 用途 |
|---|---|---|
| 娱乐 | Dog CEO API | 随机狗狗图片 |
| 天气 | OpenWeatherMap | 实时天气数据 |
| 名言 | Quotable API | 励志语录 |
| 新闻 | NewsAPI | 当前头条 |
| 趣味知识 | Numbers API | 关于数字的冷知识 |
结合作业给出的入门建议(Getting Started Tips),选型时应注意:
- 从简单开始:优先选择不需要复杂认证(甚至无需 API key)的 API,降低首次集成的门槛;
- 先读文档:彻底理解所选 API 的端点(endpoint)、请求参数与响应结构,再动手写代码;
- 先画 UI:编码前先在纸上草图扩展界面,明确表单字段、结果区与错误区的位置;
- 频繁测试:增量开发,每加一个功能就测试一次;
- 假设会失败:始终假定 API 调用可能失败,并提前规划错误处理。
一个判断 API 是否适合初学者的实操标准:能否用一次无参或单参的 GET 请求拿到 JSON?响应里是否有清晰的字段说明?是否公开文档了错误码与速率限制?这些也是正课「Challenge」部分要求你在调研任意浏览器 API 时必须回答的问题(它解决什么真实问题、如何处理错误与边缘情况、存在哪些安全考量、跨浏览器支持如何)。
三、Step 2:规划扩展——四个必答问题
作业要求在编码前书面回答以下四个问题。它们分别对应实现中的关键设计决策:
| 规划问题 | 决定什么 | 参照 Carbon Trigger 项目的答案示例 |
|---|---|---|
| 你的扩展解决什么问题? | 核心功能与展示内容 | 查询所在地区电网实时碳强度,辅助用户安排高耗电活动 |
| 目标用户是谁? | 界面复杂度与文案语气 | 关注可持续用电的普通消费者 |
| 要在 LocalStorage 里存什么数据? | 持久化键名与数据形态 | apiKey 与地区代码(字符串,key-value 形式) |
| 如何处理 API 失败或速率限制? | 错误提示与重试策略 | try/catch 捕获,向用户展示友好错误信息而非崩溃 |
以仓库中 Carbon Trigger 的实际设计为例(见 solution/src/index.js):setUpUser 函数在保存凭证后立即进入「loading 可见、错误区清空、清除按钮可见」的 UI 状态,再发起首次 API 调用;init 函数则根据 LocalStorage 中是否已有数据,把界面切换成「首次用户看表单」或「老用户直接看结果」两条分支。你的规划文档中应能对应到这样具体的状态流转描述。
四、Step 3:构建扩展——必备功能与代码要求
4.1 作业规定的必备功能(Required Features)
作业原文要求你的扩展必须包含:
- 表单输入(form inputs),覆盖 API 所需的全部参数;
- 带恰当错误处理的 API 集成;
- 使用 LocalStorage 保存用户偏好或 API key;
- 干净、响应式的用户界面;
- 加载状态(loading states)与用户反馈。
代码层面的硬性要求(Code Requirements):
- 使用现代 JavaScript(ES6+)特性;
- 对 API 调用使用
async/await; - 使用
try/catch做错误处理; - 添加解释代码意图的有意义注释;
- 保持一致的代码格式化。
4.2 从骨架代码理解实现结构
仓库提供了带编号注释的骨架文件 start/src/index.js,用 //1~//6 标记了代码应放置的六个位置。这正是「按正课分段补齐代码」的工作方式:
//1
// form fields // 表单与结果区的 DOM 引用
// results divs
//6
//call the API // 调用 API 并渲染结果
//5
//set up user's api key and region
//4
// handle form submission
//3 initial checks // init():读 LocalStorage 决定初始 UI 分支
//2
// set listeners and start app // 绑定事件并启动
对应正课讲解的完整调用链是:document.querySelector 拿到 .form-data、.region-name、.api-key、.loading、.errors、.result-container 等元素引用 → form.addEventListener('submit', ...) 与 clearBtn.addEventListener('click', ...) 绑定事件并立即调用 init() → handleSubmit 中 e.preventDefault() 阻止默认提交(否则页面会刷新、丢失全部 JS 状态)→ setUpUser 写入 LocalStorage 并触发 displayCarbonUsage → API 返回后更新多个 UI 元素。
4.3 LocalStorage:给扩展持久记忆
LocalStorage 是这条链路的核心持久化机制,其关键特性(正课原文归纳):
- 数据在浏览器会话之间持久保存(跨会话,区别于 sessionStorage);
- 以 key-value 形式存取,
getItem()/setItem()操作,键不存在时getItem()返回null——这正是init()判断「首次用户还是老用户」的依据; - 数据在关闭浏览器、重启电脑后依然保留;
- 提供无需网络延迟的即时访问。
仓库参考实现中的读写代码(solution/src/index.js):
// set up api key and region
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);
};
//initial checks
const init = async () => {
//if anything is in localStorage, pick it up
const storedApiKey = localStorage.getItem('apiKey');
const storedRegion = localStorage.getItem('region');
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';
}
};
const reset = async (e) => {
e.preventDefault();
//clear local storage for region only
localStorage.removeItem('region');
init(); // 回到首次用户分支,重新展示表单
};
你可以在 DevTools(F12)的 Application 标签下展开 Local Storage 面板验证扩展确实写入了 apiKey 与地区键值(本文开头第二张截图即该面板示例)。
安全提示(继承自正课原文):在生产环境中把 API key 存进 LocalStorage 存在风险——任何页面 JS 都能读取它。作为学习目的没有妨礙,但真实应用应将敏感凭证放在服务端安全存储。另外,浏览器扩展拥有与普通网页相互隔离的独立 LocalStorage,这提供了安全隔离、避免与其他网站冲突。
4.4 API 调用:fetch 与错误处理
正课给出的依赖零库版本使用原生 fetch,要点是 response.ok 检查 + try/catch 兜底(节选自 2-forms-browsers-local-storage/README.md 的完整函数):
async function displayCarbonUsage(apiKey, region) {
try {
const response = await fetch(
`https://api.co2signal.com/v1/latest?countryCode=${region}`, {
method: 'GET',
headers: {
'auth-token': apiKey,
'Content-Type': 'application/json'
}
});
// Check if the API request was successful
if (!response.ok) {
throw new Error(`API request failed: ${response.status}`);
}
const data = await response.json();
const carbonData = data.data;
const carbonIntensity = Math.round(carbonData.carbonIntensity);
// Update the user interface with fetched data
loading.style.display = 'none';
form.style.display = 'none';
myregion.textContent = region.toUpperCase();
usage.textContent = `${carbonIntensity} grams (grams CO₂ emitted per kilowatt hour)`;
fossilfuel.textContent = `${carbonData.fossilFuelPercentage.toFixed(2)}% (percentage of fossil fuels used to generate electricity)`;
results.style.display = 'block';
} catch (error) {
console.error('Error fetching carbon data:', error);
// Show user-friendly error message
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 版本 改用 axios(start/package.json 中声明了 axios 依赖),并多了一个值得注意的防御点——在渲染前显式校验关键字段是否存在:
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');
}
两种写法都印证了作业「错误处理」评分项的分层思路:网络层失败(try/catch 捕获)、HTTP 层失败(response.ok / axios 的 reject)、数据层缺失(字段校验)三层都要有兜底,最后统一收敛为用户可理解的错误文案。
4.5 扩展工程结构:manifest、入口页与构建
你的自选 API 扩展需要与 Carbon Trigger 相同的工程骨架。仓库中 start/dist/manifest.json 是 MV3 扩展清单的范例:
{
"manifest_version": 3,
"name": "My Carbon Trigger",
"version": "0.1.0",
"host_permissions": ["<all_urls>"],
"background": {
"service_worker": "background.js"
},
"action": {
"default_popup": "index.html"
}
}
action.default_popup 指向扩展弹窗页面,即承载表单与结果区的 start/dist/index.html——可以看到页面预留了 <!--form area--> 与 <!--result area--> 两个注释锚点,正好对应正课中「表单区 / 结果区」的 UI 双分支状态;host_permissions 则授权扩展发起跨域 API 请求。
构建与运行流程(来自 start/README.md):
npm install
npm run build
package.json 中 build 脚本即 webpack,watch 脚本为 webpack --watch(开发时可热更新),环境要求 node >= 18、npm >= 9。构建产物落在 dist 目录。在 Edge 中安装:打开右上角「三点」菜单进入 Extensions 面板,选择「Load Unpacked」,选中 dist 文件夹即可加载扩展;Chrome 对应 chrome://extensions 开启开发者模式后「加载已解压的扩展程序」。
五、Step 4:测试与打磨
作业给出的测试清单及对应的验证手段:
| 测试项 | 验证方法 |
|---|---|
| 用各种输入测试扩展 | 合法/空/超长/非法格式的地区码或参数,覆盖表单校验(required 属性依赖浏览器原生验证,正课特别强调这一点) |
| 处理边缘情况(无网络、非法 API 响应) | 断开网络模拟失败路径;用错误 key 触发 401/403,确认走 catch 分支且展示友好文案而不是白屏 |
| 确保浏览器重启后扩展仍正常 | 重启后依赖 LocalStorage 中的 apiKey/地区值走 init() 的老用户分支,自动重新拉取数据 |
| 添加用户友好的错误信息 | 错误文案应提示用户下一步动作(如「请检查 API key 与地区代码」),而非堆砌技术细节 |
正课还配了一个进阶挑战(GitHub Copilot Agent Challenge),可作为打磨阶段的验收标准:为 API 调用增加指数退避重试、调用前校验地区码、带进度指示的加载动画、带 30 分钟过期时间戳的 LocalStorage 缓存、历史数据展示,以及完整的 JSDoc 注释。这些要求恰好也是下一节的加分项。
六、Bonus Challenges:把扩展推向更高水平
作业列出的五项加分挑战,以及实现时可以参考的仓库线索:
- 接入多个 API 端点:如 CO2 数据之外增加区域列表端点(Electricity Map 的 zones 端点在 start/README.md 中被用于查询地区代码),让用户不必手输代码;
- 实现数据缓存以减少 API 调用:LocalStorage 存
{value, timestamp},读取时先判断 30 分钟内的缓存是否有效——即正课 Copilot 挑战中的缓存模式; - 为常用操作添加键盘快捷键:利用 WebExtensions 的 commands 能力绑定快捷键唤起扩展或触发查询;
- 数据导出/导入:将 LocalStorage 中的偏好序列化为 JSON 供用户下载,再支持粘贴/文件导入恢复;
- 用户自定义选项:在表单中暴露更多偏好项(单位、刷新频率、阈值告警色等),全部落入 LocalStorage。
七、提交要求与评分量表
7.1 提交物清单
- 一个能成功集成所选 API 的可运行浏览器扩展;
- 一份 README,说明:选了这个 API 及原因、如何安装与使用、需要哪些 API key 或前置配置、扩展运行中的截图;
- 干净且有注释的代码,遵循现代 JavaScript 实践。
7.2 评分量表(Rubric,完整继承自作业原文)
| 维度 | 优秀 (90-100%) | 熟练 (80-89%) | 发展中 (70-79%) | 入门 (60-69%) |
|---|---|---|---|---|
| API 集成 | 无缝的 API 集成,具备全面的错误处理与边缘情况管理 | API 集成成功,带基础错误处理 | API 可用但错误处理有限 | API 集成存在严重问题 |
| 代码质量 | 干净、注释完善的现代 JavaScript,遵循最佳实践 | 良好的代码结构与充足注释 | 代码可运行但组织待改进 | 代码质量差、注释极少 |
| 用户体验 | 打磨精致的界面,出色的加载状态与用户反馈 | 良好的界面与基础用户反馈 | 能正常运作的基础界面 | 用户体验差、界面令人困惑 |
| LocalStorage | 高水平的 LocalStorage 运用,含数据校验与管理 | 正确实现关键功能的 LocalStorage | 基础的 LocalStorage 实现 | 使用极少或错误 |
| 文档 | 全面的 README,含搭建步骤与截图 | 良好文档,覆盖大部分要求 | 基础文档,缺失部分细节 | 文档差或完全缺失 |
对照这张量表自查时,可以逐行映射到第四节各小节:API 集成对应 4.4 的三层错误兜底;代码质量对应骨架代码的注释与格式化要求;用户体验对应 loading/错误/结果三态切换;LocalStorage 对应 4.3 的存取与校验;文档对应 7.1 的 README 清单。
八、延伸阅读:仓库中可继续深入的文件
- 正课教程(DOM 引用、事件监听、init/reset、API 调用全流程讲解):5-browser-extension/2-forms-browsers-local-storage/README.md
- 本作业原文:5-browser-extension/2-forms-browsers-local-storage/assignment.md
- 待填充的骨架代码:5-browser-extension/start/src/index.js
- 完整参考实现(含数据校验与图标联动):5-browser-extension/solution/src/index.js
- 模块总览与 Credits:5-browser-extension/README.md
- 安装与部署说明:5-browser-extension/start/README.md、5-browser-extension/solution/README.md
- MV3 清单与弹窗页面示例:5-browser-extension/start/dist/manifest.json、5-browser-extension/start/dist/index.html
作业最后给出的学习资源主题(对应 MDN 官方文档):WebExtensions 扩展开发文档、Fetch API 使用指南、Window/localStorage 存储教程、JSON 解析与处理。带着上面的规划问题与评分量表开始,选一个你真正想用起来的 API,你的第一个「有记忆、有数据」的浏览器扩展就完成了。
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 StartedRust0629
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
HivisionIDPhotos⚡️HivisionIDPhotos: a lightweight and efficient AI ID photos tools. 一个轻量级的AI证件照制作算法。Python07
DragonOSDragonOS is an operating system developed from scratch using Rust, with Linux compatibility. It is designed for **Serverless** scenarios. 使用Rust从0自研内核,具有Linux兼容性的操作系统,面向云计算Serverless场景而设计。Rust00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00

