Web-Dev-For-Beginners 浏览器扩展实战二:表单提交、API 调用与 Local Storage 持久化
本文基于 Web-Dev-For-Beginners 仓库中「浏览器扩展项目」第 2 课(日文版课程文档),带你把第 1 课搭好的静态扩展页面变成真正“有记忆、会联网”的动态应用:通过 CSS 类选择器定位 DOM 元素、注册事件监听器、用 Local Storage 持久化用户设置、以 async/await 异步调用 CO2 Signal REST API 并渲染结果。读完并动手实现后,你将掌握一个可运行的碳足迹查询扩展的完整前端逻辑,理解 Web 存储、REST 认证与异步错误处理这三个职业 Web 开发的核心技能如何串联成一条数据流。
本课在整个扩展项目中的位置
浏览器扩展项目分三课推进:第 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),脚本提供 build 与 watch 两条命令。
第一步:准备要操作的 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 面板:
课程同时给出了明确的安全警告:把 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(发电中化石燃料占比,保留两位小数)分别渲染到usage、fossilfuel,并显示结果容器; - 出错时隐藏加载与结果区,向用户显示友好错误消息,而不是抛出原始异常。
代码里的 //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.json、main.js、background.js、index.html、styles.css 与 images/——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() 分支逻辑的放大版。
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 StartedRust0623
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

