IndexedDB 从入门到实战:前端本地大容量存储解决方案。

🔥 IndexedDB 从入门到实战:前端本地大容量存储解决方案

🚀 一、IndexedDB 核心介绍

1. 什么是 IndexedDB?

IndexedDB 是浏览器提供的本地非关系型(NoSQL)数据库,属于前端核心存储方案之一,专为解决传统前端存储(Cookie、LocalStorage)的容量限制和功能短板设计,支持大容量、结构化数据的本地持久化存储,是前端离线应用、数据缓存、本地业务数据存储的核心技术。

2. 核心特性

  • 大容量存储:无统一官方容量上限,由浏览器根据设备存储空间动态分配(通常数GB级别),远优于 LocalStorage(5MB 左右)和 Cookie(4KB 左右);
  • 非关系型(NoSQL) :以键值对为核心存储结构,支持存储对象、数组等结构化数据(无需手动序列化,浏览器自动处理);
  • 异步操作:所有核心操作(增删改查、库/表创建)均为异步,不会阻塞浏览器主线程,避免页面卡顿,这是与 LocalStorage(同步)的关键区别;
  • 事务支持:所有数据操作必须在事务中执行,保证操作的原子性(要么全部成功,要么全部失败),避免数据不一致;
  • 索引支持:可对对象仓库的指定字段创建索引,大幅提升数据查询效率,解决海量数据下的查询性能问题;
  • 持久化存储:数据默认持久化,除非手动删除或清除浏览器缓存,关闭页面/浏览器后数据不会丢失;
  • 跨域限制:遵循同源策略,仅能访问当前域名下创建的 IndexedDB 数据库,保证数据安全性。

3. 与传统前端存储的对比

特性 IndexedDB LocalStorage Cookie
存储容量 大(数GB级别) 小(约5MB) 极小(约4KB)
操作方式 异步 同步 同步
数据类型 支持结构化数据(对象/数组) 仅字符串 仅字符串
事务支持 支持 不支持 不支持
索引支持 支持 不支持 不支持
持久化 永久(手动清除才丢失) 永久 可设置过期时间
主线程阻塞 不会 会(大数据操作卡顿)
服务端交互 不主动携带 不主动携带 每次请求自动携带

🎯 二、IndexedDB 核心概念

IndexedDB 的操作模型围绕数据库、对象仓库、索引、事务展开,核心概念与传统数据库对应但更轻量化,先理解概念再上手操作,能大幅降低学习成本:

  1. 数据库(Database):前端域名下的独立数据容器,一个域名可创建多个数据库,每个数据库有唯一名称,是所有数据操作的根节点;
  2. 对象仓库(Object Store) :等价于传统数据库的,是存储结构化数据的核心载体,一个数据库可包含多个对象仓库,数据以键值对形式存储在其中;
  3. 主键(Primary Key) :对象仓库的唯一标识字段,类似数据库的主键,保证每条数据的唯一性,支持自增主键 (由浏览器自动生成唯一ID)和显式主键(指定自定义字段作为主键);
  4. 索引(Index):基于对象仓库的非主键字段创建的查询优化结构,可对单个字段或多个字段创建索引,支持快速根据索引字段查询数据,避免全库遍历;
  5. 事务(Transaction) :IndexedDB 所有数据操作的唯一执行载体 ,任何增删改查、对象仓库操作都必须在事务中进行。事务分为三种模式:
    • readonly:只读模式(查询数据),性能最高,支持多个只读事务同时执行;
    • readwrite:读写模式(增删改数据),同一时间仅能有一个该模式事务执行;
    • versionchange:版本变更模式(创建/删除对象仓库、创建索引),用于数据库版本升级,是唯一能修改数据库结构的模式。
  6. 数据库版本 :IndexedDB 有严格的版本管理机制,版本号为正整数 (仅支持升级,不支持降级),数据库结构的修改(如增删对象仓库、创建索引)仅能在版本升级时通过versionchange事务完成。

📁 三、IndexedDB 完整使用流程(原生 API)

IndexedDB 原生 API 基于事件和回调实现(也可封装为 Promise/async-await 简化使用),以下以学生信息管理为例,讲解从数据库创建、数据操作到索引查询的完整流程,所有代码可直接在浏览器控制台/前端页面中运行。

前置:封装 Promise 版 API(简化异步操作)

原生 IndexedDB 回调嵌套较多,先封装为 Promise 版本,后续使用async-await更简洁,核心方法全覆盖:

javascript 复制代码
/**
 * IndexedDB Promise 封装工具类
 * @param {string} dbName - 数据库名称
 * @param {number} version - 数据库版本(正整数)
 * @param {Function} upgradeCb - 版本升级回调(创建/删除对象仓库、索引)
 */
