04. 数据持久化方案

32 阅读3分钟

📚 学习目标

  • 理解 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
  • 注意存储配额,实现清理和迁移策略
  • 使用批量操作和防抖优化性能

🔗 扩展阅读