localStorage 不够用?试试 IndexedDB !

一、前言

在前端开发过程中,偶尔会遇到需要存储大量数据在前端的情况。localstorage 单个域名存储大小只有5M,此时 indexedDB 便派上了用场。

indexedDB 没有"每个域名 5MB"那种硬性限制,存储大小与浏览器策略和设备总磁盘空间有关。一般来说不少于250MB,存储在电脑上的位置(C:\用户\用户名\AppData\Local\Google\Chrome\User Data\Default\IndexedDB)

可使用 StorageManager API 来查询当前源的配额使用情况和剩余空间

js 复制代码
if ('storage' in navigator && 'estimate' in navigator.storage) {
  navigator.storage.estimate().then(estimate => {
    // 将字节转换为 GB,保留 2 位小数
    const totalGB = (estimate.quota / (1024 ** 3)).toFixed(2);
    const usedGB = (estimate.usage / (1024 ** 3)).toFixed(2);
    const usagePercent = Math.round((estimate.usage / estimate.quota) * 100);

    console.log('总配额(GB):', totalGB + ' GB');
    console.log('已使用(GB):', usedGB + ' GB');
    console.log('可用比例:', usagePercent + '%');
  });
} else {
  console.log('当前浏览器不支持 Storage Manager API');
}

提示:无痕模式下,可用空间会小很多。以我的电脑为例,使用Chrome浏览器正常模式下配额142.39 GB。使用Chrome浏览器无痕模式下配额1.59 GB。但不管怎么说,也比localstorage大不少

兼容性目前良好:

indexedDB 有较大的存储空间,这是它最大的优点。它的缺点也很明显,API比较复杂,有上手难度。如果你不在意使用细节,或者项目中可以随意添加第三方包,而不在乎项目的体积大小,可直接使用库:localforage

二、特点

  1. 储存空间大:比 LocalStorage 大得多

  2. 事务性:所有数据库操作都必须在事务中完成。这保证了数据的完整性和一致性。如果在事务中某一步操作失败,整个事务都会回滚,数据库将恢复到操作前的状态,永远不会出现"部分更新"的脏数据

  3. 键值对存储:数据以"对象存储"的形式存放,类似于数据库中的表。每个数据项通过一个唯一的主键来标识。值可以是几乎任何 JavaScript 类型(对象、数组、File、Blob 等)

  4. 异步操作:大规模的数据读写不会阻塞用户界面(UI线程)

  5. 同源限制:网页只能访问自身域名下的数据库

  6. 数据库版本管理:内置版本控制系统。当应用需要更改数据库结构时,可以通过升级版本号来触发 onupgradeneeded 事件,在此事件中执行创建或修改结构的操作。同一时刻只能有一个版本,每个域名可以建多个数据库

三、基础使用

1. 新建数据库(indexDB)

open方法接收两个参数,第一个是数据库名,第二个是数据库的版本号

在操作数据的时候,需要更新版本号才生效

js 复制代码
const request = window.indexedDB.open("myDatabase", 1);

request.onsuccess = function (res) {
  console.log("连接数据库成功", res);
};

request.onerror = function (error) {
  console.log("连接数据库失败", error);
};

request.onupgradeneeded = function (res) {
  console.log("indexedDB 升级成功", res);
};
  1. 新建表(对象仓库)

创建一个对象仓库必须在upgradeneeded事件中,而upgradeneeded事件只会在版本号更新的时候触发,这是因为不允许数据库中的数据仓库在同一版本中发生变化

js 复制代码
//版本号变为2
const request = window.indexedDB.open("myDatabase", 2);

request.onupgradeneeded = function (res) {
  const db = res.target.result;
  //如果Users表不存在则创建,并插入数据
  if (!db.objectStoreNames.contains("Users")) {
    db.createObjectStore("Users", { keyPath: "userId", autoIncrement: false });
  }
};

createObjectStore方法接受两个参数,第一个参数是对象仓库的名称,第二个参数用于指定数据的主键,主键的值必须是唯一且非空的,以及是否自增主键。

keyPath的值可以是一个数组,例如需要存储不同用户设置的标签颜色。用户的id和标签的key可共同作为主键,以满足主键值的唯一性。

