JS `IndexedDB`:浏览器端高性能结构化数据存储与离线能力

嘿,大家好,欢迎来到今天的“IndexedDB:浏览器端的瑞士军刀”讲座。今天咱们不聊虚的,直接上手,把IndexedDB这玩意儿扒个底朝天,让它成为你浏览器端数据存储的得力助手。

一、IndexedDB:它到底是个啥?

首先,咱们得搞清楚IndexedDB到底是干啥的。简单来说,它就是一个运行在浏览器端的NoSQL数据库。它可以让你在用户的浏览器里存储大量结构化数据,而且性能还相当不错。听起来是不是有点像localStorage?别急,它们之间可是有本质区别的。

特性 localStorage IndexedDB
容量 5-10MB 通常更大,取决于浏览器和用户设置
数据类型 字符串 支持更多数据类型,包括二进制
性能 简单读写,性能一般 事务性操作,性能更好
查询 只能通过键值对查询 支持索引,可以进行复杂查询
使用场景 存储少量用户配置信息等 存储大量结构化数据,离线应用等

看到了吧?localStorage只能存点小东西,IndexedDB才是正儿八经的数据库。

二、IndexedDB核心概念:先搞懂这些,再动手!

在开始写代码之前,咱们先了解一下IndexedDB的一些核心概念,就像盖房子之前要先看懂图纸一样:

  • 数据库 (Database):就是存储数据的地方,你可以理解为一个大仓库。
  • 对象仓库 (Object Store):数据库里的一张表,用来存储特定类型的数据。
  • 索引 (Index):为了加速查询,就像给书加目录一样。
  • 事务 (Transaction):一系列数据库操作,要么全部成功,要么全部失败,保证数据一致性。
  • 游标 (Cursor):用来遍历对象仓库中的数据,就像一个指向数据的指针。
  • 键 (Key):对象仓库中每条数据的唯一标识,就像身份证号码。
  • 键路径 (Key Path):指定对象中哪个属性作为键,比如'id'

三、IndexedDB实战:代码说话!

好了,概念讲完了,现在咱们撸起袖子,开始写代码。

1. 打开数据库:建立连接!

首先,我们需要打开一个数据库。IndexedDB是异步操作,所以我们需要使用Promise或者回调函数来处理结果。

const dbName = 'myDatabase';
const dbVersion = 1;

function openDatabase() {
  return new Promise((resolve, reject) => {
    const request = indexedDB.open(dbName, dbVersion);

    request.onerror = (event) => {
      console.error('打开数据库失败:', event);
      reject(event);
    };

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

    request.onupgradeneeded = (event) => {
      // 数据库版本更新时触发,用于创建或修改对象仓库
      const db = event.target.result;

      // 创建对象仓库
      if (!db.objectStoreNames.contains('users')) {
        const objectStore = db.createObjectStore('users', { keyPath: 'id', autoIncrement: true });

        // 创建索引
        objectStore.createIndex('name', 'name', { unique: false });
        objectStore.createIndex('email', 'email', { unique: true });

        console.log('对象仓库创建成功');
      }
    };
  });
}

openDatabase()
  .then((db) => {
    // 数据库操作
    console.log('数据库已准备好');
  })
  .catch((error) => {
    console.error('数据库初始化失败:', error);
  });

这段代码做了这些事情:

  • indexedDB.open(dbName, dbVersion):尝试打开名为myDatabase的数据库,版本号为1。
  • request.onerror:如果打开失败,会触发onerror事件。
  • request.onsuccess:如果打开成功,会触发onsuccess事件,event.target.result就是数据库对象。
  • request.onupgradeneeded:如果数据库版本号比已存在的版本号高,会触发onupgradeneeded事件。这个事件很重要,你可以在这里创建或修改对象仓库和索引。
  • db.createObjectStore('users', { keyPath: 'id', autoIncrement: true }):创建一个名为users的对象仓库,keyPath: 'id'表示使用id属性作为键,autoIncrement: true表示id会自动递增。
  • objectStore.createIndex('name', 'name', { unique: false }):创建一个名为name的索引,基于name属性,unique: false表示允许重复的name
  • objectStore.createIndex('email', 'email', { unique: true }):创建一个名为email的索引,基于email属性,unique: true表示不允许重复的email

2. 添加数据:往仓库里塞东西!

