Chrome 扩展开发完全指南 / 第 8 章:存储 API(Storage API)
第 8 章:存储 API(Storage API)
数据存储是几乎所有扩展的核心需求。Chrome 提供了三种存储区域——local、sync 和 session,各有特点和适用场景。本章将深入讲解每种存储的用法、限制和最佳实践。
8.1 三种存储区域
| 特性 | local | sync | session |
|---|
| 存储位置 | 本地磁盘 | Google 云端 | 内存 |
| 容量限制 | 10 MB | 100 KB | 10 MB |
| 单项限制 | 无特别限制 | 8 KB | 无特别限制 |
| 跨设备同步 | ❌ | ✅ | ❌ |
| Service Worker 重启后保留 | ✅ | ✅ | ❌(MV3 有限制) |
| 写入频率限制 | 无 | 120 次/分钟 | 无 |
| 需要权限 | "storage" | "storage" | "storage" |
| 适用场景 | 大量数据、缓存 | 用户设置、偏好 | 临时状态 |
存储架构
┌──────────────────────────────────────────────────┐
│ Chrome 扩展 │
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌───────────┐ │
│ │ local │ │ sync │ │ session │ │
│ │ 10 MB │ │ 100 KB │ │ 10 MB │ │
│ │ 本地持久化 │ │ 跨设备同步 │ │ 内存 │ │
│ └──────┬──────┘ └──────┬──────┘ └─────┬─────┘ │
│ │ │ │ │
│ ┌──────▼───────────────▼────────────────▼─────┐ │
│ │ chrome.storage API │ │
│ └─────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────┘
8.2 基本操作
8.2.1 写入数据
// 写入单个值
await chrome.storage.local.set({ key: 'value' });
// 写入多个值
await chrome.storage.local.set({
username: 'user123',
theme: 'dark',
settings: {
notifications: true,
fontSize: 14
},
recentItems: [1, 2, 3, 4, 5]
});
// sync 存储
await chrome.storage.sync.set({ preferredLanguage: 'zh-CN' });
// session 存储
await chrome.storage.session.set({ currentPage: 1 });
8.2.2 读取数据
// 读取单个值
const result = await chrome.storage.local.get('username');
console.log(result.username); // 'user123'
// 读取多个值
const data = await chrome.storage.local.get(['theme', 'settings']);
console.log(data.theme); // 'dark'
console.log(data.settings); // { notifications: true, fontSize: 14 }
// 读取全部数据
const allData = await chrome.storage.local.get(null);
console.log(allData); // { username: '...', theme: '...', ... }
// 带默认值的读取
async function getWithDefault(key, defaultValue) {
const result = await chrome.storage.local.get(key);
return result[key] ?? defaultValue;
}
const theme = await getWithDefault('theme', 'light');
8.2.3 删除数据
// 删除单个键
await chrome.storage.local.remove('username');
// 删除多个键
await chrome.storage.local.remove(['theme', 'settings']);
// 清空整个存储区域
await chrome.storage.local.clear();
8.2.4 获取存储信息
// 获取已用字节数
const localBytes = await chrome.storage.local.getBytesInUse();
console.log(`local 已用: ${localBytes} / ${10 * 1024 * 1024} bytes`);
// 获取特定键的字节数
const themeBytes = await chrome.storage.local.getBytesInUse('theme');
console.log(`theme 占用: ${themeBytes} bytes`);
// sync 存储
const syncBytes = await chrome.storage.sync.getBytesInUse();
console.log(`sync 已用: ${syncBytes} / ${100 * 1024} bytes`);
8.3 存储变更监听
8.3.1 监听变化事件
// 监听所有存储区域的变化
chrome.storage.onChanged.addListener((changes, areaName) => {
console.log(`存储区域 "${areaName}" 发生变化:`);
for (const [key, { oldValue, newValue }] of Object.entries(changes)) {
console.log(` ${key}: ${JSON.stringify(oldValue)} → ${JSON.stringify(newValue)}`);
}
});
// 只监听特定区域
chrome.storage.onChanged.addListener((changes, areaName) => {
if (areaName !== 'sync') return;
if (changes.theme) {
applyTheme(changes.theme.newValue);
}
if (changes.fontSize) {
document.documentElement.style.fontSize = changes.fontSize.newValue + 'px';
}
});
8.3.2 Service Worker 中的响应式更新
// background/service-worker.js
chrome.storage.onChanged.addListener(async (changes, areaName) => {
// 通知所有打开的 UI 页面
const views = chrome.runtime.getContexts({
contextTypes: ['POPUP', 'SIDE_PANEL', 'TAB']
});
for (const view of views) {
try {
await chrome.tabs.sendMessage(view.tabId || 0, {
type: 'STORAGE_CHANGED',
changes,
areaName
});
} catch (e) {
// 页面可能已关闭
}
}
});
8.4 高级存储模式
8.4.1 类型安全的存储封装
// lib/storage.js
class TypedStorage {
constructor(area = 'local') {
this.area = chrome.storage[area];
this.validators = new Map();
}
// 注册键的类型验证器
register(key, validator, defaultValue) {
this.validators.set(key, { validator, defaultValue });
return this;
}
async get(key) {
const result = await this.area.get(key);
const config = this.validators.get(key);
if (result[key] === undefined && config) {
return config.defaultValue;
}
return result[key];
}
async set(key, value) {
const config = this.validators.get(key);
if (config && !config.validator(value)) {
throw new Error(`Validation failed for key "${key}"`);
}
await this.area.set({ [key]: value });
}
async getMultiple(keys) {
const result = await this.area.get(keys);
const config = this.validators;
for (const key of keys) {
if (result[key] === undefined && config.has(key)) {
result[key] = config.get(key).defaultValue;
}
}
return result;
}
}
// 使用示例
const storage = new TypedStorage('local')
.register('theme', v => ['light', 'dark', 'system'].includes(v), 'system')
.register('fontSize', v => typeof v === 'number' && v >= 12 && v <= 24, 14)
.register('username', v => typeof v === 'string' && v.length > 0, '');
// 类型安全的读写
await storage.set('theme', 'dark'); // ✅
await storage.set('theme', 'invalid'); // ❌ 抛出错误
const theme = await storage.get('theme'); // 'dark'
8.4.2 带过期时间的缓存
// lib/cache.js
class CacheStorage {
constructor(ttl = 3600000) { // 默认 1 小时
this.ttl = ttl;
}
async set(key, value, ttl = this.ttl) {
const entry = {
value,
expiresAt: Date.now() + ttl
};
await chrome.storage.local.set({ [key]: entry });
}
async get(key) {
const result = await chrome.storage.local.get(key);
const entry = result[key];
if (!entry) return undefined;
if (Date.now() > entry.expiresAt) {
await chrome.storage.local.remove(key);
return undefined; // 已过期
}
return entry.value;
}
async has(key) {
return (await this.get(key)) !== undefined;
}
async clear() {
const all = await chrome.storage.local.get(null);
const keys = Object.keys(all);
await chrome.storage.local.remove(keys);
}
// 清理过期缓存
async cleanup() {
const all = await chrome.storage.local.get(null);
const now = Date.now();
const expiredKeys = [];
for (const [key, entry] of Object.entries(all)) {
if (entry?.expiresAt && now > entry.expiresAt) {
expiredKeys.push(key);
}
}
if (expiredKeys.length > 0) {
await chrome.storage.local.remove(expiredKeys);
}
return expiredKeys.length;
}
}
// 使用示例
const cache = new CacheStorage(30 * 60 * 1000); // 30 分钟 TTL
// 缓存 API 响应
async function fetchWithCache(url) {
const cacheKey = `cache:${url}`;
let data = await cache.get(cacheKey);
if (data) return data; // 命中缓存
const response = await fetch(url);
data = await response.json();
await cache.set(cacheKey, data);
return data;
}
8.4.3 数据迁移
// lib/migration.js
class StorageMigration {
constructor(storageKey = 'dbVersion') {
this.storageKey = storageKey;
this.migrations = [];
}
addMigration(version, migrateFn) {
this.migrations.push({ version, migrateFn });
this.migrations.sort((a, b) => a.version - b.version);
return this;
}
async run() {
const { [this.storageKey]: currentVersion = 0 } =
await chrome.storage.local.get(this.storageKey);
const pending = this.migrations.filter(m => m.version > currentVersion);
if (pending.length === 0) {
console.log('无需迁移');
return;
}
for (const migration of pending) {
console.log(`执行迁移 v${migration.version}...`);
try {
await migration.migrateFn();
await chrome.storage.local.set({
[this.storageKey]: migration.version
});
} catch (error) {
console.error(`迁移 v${migration.version} 失败:`, error);
throw error;
}
}
console.log('迁移完成');
}
}
// 使用示例
const migrator = new StorageMigration();
migrator
.addMigration(1, async () => {
// v1: 将 settings 从 flat 结构改为嵌套结构
const { theme, fontSize } = await chrome.storage.local.get(
['theme', 'fontSize']
);
await chrome.storage.local.set({
settings: { theme: theme || 'light', fontSize: fontSize || 14 }
});
await chrome.storage.local.remove(['theme', 'fontSize']);
})
.addMigration(2, async () => {
// v2: 添加 bookmarks 字段
const { bookmarks } = await chrome.storage.local.get('bookmarks');
if (!bookmarks) {
await chrome.storage.local.set({ bookmarks: [] });
}
});
// Service Worker 安装时执行迁移
chrome.runtime.onInstalled.addListener(async () => {
await migrator.run();
});
8.5 业务场景
场景一:表单自动保存
// content/auto-save.js
class FormAutoSave {
constructor() {
this.debounceTimer = null;
this.init();
}
init() {
document.querySelectorAll('textarea, input[type="text"]').forEach(input => {
this.restoreValue(input);
input.addEventListener('input', () => this.saveValue(input));
});
}
getElementKey(element) {
return `autosave:${window.location.pathname}:${element.name || element.id}`;
}
async saveValue(element) {
clearTimeout(this.debounceTimer);
this.debounceTimer = setTimeout(async () => {
const key = this.getElementKey(element);
await chrome.storage.session.set({ [key]: element.value });
}, 500);
}
async restoreValue(element) {
const key = this.getElementKey(element);
const result = await chrome.storage.session.get(key);
if (result[key] && !element.value) {
element.value = result[key];
}
}
}
new FormAutoSave();
场景二:浏览历史统计
// background/stats.js
class BrowsingStats {
async recordVisit(url, title) {
const { visits = {} } = await chrome.storage.local.get('visits');
const domain = new URL(url).hostname;
if (!visits[domain]) {
visits[domain] = { count: 0, lastVisit: null, pages: {} };
}
visits[domain].count++;
visits[domain].lastVisit = Date.now();
if (!visits[domain].pages[url]) {
visits[domain].pages[url] = { title, count: 0 };
}
visits[domain].pages[url].count++;
await chrome.storage.local.set({ visits });
}
async getTopDomains(limit = 10) {
const { visits = {} } = await chrome.storage.local.get('visits');
return Object.entries(visits)
.sort(([, a], [, b]) => b.count - a.count)
.slice(0, limit)
.map(([domain, data]) => ({
domain,
count: data.count,
lastVisit: data.lastVisit
}));
}
}
8.6 注意事项
| 问题 | 原因 | 解决方案 |
|---|
| sync 写入失败 | 超过频率限制(120 次/分钟) | 合并写入、使用 debounce |
| sync 数据丢失 | 超过 100KB 限制 | 大数据用 local,小配置用 sync |
| session 数据丢失 | Service Worker 重启 | 关键数据改用 local |
| 深层对象覆盖 | set() 不会合并嵌套对象 | 手动合并后写入 |
| 读取到 undefined | 键不存在 | 提供默认值 |
8.7 扩展阅读