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...

相关推荐
崔庆才丨静觅4 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60615 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了5 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅5 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅5 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅6 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment6 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅6 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊6 小时前
jwt介绍
前端
爱敲代码的小鱼6 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax