第七章:数据持久化 —— `chrome.storage` 的记忆魔法

第七章:数据持久化 ------ chrome.storage 的记忆魔法

本章目标:掌握 chrome.storage API 的使用,学会数据的增删改查,并为我们的项目创建一个"选项页"(Options Page),让用户可以自定义配置并将其持久化保存。


引子:从"金鱼的记忆"到"大象的智慧"

金鱼的记忆只有七秒(这是一个流传甚广的误解,但比喻很形象)。我们目前的扩展就像这条金鱼。你告诉它:"我讨厌这个网站,以后不要把它分组。" 它点点头,但下次你打开浏览器,它又会热情地把那个网站给你分好组,因为它已经把你昨天说过的话忘得一干二净。

而大象,以其惊人的记忆力著称。它能记住多年的水源地、家族成员,甚至是对它好或坏的人。我们希望我们的扩展能像大象一样,拥有智慧和记忆。

  • 用户设置了一个"白名单",希望某些网站永远不被自动分组。扩展必须记住这个名单。
  • 用户选择了一个喜欢的主题颜色(比如暗色模式)。扩展必须记住这个偏好。
  • 我们想统计用户总共通过我们的扩展节省了多少次点击。扩展必须记住这个数字,并不断累加。

所有这些"记忆"的需求,都指向了同一个解决方案:数据持久化 (Data Persistence)。即,将数据存储在一个不会因为程序关闭而丢失的地方。

在 Web 开发中,我们有 localStoragesessionStorage。但在扩展开发中,我们有更强大、更合适的选择:chrome.storage API

为什么不用 localStorage

  • 同步阻塞localStorage 是同步的,读写操作会阻塞主线程。在 Service Worker 这种对性能极其敏感的环境中,同步 I/O 是被严格禁止的。
  • 无痕模式下不可用 :在无痕窗口中,localStorage 的数据是隔离且临时的,不符合我们持久化的需求。
  • 容量限制:通常有 5MB 的限制。
  • 数据类型有限 :只能存储字符串。存储对象需要手动 JSON.stringifyJSON.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 已存在,则会覆盖旧值。

javascript 复制代码
// 存储用户的偏好设置
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): 用于获取一个或多个项目。它的参数非常灵活。

javascript 复制代码
// 获取单个项目
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[]): 用于删除一个或多个项目。

javascript 复制代码
async function removeTheme() {
    // 只删除 settings 对象里的 theme 属性是做不到的,
    // 需要先 get,修改后再 set。
    // remove 只能删除顶级的 key。
    await chrome.storage.sync.remove('settings');
    console.log("整个 'settings' 对象已被删除。");
}

4. 清空 (Clear)
chrome.storage.sync.clear(): 删除该存储区的所有数据。慎用!

javascript 复制代码
async function clearAllMyData() {
    await chrome.storage.sync.clear();
    console.log("云端保险箱已被清空!");
}

理论已就位,是时候为我们的管家建造一个可以让他"冥想"和"学习"的房间了------选项页。


7.2 项目实战:创建功能完善的"选项页" (Options Page)

选项页是一个独立的 HTML 页面,用户可以通过在扩展管理页面点击"扩展程序选项"或者在 Popup 中提供一个链接来访问它。它是我们与用户进行深度交互、允许他们进行复杂配置的主要场所。

我们的目标:创建一个选项页,允许用户管理一个"网站白名单"。在这个名单里的网站,将不会被我们未来的"一键分组"功能所影响。

第一步:创建选项页的 HTML 和 JS 文件
  1. 在项目根目录下,创建一个新的 HTML 文件,命名为 options.html

  2. scripts 文件夹下,创建一个新的 JS 文件,命名为 options.js

    📂 my-first-extension/
    ├── 📄 options.html <-- 新建
    └── 📂 scripts/
    ├── 📄 options.js <-- 新建
    ├── 📄 background.js
    └── 📄 content.js

第二步:在 manifest.json 中注册选项页

我们需要告诉浏览器,我们的扩展有一个选项页,它的入口文件是 options.html

打开 manifest.json,添加 options_pageoptions_ui 字段。

json 复制代码
{
  ...
  "action": { ... },

  "options_page": "options.html",

  "background": { ... },
  ...
}
  • options_page: 这是传统的方式,点击选项时会在一个新标签页中完整打开 options.html
  • options_ui: 这是一个更新的方式,它有两个属性 pageopen_in_tab。如果 open_in_tabfalse(默认),选项页会嵌入在 chrome://extensions 页面中的一个小弹窗里打开。如果为 true,则效果和 options_page 一样。

为了获得最大的空间和灵活性,我们这里使用 options_page

第三步:设计选项页的 UI (options.html)

打开 options.html,我们来搭建一个简单的配置界面。

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="例如:&#10;google.com&#10;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 文件里实现:

  1. 页面加载时,从 chrome.storage.sync 读取已保存的白名单,并显示在 <textarea> 中。
  2. 当用户点击"保存"按钮时,读取 <textarea> 的内容,将其处理成一个字符串数组,然后保存回 chrome.storage.sync