class IndexedDBUtil {
  constructor(dbName, version, upgradeCb) {
    this.dbName = dbName;
    this.version = version;
    this.upgradeCb = upgradeCb;
    this.db = null; // 数据库实例
  }

  // 打开数据库(核心方法)
  open() {
    return new Promise((resolve, reject) => {
      // 浏览器兼容性判断
      if (!window.indexedDB) {
        reject(new Error('你的浏览器不支持 IndexedDB,请升级至现代浏览器'));
        return;
      }
      // 打开数据库,版本号不匹配时触发upgradeneeded事件
      const request = indexedDB.open(this.dbName, this.version);

      // 版本升级/首次创建时触发(仅在版本号大于当前版本时执行)
      request.onupgradeneeded = (e) => {
        this.db = e.target.result;
        this.upgradeCb && this.upgradeCb(this.db); // 执行自定义升级逻辑
        console.log('数据库版本升级,当前版本:', e.target.transaction.db.version);
      };

      // 数据库打开成功
      request.onsuccess = (e) => {
        this.db = e.target.result;
        resolve(this.db);
        console.log('数据库打开成功');
      };

      // 数据库打开失败
      request.onerror = (e) => {
        reject(new Error('数据库打开失败:' + e.target.error.message));
      };

      // 数据库被意外关闭
      request.onblocked = () => {
        reject(new Error('数据库操作被阻塞,请关闭其他标签页后重试'));
      };
    });
  }

  // 开启事务
  /**
   * @param {Array<string>} storeNames - 涉及的对象仓库名称数组
   * @param {string} mode - 事务模式:readonly/readwrite/versionchange
   * @returns {IDBTransaction} 事务实例
   */
  transaction(storeNames, mode = 'readonly') {
    if (!this.db) throw new Error('数据库未打开,请先调用open方法');
    return this.db.transaction(storeNames, mode);
  }

  // 新增/修改数据(主键存在则修改,不存在则新增)
  /**
   * @param {string} storeName - 对象仓库名称
   * @param {Object} data - 要存储的数据
   */
  put(storeName, data) {
    return new Promise((resolve, reject) => {
      try {
        const tx = this.transaction([storeName], 'readwrite');
        const store = tx.objectStore(storeName);
        const request = store.put(data);

        request.onsuccess = () => resolve('操作成功,主键:' + request.result);
        request.onerror = (e) => reject(new Error('增改失败:' + e.target.error.message));
      } catch (err) {
        reject(err);
      }
    });
  }

  // 根据主键查询单条数据
  get(storeName, key) {
    return new Promise((resolve, reject) => {
      try {
        const tx = this.transaction([storeName]);
        const store = tx.objectStore(storeName);
        const request = store.get(key);

        request.onsuccess = () => resolve(request.result);
        request.onerror = (e) => reject(new Error('查询失败:' + e.target.error.message));
      } catch (err) {
        reject(err);
      }
    });
  }

  // 查询所有数据
  getAll(storeName) {
    return new Promise((resolve, reject) => {
      try {
        const tx = this.transaction([storeName]);
        const store = tx.objectStore(storeName);
        const request = store.getAll();

        request.onsuccess = () => resolve(request.result);
        request.onerror = (e) => reject(new Error('查询所有失败:' + e.target.error.message));
      } catch (err) {
        reject(err);
      }
    });
  }

  // 根据主键删除数据
  delete(storeName, key) {
    return new Promise((resolve, reject) => {
      try {
        const tx = this.transaction([storeName], 'readwrite');
        const store = tx.objectStore(storeName);
        const request = store.delete(key);

        request.onsuccess = () => resolve('删除成功');
        request.onerror = (e) => reject(new Error('删除失败:' + e.target.error.message));
      } catch (err) {
        reject(err);
      }
    });
  }

  // 清空对象仓库所有数据
  clear(storeName) {
    return new Promise((resolve, reject) => {
      try {
        const tx = this.transaction([storeName], 'readwrite');
        const store = tx.objectStore(storeName);
        const request = store.clear();

        request.onsuccess = () => resolve('清空成功');
        request.onerror = (e) => reject(new Error('清空失败:' + e.target.error.message));
      } catch (err) {
        reject(err);
      }
    });
  }

