IndexedDB实战:手把手教你打造离线笔记应用(含事务处理技巧)

IndexedDB 学习笔记 目录 参考资料 Github(一些小demo) 背景 特点 术语 创建数据库、对象仓库、索引 数据的增、删、查、改 添加数据 删除数据 获取数据 遍历数据(使用游标) localforage的使用(简化的IndexedDB) 参考资料 https://wangdoc.com/javascript/bom/indexeddb.html#%E6%89%93%E5%B... 阅读详情

IndexedDB实战:手把手教你打造离线笔记应用(含事务处理技巧)

你是否曾遇到过这样的场景:在地铁上灵感迸发,打开手机上的笔记应用奋笔疾书,突然网络中断,刚才写下的几百字瞬间消失?或者开发一个需要离线使用的PWA应用时,发现localStorage那可怜的5MB存储空间根本不够用?这些痛点正是现代Web应用从“在线工具”向“可靠应用”演进时必须跨越的鸿沟。

IndexedDB作为浏览器内置的NoSQL数据库,正是解决这些问题的利器。它不仅能存储GB级别的结构化数据,还支持事务、索引、游标等高级功能,让前端开发者也能在浏览器中构建真正意义上的离线应用。但很多开发者面对IndexedDB原生API的复杂性望而却步——回调地狱、事务管理、版本升级,每一个环节都可能成为拦路虎。

本文将带你从零开始,通过构建一个完整的离线笔记应用,深入掌握IndexedDB的核心用法。我们不仅会讲解基础操作,更会聚焦于实际开发中最关键的事务处理、冲突解决和性能优化策略。无论你是需要为现有应用添加离线功能,还是正在规划一个全新的PWA项目,这里都有你需要的实战经验。

1. 环境准备与项目架构设计

在开始编码之前,我们需要明确离线笔记应用的核心需求。一个合格的离线笔记应用应该具备以下能力:在网络不稳定或完全断开时仍能正常使用;支持富文本或Markdown格式的笔记内容;能够自动保存草稿并同步到云端;提供高效的搜索和分类功能;确保数据在浏览器崩溃或页面关闭后不会丢失。

1.1 技术选型与工具准备

对于现代前端项目,我们通常会选择TypeScript来获得更好的类型安全性和开发体验。同时,为了简化IndexedDB的操作,我们可以选择一个轻量级的封装库。这里我推荐使用idb库,它是IndexedDB API的Promise封装,由Google的Jake Archibald维护,API设计简洁且符合现代JavaScript习惯。

# 初始化项目并安装依赖
npm init -y
npm install typescript idb
npm install --save-dev @types/idb

提示:虽然可以直接使用原生IndexedDB API,但在实际项目中,使用封装库可以显著减少样板代码,并避免常见的陷阱。idb库只有约1KB的gzip大小,几乎不会增加打包体积。

1.2 数据库架构设计

合理的数据库设计是应用性能的基础。对于笔记应用,我们需要考虑以下几个核心实体:

  • 笔记(Notes):存储笔记的标题、内容、创建时间、修改时间等
  • 标签(Tags):支持笔记的分类管理
  • 附件(Attachments):存储图片、文件等二进制数据
  • 同步状态(SyncStatus):记录笔记的同步状态,用于冲突解决

下面是我们数据库的初始设计:

// types/database.ts
export interface Note {
  id?: number;           // 自增主键
  title: string;        // 笔记标题
  content: string;      // 笔记内容(Markdown格式)
  tags: string[];       // 标签数组
  createdAt: Date;      // 创建时间
  updatedAt: Date;      // 最后修改时间
  isArchived: boolean;  // 是否归档
  syncStatus: 'synced' | 'pending' | 'conflict'; // 同步状态
}

export interface Tag {
  id?: number;
  name: string;
  color?: string;       // 标签颜色
  noteCount: number;    // 关联笔记数量
}

export interface Attachment {
  id?: number;
  noteId: number;       // 外键,关联笔记
  fileName: string;
  fileType: string;
  data: ArrayBuffer;    // 二进制数据
  size: number;
  uploadedAt: Date;
}

export interface SyncLog {
  id?: number;
  noteId: number;
  action: 'create' | 'update' | 'delete';
  timestamp: Date;
  serverVersion?: number; // 服务器版本号
}

1.3 数据库初始化策略

IndexedDB的版本管理机制是其强大之处,也是容易出错的地方。我们需要精心设计数据库的升级策略,确保用户在不同版本间迁移时数据不会丢失。

// db/init.ts
import { openDB, DBSchema, IDBPDatabase } from 'idb';

interface NotesDBSchema extends DBSchema {
  notes: {
    key: number;
    value: Note;
    indexes: {
      'by-updated': Date;
      'by-tag': string;
      'by-sync-status': string;
    };
  };
  tags: {
    key: number;
    value: Tag;
    indexes: { 'by-name': string };
  };
  attachments: {
    key: number;
    value: Attachment;
    indexes: { 'by-note-id': number };
  };
  syncLogs: {
    key: number;
    value: SyncLog;
    indexes: { 'by-timestamp': Date };
  };
}

const DB_NAME = 'notes-app';
const DB_VERSION = 3; // 每次修改数据库结构时递增

export async function initDatabase(): Promise<IDBPDatabase<NotesDBSchema>> {
  return openDB<NotesDBSchema>(DB_NAME, DB_VERSION, {
    upgrade(db, oldVersion, newVersion, transaction) {
      console.log(`数据库从版本 ${oldVersion} 升级到 ${newVersion}`);
      
      // 版本1:创建基础表
      if (oldVersion < 1) {
        const notesStore = db.createObjectStore('notes', {
          keyPath: 'id',
          autoIncrement: true
        });
        
        // 创建索引
        notesStore.createIndex('by-updated', 'updatedAt');
        notesStore.createIndex('by-tag', 'tags', { multiEntry: true });
        notesStore.createIndex('by-sync-status', 'syncStatus');
        
        db.createObjectStore('tags', {
          keyPath: 'id',
          autoIncrement: true
        }).createIndex('by-name', 'name', { unique: true });
      }
      
      // 版本2:添加附件表
      if (oldVersion < 2) {
        const attachmentsStore = db.createObjectStore('attachments', {
          keyPath: 'id',
          autoIncrement: true
        });
        attachmentsStore.createIndex('by-note-id', 'noteId');
      }
      
      // 版本3:添加同步日志表
      if (oldVersion < 3) {
        const syncLogsStore = db.createObjectStore('syncLogs', {
          keyPath: 'id',
          autoIncrement: true
        });
        syncLogsStore.createIndex('by-timestamp', 'timestamp');
        
        // 为notes表添加syncStatus字段的默认值
        const notesStore = transaction.objectStore('notes');
        notesStore.getAll().then(notes => {
          notes.forEach(note => {
            if (!note.syncStatus) {
              note.syncStatus = 'synced';
              notesStore.put(note);
            }
          });
        });
      }
    }
  });
}

注意:数据库版本升级是IndexedDB中最容易出错的环节。务必确保升级逻辑的健壮性,特别是当需要迁移现有数据时。建议在开发阶段频繁测试升级路径,确保用户从任意旧版本都能平滑升级到最新版本。

2. 核心数据操作与事务管理

掌握了数据库初始化后,我们进入最核心的部分:数据操作。IndexedDB的所有操作都必须在事务中进行,这是保证数据一致性的关键,也是初学者最容易困惑的地方。

2.1 事务的基本概念与类型

IndexedDB支持三种事务模式:readonly、readwrite和versionchange。理解它们的区别对编写正确的代码至关重要:

事务模式描述使用场景
readonly只读事务,性能最佳查询、统计等不修改数据的操作
readwrite读写事务,支持修改增删改操作
versionchange数据库结构变更仅在onupgradeneeded中自动创建
// db/operations.ts
import { IDBPDatabase } from 'idb';

export class NoteRepository {
  constructor(private db: IDBPDatabase<NotesDBSchema>) {}
  
  // 只读事务示例:获取所有笔记
  async getAllNotes(): Promise<Note[]> {
    // 使用只读事务,性能更好
    const tx = this.db.transaction('notes', 'readonly');
    const store = tx.objectStore('notes');
    
    // 按更新时间倒序排列
    const index = store.index('by-updated');
    return index.getAll(undefined, 100); // 限制返回100条
  }
  
  // 读写事务示例:创建新笔记
  async createNote(noteData: Omit<Note, 'id'>): Promise<number> {
    const tx = this.db.transaction(['notes', 'syncLogs'], 'readwrite');
    
    try {
      const notesStore = tx.objectStore('notes');
      const syncLogsStore = tx.objectStore('syncLogs');
      
      // 设置时间戳和同步状态
      const now = new Date();
      const note: Note = {
        ...noteData,
        createdAt: now,
        updatedAt: now,
        syncStatus: 'pending' // 新创建的笔记需要同步
      };
      
      // 添加笔记
      const noteId = await notesStore.add(note);
      
      // 记录同步日志
      await syncLogsStore.add({
        noteId,
        action: 'create',
        timestamp: now
      });
      
      // 等待事务完成
      await tx.done;
      
      return noteId;
    } catch (error) {
      // 事务会自动回滚
      console.error('创建笔记失败:', error);
      throw error;
    }
  }
}