db.createObjectStore("Users", { keyPath: ['userId', 'key'] });

3. 事务

IndexDB的读/写,都要通过事务操作。

特点一:事务保证数据库操作要么全部成功,要么全部失败进行回滚,恢复到上一状态。

js 复制代码
// 开启事务
const transaction = db.transaction("Users", "readwrite");

第一个参数是要操作的表的名称

第二个是要创建的事务模式,如readonly只能进行读操作,readwrite能进行读写操作

特点二:它是自动提交的。即所有请求都已完成,并且事件循环再次变为空闲状态时,事务会自动提交并随之关闭。这意味着,不能在异步中调用事务

js 复制代码
// ❌ 错误写法:事务会在 setTimeout 回调执行前就自动提交并关闭了!
let transaction = db.transaction(["books"], "readwrite");
let store = transaction.objectStore("books");

setTimeout(() => {
  // 这里会抛出错误:Transaction is inactive
  store.put({id: 1, title: "Book Title"});
}, 1000);

4. 增加数据 add

1. 增加单条数据

在数据库连接成功的onsuccess事件中,进行数据操作

js 复制代码
request.onsuccess = function (res) {
  const db = res.target.result;
  //判断是否存在Users表
  if (db.objectStoreNames.contains("Users")) {
    //开启事务,允许读写操作
    const transaction = db.transaction("Users", "readwrite");
    //获取对象存储空间,以便后续的数据操作
    const store = transaction.objectStore("Users");
    //add函数传入数据,这里userId是主键
    const reqAdd = store.add({ userId: 1, userName: "李白", age: 24 });
    reqAdd.onsuccess = function (event) {
      console.log("数据添加成功", event);
    };
    reqAdd.onerror = function (event) {
      console.log("数据添加失败", event);
    };
  }
};

在开发者工具中,点击 Refresh database,可以看到数据已被添加到Users对象仓库中

2. 增加多条数据

通过监听事务的oncomplete事件,判断是否操作完成

js 复制代码
function addDataToDB(dataArray, store, transaction) {
  dataArray.forEach((data) => {
    store.add(data);
  });

  return new Promise((resolve, reject) => {
    transaction.oncomplete = () => {
      resolve();
    };

    transaction.onerror = (event) => {
      reject(event.target.error);
    };
  });
}

// 使用示例
const dataToAdd = [
  { userId: 1, name: "John", age: 20 },
  { userId: 2, name: "Jane", age: 20 },
  { userId: 3, name: "Mike", age: 20 },
];

request.onsuccess = function (res) {
  const db = res.target.result;
  //如果存在Users表
  if (db.objectStoreNames.contains("Users")) {
    //开启事务
    const transaction = db.transaction("Users", "readwrite");
    //获取对象存储空间,进行数据操作
    const store = transaction.objectStore("Users");

    addDataToDB(dataToAdd, store, transaction)
      .then(() => {
        console.log("数据添加成功");
      })
      .catch((error) => {
        console.error("数据添加失败", error);
      });
  }
};

5. 读取数据 get

  1. 获取单条数据
js 复制代码
request.onsuccess = function (res) {
  const db = res.target.result;
  if (db.objectStoreNames.contains("Users")) {
    //开启事务,只允许读操作
    const transaction = db.transaction("Users", "readonly");
    //获取对象存储空间,进行数据操作
    const store = transaction.objectStore("Users");
    //传入主键值
    const reqGet = store.get(1);
    reqGet.onsuccess = function (event) {
      if (event.target.result) {
        console.log("数据获取成功", event.target.result);
      } else {
        console.log("未获取到数据");
      }
    };
    reqGet.onerror = function (event) {
      console.log("数据获取失败", event);
    };
  }
};

如果主键是由多个值组成,例如创建表时,keyPath是个数组

js 复制代码
db.createObjectStore("Users", { keyPath: \['userId', 'key'] });

那么取值时,也通过数组取值

js 复制代码
//userId与key是变量,根据实际情况传入具体的值 
const reqGet = store.get(\[userId, key]);
  1. 获取所有数据
js 复制代码
const reqGet = store.getAll();

