LOGO OA教程 ERP教程 模切知识交流 PMS教程 CRM教程 开发文档 其他文档  
 
网站管理员

localStorage 不够用?试试 IndexedDB !

zhenglin
2026年2月6日 16:32 本文热度 62

一、前言

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


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

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

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


二、特点

    • 储存空间大:比 LocalStorage 大得多

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

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

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

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

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



    三、基础使用

    1. 新建数据库(indexDB)

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

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

    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事件只会在版本号更新的时候触发,这是因为不允许数据库中的数据仓库在同一版本中发生变化

    //版本号变为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的读/写,都要通过事务操作。

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

    代码高亮:

    // 开启事务

    const transaction = db.transaction("Users", "readwrite");

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

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


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

    // ❌ 错误写法:事务会在 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事件中,进行数据操作

    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事件,判断是否操作完成


    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. 获取单条数据
    代码高亮:

    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是个数组

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

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

    //userId与key是变量,根据实际情况传入具体的值 

    const reqGet = store.get(\[userId, key]);

    1. 获取所有数据

    const reqGet = store.getAll();


    6. 更新数据 put

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

    代码高亮:

    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

    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的数据

    const range = IDBKeyRange.only("1");

    2. 范围匹配

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

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

    const range = IDBKeyRange.bound(1, 10, false, false);


    3. 上下界匹配

    (1)下界匹配

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

    代码高亮:

    const range = IDBKeyRange.lowerBound(1, false);


     2)上界匹配

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

    const range = IDBKeyRange.upperBound(10, false);

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

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

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

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

    openCursor(range, direction);

    range:表示范围,IDBKeyRange的值

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

    代码高亮:

    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的数据时,就需要借助索引。

    createIndex(name, keyPath, optionalParameters)

    name:索引名称

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

    optionalParameters:可选参数{unique, multiEntry}

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

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



    1. 创建索引

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

    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

    //确保存储的age值唯一 

    store.createIndex("ageIndex", "age", { unique: true });

    查询值

    代码高亮:

    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

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

    const request = index.getAll(20);

    4. 通过索引打开游标

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

    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的操作封装成一个类

    /**

     * 通用 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();

          };

        });

      }

    }

    参考文章:原文链接



    该文章在 2026/2/6 16:32:10 编辑过
    相关文章
    正在查询...
    点晴ERP是一款针对中小制造业的专业生产管理软件系统,系统成熟度和易用性得到了国内大量中小企业的青睐。
    点晴PMS码头管理系统主要针对港口码头集装箱与散货日常运作、调度、堆场、车队、财务费用、相关报表等业务管理,结合码头的业务特点,围绕调度、堆场作业而开发的。集技术的先进性、管理的有效性于一体,是物流码头及其他港口类企业的高效ERP管理信息系统。
    点晴WMS仓储管理系统提供了货物产品管理,销售管理,采购管理,仓储管理,仓库管理,保质期管理,货位管理,库位管理,生产管理,WMS管理系统,标签打印,条形码,二维码管理,批号管理软件。
    点晴免费OA是一款软件和通用服务都免费,不限功能、不限时间、不限用户的免费OA协同办公管理系统。
    Copyright 2010-2026 ClickSun All Rights Reserved