2.2 复杂事务与原子性操作

在实际应用中,我们经常需要执行多个相关操作,这些操作要么全部成功,要么全部失败。IndexedDB的事务机制完美支持这种需求。

// db/operations.ts - 继续扩展
export class NoteRepository {
  // 复杂事务示例:移动笔记到新分类并更新标签
  async moveNoteToCategory(
    noteId: number, 
    newTags: string[], 
    removeOldTags: boolean = false
  ): Promise<void> {
    const tx = this.db.transaction(
      ['notes', 'tags', 'syncLogs'], 
      'readwrite'
    );
    
    try {
      const notesStore = tx.objectStore('notes');
      const tagsStore = tx.objectStore('tags');
      const syncLogsStore = tx.objectStore('syncLogs');
      
      // 1. 获取原始笔记
      const note = await notesStore.get(noteId);
      if (!note) {
        throw new Error(`笔记 ${noteId} 不存在`);
      }
      
      const oldTags = [...note.tags];
      
      // 2. 更新笔记标签
      note.tags = removeOldTags ? newTags : [...new Set([...note.tags, ...newTags])];
      note.updatedAt = new Date();
      note.syncStatus = 'pending';
      
      await notesStore.put(note);
      
      // 3. 更新标签统计(需要原子性操作)
      const tagUpdates = newTags.map(async tagName => {
        // 查找或创建标签
        const tagIndex = tagsStore.index('by-name');
        let tag = await tagIndex.get(tagName);
        
        if (tag) {
          tag.noteCount += 1;
        } else {
          tag = {
            name: tagName,
            noteCount: 1,
            color: this.generateTagColor(tagName)
          };
        }
        
        await tagsStore.put(tag);
      });
      
      // 如果需要移除旧标签,减少计数
      if (removeOldTags) {
        const removeUpdates = oldTags.map(async tagName => {
          const tagIndex = tagsStore.index('by-name');
          const tag = await tagIndex.get(tagName);
          
          if (tag && tag.noteCount > 1) {
            tag.noteCount -= 1;
            await tagsStore.put(tag);
          } else if (tag) {
            // 如果没有笔记使用这个标签,删除它
            await tagsStore.delete(tag.id!);
          }
        });
        
        await Promise.all(removeUpdates);
      }
      
      await Promise.all(tagUpdates);
      
      // 4. 记录同步日志
      await syncLogsStore.add({
        noteId,
        action: 'update',
        timestamp: new Date()
      });
      
      await tx.done;
      
    } catch (error) {
      console.error('移动笔记失败:', error);
      throw error;
    }
  }
  
  private generateTagColor(tagName: string): string {
    // 简单的颜色生成算法
    const colors = [
      '#FF6B6B', '#4ECDC4', '#FFD166', '#06D6A0',
      '#118AB2', '#073B4C', '#EF476F', '#7209B7'
    ];
    const hash = tagName.split('').reduce((acc, char) => {
      return char.charCodeAt(0) + ((acc << 5) - acc);
    }, 0);
    
    return colors[Math.abs(hash) % colors.length];
  }
}

2.3 事务错误处理与重试机制

网络不稳定或用户操作冲突可能导致事务失败。合理的错误处理和重试机制能显著提升应用稳定性。

// utils/transaction-retry.ts
export async function withTransactionRetry<T>(
  operation: () => Promise<T>,
  maxRetries: number = 3,
  baseDelay: number = 100
): Promise<T> {
  let lastError: Error;
  
  for (let attempt = 0; attempt < maxRetries; attempt++) {
    try {
      return await operation();
    } catch (error) {
      lastError = error as Error;
      
      // 判断是否为可重试的错误
      if (this.isRetryableError(error)) {
        if (attempt < maxRetries - 1) {
          // 指数退避策略
          const delay = baseDelay * Math.pow(2, attempt);
          console.warn(`事务失败,${delay}ms后重试 (${attempt + 1}/${maxRetries})`);
          await new Promise(resolve => setTimeout(resolve, delay));
          continue;
        }
      }
      
      // 不可重试的错误或达到最大重试次数
      break;
    }
  }
  
  throw lastError;
}

function isRetryableError(error: any): boolean {
  // IndexedDB的可重试错误
  const retryableErrors = [
    'TransactionInactiveError',
    'ReadOnlyError',
    'ConstraintError'
  ];
  
  return retryableErrors.some(errorName => 
    error.name?.includes(errorName) || 
    error.message?.includes(errorName)
  );
}

// 使用示例
export class NoteRepository {
  async safeCreateNote(noteData: Omit<Note, 'id'>): Promise<number> {
    return withTransactionRetry(async () => {
      return this.createNote(noteData);
    });
  }
  
  async batchUpdateNotes(updates: Array<{id: number, changes: Partial<Note>}>): Promise<void> {
    return withTransactionRetry(async () => {
      const tx = this.db.transaction('notes', 'readwrite');
      const store = tx.objectStore('notes');
      
      const promises = updates.map(async ({id, changes}) => {
        const note = await store.get(id);
        if (note) {
          Object.assign(note, changes, {
            updatedAt: new Date(),
            syncStatus: 'pending'
          });
          await store.put(note);
        }
      });
      
      await Promise.all(promises);
      await tx.done;
    });
  }
}

3. 高级查询与性能优化

当笔记数量增长到数千甚至数万条时,高效的查询和索引策略变得至关重要。IndexedDB提供了多种查询方式,我们需要根据具体场景选择最合适的方案。

3.1 索引设计与查询优化

合理的索引设计能大幅提升查询性能。对于笔记应用,我们通常需要支持以下几种查询模式:

  1. 按时间范围查询(最近修改、某时间段内的笔记)
  2. 按标签筛选(查找包含特定标签的笔记)
  3. 全文搜索(在标题和内容中搜索关键词)
  4. 复合条件查询(同时满足多个条件)
// db/query.ts
export class NoteQueryService {
  constructor(private db: IDBPDatabase<NotesDBSchema>) {}
  
  // 1. 按时间范围查询(使用索引)
  async getNotesByDateRange(
    startDate: Date, 
    endDate: Date, 
    limit: number = 50
  ): Promise<Note[]> {
    const tx = this.db.transaction('notes', 'readonly');
    const store = tx.objectStore('notes');
    const index = store.index('by-updated');
    
    // 使用IDBKeyRange创建范围
    const range = IDBKeyRange.bound(startDate, endDate);
    
    // 按时间倒序排列
    return index.getAll(range, limit);
  }
  
  // 2. 按标签查询(多值索引)
  async getNotesByTag(tagName: string): Promise<Note[]> {
    const tx = this.db.transaction('notes', 'readonly');
    const store = tx.objectStore('notes');
    const index = store.index('by-tag');
    
    // 对于多值索引,使用精确匹配
    return index.getAll(IDBKeyRange.only(tagName));
  }
  
  // 3. 复合查询:多个标签 + 时间范围
  async getNotesByTagsAndDate(
    tags: string[], 
    startDate?: Date, 
    endDate?: Date
  ): Promise<Note[]> {
    const tx = this.db.transaction('notes', 'readonly');
    const store = tx.objectStore('notes');
    
    // 先按时间范围筛选
    let notes: Note[];
    if (startDate || endDate) {
      const dateIndex = store.index('by-updated');
      const dateRange = IDBKeyRange.bound(
        startDate || new Date(0), 
        endDate || new Date()
      );
      notes = await dateIndex.getAll(dateRange);
    } else {
      notes = await store.getAll();
    }
    
    // 在内存中进一步筛选标签
    return notes.filter(note => 
      tags.every(tag => note.tags.includes(tag))
    );
  }
  
  // 4. 分页查询(游标方式,性能最佳)
  async getNotesPaginated(
    page: number = 1, 
    pageSize: number = 20,
    sortBy: 'updatedAt' | 'createdAt' = 'updatedAt'
  ): Promise<{notes: Note[], total: number}> {
    const tx = this.db.transaction('notes', 'readonly');
    const store = tx.objectStore('notes');
    
    // 获取总数
    const total = await store.count();
    
    // 计算偏移量
    const offset = (page - 1) * pageSize;
    
    // 使用游标进行分页(性能优于getAll + slice)
    const index = store.index(`by-${sortBy}`);
    const notes: Note[] = [];
    
    let cursor = await index.openCursor(null, 'prev'); // 倒序,最新的在前
    
    // 跳过前面的记录
    let skipped = 0;
    while (cursor && skipped < offset) {
      cursor = await cursor.continue();
      skipped++;
    }
    
    // 获取当前页的记录
    let fetched = 0;
    while (cursor && fetched < pageSize) {
      notes.push(cursor.value);
      cursor = await cursor.continue();
      fetched++;
    }
    
    return { notes, total };
  }
}