  // 根据索引查询数据
  /**
   * @param {string} storeName - 对象仓库名称
   * @param {string} indexName - 索引名称
   * @param {*} value - 索引字段匹配值
   */
  getByIndex(storeName, indexName, value) {
    return new Promise((resolve, reject) => {
      try {
        const tx = this.transaction([storeName]);
        const store = tx.objectStore(storeName);
        const index = store.index(indexName); // 获取索引
        const request = index.get(value); // 根据索引值查询

        request.onsuccess = () => resolve(request.result);
        request.onerror = (e) => reject(new Error('索引查询失败:' + e.target.error.message));
      } catch (err) {
        reject(err);
      }
    });
  }

  // 关闭数据库
  close() {
    if (this.db) {
      this.db.close();
      this.db = null;
      console.log('数据库已关闭');
    }
  }

  // 删除整个数据库
  deleteDB() {
    return new Promise((resolve, reject) => {
      const request = indexedDB.deleteDatabase(this.dbName);
      request.onsuccess = () => {
        this.db = null;
        resolve('数据库删除成功');
      };
      request.onerror = (e) => reject(new Error('数据库删除失败:' + e.target.error.message));
    });
  }
}

步骤1:初始化数据库,创建对象仓库和索引

创建名为studentDB的数据库,版本号1,创建student对象仓库(存储学生信息),并为phone(手机号)字段创建唯一索引 (保证手机号不重复)、为className(班级)字段创建普通索引(支持按班级查询)。

核心规则

  • 自增主键:通过{ autoIncrement: true }设置,浏览器自动生成唯一数字ID(主键名默认为id);
  • 唯一索引:{ unique: true },保证索引字段值不重复;
  • 普通索引:{ unique: false },支持字段值重复。
javascript 复制代码
// 1. 初始化数据库工具类,定义版本升级逻辑(创建对象仓库和索引)
const dbUtil = new IndexedDBUtil(
  'studentDB', // 数据库名称
  1, // 版本号
  (db) => { // 版本升级回调(首次创建/版本升级时执行)
    // 判断对象仓库是否已存在,避免重复创建
    if (!db.objectStoreNames.contains('student')) {
      // 创建student对象仓库,设置自增主键
      const studentStore = db.createObjectStore('student', {
        keyPath: 'id', // 主键字段
        autoIncrement: true // 自增主键
      });
      // 为phone字段创建唯一索引,索引名:idx_phone
      studentStore.createIndex('idx_phone', 'phone', { unique: true });
      // 为className字段创建普通索引,索引名:idx_className
      studentStore.createIndex('idx_className', 'className', { unique: false });
      console.log('对象仓库和索引创建成功');
    }
  }
);

// 2. 打开数据库(必须执行,否则无法进行后续操作)
await dbUtil.open();

步骤2:核心数据操作(增、删、改、查)

所有操作基于上述封装的工具类,使用async-await简化异步流程,操作对象为student对象仓库,数据结构为:{ id: 自增ID, name: 姓名, age: 年龄, phone: 手机号, className: 班级 }

2.1 新增数据(put 方法)

主键(id)由浏览器自动生成,直接传入其他字段即可:

javascript 复制代码
// 新增单条学生数据
const addRes1 = await dbUtil.put('student', {
  name: '张三',
  age: 18,
  phone: '13800138000',
  className: '高一(1)班'
});
console.log(addRes1); // 输出:操作成功,主键:1

// 批量新增(循环调用put即可)
const students = [
  { name: '李四', age: 17, phone: '13900139000', className: '高一(1)班' },
  { name: '王五', age: 18, phone: '13700137000', className: '高一(2)班' }
];
for (const s of students) {
  const res = await dbUtil.put('student', s);
  console.log(res); // 依次输出:操作成功,主键:2、操作成功,主键:3
}
2.2 根据主键查询单条数据(get 方法)

传入对象仓库名称和主键值(id),返回匹配的整条数据:

javascript 复制代码
const student = await dbUtil.get('student', 1);
console.log('主键为1的学生:', student);
// 输出:{ id: 1, name: '张三', age: 18, phone: '13800138000', className: '高一(1)班' }
2.3 查询所有数据(getAll 方法)

返回对象仓库中的所有数据,返回结果为数组:

javascript 复制代码
const allStudents = await dbUtil.getAll('student');
console.log('所有学生信息:', allStudents);
// 输出:[{id:1,...}, {id:2,...}, {id:3,...}]
2.4 修改数据(put 方法)

IndexedDB 中修改和新增共用 put 方法 ,只需传入包含主键的完整数据,浏览器会根据主键匹配并覆盖原有数据:

javascript 复制代码
// 修改主键为1的学生信息(修改年龄和班级)
const updateRes = await dbUtil.put('student', {
  id: 1, // 必须传入主键,否则会新增一条数据
  name: '张三', // 未修改的字段也需传入,否则会被覆盖为undefined
  age: 19,
  phone: '13800138000',
  className: '高二(1)班'
});
console.log(updateRes); // 输出:操作成功,主键:1

