Web-Dev-For-Beginners 实战:为你的浏览器扩展采用一个公开 API,并用 localStorage 赋予它记忆
本文基于 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 失败或限流? | 决定错误处理与重试策略 |
这四个问题不是形式主义:示例代码中的每个结构都能对应到一个答案。例如“存什么”对应 apiKey 与 region 两个 localStorage 键;“如何失败”对应 try/catch 与面向用户的错误文案(详见下文)。
第三步:构建扩展——必备功能与代码要求
必备功能(Required Features):
- 为所需 API 参数提供表单输入;
- 带恰当错误处理的 API 集成;
- 用 local storage 保存用户偏好或 API key;
- 干净、响应式的用户界面;
- 加载状态与用户反馈。
代码要求(Code Requirements):
- 使用现代 JavaScript(ES6+)特性;
- API 调用使用 async/await;
- 用 try/catch 实现恰当的错误处理;
- 添加解释性注释;
- 保持一致的代码格式。
第四步:测试与打磨
- 用多种输入测试扩展;
- 处理边缘情况(无网络、无效的 API 响应);
- 确保浏览器重启后扩展仍然可用——这正是 local storage 持久化的验收点;
- 添加用户友好的错误消息。
加分挑战(Bonus Challenges)
- 接入多个 API 端点以获得更丰富的功能;
- 实现数据缓存以减少 API 调用;
- 为常用操作添加键盘快捷键;
- 添加数据导出/导入功能;
- 提供用户自定义选项。
提交要求与评分标准
提交要求(Submission Requirements):
- 一个能成功对接所选 API 的可用浏览器扩展;
- 一份 README,说明:选择了哪个 API 及原因、如何安装和使用、需要什么 API key 或前置配置、扩展运行截图;
- 遵循现代 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 才能看到数据,如下图所示。
安全提示(继承自教程原文):在 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.';
}
};
从仓库最终解决方案源码看,它对作业“代码要求”与“边缘情况处理”的落实体现在四处:
- 认证方式:通过
auth-token请求头携带用户填写的 API key,正是作业“API key 存 local storage、随请求认证”要求的最小闭环; - 响应校验:在渲染前用可选链判断
carbonIntensity/fossilFuelPercentage是否缺失,缺失即抛错进入 catch——这覆盖了作业中“无效 API 响应”这一边缘情况; - 错误分级:网络失败、HTTP 非 2xx、数据缺字段三条路径都汇入同一个
catch,统一显示面向用户的错误文案(而非把堆栈暴露给用户),并在控制台保留console.warn便于调试; - 异步全程非阻塞:所有涉及请求的函数(
displayCarbonUsage、setUpUser、handleSubmit、init、reset)均声明为async,符合“async/await 处理 API 调用”的要求。
另外,calculateColor 函数(solution/src/index.js#L17-L32)按碳强度分档(阈值 0/150/600/750/800)选出代表色,并通过 chrome.runtime.sendMessage({ action: 'updateIcon', ... }) 让后台脚本更新扩展图标——这一消息通道属于下一课的预告,但已展示了扩展页面脚本与后台脚本通信的典型模式。
构建与运行
两个项目(start 与 solution)的 package.json 声明了相同的环境约束与脚本:
- 运行环境:
node >= 18、npm >= 9; npm run build:用 webpack 一次性构建(教程结尾要求“运行npm run build后在浏览器中刷新扩展来验证”);npm run watch:webpack 监听模式,便于增量开发——对应作业“增量构建、频繁测试”的建议。
交作业前的自检清单
按评分标准逐项核对,可以压缩为五个可验证动作:
- 重启验收:关闭浏览器后重新打开扩展,确认凭表单逻辑能自动拉取并显示数据(
init()的回头用户分支被走到); - 断网验收:拔掉网络提交表单,确认看到的是友好错误文案而不是空白或报错堆栈;
- 无效输入验收:故意填错地区代码或 API key,确认
response.ok/ 数据校验分支触发并回退到可重试状态; - 存储验收:在 DevTools 的 Application → Local Storage 中确认键名与值符合预期,并验证“清除”按钮能
removeItem后回到初始表单; - 文档验收:README 覆盖“选了哪个 API、为什么、如何安装、需要什么 key、运行截图”五项,代码注释解释关键函数意图。
参考路径
- 法语版作业(本文主体):translations/assignment.fr.md
- 英文版作业(含完整步骤与五维评分表):assignment.md
- 本部分教程(DOM、事件、localStorage、fetch/axios 详解):README.md
- 最终解决方案:solution/src/index.js、solution/package.json
- 学生骨架(带编号注释的填空文件):start/src/index.js
- 模块总览:5-browser-extension/README.md
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 StartedRust0624
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