3.2 全文搜索的实现策略

IndexedDB本身不支持全文搜索,但我们可以通过几种策略来实现:

// db/search.ts
export class NoteSearchService {
  constructor(private db: IDBPDatabase<NotesDBSchema>) {}
  
  // 策略1:简单的内存搜索(适用于数据量较小的情况)
  async simpleSearch(query: string): Promise<Note[]> {
    const tx = this.db.transaction('notes', 'readonly');
    const store = tx.objectStore('notes');
    const allNotes = await store.getAll();
    
    const lowerQuery = query.toLowerCase();
    
    return allNotes.filter(note => 
      note.title.toLowerCase().includes(lowerQuery) ||
      note.content.toLowerCase().includes(lowerQuery)
    );
  }
  
  // 策略2:构建搜索索引(适用于数据量较大的情况)
  async buildSearchIndex(): Promise<Map<string, Set<number>>> {
    const tx = this.db.transaction('notes', 'readonly');
    const store = tx.objectStore('notes');
    const allNotes = await store.getAll();
    
    const index = new Map<string, Set<number>>();
    
    allNotes.forEach(note => {
      // 分词函数
      const words = this.tokenize(`${note.title} ${note.content}`);
      
      words.forEach(word => {
        if (!index.has(word)) {
          index.set(word, new Set());
        }
        index.get(word)!.add(note.id!);
      });
    });
    
    // 将索引存储到IndexedDB中
    const indexTx = this.db.transaction('searchIndex', 'readwrite');
    const indexStore = indexTx.objectStore('searchIndex');
    
    // 清空旧索引
    await indexStore.clear();
    
    // 存储新索引
    for (const [word, noteIds] of index) {
      await indexStore.put({
        word,
        noteIds: Array.from(noteIds)
      });
    }
    
    await indexTx.done;
    
    return index;
  }
  
  // 策略3:使用Web Workers进行后台搜索
  async searchWithWorker(query: string): Promise<Note[]> {
    return new Promise((resolve, reject) => {
      const worker = new Worker('/workers/search.worker.js');
      
      worker.postMessage({
        type: 'SEARCH',
        query,
        dbName: 'notes-app'
      });
      
      worker.onmessage = (event) => {
        if (event.data.type === 'SEARCH_RESULT') {
          resolve(event.data.results);
          worker.terminate();
        } else if (event.data.type === 'SEARCH_ERROR') {
          reject(new Error(event.data.error));
          worker.terminate();
        }
      };
      
      worker.onerror = (error) => {
        reject(error);
        worker.terminate();
      };
    });
  }
  
  private tokenize(text: string): string[] {
    // 简单的分词实现
    return text
      .toLowerCase()
      .replace(/[^\w\s]/g, ' ')  // 移除标点
      .split(/\s+/)              // 按空白字符分割
      .filter(word => word.length > 2)  // 过滤短词
      .filter((word, index, array) => array.indexOf(word) === index); // 去重
  }
}

// workers/search.worker.js
self.onmessage = async (event) => {
  if (event.data.type === 'SEARCH') {
    try {
      const { query, dbName } = event.data;
      
      // 在Worker中打开数据库
      const db = await openDB(dbName);
      const tx = db.transaction('notes', 'readonly');
      const store = tx.objectStore('notes');
      const allNotes = await store.getAll();
      
      const results = allNotes.filter(note => 
        note.title.toLowerCase().includes(query.toLowerCase()) ||
        note.content.toLowerCase().includes(query.toLowerCase())
      );
      
      self.postMessage({
        type: 'SEARCH_RESULT',
        results
      });
    } catch (error) {
      self.postMessage({
        type: 'SEARCH_ERROR',
        error: error.message
      });
    }
  }
};

3.3 性能监控与优化建议

对于数据量较大的应用,性能监控至关重要。以下是一些实用的性能优化技巧:

// utils/performance-monitor.ts
export class IndexedDBPerformanceMonitor {
  private metrics = {
    readOperations: 0,
    writeOperations: 0,
    totalReadTime: 0,
    totalWriteTime: 0,
    slowQueries: [] as Array<{
      operation: string;
      duration: number;
      timestamp: Date;
    }>
  };
  
  // 包装数据库操作,添加性能监控
  wrapOperation<T>(
    operation: () => Promise<T>,
    operationName: string
  ): Promise<T> {
    const startTime = performance.now();
    
    return operation().finally(() => {
      const duration = performance.now() - startTime;
      
      if (operationName.startsWith('read')) {
        this.metrics.readOperations++;
        this.metrics.totalReadTime += duration;
      } else if (operationName.startsWith('write')) {
        this.metrics.writeOperations++;
        this.metrics.totalWriteTime += duration;
      }
      
      // 记录慢查询
      if (duration > 100) { // 超过100ms视为慢查询
        this.metrics.slowQueries.push({
          operation: operationName,
          duration,
          timestamp: new Date()
        });
        
        // 只保留最近100条慢查询记录
        if (this.metrics.slowQueries.length > 100) {
          this.metrics.slowQueries.shift();
        }
      }
    });
  }
  
  getPerformanceReport() {
    const avgReadTime = this.metrics.readOperations > 0 
      ? this.metrics.totalReadTime / this.metrics.readOperations 
      : 0;
    
    const avgWriteTime = this.metrics.writeOperations > 0 
      ? this.metrics.totalWriteTime / this.metrics.writeOperations 
      : 0;
    
    return {
      ...this.metrics,
      avgReadTime,
      avgWriteTime,
      readThroughput: avgReadTime > 0 ? 1000 / avgReadTime : 0,
      writeThroughput: avgWriteTime > 0 ? 1000 / avgWriteTime : 0
    };
  }
}

// 使用示例
export class OptimizedNoteRepository extends NoteRepository {
  private monitor = new IndexedDBPerformanceMonitor();
  
  async getNotesWithMonitoring(): Promise<Note[]> {
    return this.monitor.wrapOperation(
      () => super.getAllNotes(),
      'read-all-notes'
    );
  }
  
  async createNoteWithMonitoring(noteData: Omit<Note, 'id'>): Promise<number> {
    return this.monitor.wrapOperation(
      () => super.createNote(noteData),
      'write-create-note'
    );
  }
}

4. 离线同步与冲突解决策略

离线应用的核心挑战在于数据同步。当用户在不同设备上编辑同一份笔记,或者离线编辑后重新联网时,我们需要处理可能的数据冲突。

4.1 同步状态管理与变更追踪

首先,我们需要一个机制来追踪哪些数据需要同步,以及同步的状态。

// sync/sync-manager.ts
export interface SyncOperation {
  id?: number;
  noteId: number;
  action: 'create' | 'update' | 'delete';
  localVersion: number;      // 本地版本号
  serverVersion?: number;    // 服务器版本号
  timestamp: Date;
  data?: Partial<Note>;      // 变更数据(用于冲突解决)
  resolved?: boolean;        // 是否已解决冲突
}

export class SyncManager {
  private pendingOperations = new Map<number, SyncOperation>();
  private isSyncing = false;
  private syncInterval: number | null = null;
  
  constructor(
    private db: IDBPDatabase<NotesDBSchema>,
    private apiClient: NoteApiClient
  ) {}
  
  // 开始自动同步(每30秒检查一次)
  startAutoSync(intervalMs: number = 30000): void {
    if (this.syncInterval) {
      clearInterval(this.syncInterval);
    }
    
    this.syncInterval = window.setInterval(() => {
      if (navigator.onLine && !this.isSyncing) {
        this.sync().catch(console.error);
      }
    }, intervalMs);
    
    // 监听网络状态变化
    window.addEventListener('online', () => {
      console.log('网络恢复,开始同步');
      this.sync().catch(console.error);
    });
  }
  
  // 停止自动同步
  stopAutoSync(): void {
    if (this.syncInterval) {
      clearInterval(this.syncInterval);
      this.syncInterval = null;
    }
  }
  
  // 记录本地变更
  async recordChange(
    noteId: number, 
    action: SyncOperation['action'],
    data?: Partial<Note>
  ): Promise<void> {
    const tx = this.db.transaction(['notes', 'syncLogs'], 'readwrite');
    const notesStore = tx.objectStore('notes');
    const syncLogsStore = tx.objectStore('syncLogs');
    
    // 获取当前笔记
    const note = await notesStore.get(noteId);
    if (!note) {
      throw new Error(`笔记 ${noteId} 不存在`);
    }
    
    // 更新本地版本号
    const localVersion = (note.localVersion || 0) + 1;
    note.localVersion = localVersion;
    note.syncStatus = 'pending';
    note.updatedAt = new Date();
    
    await notesStore.put(note);
    
    // 记录同步操作
    const operation: SyncOperation = {
      noteId,
      action,
      localVersion,
      timestamp: new Date(),
      data
    };
    
    await syncLogsStore.add(operation);
    await tx.done;
    
    // 添加到待同步队列
    this.pendingOperations.set(noteId, operation);
    
    // 如果在线,立即尝试同步
    if (navigator.onLine && !this.isSyncing) {
      setTimeout(() => this.sync().catch(console.error), 100);
    }
  }
  