6. 更新数据 put

使用 put 进行数据操作时,"无则增,有则改"。如果主键不存在,则新建数据。如果主键存在,则更新数据。除非明确数据只能新增,不能更新,其余情况下,put 可以代替add。

js 复制代码
request.onsuccess = function (res) {
  const db = res.target.result;
  if (db.objectStoreNames.contains("Users")) {
    //开启事务,允许读写操作
    const transaction = db.transaction("Users", "readwrite");
    //获取对象存储空间,进行数据操作
    const store = transaction.objectStore("Users");
    //put方法根据主键值更新数据
    const reqPut = store.put({ userId: 1, userName: "张三", age: 20 });
    reqPut.onsuccess = function (event) {
      console.log("数据更新成功", event);
    };
    reqPut.onerror = function (event) {
      console.log("数据更新失败", event);
    };
  }
};

7. 删除数据 delete

js 复制代码
request.onsuccess = function (res) {
  const db = res.target.result;
  if (db.objectStoreNames.contains("Users")) {
    //开启事务,允许读写操作
    const transaction = db.transaction("Users", "readwrite");
    //获取对象存储空间,进行数据操作
    const store = transaction.objectStore("Users");
    //delete方法根据主键值删除数据
    const reqDelete = store.delete(1);
    reqDelete.onsuccess = function (event) {
      console.log("数据删除成功", event);
    };
    reqDelete.onerror = function (event) {
      console.log("数据删除失败", event);
    };
  }
};

四、进阶

1. IDBKeyRange

IDBKeyRange:用于定义和限制在数据库查询中使用的键的范围,从而根据条件查询多条数据

1. 精确匹配

查找主键值为1的数据

js 复制代码
const range = IDBKeyRange.only("1");

2. 范围匹配

查找主键值为1到10的数据

如果第三个参数为true,则表示不包含最小键值1,如果第四参数为true,则表示不包含最大键值10,默认都为false

js 复制代码
const range = IDBKeyRange.bound(1, 10, false, false);

3. 上下界匹配

(1)下界匹配

第二个参数可选,为true则表示不包含最小主键1,false则包含,默认为false

js 复制代码
const range = IDBKeyRange.lowerBound(1, false);

(2)上界匹配

第二个参数可选,为true则表示不包含最大主键10,false则包含,默认为false

js 复制代码
const range = IDBKeyRange.upperBound(10, false);

当主键由一个字段组成,并且根据主键查询时,可使用 store.getAll+ IDBKeyRange 直接查询数据。

示例:获取所有主键值小于等于10的数据

js 复制代码
request.onsuccess = function (res) {
  const db = res.target.result;
  //如果存在Users表
  if (db.objectStoreNames.contains("Users")) {
    //开启事务
    const transaction = db.transaction("Users", "readonly");
    //获取对象存储空间,进行数据操作
    const store = transaction.objectStore("Users");
    //获取所有主键值小于等于10的数据
    const reqGet = store.getAll(IDBKeyRange.upperBound(10));
    reqGet.onsuccess = function (event) {
      if (event.target.result) {
        console.log("数据获取成功", event.target.result);
      } else {
        console.log("未获取到数据");
      }
    };
    reqGet.onerror = function (event) {
      console.log("数据获取失败", event);
    };
  }
};

2. 游标

作用:选定范围,并遍历和操作数据

js 复制代码
openCursor(range, direction);

range:表示范围,IDBKeyRange的值

direction:表示方向,next 升序(默认);nextunique 升序,索引值相等则只读第一条;prev 降序;prevunique 降序,索引值相等则只读第一条;

js 复制代码
request.onsuccess = function (res) {
  const db = res.target.result;
  //如果存在Users表
  if (db.objectStoreNames.contains("Users")) {
    //开启事务
    const transaction = db.transaction("Users", "readwrite");
    //获取对象存储空间,进行数据操作
    const store = transaction.objectStore("Users");
    //主键1到10
    const range = IDBKeyRange.bound(1, 10);
    const request = store.openCursor(range);
    request.onsuccess = function (event) {
      const cursor = event.target.result;
      if (cursor) {
        console.log(cursor.value); //拿到数据
        // cursor.updata(value) 更新数据
        // cursor.delete() 删除数据
        // cursor.advance(count) 向前跳过指定数量的记录,可用作分页
        cursor.continue();//继续读取下一条
      } else {
        console.log("没有更多result");
      }
    };
    request.onerror = function (event) {
      console.log("数据查找失败", event);
    };
  }
};

3. 索引

索引极大地扩展了数据库的查询能力,上述示例是基于主键查询。如果一个数据结构为{ userId: 1, userName: "张三", age: 20 },其中userId为主键,那么要查询所有age小于18的数据时,就需要借助索引。

js 复制代码
createIndex(name, keyPath, optionalParameters)

name:索引名称

keyPath:要索引的对象属性,可以是单个key值,也可以是key值组成的数组

optionalParameters:可选参数{unique, multiEntry}

unique:指定被索引的属性值是否可以重复,为true代表不能重复,为false时可以重复。默认false

multiEntry:当第二个参数keyPath为数组时,如果multiEntry是true,则会以数组中的每个元素建立一条索引。如果是false,将整个数组作为一个单一的复合键存入索引。默认为false。

1. 创建索引

在数据库版本升级时(在 onupgradeneeded 事件中)创建

js 复制代码
request.onupgradeneeded = function (res) {
  const db = res.target.result;
  //如果Users表不存在则创建,并插入数据
  if (!db.objectStoreNames.contains("Users")) {
    const store = db.createObjectStore("Users", {
      keyPath: "userId",
      autoIncrement: false,
    });
    //建立索引,索引名ageIndex,与原age属性建立索引,索引值可以重复
    store.createIndex("ageIndex", "age", { unique: false });
  }
};

2. 获取单个值 get

获取单个值的前提,创建索引时unique为true,否则查询到的结果一直为undefined

js 复制代码
//确保存储的age值唯一 
store.createIndex("ageIndex", "age", { unique: true });

查询值

js 复制代码
const transaction = db.transaction("Users", "readonly");

const store = transaction.objectStore("Users");

const index = store.index("ageIndex");

const request = index.get(20);

request.onsuccess = function (event) {
  let user = event.target.result;
  console.log("Found user:", user);
};

3. 获取所有值 getAll

不管值是否唯一,获取所有值

js 复制代码
const request = index.getAll(20);

4. 通过索引打开游标

可对范围内的数据进行读取或修改

js 复制代码
request.onsuccess = function (res) {
  const db = res.target.result;
  //如果存在Users表
  if (db.objectStoreNames.contains("Users")) {
    //开启事务
    const transaction = db.transaction("Users", "readonly");
    //获取对象存储空间,进行数据操作
    const store = transaction.objectStore("Users");
    //获取索引
    const index = store.index("ageIndex");
    //使用索引查询年龄最低为20的数据
    const request = index.openCursor(IDBKeyRange.lowerBound(20));
    request.onsuccess = function (event) {
      const cursor = event.target.result;
      if (cursor) {
        console.log(cursor.value); //拿到数据
        cursor.continue(); //继续读取下一条
      } else {
        console.log("没有更多result");
      }
    };
    request.onerror = function (event) {
      console.log("数据查找失败", event);
    };
  }
};
  1. 封装示例

为了更方便的使用,可以将IndexDB的操作封装成一个类

js 复制代码
/**
 * 通用 IndexedDB 单表 CRUD 封装类
 * 支持自定义对象结构
 */

export interface IndexedDBOptions {
  dbName: string;
  version?: number;
  storeName: string;
  keyPath: string;
}

export class IndexedDBManager<T extends Record<string, any>> {
  private db: IDBDatabase | null = null;
  private isInitialized = false;
  private options: IndexedDBOptions;

  constructor(options: IndexedDBOptions) {
    this.options = {
      version: 1,
      ...options,
    };
  }