// 验证修改结果
const updatedStudent = await dbUtil.get('student', 1);
console.log('修改后的学生信息:', updatedStudent);
// 输出:{ id:1, name:'张三', age:19, phone:'13800138000', className:'高二(1)班' }
2.5 根据主键删除数据(delete 方法)

传入对象仓库名称和主键值,删除对应数据:

javascript 复制代码
const delRes = await dbUtil.delete('student', 3);
console.log(delRes); // 输出:删除成功

// 验证删除结果
const allStudentsAfterDel = await dbUtil.getAll('student');
console.log('删除后的所有学生:', allStudentsAfterDel); // 仅保留id=1、2的学生
2.6 清空对象仓库(clear 方法)

删除对象仓库中的所有数据,但保留对象仓库和索引结构(与删除数据库不同):

javascript 复制代码
// 注意:执行后所有学生数据会被清空,谨慎使用
const clearRes = await dbUtil.clear('student');
console.log(clearRes); // 输出:清空成功

步骤3:索引查询(核心优化手段)

如果直接根据非主键字段(如phoneclassName)查询,原生需要遍历所有数据,效率极低;通过索引查询可直接定位数据,海量数据下性能提升显著。

以下使用步骤1中创建的两个索引,演示索引查询的使用:

javascript 复制代码
// 1. 根据唯一索引idx_phone查询(手机号查询)
const studentByPhone = await dbUtil.getByIndex('student', 'idx_phone', '13900139000');
console.log('手机号为13900139000的学生:', studentByPhone);
// 输出:{ id:2, name:'李四', age:17, phone:'13900139000', className:'高一(1)班' }

// 2. 根据普通索引idx_className查询(班级查询)
// 注:封装的getByIndex返回单条匹配数据,若需多条可扩展封装index.getAll(value)
const studentByClass = await dbUtil.getByIndex('student', 'idx_className', '高一(1)班');
console.log('高一(1)班的学生:', studentByClass);

步骤4:数据库高级操作(关闭、删除、版本升级)

4.1 关闭数据库

使用完成后关闭数据库,释放资源:

javascript 复制代码
dbUtil.close(); // 输出:数据库已关闭
4.2 删除整个数据库

删除数据库后,所有对象仓库、索引、数据都会被清除,谨慎使用:

javascript 复制代码
const delDBRes = await dbUtil.deleteDB();
console.log(delDBRes); // 输出:数据库删除成功
4.3 数据库版本升级(修改结构)

若需新增对象仓库、创建/删除索引,需升级版本号 (如从1升级为2),并在升级回调中编写结构修改逻辑,示例:新增teacher对象仓库,为student仓库新增email索引:

javascript 复制代码
// 版本升级为2,新增teacher仓库和email索引
const dbUtilV2 = new IndexedDBUtil(
  'studentDB', // 同一数据库名称
  2, // 版本号必须大于之前的1
  (db) => { // 新版本升级回调
    const tx = db.transaction(db.objectStoreNames, 'versionchange');
    // 1. 新增teacher对象仓库,自增主键
    if (!db.objectStoreNames.contains('teacher')) {
      const teacherStore = db.createObjectStore('teacher', {
        keyPath: 'id',
        autoIncrement: true
      });
      console.log('新增teacher对象仓库成功');
    }
    // 2. 为student仓库新增email普通索引
    const studentStore = tx.objectStore('student');
    if (!studentStore.indexNames.contains('idx_email')) {
      studentStore.createIndex('idx_email', 'email', { unique: false });
      console.log('新增idx_email索引成功');
    }
  }
);

// 打开升级后的数据库
await dbUtilV2.open();

版本升级核心规则

  • 版本号仅能递增(1→2→3,不能2→1);
  • 若数据库已存在且版本号与当前一致,不会触发升级回调;
  • 升级回调中可通过db.objectStoreNamesstore.indexNames判断对象仓库/索引是否存在,避免重复创建。

🚨 四、IndexedDB 实用开发建议

1. 兼容性处理

现代浏览器均支持 IndexedDB,但仍需对低版本浏览器做兼容判断(已在封装工具类中实现),兼容范围:

  • Chrome ≥ 23、Firefox ≥ 10、Edge ≥ 12、Safari ≥ 7.1;
  • 移动端:微信小程序/公众号、App 内置浏览器(基于 Chromium)均支持,IE 完全不支持。