  // 执行同步
  async sync(): Promise<void> {
    if (this.isSyncing || this.pendingOperations.size === 0) {
      return;
    }
    
    this.isSyncing = true;
    
    try {
      const operations = Array.from(this.pendingOperations.values());
      
      // 分批同步,每批10个操作
      const batchSize = 10;
      for (let i = 0; i < operations.length; i += batchSize) {
        const batch = operations.slice(i, i + batchSize);
        await this.syncBatch(batch);
      }
      
      console.log('同步完成');
    } catch (error) {
      console.error('同步失败:', error);
      throw error;
    } finally {
      this.isSyncing = false;
    }
  }
  
  private async syncBatch(operations: SyncOperation[]): Promise<void> {
    try {
      // 发送到服务器
      const response = await this.apiClient.syncNotes(operations);
      
      // 处理服务器响应
      const tx = this.db.transaction(['notes', 'syncLogs'], 'readwrite');
      const notesStore = tx.objectStore('notes');
      const syncLogsStore = tx.objectStore('syncLogs');
      
      for (const result of response.results) {
        if (result.success) {
          // 同步成功,更新本地状态
          const note = await notesStore.get(result.noteId);
          if (note) {
            note.syncStatus = 'synced';
            note.serverVersion = result.serverVersion;
            note.updatedAt = new Date();
            await notesStore.put(note);
          }
          
          // 从待同步队列中移除
          this.pendingOperations.delete(result.noteId);
          
          // 标记同步日志为已处理
          if (result.operationId) {
            await syncLogsStore.delete(result.operationId);
          }
        } else if (result.conflict) {
          // 检测到冲突
          await this.handleConflict(result.noteId, result.serverData);
        }
      }
      
      await tx.done;
    } catch (error) {
      console.error('批量同步失败:', error);
      throw error;
    }
  }
}

4.2 冲突检测与解决策略

当多个客户端同时修改同一份数据时,冲突不可避免。我们需要一个健壮的冲突解决策略。

// sync/conflict-resolver.ts
export interface ConflictResolution {
  noteId: number;
  strategy: 'client-wins' | 'server-wins' | 'merge' | 'manual';
  resolvedData: Note;
  resolutionTime: Date;
}

export class ConflictResolver {
  constructor(private db: IDBPDatabase<NotesDBSchema>) {}
  
  // 检测冲突
  async detectConflict(
    noteId: number, 
    serverData: Note
  ): Promise<boolean> {
    const tx = this.db.transaction('notes', 'readonly');
    const store = tx.objectStore('notes');
    const localNote = await store.get(noteId);
    
    if (!localNote) {
      return false;
    }
    
    // 简单的版本号冲突检测
    if (serverData.serverVersion && localNote.serverVersion) {
      return serverData.serverVersion > localNote.serverVersion && 
             localNote.syncStatus === 'pending';
    }
    
    // 基于时间戳的冲突检测
    const serverTime = new Date(serverData.updatedAt).getTime();
    const localTime = new Date(localNote.updatedAt).getTime();
    
    // 如果服务器版本更新,且本地有未同步的修改,则存在冲突
    return serverTime > localTime && localNote.syncStatus === 'pending';
  }
  
  // 自动解决冲突(基于策略)
  async resolveConflict(
    noteId: number, 
    serverData: Note,
    strategy: ConflictResolution['strategy'] = 'merge'
  ): Promise<ConflictResolution> {
    const tx = this.db.transaction('notes', 'readwrite');
    const store = tx.objectStore('notes');
    const localNote = await store.get(noteId);
    
    if (!localNote) {
      throw new Error(`笔记 ${noteId} 不存在`);
    }
    
    let resolvedData: Note;
    
    switch (strategy) {
      case 'client-wins':
        // 客户端优先:保留本地修改,但更新服务器版本号
        resolvedData = {
          ...localNote,
          serverVersion: serverData.serverVersion,
          syncStatus: 'synced' as const,
          updatedAt: new Date()
        };
        break;
        
      case 'server-wins':
        // 服务器优先:使用服务器数据
        resolvedData = {
          ...serverData,
          id: noteId,
          syncStatus: 'synced' as const
        };
        break;
        
      case 'merge':
        // 合并策略:智能合并双方修改
        resolvedData = this.mergeNotes(localNote, serverData);
        break;
        
      case 'manual':
        // 手动解决:标记为冲突,等待用户处理
        resolvedData = {
          ...localNote,
          syncStatus: 'conflict' as const,
          conflictWith: serverData
        };
        break;
        
      default:
        throw new Error(`未知的冲突解决策略: ${strategy}`);
    }
    
    await store.put(resolvedData);
    await tx.done;
    
    return {
      noteId,
      strategy,
      resolvedData,
      resolutionTime: new Date()
    };
  }
  
  // 智能合并算法
  private mergeNotes(localNote: Note, serverNote: Note): Note {
    // 简单的合并策略:以最新修改为准
    const localTime = new Date(localNote.updatedAt).getTime();
    const serverTime = new Date(serverNote.updatedAt).getTime();
    
    const baseNote = serverTime > localTime ? serverNote : localNote;
    const otherNote = serverTime > localTime ? localNote : serverNote;
    
    // 合并标题:如果双方都修改了标题,添加冲突标记
    let mergedTitle = baseNote.title;
    if (baseNote.title !== otherNote.title && 
        baseNote.updatedAt !== otherNote.updatedAt) {
      mergedTitle = `${baseNote.title} (合并自: ${otherNote.title})`;
    }
    
    // 合并内容:使用操作变换(OT)或简单的追加
    let mergedContent = baseNote.content;
    if (baseNote.content !== otherNote.content) {
      mergedContent = this.mergeContent(baseNote.content, otherNote.content);
    }
    
    // 合并标签:取并集
    const mergedTags = Array.from(
      new Set([...baseNote.tags, ...otherNote.tags])
    );
    
    return {
      ...baseNote,
      id: localNote.id, // 保持原有ID
      title: mergedTitle,
      content: mergedContent,
      tags: mergedTags,
      syncStatus: 'synced' as const,
      updatedAt: new Date(),
      serverVersion: Math.max(
        localNote.serverVersion || 0,
        serverNote.serverVersion || 0
      ) + 1
    };
  }
  
  private mergeContent(baseContent: string, otherContent: string): string {
    // 简单的合并:检测是否只是追加
    if (baseContent.includes(otherContent)) {
      return baseContent;
    }
    
    if (otherContent.includes(baseContent)) {
      return otherContent;
    }
    
    // 复杂合并:使用分隔符合并
    return `${baseContent}\n\n--- 合并内容 ---\n\n${otherContent}`;
  }
  
  // 获取所有冲突笔记
  async getConflictingNotes(): Promise<Note[]> {
    const tx = this.db.transaction('notes', 'readonly');
    const store = tx.objectStore('notes');
    const index = store.index('by-sync-status');
    
    return index.getAll(IDBKeyRange.only('conflict'));
  }
  
  // 手动解决冲突
  async manualResolveConflict(
    noteId: number, 
    resolution: 'keep-local' | 'use-server' | 'custom',
    customData?: Partial<Note>
  ): Promise<void> {
    const tx = this.db.transaction('notes', 'readwrite');
    const store = tx.objectStore('notes');
    const note = await store.get(noteId);
    
    if (!note || note.syncStatus !== 'conflict') {
      throw new Error(`笔记 ${noteId} 没有冲突需要解决`);
    }
    
    let resolvedNote: Note;
    
    switch (resolution) {
      case 'keep-local':
        resolvedNote = {
          ...note,
          syncStatus: 'pending' as const,
          conflictWith: undefined
        };
        break;
        
      case 'use-server':
        if (!note.conflictWith) {
          throw new Error('服务器数据不存在');
        }
        resolvedNote = {
          ...note.conflictWith,
          id: noteId,
          syncStatus: 'pending' as const,
          conflictWith: undefined
        };
        break;
        
      case 'custom':
        if (!customData) {
          throw new Error('自定义数据不能为空');
        }
        resolvedNote = {
          ...note,
          ...customData,
          syncStatus: 'pending' as const,
          conflictWith: undefined,
          updatedAt: new Date()
        };
        break;
    }
    
    await store.put(resolvedNote);
    await tx.done;
  }
}

4.3 离线优先的UI/UX设计

良好的用户体验需要UI能够反映数据的同步状态,并在离线时提供清晰的反馈。

// ui/sync-status.tsx
import React, { useState, useEffect } from 'react';
import { SyncManager } from '../sync/sync-manager';