  async init(): Promise<void> {
    if (this.isInitialized) return;
    const { dbName, version, storeName, keyPath } = this.options;
    return new Promise((resolve, reject) => {
      const request = indexedDB.open(dbName, version);
      request.onerror = () => {
        console.error("Failed to open IndexedDB:", request.error);
        reject(request.error);
      };
      request.onsuccess = () => {
        this.db = request.result;
        this.isInitialized = true;
        resolve();
      };
      request.onupgradeneeded = (event) => {
        const db = (event.target as IDBOpenDBRequest).result;
        if (!db.objectStoreNames.contains(storeName)) {
          db.createObjectStore(storeName, { keyPath });
        }
      };
    });
  }

  async get(key: IDBValidKey): Promise<T | undefined> {
    await this.init();
    return new Promise((resolve, reject) => {
      if (!this.db) {
        reject(new Error("Database not initialized"));
        return;
      }
      const transaction = this.db.transaction([this.options.storeName], "readonly");
      const store = transaction.objectStore(this.options.storeName);
      const request = store.get(key);
      request.onerror = () => {
        reject(request.error);
      };
      request.onsuccess = () => {
        resolve(request.result as T | undefined);
      };
    });
  }

  async getAll(): Promise<T[]> {
    await this.init();
    return new Promise((resolve, reject) => {
      if (!this.db) {
        reject(new Error("Database not initialized"));
        return;
      }
      const transaction = this.db.transaction([this.options.storeName], "readonly");
      const store = transaction.objectStore(this.options.storeName);
      const request = store.getAll();
      request.onerror = () => {
        reject(request.error);
      };
      request.onsuccess = () => {
        resolve(request.result as T[]);
      };
    });
  }

  async put(data: T): Promise<void> {
    await this.init();
    return new Promise((resolve, reject) => {
      if (!this.db) {
        reject(new Error("Database not initialized"));
        return;
      }
      const transaction = this.db.transaction([this.options.storeName], "readwrite");
      const store = transaction.objectStore(this.options.storeName);
      const request = store.put(data);
      request.onerror = () => {
        reject(request.error);
      };
      request.onsuccess = () => {
        resolve();
      };
    });
  }

  async delete(key: IDBValidKey): Promise<void> {
    await this.init();
    return new Promise((resolve, reject) => {
      if (!this.db) {
        reject(new Error("Database not initialized"));
        return;
      }
      const transaction = this.db.transaction([this.options.storeName], "readwrite");
      const store = transaction.objectStore(this.options.storeName);
      const request = store.delete(key);
      request.onerror = () => {
        reject(request.error);
      };
      request.onsuccess = () => {
        resolve();
      };
    });
  }

  async clear(): Promise<void> {
    await this.init();
    return new Promise((resolve, reject) => {
      if (!this.db) {
        reject(new Error("Database not initialized"));
        return;
      }
      const transaction = this.db.transaction([this.options.storeName], "readwrite");
      const store = transaction.objectStore(this.options.storeName);
      const request = store.clear();
      request.onerror = () => {
        reject(request.error);
      };
      request.onsuccess = () => {
        resolve();
      };
    });
  }
}

五、结尾

参考文档:juejin.cn/post/723925...

相关推荐
芜青43 分钟前
HTML+CSS:浮动详解
前端·css·html
SchuylerEX1 小时前
第六章 JavaScript 互操(2).NET调用JS
前端·c#·.net·blazor·ui框架
东风西巷2 小时前
Rubick:基于Electron的开源桌面效率工具箱
前端·javascript·electron·软件需求
探码科技2 小时前
AI知识管理软件推荐:九大解决方案与企业应用
前端·ruby
编程小黑马2 小时前
解决flutter 在控制器如controller 无法直接访问私有类方法的问题
前端
Miracle_G3 小时前
每日一个知识点:JavaScript 箭头函数与普通函数比较
javascript
unfetteredman3 小时前
Error: /lib/x86_64-linux-gnu/libc.so.6: version `GLIBC_2.32' not found
前端·javascript·vite
云存储小精灵3 小时前
Dify x 腾讯云 COS MCP:自然语言解锁智能数据处理,零代码构建 AI 新世界
前端·开源
山间板栗3 小时前
微信小程序环境变量设置方案
前端
电商API大数据接口开发Cris3 小时前
Java Spring Boot 集成淘宝 SDK:实现稳定可靠的商品信息查询服务
前端·数据挖掘·api