2. 性能优化

  • 尽量使用只读事务readonly)做查询操作,避免占用读写事务资源;
  • 批量操作时,复用同一个读写事务,而非多次创建事务(减少事务开销);
  • 对高频查询的非主键字段创建索引,避免全库遍历;
  • 避免在主线程中执行超大批量数据操作,可拆分为多个小批次,防止页面卡顿。

3. 数据安全

  • IndexedDB 遵循同源策略,跨域无法访问,无需担心数据泄露;
  • 敏感数据(如用户密码、令牌)不建议存储 在 IndexedDB 中,可使用浏览器sessionStorage(会话级存储)或加密后存储;
  • 清除浏览器缓存(如清除Cookie、本地存储)时,IndexedDB 数据会被一并删除,若需持久化核心数据,可结合后端云存储。

4. 错误处理

  • 所有 IndexedDB 操作都需增加异常捕获try-catch),处理数据库打开失败、事务阻塞、主键/索引重复等错误;
  • 处理versionchange事务的阻塞问题(当其他标签页打开同一数据库时,版本升级会被阻塞),可提示用户关闭其他标签页后重试。

5. 替代方案(第三方库)

原生 IndexedDB API 略显繁琐,实际开发中可使用成熟的第三方封装库,简化开发:

  • localForage:最流行的 IndexedDB 封装库,API 与 LocalStorage 一致,自动降级(不支持 IndexedDB 则使用 LocalStorage);
  • Dexie.js:专为 IndexedDB 设计的轻量级库,支持链式调用、高级查询、事务管理,体积小(≈20KB);
  • PouchDB:支持离线同步的 IndexedDB 库,可与后端 CouchDB 同步数据,适合离线应用。

五、适用场景

IndexedDB 适用于前端需要本地存储大容量、结构化数据的场景,典型应用:

  1. 离线应用:如离线文档、离线地图、离线电商App,在无网络时存储业务数据,联网后同步至后端;
  2. 数据缓存:缓存高频访问的接口数据(如商品列表、用户信息),减少网络请求,提升页面加载速度;
  3. 本地业务数据存储:如前端表单草稿、本地编辑的文档、购物车数据(无需实时同步至后端的场景);
  4. 海量本地数据管理:如前端日志收集、本地数据报表,需要存储数万条甚至更多结构化数据的场景。

不适用场景

  • 存储少量简单数据(如开关状态、用户昵称),直接使用 LocalStorage 更简洁;
  • 需实时同步至后端的敏感数据(如支付信息、用户身份信息);
  • 跨域数据共享(IndexedDB 不支持跨域)。

📌 六、总结

  1. IndexedDB 是浏览器端大容量、异步、支持事务和索引的非关系型本地数据库,是解决传统前端存储容量限制的核心方案;
  2. 核心概念:数据库、对象仓库(表)、主键、索引、事务(三种模式),数据库结构修改仅能在版本升级时完成;
  3. 原生 API 基于事件回调,可封装为 Promise/async-await 简化使用,实际开发中也可使用 localForage、Dexie.js 等第三方库;
  4. 所有数据操作必须在事务中执行,查询用只读事务,增删改用读写事务,结构修改用版本变更事务;
  5. 优势:大容量、异步无阻塞、支持索引和事务、持久化存储;短板:API 繁琐、版本管理严格、IE 不兼容;
  6. 适用场景:离线应用、数据缓存、本地大容量结构化数据存储,是前端工程化、离线化的重要技术支撑。

IndexedDB 作为前端本地存储的"终极方案",弥补了 LocalStorage 和 Cookie 的不足,掌握其核心使用方法,能大幅提升前端应用的离线能力和用户体验,是现代前端开发者的必备技能之一。

相关推荐
jiayong232 小时前
Vue2 与 Vue3 常见面试题精选 - 综合宝典
前端·vue.js·面试
We་ct2 小时前
LeetCode 383. 赎金信:解题思路+代码解析+优化实战
前端·算法·leetcode·typescript
东东5162 小时前
OA自动化居家办公管理系统 ssm+vue
java·前端·vue.js·后端·毕业设计·毕设
周某人姓周2 小时前
DOM型XSS案例
前端·安全·web安全·网络安全·xss
程序员鱼皮2 小时前
前特斯拉 AI 总监:AI 编程最大的谎言,是 “提效”
前端·后端·ai·程序员·开发
pusheng20252 小时前
普晟传感2026年新春年会总结与分析
前端·javascript·html
谢尔登2 小时前
React19事件调度的设计思路
前端·javascript·react.js
Emma_Maria2 小时前
本地项目html和jquery,访问地址报跨域解决
前端·html·jquery
奋斗吧程序媛3 小时前
常用且好用的命令
前端·编辑器