interface SyncStatusProps {
  syncManager: SyncManager;
}

export const SyncStatus: React.FC<SyncStatusProps> = ({ syncManager }) => {
  const [isOnline, setIsOnline] = useState(navigator.onLine);
  const [pendingCount, setPendingCount] = useState(0);
  const [lastSync, setLastSync] = useState<Date | null>(null);
  const [isSyncing, setIsSyncing] = useState(false);
  
  useEffect(() => {
    // 监听网络状态
    const handleOnline = () => setIsOnline(true);
    const handleOffline = () => setIsOnline(false);
    
    window.addEventListener('online', handleOnline);
    window.addEventListener('offline', handleOffline);
    
    // 定期检查待同步数量
    const checkPending = () => {
      // 这里需要从syncManager获取实际数量
      // 简化示例
      setPendingCount(Math.floor(Math.random() * 10)); // 模拟数据
    };
    
    const interval = setInterval(checkPending, 5000);
    checkPending();
    
    return () => {
      window.removeEventListener('online', handleOnline);
      window.removeEventListener('offline', handleOffline);
      clearInterval(interval);
    };
  }, [syncManager]);
  
  const handleManualSync = async () => {
    if (!isOnline || isSyncing) return;
    
    setIsSyncing(true);
    try {
      await syncManager.sync();
      setLastSync(new Date());
    } catch (error) {
      console.error('手动同步失败:', error);
    } finally {
      setIsSyncing(false);
    }
  };
  
  return (
    <div className="sync-status">
      <div className="status-indicator">
        <span className={`status-dot ${isOnline ? 'online' : 'offline'}`} />
        <span className="status-text">
          {isOnline ? '在线' : '离线'}
        </span>
      </div>
      
      {pendingCount > 0 && (
        <div className="pending-count">
          <span className="count-badge">{pendingCount}</span>
          <span className="count-text">待同步</span>
        </div>
      )}
      
      {lastSync && (
        <div className="last-sync">
          最后同步: {lastSync.toLocaleTimeString()}
        </div>
      )}
      
      <button
        className="sync-button"
        onClick={handleManualSync}
        disabled={!isOnline || isSyncing || pendingCount === 0}
      >
        {isSyncing ? '同步中...' : '立即同步'}
      </button>
      
      {!isOnline && (
        <div className="offline-warning">
          <span className="warning-icon">⚠️</span>
          <span>当前处于离线状态,更改将在恢复网络后同步</span>
        </div>
      )}
    </div>
  );
};

// 样式示例
const styles = `
.sync-status {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 8px 16px;
  background: #f5f5f5;
  border-radius: 6px;
  font-size: 14px;
}

.status-dot {
  display: inline-block;
  width: 8px;
  height: 8px;
  border-radius: 50%;
  margin-right: 6px;
}

.status-dot.online {
  background-color: #4caf50;
  animation: pulse 2s infinite;
}

.status-dot.offline {
  background-color: #f44336;
}

@keyframes pulse {
  0% { opacity: 1; }
  50% { opacity: 0.5; }
  100% { opacity: 1; }
}

.pending-count {
  display: flex;
  align-items: center;
  gap: 4px;
}

.count-badge {
  background: #ff9800;
  color: white;
  border-radius: 10px;
  padding: 2px 8px;
  font-size: 12px;
  font-weight: bold;
}

.sync-button {
  background: #2196f3;
  color: white;
  border: none;
  border-radius: 4px;
  padding: 6px 12px;
  cursor: pointer;
  font-size: 14px;
}

.sync-button:disabled {
  background: #ccc;
  cursor: not-allowed;
}

.offline-warning {
  display: flex;
  align-items: center;
  gap: 6px;
  color: #f44336;
  font-size: 12px;
}

.warning-icon {
  font-size: 16px;
}
`;

5. 实战:构建完整的离线笔记应用

现在让我们将所有组件组合起来,构建一个完整的离线笔记应用。这个应用将展示如何在实际项目中使用IndexedDB处理复杂的数据操作。

5.1 应用架构与状态管理

// app/note-app.ts
import { initDatabase } from '../db/init';
import { NoteRepository } from '../db/operations';
import { NoteQueryService } from '../db/query';
import { SyncManager } from '../sync/sync-manager';
import { ConflictResolver } from '../sync/conflict-resolver';
import { NoteApiClient } from '../api/client';

export class NoteApp {
  private db: IDBPDatabase<NotesDBSchema> | null = null;
  private noteRepo: NoteRepository | null = null;
  private queryService: NoteQueryService | null = null;
  private syncManager: SyncManager | null = null;
  private conflictResolver: ConflictResolver | null = null;
  
  private isInitialized = false;
  private initializationPromise: Promise<void> | null = null;
  
  // 单例模式
  private static instance: NoteApp;
  static getInstance(): NoteApp {
    if (!NoteApp.instance) {
      NoteApp.instance = new NoteApp();
    }
    return NoteApp.instance;
  }
  
  private constructor() {}
  
  // 初始化应用
  async initialize(): Promise<void> {
    if (this.isInitialized) return;
    
    if (this.initializationPromise) {
      return this.initializationPromise;
    }
    
    this.initializationPromise = this._initialize();
    return this.initializationPromise;
  }
  
  private async _initialize(): Promise<void> {
    try {
      // 1. 初始化数据库
      this.db = await initDatabase();
      
      // 2. 初始化各个服务
      this.noteRepo = new NoteRepository(this.db);
      this.queryService = new NoteQueryService(this.db);
      this.conflictResolver = new ConflictResolver(this.db);
      
      // 3. 初始化API客户端和同步管理器
      const apiClient = new NoteApiClient({
        baseURL: process.env.API_BASE_URL,
        timeout: 10000
      });
      
      this.syncManager = new SyncManager(this.db, apiClient);
      
      // 4. 启动自动同步
      this.syncManager.startAutoSync();
      
      // 5. 检查并解决现有冲突
      await this.checkAndResolveConflicts();
      
      this.isInitialized = true;
      console.log('笔记应用初始化完成');
      
    } catch (error) {
      console.error('应用初始化失败:', error);
      throw error;
    }
  }
  
  // 笔记CRUD操作
  async createNote(title: string, content: string, tags: string[] = []): Promise<number> {
    await this.ensureInitialized();
    
    const noteData = {
      title,
      content,
      tags,
      createdAt: new Date(),
      updatedAt: new Date(),
      isArchived: false,
      syncStatus: 'pending' as const
    };
    
    const noteId = await this.noteRepo!.createNote(noteData);
    
    // 记录同步操作
    await this.syncManager!.recordChange(noteId, 'create', noteData);
    
    return noteId;
  }
  
  async updateNote(
    noteId: number, 
    updates: Partial<Omit<Note, 'id'>>
  ): Promise<void> {
    await this.ensureInitialized();
    
    const tx = this.db!.transaction('notes', 'readwrite');
    const store = tx.objectStore('notes');
    
    const note = await store.get(noteId);
    if (!note) {
      throw new Error(`笔记 ${noteId} 不存在`);
    }
    
    const updatedNote = {
      ...note,
      ...updates,
      updatedAt: new Date(),
      syncStatus: 'pending' as const
    };
    
    await store.put(updatedNote);
    await tx.done;
    
    // 记录同步操作
    await this.syncManager!.recordChange(noteId, 'update', updates);
  }
  
  async deleteNote(noteId: number): Promise<void> {
    await this.ensureInitialized();
    
    const tx = this.db!.transaction(['notes', 'attachments'], 'readwrite');
    const notesStore = tx.objectStore('notes');
    const attachmentsStore = tx.objectStore('attachments');
    
    // 删除笔记
    await notesStore.delete(noteId);
    
    // 删除关联的附件
    const attachmentsIndex = attachmentsStore.index('by-note-id');
    const attachments = await attachmentsIndex.getAll(IDBKeyRange.only(noteId));
    
    for (const attachment of attachments) {
      await attachmentsStore.delete(attachment.id!);
    }
    
    await tx.done;
    
    // 记录同步操作
    await this.syncManager!.recordChange(noteId, 'delete');
  }
  