现在咱们有了数据库和对象仓库,可以往里面添加数据了。

function addUser(db, user) {
  return new Promise((resolve, reject) => {
    const transaction = db.transaction(['users'], 'readwrite');
    const objectStore = transaction.objectStore('users');

    const request = objectStore.add(user);

    request.onsuccess = (event) => {
      console.log('用户添加成功,id:', event.target.result);
      resolve(event.target.result);
    };

    request.onerror = (event) => {
      console.error('用户添加失败:', event);
      reject(event);
    };

    transaction.oncomplete = () => {
      console.log('事务完成');
    };

    transaction.onerror = (event) => {
      console.error('事务失败:', event);
    };
  });
}

openDatabase()
  .then((db) => {
    const newUser = {
      name: '张三',
      email: '[email protected]',
      age: 30,
    };

    return addUser(db, newUser);
  })
  .then((userId) => {
    console.log('用户添加成功,用户id:', userId);
  })
  .catch((error) => {
    console.error('添加用户失败:', error);
  });

这段代码做了这些事情:

  • db.transaction(['users'], 'readwrite'):创建一个事务,指定要操作的对象仓库是users,模式是readwrite(读写)。
  • transaction.objectStore('users'):获取users对象仓库。
  • objectStore.add(user):往对象仓库里添加数据。
  • request.onsuccess:如果添加成功,会触发onsuccess事件,event.target.result就是新添加数据的id
  • request.onerror:如果添加失败,会触发onerror事件。
  • transaction.oncomplete:事务完成时触发。
  • transaction.onerror:事务失败时触发。

3. 获取数据:把东西拿出来看看!

有了数据,当然要能取出来。

function getUser(db, userId) {
  return new Promise((resolve, reject) => {
    const transaction = db.transaction(['users'], 'readonly');
    const objectStore = transaction.objectStore('users');

    const request = objectStore.get(userId);

    request.onsuccess = (event) => {
      const user = event.target.result;
      if (user) {
        console.log('获取用户成功:', user);
        resolve(user);
      } else {
        console.log('未找到用户');
        resolve(null);
      }
    };

    request.onerror = (event) => {
      console.error('获取用户失败:', event);
      reject(event);
    };
  });
}

openDatabase()
  .then((db) => {
    const userId = 1; // 假设要获取id为1的用户

    return getUser(db, userId);
  })
  .then((user) => {
    if (user) {
      console.log('用户信息:', user);
    } else {
      console.log('未找到用户');
    }
  })
  .catch((error) => {
    console.error('获取用户失败:', error);
  });

这段代码做了这些事情:

  • db.transaction(['users'], 'readonly'):创建一个事务,指定要操作的对象仓库是users,模式是readonly(只读)。
  • objectStore.get(userId):根据id获取数据。
  • request.onsuccess:如果获取成功,会触发onsuccess事件,event.target.result就是获取到的数据。

4. 更新数据:修改仓库里的东西!

数据错了怎么办?当然要能更新。

function updateUser(db, user) {
  return new Promise((resolve, reject) => {
    const transaction = db.transaction(['users'], 'readwrite');
    const objectStore = transaction.objectStore('users');

    const request = objectStore.put(user);

    request.onsuccess = (event) => {
      console.log('用户更新成功');
      resolve();
    };

    request.onerror = (event) => {
      console.error('用户更新失败:', event);
      reject(event);
    };
  });
}

openDatabase()
  .then((db) => {
    const updatedUser = {
      id: 1, // 假设要更新id为1的用户
      name: '李四',
      email: '[email protected]',
      age: 35,
    };

    return updateUser(db, updatedUser);
  })
  .then(() => {
    console.log('用户更新成功');
  })
  .catch((error) => {
    console.error('更新用户失败:', error);
  });

这段代码和添加数据很像,只不过用的是objectStore.put(user),而不是objectStore.add(user)put方法会根据userid来更新数据,如果id不存在,则会添加一条新的数据。

5. 删除数据:把仓库里的东西扔掉!

有些数据不需要了,当然要能删除。

function deleteUser(db, userId) {
  return new Promise((resolve, reject) => {
    const transaction = db.transaction(['users'], 'readwrite');
    const objectStore = transaction.objectStore('users');

    const request = objectStore.delete(userId);

    request.onsuccess = (event) => {
      console.log('用户删除成功');
      resolve();
    };

    request.onerror = (event) => {
      console.error('用户删除失败:', event);
      reject(event);
    };
  });
}

