如何构建一个基于浏览器多存储策略的数据库?

331 阅读5分钟

现在web前端的开发中,客户端数据的存储与管理是不可忽视的一环。无论是临时数据缓存、表单数据保存还是简单的配置存储,选择适当的本地存储策略都至关重要。在这篇文章中,我将带大家实现一个支持三种存储策略(内存LocalStorageIndexedDB)的简单数据库,并分享我对于各类存储技术的拙见。

为什么我们需要本地存储?

性能优化:减少与服务器的通信,提升用户体验。

离线支持:在网络不可用的情况下,让应用依旧能够提供基本的功能。

数据持久化:存储用户个性化信息或临时数据,避免页面刷新或关闭丢失。

存储策略

内存存储:数据仅存在于页面刷新前,适合临时数据。

LocalStorage:持久化存储,数据容量较小(一般为5MB)。

IndexedDB:结构化数据存储,适合复杂数据存储,支持异步操作。

实现思路

接下来,我将带你一步步实现一个基于这三种存储策略的类,名字就叫SimpleDatabase。该类包含一系列用于数据存储、读取、删除、查询的同步和异步方法。

初始化构造函数

构造函数用于初始化数据库,接收一个存储类型作为参数,默认值为memory(也就是内存)。如果选择indexedDB存储,则需要对此数据库进行初始化。

class SimpleDatabase {
  private storageType: StorageType;
  private memoryStorage: Record<string, any> = {};
  private localStorageKey = 'SimpleDatabase';
  private dbName = 'SimpleDatabase';
  private storeName = 'store';

  constructor(storageType: StorageType = 'memory') {
    this.storageType = storageType;
    if (storageType === 'indexedDB') {
      this.initIndexedDB();
    }
  }

  private initIndexedDB() {
    const request = indexedDB.open(this.dbName, 1);
    request.onerror = (event) => {
      console.error('Database error:', event.target);
    };
    request.onupgradeneeded = (event) => {
      const db = (event.target as IDBOpenDBRequest).result;
      if (!db.objectStoreNames.contains(this.storeName)) {
        db.createObjectStore(this.storeName, { keyPath: 'id' });
      }
    };
  }

  // ...
}

异步与同步操作

对于每个操作(保存、获取、删除),我们都需要既实现异步方法,也实现同步方法。异步方法对于indexedDB操作尤为重要,因为这类操作本身就是异步的,本来是打算都做成同步执行的,但是indexedDB同步执行的话,性能影响比较严重,所以就同步异步都写一下。

保存数据

1.异步保存数据

public async saveAsync(key: string, value: any): Promise<void> {
  try {
    if (this.storageType === 'localStorage') {
      const data = this.retrieveAllSync() || {};
      data[key] = value;
      localStorage.setItem(this.localStorageKey, JSON.stringify(data));
    } else if (this.storageType === 'indexedDB') {
      const db = await this.getIDBDatabase();
      const transaction = db.transaction([this.storeName], 'readwrite');
      const store = transaction.objectStore(this.storeName);
      await this.requestAsPromise(store.put({ id: key, value }));
    } else {
      this.memoryStorage[key] = value;
    }
  } catch (error) {
    console.error('Error saving data:', error);
  }
}

2.同步保存数据

public saveSync(key: string, value: any): void {
  if (this.storageType === 'localStorage') {
    const data = this.retrieveAllSync() || {};
    data[key] = value;
    localStorage.setItem(this.localStorageKey, JSON.stringify(data));
  } else {
    this.memoryStorage[key] = value;
  }
}

获取数据

1.异步获取数据

public async retrieveAsync(key: string): Promise<any> {
  try {
    if (this.storageType === 'localStorage') {
      const data = this.retrieveAllSync();
      return data ? data[key] : null;
    } else if (this.storageType === 'indexedDB') {
      const db = await this.getIDBDatabase();
      const transaction = db.transaction([this.storeName], 'readonly');
      const store = transaction.objectStore(this.storeName);
      const result = await this.requestAsPromise(store.get(key));
      return result?.value ?? null;
    } else {
      return this.memoryStorage[key] || null;
    }
  } catch (error) {
    console.error('Error retrieving data:', error);
    return null;
  }
}

2.同步获取数据

public retrieveSync(key: string): any {
  if (this.storageType === 'localStorage') {
    const data = this.retrieveAllSync();
    return data ? data[key] : null;
  } else {
    return this.memoryStorage[key] || null;
  }
}

批量操作与查询

支持批量保存数据和基于自定义过滤函数的查询操作。

批量保存数据

为了提高性能,我们可以批量保存数据。从数据结构上看,一种常见的做法是接收一个键值对数组,并使用forEach或者for...of循环进行保存。

// 批量异步保存
public async saveAllAsync(keyValuePairs: [string, any][]): Promise<void> {
  for (const [key, value] of keyValuePairs) {
    await this.saveAsync(key, value);
  }
}

// 批量同步保存
public saveAllSync(keyValuePairs: [string, any][]): void {
  keyValuePairs.forEach(([key, value]) => this.saveSync(key, value));
}

查询数据

再实现一个简单的查询方法,该方法接收一个过滤函数,并返回满足条件的数据。

// 异步查询
public async findAsync(filterFn: (value: any) => boolean): Promise<any[]> {
  const allData = await this.retrieveAllAsync();
  return Object.values(allData).filter(filterFn);
}

// 同步查询
public findSync(filterFn: (value: any) => boolean): any[] {
  const allData = this.retrieveAllSync();
  return Object.values(allData).filter(filterFn);
}

错误处理

为了确保代码的健壮性,我们在每个异步方法中都加入了基础的错误处理。大家可以根据自己相关业务需求进一步完善错误处理逻辑,这个方法再上述代码中有多处都使用了,大家可以回到上面看看。

private requestAsPromise<T>(request: IDBRequest): Promise<T> {
  return new Promise((resolve, reject) => {
    request.onerror = () => reject(request.error);
    request.onsuccess = () => resolve(request.result);
  });
}

三种存储策略优劣分析

优点缺点
内存存储速度快,实现简单数据不持久化,页面刷新数据丢失
LocalStorage简单易用,页面刷新数据保留存储大小有限(一般为5M),不适合存储复杂的数据结构
IndexedDB支持大容量数据存储(通常可达100+M);结构化存储,支持事务及索引,异步操作,离线存储学习成本高,不支持跨域,兼容性问题,使用繁琐,不同浏览器对IndexedDB的支持程度和具体实现可能存在差异

总结

本文通过实现一个SimpleDatabase类,展示了如何在我们的Web应用中实现基于内存LocalStorageIndexedDB的多种存储策略。每种存储方式都有自己的优缺点,选择适当的存储方式需要根据具体的业务需求和平衡点来决定。

在现代Web前端开发中,数据存储管理是不可忽视的重要环节。通过对比和实现不同的存储策略,我们不仅能更好地理解这些存储技术的特性与限制,也能为实际项目选择最合适的解决方案提供借鉴。

当然还有其他的存储方式,大家可以自行去了解一下,我就不多赘述了。