  // 查询方法
  async searchNotes(query: string, options: {
    tags?: string[];
    dateRange?: { start?: Date; end?: Date };
    page?: number;
    pageSize?: number;
  } = {}): Promise<{notes: Note[], total: number}> {
    await this.ensureInitialized();
    
    const { tags, dateRange, page = 1, pageSize = 20 } = options;
    
    // 如果有搜索词,使用全文搜索
    if (query.trim()) {
      const searchService = new NoteSearchService(this.db!);
      const searchResults = await searchService.searchWithWorker(query);
      
      // 进一步筛选
      let filtered = searchResults;
      
      if (tags && tags.length > 0) {
        filtered = filtered.filter(note => 
          tags.every(tag => note.tags.includes(tag))
        );
      }
      
      if (dateRange) {
        filtered = filtered.filter(note => {
          const noteTime = new Date(note.updatedAt).getTime();
          const startTime = dateRange.start?.getTime() || 0;
          const endTime = dateRange.end?.getTime() || Date.now();
          return noteTime >= startTime && noteTime <= endTime;
        });
      }
      
      // 分页
      const start = (page - 1) * pageSize;
      const end = start + pageSize;
      const paginated = filtered.slice(start, end);
      
      return {
        notes: paginated,
        total: filtered.length
      };
    }
    
    // 如果没有搜索词,使用常规查询
    if (tags && tags.length > 0) {
      if (tags.length === 1) {
        // 单个标签,使用索引查询
        return {
          notes: await this.queryService!.getNotesByTag(tags[0]),
          total: 0 // 简化处理
        };
      } else {
        // 多个标签,使用复合查询
        return {
          notes: await this.queryService!.getNotesByTagsAndDate(
            tags, 
            dateRange?.start, 
            dateRange?.end
          ),
          total: 0
        };
      }
    }
    
    // 分页查询
    return this.queryService!.getNotesPaginated(page, pageSize);
  }
  
  // 冲突处理
  private async checkAndResolveConflicts(): Promise<void> {
    const conflicts = await this.conflictResolver!.getConflictingNotes();
    
    if (conflicts.length > 0) {
      console.warn(`发现 ${conflicts.length} 个冲突需要处理`);
      
      // 自动解决简单冲突
      for (const conflict of conflicts) {
        if (conflict.conflictWith) {
          const shouldAutoResolve = this.shouldAutoResolveConflict(
            conflict, 
            conflict.conflictWith
          );
          
          if (shouldAutoResolve) {
            await this.conflictResolver!.resolveConflict(
              conflict.id!,
              conflict.conflictWith,
              'merge'
            );
            console.log(`自动解决笔记 ${conflict.id} 的冲突`);
          }
        }
      }
    }
  }
  
  private shouldAutoResolveConflict(local: Note, server: Note): boolean {
    // 简单的自动解决策略:如果只有一方修改了内容,自动解决
    const localTime = new Date(local.updatedAt).getTime();
    const serverTime = new Date(server.updatedAt).getTime();
    
    // 如果时间差小于5分钟,且内容差异不大,自动合并
    const timeDiff = Math.abs(localTime - serverTime);
    const contentDiff = this.calculateContentDiff(local.content, server.content);
    
    return timeDiff < 5 * 60 * 1000 && contentDiff < 0.1; // 10%以内的差异
  }
  
  private calculateContentDiff(content1: string, content2: string): number {
    // 简单的文本差异计算
    const words1 = content1.split(/\s+/);
    const words2 = content2.split(/\s+/);
    
    const set1 = new Set(words1);
    const set2 = new Set(words2);
    
    const intersection = new Set([...set1].filter(x => set2.has(x)));
    const union = new Set([...set1, ...set2]);
    
    return 1 - (intersection.size / union.size);
  }
  
  private async ensureInitialized(): Promise<void> {
    if (!this.isInitialized) {
      await this.initialize();
    }
  }
  
  // 清理资源
  async cleanup(): Promise<void> {
    if (this.syncManager) {
      this.syncManager.stopAutoSync();
    }
    
    if (this.db) {
      this.db.close();
    }
    
    this.isInitialized = false;
    this.initializationPromise = null;
  }
}

5.2 错误处理与数据恢复

在生产环境中,我们需要考虑各种异常情况,并提供数据恢复机制。

// app/error-handler.ts
export class NoteAppErrorHandler {
  private errorCount = new Map<string, number>();
  private readonly MAX_RETRIES = 3;
  
  async handleDatabaseError(error: Error, operation: string): Promise<void> {
    console.error(`数据库操作失败 [${operation}]:`, error);
    
    const errorKey = `${operation}:${error.name}`;
    const count = this.errorCount.get(errorKey) || 0;
    
    if (count >= this.MAX_RETRIES) {
      throw new Error(`操作失败,已重试${this.MAX_RETRIES}次: ${error.message}`);
    }
    
    this.errorCount.set(errorKey, count + 1);
    
    // 根据错误类型采取不同策略
    if (error.name.includes('QuotaExceededError')) {
      await this.handleStorageQuotaExceeded();
    } else if (error.name.includes('VersionError')) {
      await this.handleVersionError();
    } else if (error.name.includes('ConstraintError')) {
      await this.handleConstraintError(error, operation);
    } else {
      // 通用重试
      await this.retryWithBackoff(operation, count + 1);
    }
  }
  
  private async handleStorageQuotaExceeded(): Promise<void> {
    console.warn('存储空间不足,尝试清理缓存');
    
    // 1. 清理过期的同步日志
    const db = await initDatabase();
    const tx = db.transaction('syncLogs', 'readwrite');
    const store = tx.objectStore('syncLogs');
    const index = store.index('by-timestamp');
    
    // 删除30天前的日志
    const thirtyDaysAgo = new Date();
    thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
    
    const range = IDBKeyRange.upperBound(thirtyDaysAgo);
    const oldLogs = await index.getAll(range);
    
    for (const log of oldLogs) {
      await store.delete(log.id!);
    }
    
    await tx.done;
    db.close();
    
    // 2. 清理已删除笔记的附件
    await this.cleanupOrphanedAttachments();
    
    // 3. 如果还是空间不足,提示用户
    if (!await this.checkStorageQuota()) {
      throw new Error('存储空间不足,请手动清理数据');
    }
  }
  
  private async handleVersionError(): Promise<void> {
    console.warn('数据库版本错误,尝试重新初始化');
    
    // 备份现有数据
    const backup = await this.createBackup();
    
    try {
      // 删除并重建数据库
      await this.recreateDatabase();
      
      // 恢复数据
      await this.restoreFromBackup(backup);
      
    } catch (recoveryError) {
      console.error('数据库恢复失败:', recoveryError);
      throw new Error('数据库损坏,已重置为初始状态');
    }
  }
  
  private async handleConstraintError(error: Error, operation: string): Promise<void> {
    console.warn(`约束错误 [${operation}]:`, error);
    
    // 对于唯一约束错误,尝试生成新的ID
    if (error.message.includes('unique')) {
      await new Promise(resolve => setTimeout(resolve, 100));
      // 重试操作时会生成新的ID
    }
  }
  
  private async retryWithBackoff(operation: string, attempt: number): Promise<void> {
    const delay = Math.min(1000 * Math.pow(2, attempt), 10000);
    console.log(`第${attempt}次重试 [${operation}],等待${delay}ms`);
    
    await new Promise(resolve => setTimeout(resolve, delay));
  }
  
  private async createBackup(): Promise<BackupData> {
    const db = await initDatabase();
    const tx = db.transaction(
      ['notes', 'tags', 'attachments', 'syncLogs'], 
      'readonly'
    );
    
    const backup: BackupData = {
      timestamp: new Date(),
      notes: await tx.objectStore('notes').getAll(),
      tags: await tx.objectStore('tags').getAll(),
      attachments: await tx.objectStore('attachments').getAll(),
      syncLogs: await tx.objectStore('syncLogs').getAll()
    };
    
    await tx.done;
    db.close();
    
    // 压缩备份数据
    const compressed = this.compressBackup(backup);
    
    // 存储到localStorage(作为最后的手段)
    try {
      localStorage.setItem('notes-app-backup', JSON.stringify(compressed));
      localStorage.setItem('notes-app-backup-time', new Date().toISOString());
    } catch (e) {
      console.warn('无法存储备份到localStorage:', e);
    }
    
    return backup;
  }
  
  private async recreateDatabase(): Promise<void> {
    // 关闭现有连接
    if (NoteApp.getInstance()) {
      await NoteApp.getInstance().cleanup();
    }
    
    // 删除数据库
    const dbName = 'notes-app';
    const req = indexedDB.deleteDatabase(dbName);
    
    await new Promise<void>((resolve, reject) => {
      req.onsuccess = () => resolve();
      req.onerror = () => reject(new Error('删除数据库失败'));
      req.onblocked = () => {
        console.warn('数据库被其他标签页占用,请关闭其他标签页后重试');
        reject(new Error('数据库被占用'));
      };
    });
    
    // 重新初始化
    await NoteApp.getInstance().initialize();
  }
  
  private compressBackup(backup: BackupData): CompressedBackup {
    // 简单的压缩:移除元数据,只保留必要数据
    return {
      timestamp: backup.timestamp,
      notes: backup.notes.map(note => ({
        title: note.title,
        content: note.content,
        tags: note.tags,
        createdAt: note.createdAt,
        updatedAt: note.updatedAt
      })),
      tags: backup.tags
    };
  }
  