打开 scripts/options.js,写入以下代码:

javascript 复制代码
// 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 的方式重写一遍,代码会更现代和易读:

javascript 复制代码
// 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);

代码解读:

  1. restoreOptions 函数:

    • 在页面加载时(DOMContentLoaded)被调用。
    • 它使用 chrome.storage.sync.get 来获取 whitelistedSites 的值。注意我们提供的默认值 { whitelistedSites: [] },这能保证即使用户是第一次打开选项页,items.whitelistedSites 也是一个安全的空数组,而不会是 undefined,从而避免 join 方法报错。
    • 获取到数组后,使用 join('\n') 方法将数组元素用换行符连接成一个字符串,完美地还原回 <textarea> 的格式。
  2. saveOptions 函数:

    • 在用户点击保存按钮时被调用。
    • 它获取 <textarea>value
    • split('\n'): 将字符串按换行符分割成一个数组。
    • filter(Boolean)filter(site => site.trim() !== ''): 一个小技巧,用于过滤掉用户可能输入的多余的空行。
    • map(s => s.trim()): 确保每个域名前后的空格都被去掉。
    • chrome.storage.sync.set: 将处理好的 sites 数组,以 whitelistedSites 为键,保存到云端存储中。
    • 保存成功后,我们在 statusDiv 中显示一个短暂的成功提示,给用户一个明确的反馈。
第五步:部署与验证
  1. 保存所有修改过的文件 (manifest.json, options.html, options.js)。
  2. chrome://extensions 页面,刷新扩展。
  3. 现在,在你的扩展卡片上,你应该能看到一个蓝色的链接 "扩展程序选项"。点击它!
  4. 一个新的标签页会打开,内容就是我们的 options.html
  5. 进行测试
    • 在文本框里输入几个域名,比如 google.comwikipedia.org,每行一个。
    • 点击"保存设置"按钮,你应该能看到"选项已保存!"的提示。
    • 刷新 这个选项页。你会发现,你刚才输入的内容依然存在 !我们的 restoreOptions 函数起作用了。
    • 关闭 这个选项页,甚至关闭整个浏览器再重新打开 ,然后再次进入选项页。内容仍然 在那里!chrome.storage 的持久化魔法成功了!
    • 打开后台 Service Worker 的开发者工具,在 Console 中输入 await chrome.storage.sync.get('whitelistedSites') 并回车,你就能亲眼看到我们存储的数据。

我们已经成功地为我们的扩展创建了一个功能齐全的配置中心,并赋予了它跨会话的记忆能力!


本章总结与展望

在这一章,我们为我们的扩展植入了"记忆",让它从一个健忘的工具,向一个智能的管家迈出了决定性的一步。

我们学到了:

  1. chrome.storage 的优势 :理解了它相比 localStorage 在异步、同步、数据类型支持上的巨大优势。
  2. local vs sync:明确了两个存储区的区别和适用场景。
  3. 熟练掌握了 CRUD 操作 :通过 async/await 的方式,我们能流畅地对存储进行增删改查。
  4. 创建了功能性的选项页:我们从零开始,创建了一个允许用户自定义配置并持久化保存的 Options Page。

现在,我们的扩展不仅有了强大的执行能力,还有了可靠的记忆能力。它已经准备好去执行更复杂的、依赖于用户配置的任务了。

在下一章,我们将回到我们的主线功能,并把今天所学的一切付诸实践。我们将要实现那个炫酷的**"一键分组"功能,并且,这个功能会智能地 读取我们保存在 chrome.storage 中的白名单**,跳过那些用户不希望被分组的网站。

相关推荐
拾光拾趣录11 分钟前
基础 | 🔥闭包99%盲区?内存泄漏炸弹💣已埋!
前端·面试
拾光拾趣录32 分钟前
🔥前端性能优化9大杀招,第5招面试必挂?📉
前端·面试
用户214118326360238 分钟前
dify案例分享-AI 助力初中化学学习:用 Qwen Code+Dify 一键生成交互式元素周期表网页
前端
上海大哥1 小时前
Flutter 实现工程组件化(Windows电脑操作流程)
前端·flutter
刘语熙2 小时前
vue3使用useVmode简化组件通信
前端·vue.js
Code季风2 小时前
深入理解 Gin 框架的路由机制:从基础使用到核心原理
ide·后端·macos·go·web·xcode·gin
XboxYan2 小时前
借助CSS实现一个花里胡哨的点赞粒子动效
前端·css
码侯烧酒2 小时前
前端视角下关于 WebSocket 的简单理解
前端·websocket·网络协议
OEC小胖胖3 小时前
第六章:玩转浏览器 —— `chrome.tabs` API 精讲与实战
前端·chrome·浏览器·web·扩展