openDatabase()
  .then((db) => {
    const userId = 1; // 假设要删除id为1的用户

    return deleteUser(db, userId);
  })
  .then(() => {
    console.log('用户删除成功');
  })
  .catch((error) => {
    console.error('删除用户失败:', error);
  });

这段代码和获取数据很像,只不过用的是objectStore.delete(userId)

6. 使用索引查询:告别暴力搜索!

前面咱们创建了nameemail索引,现在咱们来用它们查询数据。

function getUserByEmail(db, email) {
  return new Promise((resolve, reject) => {
    const transaction = db.transaction(['users'], 'readonly');
    const objectStore = transaction.objectStore('users');
    const index = objectStore.index('email');

    const request = index.get(email);

    request.onsuccess = (event) => {
      const user = event.target.result;
      if (user) {
        console.log('通过邮箱获取用户成功:', user);
        resolve(user);
      } else {
        console.log('未找到用户');
        resolve(null);
      }
    };

    request.onerror = (event) => {
      console.error('通过邮箱获取用户失败:', event);
      reject(event);
    };
  });
}

openDatabase()
  .then((db) => {
    const email = '[email protected]'; // 假设要获取邮箱为[email protected]的用户

    return getUserByEmail(db, email);
  })
  .then((user) => {
    if (user) {
      console.log('用户信息:', user);
    } else {
      console.log('未找到用户');
    }
  })
  .catch((error) => {
    console.error('通过邮箱获取用户失败:', error);
  });

这段代码做了这些事情:

  • objectStore.index('email'):获取名为email的索引。
  • index.get(email):根据email的值获取数据。

7. 使用游标遍历:一个一个来!

有时候我们需要遍历整个对象仓库,这时候就可以使用游标。

function getAllUsers(db) {
  return new Promise((resolve, reject) => {
    const transaction = db.transaction(['users'], 'readonly');
    const objectStore = transaction.objectStore('users');

    const request = objectStore.openCursor();

    const users = [];

    request.onsuccess = (event) => {
      const cursor = event.target.result;

      if (cursor) {
        // 如果有数据,就添加到数组里
        users.push(cursor.value);

        // 继续下一个
        cursor.continue();
      } else {
        // 没有数据了,遍历结束
        console.log('所有用户:', users);
        resolve(users);
      }
    };

    request.onerror = (event) => {
      console.error('遍历用户失败:', event);
      reject(event);
    };
  });
}

openDatabase()
  .then((db) => {
    return getAllUsers(db);
  })
  .then((users) => {
    console.log('所有用户信息:', users);
  })
  .catch((error) => {
    console.error('获取所有用户失败:', error);
  });

这段代码做了这些事情:

  • objectStore.openCursor():打开一个游标。
  • request.onsuccess:每次游标指向一个数据时,会触发onsuccess事件。
  • cursor.value:当前游标指向的数据。
  • cursor.continue():让游标指向下一个数据。
  • cursornull时,表示遍历结束。

四、IndexedDB最佳实践:避免踩坑!

  • 版本控制:每次修改对象仓库或索引,都要升级数据库版本号,并在onupgradeneeded事件中进行修改。
  • 事务管理:使用事务保证数据一致性,不要在事务中进行耗时操作。
  • 错误处理:一定要处理onerror事件,避免程序崩溃。
  • 异步编程:IndexedDB是异步操作,使用Promise或async/await来处理结果。
  • 索引优化:根据查询需求创建合适的索引,避免全表扫描。
  • 大容量数据:IndexedDB适合存储大量数据,但也要注意内存占用,避免影响浏览器性能。
  • 安全问题:不要存储敏感信息,例如密码,可以使用加密算法进行加密。

五、总结:IndexedDB,你的数据存储小能手!

好了,今天的IndexedDB之旅就到这里了。希望通过今天的讲解,你已经对IndexedDB有了更深入的了解。它虽然有点复杂,但功能强大,只要掌握了核心概念和使用方法,就能在浏览器端实现高性能的结构化数据存储和离线能力。

记住,多写代码,多实践,才能真正掌握IndexedDB。祝大家学习愉快!下次再见!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注