  private async checkStorageQuota(): Promise<boolean> {
    if ('storage' in navigator && 'estimate' in navigator.storage) {
      try {
        const estimate = await navigator.storage.estimate();
        const usageRatio = (estimate.usage || 0) / (estimate.quota || 1);
        
        return usageRatio < 0.9; // 使用率低于90%
      } catch (e) {
        console.warn('无法获取存储配额:', e);
      }
    }
    
    return true; // 如果不支持配额API,假设空间足够
  }
}

interface BackupData {
  timestamp: Date;
  notes: Note[];
  tags: Tag[];
  attachments: Attachment[];
  syncLogs: SyncLog[];
}

interface CompressedBackup {
  timestamp: Date;
  notes: Array<Pick<Note, 'title' | 'content' | 'tags' | 'createdAt' | 'updatedAt'>>;
  tags: Tag[];
}

5.3 性能优化与最佳实践总结

经过前面的实战,我们总结出一些IndexedDB的最佳实践:

  1. 合理设计数据库结构:根据查询模式创建索引,避免全表扫描
  2. 使用事务批处理:将多个操作放在一个事务中,减少事务开销
  3. 实现优雅的错误处理:区分可重试错误和不可恢复错误
  4. 监控性能指标:跟踪查询时间、事务成功率等关键指标
  5. 提供数据恢复机制:定期备份,支持从错误中恢复
  6. 优化存储空间:清理过期数据,监控存储配额
  7. 实现智能同步:冲突检测与自动解决策略
  8. 考虑离线体验:UI反映同步状态,提供离线提示

在实际项目中,我遇到过这样一个案例:一个团队协作的笔记应用,用户经常在移动网络不稳定的环境下使用。最初版本没有完善的冲突解决机制,导致用户经常丢失数据。后来我们实现了基于操作变换(OT)的冲突解决算法,并添加了详细的同步日志,问题才得到彻底解决。关键是要理解,离线同步不是简单的“最后写入获胜”,而是需要根据业务逻辑设计合适的合并策略。

另一个重要的经验是:一定要在生产环境中监控IndexedDB的性能。我们曾经发现某个查询在数据量达到5000条时变得异常缓慢,通过分析发现是因为没有正确使用索引。添加合适的索引后,查询时间从几秒降低到几十毫秒。

最后,记住IndexedDB虽然强大,但也不是万能的。对于特别复杂的关系型数据操作,或者需要跨浏览器标签页实时同步的场景,可能需要结合其他技术(如WebSocket、Service Workers等)。选择合适的技术组合,才能构建出既强大又用户友好的Web应用。

打造离线优先的渐进式Web应用:IndexedDB与后台同步的实现 本章节详细介绍了如何通过IndexedDB和后台同步(Background Sync)功能,将任务列表应用程序转变为一个完全离线优先的渐进式Web应用程序。通过引入两个对象存储(task-store和queue-store),确保了任务数据的本地持久化和与后端服务的同步。文中还展示了如何在应用程序中实现添加、删除、更新任务等操作,并确保这些操作在后台同步事件中得到正确处理。 阅读详情

相关推荐

WinForm轻松打造AntDesign现代化工具栏

摘要: 本文介绍如何在WinForm中利用AntdUI库实现AntDesign风格工具栏,包括安装配置、动态生成按钮/下拉菜单、布局优化及主题美化。通过AntButton、AntDropdown等控件结合事件绑定,可快速构建支持图标、工具提示、权限控制的现代化工具栏,并解决常见图标加载、布局错乱问题。最后提供代码示例与响应式设计技巧,助你打造高效美观的WinForm界面。 (字数:150)

java专栏 1581

手把手教你用IndexedDB打造离线笔记应用(含完整增删改查示例)

本文详细介绍了如何使用IndexedDB构建离线笔记应用,涵盖数据库初始化、CRUD操作实现、高级功能开发及性能优化等核心内容。通过完整代码示例,帮助前端开发者掌握这一强大的浏览器数据库技术,打造无需网络连接的PWA应用。

grafana8visual的博客 171

DIC新手避坑指南:压缩实验中的应变测量常见错误与数据校验技巧

本文为DIC技术新手提供了压缩实验中应变测量的全面避坑指南。文章系统梳理了从试样尺寸录入、散斑质量到数据后处理中应变方向与符号混淆、数据插值对齐等常见错误,并分享了基于MATLAB的自动化数据校验与可视化对比技巧,帮助建立可靠的实验数据分析SOP,确保DIC测量结果的准确性。

weixin_29313423的博客 103

IndexedDB实战:手把手教你打造离线笔记应用(含事务处理与错误调试技巧)

本文通过构建离线笔记应用的实战案例,深入讲解IndexedDB的核心应用。涵盖数据库设计、事务处理、游标查询、版本升级与错误调试等高级技巧,帮助前端开发者掌握这一强大的浏览器数据库,为Web应用提供稳定可靠的离线数据管理能力。

a0b1c2d3的专栏 990

Cisco思科 电子科技大学计算机网络 SRWE 期末技能考试 (PTSA)

电子科技大学2024春_SRWE_计算机网络系统 SRWE 期末技能考试 (PTSA)

2301_76745065的博客 5085

Blender骨骼绑定实战:如何用IK控制器让角色动作更自然(附常见问题解决)

本文深入探讨了Blender中IK控制器的实战应用,旨在解决角色动作僵硬问题。通过对比IK与FK的核心差异,详细解析了IK链的构建、参数设置及极向目标的使用,并提供了控制器定制、IK/FK无缝切换等高级技巧,帮助动画师实现更自然、符合直觉的角色动画。

weixin_29243279的博客 426

IndexedDB实战:手把手教你打造离线笔记应用(含完整代码示例)

本文通过一个完整的离线笔记应用实战项目,手把手教你如何使用IndexedDB进行前端数据库开发。文章详细讲解了数据库设计、核心CRUD操作、高级查询、离线同步策略以及性能优化,提供了可直接复用的完整代码示例,帮助开发者构建功能强大的离线优先Web应用。

weixin_29229989的博客 299

制作Ubuntu22.04启动盘

现在越来越多程序代码都需要运行在linux环境下,运行这些代码的前提是安装一个稳定的linux操作系统。本文选择Ubuntu 22.04去制作系统启动盘。本文主要对ubuntu22.04的启动盘制作过程进行了详细的说明,希望能帮助到有需要的朋友。

lyx_ok的博客 1万+

IndexedDB在电商离线应用中的实战案例

这个项目需要实现商品展示、购物车管理和订单同步等功能,即使在弱网环境下也能流畅使用。下面分享我的实战经验,希望能帮助到有类似需求的开发者。我们需要让用户在没有网络时也能浏览商品、管理购物车,等网络恢复后再同步数据。它内置的代码编辑器和实时预览功能让调试变得非常方便,特别是可以随时查看IndexedDB的实际存储状态。最让我惊喜的是它的一键部署功能,不需要折腾服务器配置就能把demo分享给团队成员测试。如果你也在开发类似的Web应用,不妨从简单的功能开始尝试,逐步构建完整的离线体验。

StarfallRaven13的博客 955

深入理解Android音视频同步机制(三)NuPlayer的avsync逻辑

本文深入介绍了NuPlayer的avsync逻辑和原理

张晖的专栏 1万+

Python 爬虫实战:就业市场趋势数据抓取与行业洞察

在当今竞争激烈的就业市场中,了解行业趋势和就业形势对于求职者、学生以及人力资源专业人士来说至关重要。通过获取和分析就业市场数据,我们可以深入了解不同行业的招聘需求、薪资水平、技能要求等关键信息,从而为职业规划和人才发展提供有力支持。

yansideyucsdn的博客 1399

用快马AI三分钟打造IndexedDB离线应用 - 告别网络依赖的智能开发方案

最近在开发一个需要离线功能的笔记应用时,发现了IndexedDB这个浏览器内置的数据库神器。它不仅能存储大量结构化数据,还支持事务处理,非常适合需要本地缓存的Web应用。对于前端开发者来说,IndexedDB+快马的组合简直是开发离线应用的利器。从零开始到可用的原型,真的只用了不到三小时。如果你也在考虑为应用添加离线功能,强烈推荐试试这个方案。平台不仅帮我生成了基础代码框架,还能实时预览效果。最棒的是,一键部署功能让我能立即分享给团队成员测试,不用操心服务器配置问题。实现实时搜索反馈(防抖处理优化性能)

NightshadeEagle34的博客 618

【二】MADDPG多智能体算法实现(parl)【追逐游戏复现】

论文全称:Multi-Agent Actor-Critic for Mixed Cooperative-Competitive Environments 论文原文:https://download.csdn.net/download/sinat_39620217/16203960 论文翻译:https://blog.csdn.net/qiusuoxiaozi/article/details/79066612 ...

丨汀、的博客 2万+

ZangoDB实战案例:构建离线优先的PWA应用完整教程

