📚 学习目标
- 理解 Chrome Storage API 的三种存储类型
- 封装类似 Pinia/Redux 的状态管理
- 掌握存储配额和性能优化
- 实现数据同步和迁移策略
🎯 核心知识点
1. Chrome Storage API 概览
Chrome Extension 提供三种存储类型:
| 类型 | 作用域 | 配额 | 同步 | 用途 |
|---|---|---|---|---|
local | 本地设备 | ~10MB | ❌ | 用户设置、缓存数据 |
sync | 跨设备同步 | ~100KB | ✅ | 用户偏好、小量配置 |
session | 会话级 | 内存限制 | ❌ | 临时数据、运行时状态 |
2. Storage 封装设计
类型定义
// src/shared/types/storage.ts
export type StorageArea = 'local' | 'sync' | 'session';
export interface StorageOptions {
area?: StorageArea;
defaultValue?: any;
serializer?: {
serialize: (value: any) => any;
deserialize: (value: any) => any;
};
}
export interface StorageChange<T = any> {
oldValue?: T;
newValue?: T;
}
基础 Storage 类
// src/shared/utils/storage.ts
import { StorageArea, StorageOptions, StorageChange } from '@/shared/types/storage';
export class Storage {
private area: chrome.storage.StorageArea;
constructor(area: StorageArea = 'local') {
this.area = chrome.storage[area];
}
async get<T = any>(key: string, options?: StorageOptions): Promise<T | null> {
try {
const result = await this.area.get(key);
const value = result[key];
if (value === undefined) {
return options?.defaultValue ?? null;
}
if (options?.serializer) {
return options.serializer.deserialize(value);
}
return value as T;
} catch (error) {
console.error(`Storage get error for key "${key}":`, error);
return options?.defaultValue ?? null;
}
}
async set<T = any>(key: string, value: T, options?: StorageOptions): Promise<void> {
try {
let serializedValue = value;
if (options?.serializer) {
serializedValue = options.serializer.serialize(value);
}
await this.area.set({ [key]: serializedValue });
} catch (error) {
console.error(`Storage set error for key "${key}":`, error);
throw error;
}
}
async remove(key: string): Promise<void> {
await this.area.remove(key);
}
async clear(): Promise<void> {
await this.area.clear();
}
async getAll(): Promise<Record<string, any>> {
return await this.area.get(null);
}
onChange(callback: (changes: Record<string, StorageChange>) => void) {
chrome.storage.onChanged.addListener((changes, areaName) => {
if (areaName === this.areaName) {
callback(changes);
}
});
}
private get areaName(): StorageArea {
if (this.area === chrome.storage.local) return 'local';
if (this.area === chrome.storage.sync) return 'sync';
return 'session';
}
}
3. 状态管理封装(类似 Pinia)
Store 基类
// src/shared/stores/baseStore.ts
import { Storage } from '@/shared/utils/storage';
import { StorageArea } from '@/shared/types/storage';
export abstract class BaseStore<T extends Record<string, any>> {
protected storage: Storage;
protected state: T;
private listeners: Set<(state: T) => void> = new Set();
constructor(
protected storeName: string,
protected initialState: T,
area: StorageArea = 'local'
) {
this.storage = new Storage(area);
this.state = { ...initialState };
this.init();
}
private async init() {
// 从存储加载状态
const savedState = await this.storage.get<T>(this.storeName);
if (savedState) {
this.state = { ...this.initialState, ...savedState };
}
// 监听存储变化
this.storage.onChange((changes) => {
if (changes[this.storeName]) {
const newState = changes[this.storeName].newValue as T;
if (newState) {
this.state = newState;
this.notify();
}
}
});
}
getState(): T {
return { ...this.state };
}
setState(updates: Partial<T>): void {
this.state = { ...this.state, ...updates };
this.persist();
this.notify();
}
reset(): void {
this.state = { ...this.initialState };
this.persist();
this.notify();
}
subscribe(listener: (state: T) => void): () => void {
this.listeners.add(listener);
return () => {
this.listeners.delete(listener);
};
}
private notify(): void {
this.listeners.forEach(listener => listener(this.getState()));
}
private async persist(): Promise<void> {
await this.storage.set(this.storeName, this.state);
}
}
使用示例:用户设置 Store
// src/shared/stores/settingsStore.ts
import { BaseStore } from './baseStore';
interface SettingsState {
theme: 'light' | 'dark' | 'auto';
language: string;
notifications: boolean;
autoSync: boolean;
}
class SettingsStore extends BaseStore<SettingsState> {
constructor() {
super(
'settings',
{
theme: 'auto',
language: 'zh-CN',
notifications: true,
autoSync: false,
},
'sync' // 使用 sync 跨设备同步
);
}
setTheme(theme: SettingsState['theme']) {
this.setState({ theme });
}
setLanguage(language: string) {
this.setState({ language });
}
toggleNotifications() {
this.setState({ notifications: !this.state.notifications });
}
}
export const settingsStore = new SettingsStore();
在 Vue 组件中使用
<!-- src/popup/components/Settings.vue -->
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue';
import { settingsStore } from '@/shared/stores/settingsStore';
const state = ref(settingsStore.getState());
let unsubscribe: (() => void) | null = null;
onMounted(() => {
unsubscribe = settingsStore.subscribe((newState) => {
state.value = newState;
});
});
onUnmounted(() => {
unsubscribe?.();
});
function handleThemeChange(theme: 'light' | 'dark' | 'auto') {
settingsStore.setTheme(theme);
}
</script>
<template>
<div>
<select :value="state.theme" @change="handleThemeChange($event.target.value)">
<option value="light">浅色</option>
<option value="dark">深色</option>
<option value="auto">自动</option>
</select>
</div>
</template>
4. 高级功能
序列化器(处理复杂数据类型)
// src/shared/utils/serializers.ts
export const serializers = {
date: {
serialize: (value: Date) => value.toISOString(),
deserialize: (value: string) => new Date(value),
},
map: {
serialize: (value: Map<any, any>) => Array.from(value.entries()),
deserialize: (value: [any, any][]) => new Map(value),
},
set: {
serialize: (value: Set<any>) => Array.from(value),
deserialize: (value: any[]) => new Set(value),
},
};
// 使用
const storage = new Storage('local');
await storage.set('dates', [new Date()], {
serializer: {
serialize: (dates: Date[]) => dates.map(d => d.toISOString()),
deserialize: (strings: string[]) => strings.map(s => new Date(s)),
}
});
存储配额管理
// src/shared/utils/storageQuota.ts
export class StorageQuota {
static async getUsage(area: StorageArea = 'local'): Promise<{
used: number;
quota: number;
percentage: number;
}> {
const storage = chrome.storage[area];
const usage = await storage.getBytesInUse(null);
const quota = area === 'sync' ? 102400 : 10485760; // 100KB or 10MB
return {
used: usage,
quota,
percentage: (usage / quota) * 100,
};
}
static async checkAndClean(area: StorageArea = 'local', threshold = 0.8) {
const usage = await this.getUsage(area);
if (usage.percentage > threshold * 100) {
// 清理旧数据
const allData = await chrome.storage[area].get(null);
const sortedKeys = Object.keys(allData).sort((a, b) => {
// 按最后访问时间排序(需要自己维护)
return 0;
});
// 删除最旧的 20% 数据
const keysToRemove = sortedKeys.slice(0, Math.floor(sortedKeys.length * 0.2));
await chrome.storage[area].remove(keysToRemove);
}
}
}
数据迁移
// src/shared/utils/migration.ts
interface Migration {
version: number;
up: (data: any) => Promise<any>;
down?: (data: any) => Promise<any>;
}
export class StorageMigration {
private migrations: Migration[] = [];
private currentVersion: number = 1;
addMigration(migration: Migration) {
this.migrations.push(migration);
this.migrations.sort((a, b) => a.version - b.version);
}
async migrate(storage: Storage, key: string) {
const versionKey = `${key}_version`;
const currentVersion = await storage.get<number>(versionKey) || 1;
const migrationsToRun = this.migrations.filter(m => m.version > currentVersion);
for (const migration of migrationsToRun) {
const data = await storage.get(key);
const migratedData = await migration.up(data);
await storage.set(key, migratedData);
await storage.set(versionKey, migration.version);
}
}
}
// 使用示例
const migration = new StorageMigration();
migration.addMigration({
version: 2,
up: async (data) => {
// 迁移逻辑:添加新字段
return {
...data,
newField: 'defaultValue',
};
},
});
5. 性能优化
批量操作
// src/shared/utils/storage.ts (扩展)
async setBatch(items: Record<string, any>): Promise<void> {
await this.area.set(items);
}
async getBatch(keys: string[]): Promise<Record<string, any>> {
return await this.area.get(keys);
}
async removeBatch(keys: string[]): Promise<void> {
await this.area.remove(keys);
}
防抖写入
// src/shared/utils/debouncedStorage.ts
import { Storage } from './storage';
export class DebouncedStorage extends Storage {
private debounceTimers = new Map<string, NodeJS.Timeout>();
private debounceDelay = 300;
async set<T = any>(key: string, value: T, options?: StorageOptions): Promise<void> {
// 清除之前的定时器
const existingTimer = this.debounceTimers.get(key);
if (existingTimer) {
clearTimeout(existingTimer);
}
// 设置新的定时器
const timer = setTimeout(async () => {
await super.set(key, value, options);
this.debounceTimers.delete(key);
}, this.debounceDelay);
this.debounceTimers.set(key, timer);
}
async flush(): Promise<void> {
// 立即执行所有待处理的写入
const timers = Array.from(this.debounceTimers.values());
this.debounceTimers.clear();
timers.forEach(timer => clearTimeout(timer));
// 需要实现一个机制来获取待写入的数据
}
}
🛠️ 实战练习
练习 1:实现缓存 Store
// src/shared/stores/cacheStore.ts
interface CacheItem<T> {
data: T;
timestamp: number;
ttl: number; // 生存时间(毫秒)
}
class CacheStore extends BaseStore<Record<string, CacheItem<any>>> {
constructor() {
super('cache', {});
}
set<T>(key: string, data: T, ttl = 3600000): void {
this.setState({
[key]: {
data,
timestamp: Date.now(),
ttl,
},
});
}
get<T>(key: string): T | null {
const item = this.state[key] as CacheItem<T> | undefined;
if (!item) return null;
if (Date.now() - item.timestamp > item.ttl) {
// 过期,删除
const newState = { ...this.state };
delete newState[key];
this.setState(newState);
return null;
}
return item.data;
}
clearExpired(): void {
const now = Date.now();
const newState = { ...this.state };
Object.keys(newState).forEach(key => {
const item = newState[key];
if (now - item.timestamp > item.ttl) {
delete newState[key];
}
});
this.setState(newState);
}
}
📝 总结
- 根据数据特性选择合适的存储类型(local/sync/session)
- 封装 Store 模式实现状态管理,类似 Pinia
- 注意存储配额,实现清理和迁移策略
- 使用批量操作和防抖优化性能