第七章:数据持久化 —— `chrome.storage` 的记忆魔法
第七章:数据持久化 —— chrome.storage
的记忆魔法
本章目标:掌握 chrome.storage
API 的使用,学会数据的增删改查,并为我们的项目创建一个“选项页”(Options Page),让用户可以自定义配置并将其持久化保存。
引子:从“金鱼的记忆”到“大象的智慧”
金鱼的记忆只有七秒(这是一个流传甚广的误解,但比喻很形象)。我们目前的扩展就像这条金鱼。你告诉它:“我讨厌这个网站,以后不要把它分组。” 它点点头,但下次你打开浏览器,它又会热情地把那个网站给你分好组,因为它已经把你昨天说过的话忘得一干二净。
而大象,以其惊人的记忆力著称。它能记住多年的水源地、家族成员,甚至是对它好或坏的人。我们希望我们的扩展能像大象一样,拥有智慧和记忆。
- 用户设置了一个“白名单”,希望某些网站永远不被自动分组。扩展必须记住这个名单。
- 用户选择了一个喜欢的主题颜色(比如暗色模式)。扩展必须记住这个偏好。
- 我们想统计用户总共通过我们的扩展节省了多少次点击。扩展必须记住这个数字,并不断累加。
所有这些“记忆”的需求,都指向了同一个解决方案:数据持久化 (Data Persistence)。即,将数据存储在一个不会因为程序关闭而丢失的地方。
在 Web 开发中,我们有 localStorage
和 sessionStorage
。但在扩展开发中,我们有更强大、更合适的选择:chrome.storage
API。
为什么不用 localStorage
?
- 同步阻塞:
localStorage
是同步的,读写操作会阻塞主线程。在 Service Worker 这种对性能极其敏感的环境中,同步 I/O 是被严格禁止的。 - 无痕模式下不可用:在无痕窗口中,
localStorage
的数据是隔离且临时的,不符合我们持久化的需求。 - 容量限制:通常有 5MB 的限制。
- 数据类型有限:只能存储字符串。存储对象需要手动
JSON.stringify
和JSON.parse
。
而 chrome.storage
API 完美地解决了这些问题:
- 异步非阻塞:所有操作都是异步的,返回 Promise 或接受回调函数,绝不阻塞。
- 跨设备同步:提供了
sync
存储区,可以(如果用户登录了 Google 账号并开启同步)在用户的不同设备间自动同步数据! - 为扩展优化:可以直接存储 JSON 对象,无需手动序列化。对读写频率有优化。
- 对无痕模式友好。
今天,我们将解锁这门“记忆魔法”,并为我们的管家打造一个专属的“大脑皮层”——一个功能完善的选项页。
7.1 chrome.storage
API 精讲:你的两个“记忆宫殿”
chrome.storage
API 提供了两个主要的存储区域,你可以把它们想象成两个功能不同的“记忆宫殿”。
1. chrome.storage.local
—— 本地私人仓库
- 特点:
- 本地存储:数据只存在于当前用户安装扩展的这台电脑上。不会同步到其他设备。
- 容量较大:通常有 5MB 的存储空间,对于绝大多数扩展来说绰绰有余。
- 适用场景:存储那些只对当前设备有意义的数据,或者数据量较大的缓存。例如:用户的本地草稿、大量的历史记录缓存、不需要跨设备同步的复杂配置。
2. chrome.storage.sync
—— 云端同步保险箱
- 特点:
- 自动同步:这是它的核心魅力!只要用户登录了 Chrome/Edge 账号并开启了同步功能,这里的数据就会自动在他们的所有设备间同步。你在公司的电脑上设置了白名单,回到家的电脑上打开扩展,白名单就已经在那里了。
- 容量较小:为了保证同步效率,
sync
存储区的总容量限制较小(约 100KB),并且对单个条目的尺寸(约 8KB)和每分钟的读写次数都有严格限制。 - 适用场景:存储用户的核心配置、偏好设置、授权 token 等小体积但至关重要的数据。我们的“白名单”和“主题偏好”就非常适合放在这里。
我们项目用哪个? 对于我们的“智能标签页管家”来说,用户的配置(如白名单)属于核心偏好,我们希望它能在用户的所有设备上保持一致。因此,
chrome.storage.sync
是我们的首选。
核心操作:CRUD (增、查、改、删)
chrome.storage
的操作非常直观,主要就是四个动作。我们以 chrome.storage.sync
为例,local
的用法完全相同。
所有方法都支持两种调用方式:回调函数和 Promise。在现代 JavaScript 中,使用 async/await
搭配 Promise 会让代码更清晰,我们将主要使用这种方式。
1. 增/改 (Set)
chrome.storage.sync.set(items: object)
: 用于存储一个或多个项目。它接受一个对象,对象的 key 就是你要存储的数据名,value 就是数据本身。如果 key 已存在,则会覆盖旧值。
// 存储用户的偏好设置
async function saveSettings() {const userSettings = {theme: 'dark',enableNotifications: true,blockedSites: ['facebook.com', 'twitter.com']};await chrome.storage.sync.set({ settings: userSettings });console.log("设置已保存!");
}
2. 查 (Get)
chrome.storage.sync.get(keys: string | string[] | object | null)
: 用于获取一个或多个项目。它的参数非常灵活。
// 获取单个项目
async function getTheme() {const data = await chrome.storage.sync.get('settings');// 注意:返回的结果总是一个对象// data 的形式是 { settings: { theme: 'dark', ... } }return data.settings ? data.settings.theme : 'light'; // 做个空值判断
}// 获取多个项目
async function getMultiple() {const data = await chrome.storage.sync.get(['settings', 'lastLoginTime']);// data: { settings: {...}, lastLoginTime: 167... }
}// 获取所有项目
async function getAll() {const allData = await chrome.storage.sync.get(null);console.log("所有存储的数据:", allData);
}// 获取时指定默认值 (非常有用!)
async function getSettingsWithDefaults() {const data = await chrome.storage.sync.get({// 如果 'settings' 不存在,则返回这个默认值settings: {theme: 'light',enableNotifications: false,blockedSites: []}});return data.settings;
}
3. 删 (Remove)
chrome.storage.sync.remove(keys: string | string[])
: 用于删除一个或多个项目。
async function removeTheme() {// 只删除 settings 对象里的 theme 属性是做不到的,// 需要先 get,修改后再 set。// remove 只能删除顶级的 key。await chrome.storage.sync.remove('settings');console.log("整个 'settings' 对象已被删除。");
}
4. 清空 (Clear)
chrome.storage.sync.clear()
: 删除该存储区的所有数据。慎用!
async function clearAllMyData() {await chrome.storage.sync.clear();console.log("云端保险箱已被清空!");
}
理论已就位,是时候为我们的管家建造一个可以让他“冥想”和“学习”的房间了——选项页。
7.2 项目实战:创建功能完善的“选项页” (Options Page)
选项页是一个独立的 HTML 页面,用户可以通过在扩展管理页面点击“扩展程序选项”或者在 Popup 中提供一个链接来访问它。它是我们与用户进行深度交互、允许他们进行复杂配置的主要场所。
我们的目标:创建一个选项页,允许用户管理一个“网站白名单”。在这个名单里的网站,将不会被我们未来的“一键分组”功能所影响。
第一步:创建选项页的 HTML 和 JS 文件
- 在项目根目录下,创建一个新的 HTML 文件,命名为
options.html
。 - 在
scripts
文件夹下,创建一个新的 JS 文件,命名为options.js
。
📂 my-first-extension/
├── 📄 options.html <-- 新建
└── 📂 scripts/├── 📄 options.js <-- 新建├── 📄 background.js└── 📄 content.js
第二步:在 manifest.json
中注册选项页
我们需要告诉浏览器,我们的扩展有一个选项页,它的入口文件是 options.html
。
打开 manifest.json
,添加 options_page
或 options_ui
字段。
{..."action": { ... },"options_page": "options.html","background": { ... },...
}
options_page
: 这是传统的方式,点击选项时会在一个新标签页中完整打开options.html
。options_ui
: 这是一个更新的方式,它有两个属性page
和open_in_tab
。如果open_in_tab
为false
(默认),选项页会嵌入在chrome://extensions
页面中的一个小弹窗里打开。如果为true
,则效果和options_page
一样。
为了获得最大的空间和灵活性,我们这里使用 options_page
。
第三步:设计选项页的 UI (options.html
)
打开 options.html
,我们来搭建一个简单的配置界面。
<!DOCTYPE html>
<html lang="zh-CN">
<head><meta charset="UTF-8"><title>智能标签页管家 - 选项</title><style>/* 这里可以放一些和 popup.html 类似的通用样式 */body { font-family: sans-serif; padding: 20px; max-width: 600px; margin: auto; }h1 { color: #4A90E2; }#whitelist { width: 100%; min-height: 150px; font-size: 14px; padding: 5px; }button { padding: 10px 15px; background-color: #50C878; color: white; border: none; border-radius: 4px; cursor: pointer; }#status { margin-top: 10px; color: green; font-weight: bold; }</style>
</head>
<body><h1>设置</h1><h2>网站白名单</h2><p>在此处添加的网站域名(每行一个),将不会被“一键分组”功能影响。</p><textarea id="whitelist" placeholder="例如: google.com github.com"></textarea><button id="save">保存设置</button><div id="status"></div><script src="scripts/options.js"></script>
</body>
</html>
这个界面非常简单:
- 一个
<textarea>
让用户输入域名列表,每行一个。 - 一个“保存”按钮。
- 一个
div
用来显示保存成功或失败的状态。 - 在
<body>
底部,我们引入了options.js
脚本。
第四步:编写选项页的交互逻辑 (options.js
)
这是本章的核心。我们需要在这个 JS 文件里实现:
- 页面加载时,从
chrome.storage.sync
读取已保存的白名单,并显示在<textarea>
中。 - 当用户点击“保存”按钮时,读取
<textarea>
的内容,将其处理成一个字符串数组,然后保存回chrome.storage.sync
。
打开 scripts/options.js
,写入以下代码:
// scripts/options.js// --- DOM 元素获取 ---
const whitelistTextarea = document.getElementById('whitelist');
const saveButton = document.getElementById('save');
const statusDiv = document.getElementById('status');// --- 功能函数 ---// 1. 保存选项到 chrome.storage
function save_options() {const whitelistValue = whitelistTextarea.value;// 将 textarea 的内容按换行符分割,过滤掉空行,并去除每行首尾的空格const sites = whitelistValue.split('\n').filter(site => site.trim() !== '').map(site => site.trim());chrome.storage.sync.set({whitelistedSites: sites}, () => {// 保存成功后,显示一个状态消息statusDiv.textContent = '选项已保存!';setTimeout(() => {statusDiv.textContent = '';}, 1500); // 1.5秒后自动消失});
}// 2. 从 chrome.storage 读取选项并恢复到页面上
function restore_options() {// 使用带默认值的方式获取,防止第一次使用时出错chrome.storage.sync.get({whitelistedSites: [] // 默认是一个空数组}, (items) => {// 将数组转换回以换行符分隔的字符串,并设置到 textarea 中whitelistTextarea.value = items.whitelistedSites.join('\n');});
}// --- 事件监听 ---// 页面加载完成后,立即恢复之前保存的选项
document.addEventListener('DOMContentLoaded', restore_options);// 点击保存按钮时,执行保存操作
saveButton.addEventListener('click', save_options);
让我们用 async/await
的方式重写一遍,代码会更现代和易读:
// scripts/options.js (async/await 版本)const whitelistTextarea = document.getElementById('whitelist');
const saveButton = document.getElementById('save');
const statusDiv = document.getElementById('status');// 1. 保存选项 (async)
const saveOptions = async () => {const whitelistValue = whitelistTextarea.value;const sites = whitelistValue.split('\n').filter(Boolean).map(s => s.trim());try {await chrome.storage.sync.set({ whitelistedSites: sites });statusDiv.textContent = '选项已保存!';setTimeout(() => { statusDiv.textContent = ''; }, 1500);} catch (error) {statusDiv.textContent = `保存失败: ${error.message}`;statusDiv.style.color = 'red';}
};// 2. 恢复选项 (async)
const restoreOptions = async () => {try {const items = await chrome.storage.sync.get({ whitelistedSites: [] });whitelistTextarea.value = items.whitelistedSites.join('\n');} catch (error) {console.error("恢复选项失败:", error);}
};document.addEventListener('DOMContentLoaded', restoreOptions);
saveButton.addEventListener('click', saveOptions);
代码解读:
-
restoreOptions
函数:- 在页面加载时(
DOMContentLoaded
)被调用。 - 它使用
chrome.storage.sync.get
来获取whitelistedSites
的值。注意我们提供的默认值{ whitelistedSites: [] }
,这能保证即使用户是第一次打开选项页,items.whitelistedSites
也是一个安全的空数组,而不会是undefined
,从而避免join
方法报错。 - 获取到数组后,使用
join('\n')
方法将数组元素用换行符连接成一个字符串,完美地还原回<textarea>
的格式。
- 在页面加载时(
-
saveOptions
函数:- 在用户点击保存按钮时被调用。
- 它获取
<textarea>
的value
。 split('\n')
: 将字符串按换行符分割成一个数组。filter(Boolean)
或filter(site => site.trim() !== '')
: 一个小技巧,用于过滤掉用户可能输入的多余的空行。map(s => s.trim())
: 确保每个域名前后的空格都被去掉。chrome.storage.sync.set
: 将处理好的sites
数组,以whitelistedSites
为键,保存到云端存储中。- 保存成功后,我们在
statusDiv
中显示一个短暂的成功提示,给用户一个明确的反馈。
第五步:部署与验证
- 保存所有修改过的文件 (
manifest.json
,options.html
,options.js
)。 - 去
chrome://extensions
页面,刷新扩展。 - 现在,在你的扩展卡片上,你应该能看到一个蓝色的链接 “扩展程序选项”。点击它!
- 一个新的标签页会打开,内容就是我们的
options.html
。 - 进行测试:
- 在文本框里输入几个域名,比如
google.com
和wikipedia.org
,每行一个。 - 点击“保存设置”按钮,你应该能看到“选项已保存!”的提示。
- 刷新这个选项页。你会发现,你刚才输入的内容依然存在!我们的
restoreOptions
函数起作用了。 - 关闭这个选项页,甚至关闭整个浏览器再重新打开,然后再次进入选项页。内容仍然在那里!
chrome.storage
的持久化魔法成功了! - 打开后台 Service Worker 的开发者工具,在 Console 中输入
await chrome.storage.sync.get('whitelistedSites')
并回车,你就能亲眼看到我们存储的数据。
- 在文本框里输入几个域名,比如
我们已经成功地为我们的扩展创建了一个功能齐全的配置中心,并赋予了它跨会话的记忆能力!
本章总结与展望
在这一章,我们为我们的扩展植入了“记忆”,让它从一个健忘的工具,向一个智能的管家迈出了决定性的一步。
我们学到了:
chrome.storage
的优势:理解了它相比localStorage
在异步、同步、数据类型支持上的巨大优势。local
vssync
:明确了两个存储区的区别和适用场景。- 熟练掌握了 CRUD 操作:通过
async/await
的方式,我们能流畅地对存储进行增删改查。 - 创建了功能性的选项页:我们从零开始,创建了一个允许用户自定义配置并持久化保存的 Options Page。
现在,我们的扩展不仅有了强大的执行能力,还有了可靠的记忆能力。它已经准备好去执行更复杂的、依赖于用户配置的任务了。
在下一章,我们将回到我们的主线功能,并把今天所学的一切付诸实践。我们将要实现那个炫酷的**“一键分组”功能,并且,这个功能会智能地读取我们保存在 chrome.storage
中的白名单**,跳过那些用户不希望被分组的网站。