ZangoDB是一个MongoDB-like接口的HTML5 IndexedDB工具,专为现代浏览器设计,支持过滤、投影、排序、更新和聚合等MongoDB核心功能。通过ZangoDB,开发者可以轻松构建离线优先的渐进式Web应用(PWA),为用户提供流畅的离线体验。 ## 为什么选择ZangoDB开发离线PWA? ZangoDB将MongoDB的易用性与IndexedDB的本地存储能力完美结合

gitblog_00829的博客 1049

大中型超市收银系统(可直接运行)

光步前台收银系统系统功能:商品销售、商品退货、挂单、收银员权限、会员销售、单品取消、整单取消、商品查询、脱机销售、脱机数据上传,基本资料下载,开钱箱、小票打印、收款结算,支持多币种付款方式(现金、储值卡、礼券、微信等),支持多种促销。运行环境:.net 4.0登陆信息用户名:1001 密码:123456内置商品信息,商品编码可输入00001000 - 00010000,测试效果相关功能键F5 结算 F9挂单 F11商品查询 F8退货 Insert单品取消 Del整单取消 F12开钱箱F2 会员 Ctrl+Z退出系统 Home 锁屏 相关源码待公开

C# WinForm 基于DirectShow.dll编写的程序,可边预览边保存视频,音频和图片

基于微软的DirectShow.dll的二次开发1.自动识别连接的摄像头跟麦克风且可以对其进行选择(默认选中第一个)2.可录制wmv和avi两种格式的视频并且都带有音频,avi可以录制的分辨率和帧数进行选择,wmv默认进行了淸晰度优化如不喜欢可去除代码中有相关提示3.可在预览点开之后进行拍照,也可边录制视频边拍照4.添加了视频和音频的压缩功能(酌情使用)这次二次开发发现C#对DirectShow的相关信息太少,这次做完也不敢藏私共享出来,还望大家指正

java indexeddb_IndexedDB 打造靠谱 Web 离线数据库

在知乎和我在平常工作中,常常会看到一个问题:前端现在还火吗?这个我只想说:隔岸观火的人永远无法明白起火的原因,只有置身风暴,才能找到风眼之所在 ——『秦时明月』你 TM 看都不看前端现在的发展,怎么去评判前端火不火,我该不该尝试一下其他方面的内容呢?本人为啥为这么热衷于新的技术呢?主要原因在于,生怕会被某一项颠覆性的内容淘汰掉,从前沿领域掉队下来。说句人话就是:穷,所以只能学了...。所以本文会从...

weixin_35909443的博客 481

STM32F407中文手册

STM32f407中文手册 本参考手册面向应用开发人员,提供有关使用 STM32F405xx/07xx、STM32F415xx/17xx、 STM32F42xxx 和 STM32F43xxx 微控制器存储器与外设的完整信息。STM32F405xx/07xx、STM32F415xx/17xx、STM32F42xxx 和 STM32F43xxx 构成一个微 控制器系列,各产品具有不同的存储器大小、封装和外设。有关订购信息以及器件的机械与电气特性,请参见数据手册。有关 ARM Cortex™-M4F 内核的信息,请参见《Cortex™-M4F 技术参考手册》。

前端存储终极指南:IndexedDB与LocalStorage深度对比与实战技巧

在前端开发中,数据存储技术是构建现代化Web应用的关键基础。无论您是要开发离线应用、提升用户体验,还是优化性能,掌握前端存储技术都至关重要。本文将为您全面解析IndexedDB和LocalStorage这两种主流前端存储方案,帮助您在实际项目中做出最佳选择。💪 ## 🔍 前端存储技术概述 前端存储技术主要分为三种类型:Cookie、LocalStorage/SessionStorage和I

gitblog_00757的博客 1144

3个关键步骤打造极致离线优先Web应用:offline-first项目完全指南

在当今网络环境复杂多变的时代,构建**离线优先**的Web应用已成为提升用户体验的关键策略。offline-first项目为开发者提供了构建可靠离线应用的完整资源集合,帮助你的应用在网络不稳定甚至完全断网时依然能够正常工作。无论你是Web开发新手还是经验丰富的工程师,这个项目都能为你提供构建离线优先应用所需的一切工具和知识。 ## 🌟 项目亮点与核心优势 offline-first项目不仅仅

gitblog_00558的博客 1065

如何用10个简单步骤打造终极离线优先Web应用:完整实践指南

你是否曾经在地铁里、飞机上或网络信号差的地区,试图使用一个Web应用却因为无法连接网络而失败?这种令人沮丧的经历正是离线优先Web应用要解决的问题。Offline-First项目为你提供了构建可靠离线Web应用所需的一切资源和工具,让你能够打造即使在没有网络连接的情况下也能正常工作的现代Web应用。 ## 🚀 从网络焦虑到离线自由:为什么你需要离线优先应用? 想象一下这样的场景:你正在使用一

gitblog_00258的博客 927

告别数据丢失:Riot.js+IndexedDB构建离线优先Web应用

你是否遇到过这样的情况:用户填写了一半的表单因浏览器崩溃前功尽弃?精心筛选的商品列表在刷新页面后消失无踪?在Web应用开发中,客户端数据持久化一直是提升用户体验的关键痛点。本文将带你使用轻量级UI库Riot.js结合IndexedDB API,构建一套完整的客户端数据存储解决方案,让你的Web应用在离线环境下也能如原生应用般可靠运行。 ## 技术选型:为什么是Riot.js+IndexedDB?...

gitblog_00989的博客 1143

离线优先Web应用:打造永不掉线的现代网页体验

在当今这个网络无处不在的时代,我们常常会遇到网络连接不稳定的情况——地铁隧道、偏远地区、甚至只是短暂的信号中断。**离线优先Web应用**正是为了解决这一问题而生的革命性技术,它让网页应用即使在离线状态下也能正常工作,待网络恢复后再自动同步数据。本文将为你详细介绍这一技术理念,并展示如何使用offline-first项目资源来构建更可靠的Web应用。 ## 🌟 什么是离线优先?为何它如此重要?

gitblog_00325的博客 824

JavaScript IndexedDB 完整指南

本文将通过一个小教程向你介绍 IndexedDB,并将 IndexedDB 与其他可用选项进行比较。IndexedDB 用于在浏览器中存储数据,对于需要离线工作的 web 应用程序(如大多数进步的 web 应用程序)尤其重要。

CRMEB小程序商城的博客 2807

DBER本地存储方案详解:IndexedDB如何保障数据安全与离线使用

DBER作为一款基于实体关系图的数据库设计工具,其本地存储方案采用IndexedDB技术,为用户提供了安全可靠的数据存储和离线使用体验。本文将深入剖析DBER的IndexedDB实现原理,帮助用户了解数据如何在本地安全存储以及如何实现离线工作。 ## 为什么选择IndexedDB作为本地存储方案 IndexedDB是一种低级API,用于客户端存储大量结构化数据。与其他本地存储方案相比,它具有以

gitblog_00882的博客 1080

如何构建离线优先Web应用:5个关键步骤打造终极离线体验

在当今移动互联网时代,网络连接并不总是可靠,但用户期望应用能够随时可用。这就是**离线优先**设计理念的价值所在——它确保您的Web应用即使在网络不稳定或完全离线的情况下,也能提供流畅的用户体验。本文将为您详细介绍如何利用offline-first项目资源,快速掌握构建离线优先Web应用的核心技术。 ## 🚀 什么是离线优先设计模式? 离线优先是一种革命性的Web开发理念,它将应用的离线可用

gitblog_00705的博客 276

电商PWA实战:IndexedDB离线购物车系统开发

这个项目让我深刻体会到,好的离线体验对电商转化率的提升有多重要。,它的一键部署功能让我省去了大量环境配置时间,直接就能看到实际运行效果,特别适合快速验证想法。- 商品表:存储商品ID、名称、价格、库存等基本信息。- 购物车表:记录用户选择的商品ID、数量、选中状态。3. 如果网络不可用,将操作加入待同步队列。- 本地修改了商品数量,但服务器库存已不足。- 用户因网络问题放弃购买的情况减少65%- 操作队列:保存网络中断时的待同步操作。- 商品已下架但本地还在购物车中。1. 同步时带上本地操作时间戳。

CrystalwaveStag的博客 415

终极AIri离线存储方案:IndexedDB与LocalStorage全面对比指南

AIri作为一款支持实时语音聊天、Minecraft游戏和跨平台运行的自托管虚拟角色应用,其离线数据存储方案直接影响用户体验与数据安全。本文将深入分析AIri项目中两种核心客户端存储技术——IndexedDB与LocalStorage的技术特性、应用场景及性能表现,帮助开发者为不同功能模块选择最优存储方案。 [![AIri项目离线存储方案对比](https://raw.gitcode.com/G

gitblog_00379的博客 730
上一篇: 避坑指南:PyCINRAD处理CINRAD雷达数据时遇到的5个典型报错及解决方案
下一篇: 动态圣诞树网页效果:HTML与CSS创意实现
cc789
博客等级 码龄17年 55粉丝 1517原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值