你知道其实也有专属于前端的数据库吗?

894 阅读6分钟

很多场景其实不用跟后端进行一个交互也能实现长效的持久化,想必各位在开发中也会用到浏览器缓存这个概念,那么你知道在缓存里其实还存在着indexedDB这个功能莫?

什么是IndexedDB?

IndexedDB是一种低级API,用于客户端存储大量结构化数据,并提供了索引、事务、查询和游标等数据库特性。与localStoragesessionStorage不同,IndexedDB是一个真正的数据库,它允许我们在用户的浏览器中存储几乎无限制的数据量,并通过复杂的查询来访问这些数据。

方法介绍:

  1. open()

    • 功能:打开或创建一个数据库。

    • 参数

      • name:数据库的名称(字符串)。
      • version:数据库的版本号(整数)。如果省略,打开已有数据库时默认为当前版本;新建数据库时默认为1。
    • 返回值:返回一个IDBOpenDBRequest对象,用于处理打开数据库的结果。

    • 事件onsuccessonerroronupgradeneeded

  2. createObjectStore()

    • 功能:在数据库的版本升级事件中创建新的对象仓库(Object Store)。

    • 参数

      • name:对象仓库的名称(字符串)。
      • options:一个对象,包含对象仓库的配置选项,如keyPath(主键路径)和autoIncrement(是否自动生成主键)。
    • 返回值:无。

  3. createIndex()

    • 功能:在对象仓库中创建索引。

    • 参数

      • indexName:索引的名称(字符串)。
      • keyPath:索引基于的键路径(字符串或数组)。
      • options:一个对象,包含索引的配置选项,如unique(是否唯一)。
    • 返回值:无。

  4. transaction()

    • 功能:开始一个事务,用于执行对数据库的读写操作。

    • 参数

      • storeNames:一个包含对象仓库名称的数组或单个对象仓库名称,指定事务将访问哪些对象仓库。
      • mode:事务的模式(字符串),如readonly(只读)、readwrite(读写)。
    • 返回值:返回一个IDBTransaction对象,用于管理事务。

  5. objectStore()

    • 功能:通过事务对象获取指定的对象仓库。
    • 参数:对象仓库的名称(字符串)。
    • 返回值:返回一个IDBObjectStore对象,用于对对象仓库进行操作。
  6. add()put()delete()get()clear()

    • 这些方法都是在IDBObjectStore对象上调用的,用于向对象仓库中添加、更新、删除、获取和清空数据。

具体点我查看MDN

为什么用IndexedDB?

其实简单的存储一些小东西localStorage类完全够用,因为indexedDB它具有数据库的一些特性,例如原子性, 那么什么情况下要用到它呢?

1、存储量大,超过5M:例如存储一些大图预览、用canvas画一次之后存到indexedDB上就可直接拿。

2、庞大的数据列表:我们可以创建索引,通过索引,我们可以快速地检索到满足特定条件的数据,而无需遍历整个数据集。

3、事务支持:这是数据库的一个特性,事务可以确保数据的一致性和完整性,即使在发生错误或异常时也能保持数据的正确性。通过事务,我们可以执行一系列的数据库操作,要么全部成功,要么在遇到错误时全部回滚。

4、异步操作:它的所有操作都是异步的,这意味着它们不会阻塞UI线程,从而提高了Web应用的响应性和用户体验。我们可以使用回调函数、Promisesasync/await等机制来处理异步操作的结果。

原子性:是事务的一个特性,要么全部成功,要么在遇到错误时全部回滚。

用法例子

1、基本用法

打开/新建数据库

    let request = window.indexedDB.open('MyDatabase', 1);  
  
    request.onerror = (event) => {  
        console.error("error: " + event.target.errorCode);  
    };  
      
    request.onsuccess = (event) => {  
        let db = event.target.result;  
        console.log("数据库打开成功!");  
    };  

image.png 当看到这里的时候就证明创建连接成功了!

创建表(创建对象存储空间)

在这里我们接着用上面的数据库的onupgradeneeded方法创建表,该方法接收两个参数:对象存储空间的名称和一个配置对象(可选),配置对象中可以指定主键(keyPath)和是否自动增长(autoIncrement)等。

     request.onupgradeneeded = (event) => {  
        let db = event.target.result;  

        let objectStore = db.createObjectStore('MyObjectStore', { keyPath: 'id', autoIncrement: true });  
  
        // 创建索引  
        objectStore.createIndex('name', 'name', { unique: false });  
        objectStore.createIndex('email', 'email', { unique: true });

        console.log(objectStore)
    };

image.png 执行过后你就会看到一个MyObjectStore的表并且存在nameemail这两个索引。

增加和删除数据

对数据库的操作(增删查改等)都需要通过事务(Transaction)来完成。事务具有三种模式:readonly(只读)、readwrite(读写)和versionchange(版本变更)。增加数据通常使用add()方法,删除数据使用delete()方法。

新增:

request.onsuccess = (event) => {
        let db = event.target.result;

        let transaction = db.transaction(['MyObjectStore'], 'readwrite');
        let objectStore = transaction.objectStore('MyObjectStore');
        let request = objectStore.get('san@example.com');

        request.onsuccess = (event) => {
          if (request.result) {
            // 数据已存在,可以选择更新或不做任何操作
            console.log('邮件已存在:', 'san@example.com');
          } else {
            let data = { name: '张三', age: 25, email: 'san@example.com' };
            objectStore.add(data);
          }
        };

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

删除:

request.onsuccess = (event) => {
        let db = event.target.result;
        let transaction = db.transaction(['MyObjectStore'], 'readwrite');  
        let objectStore = transaction.objectStore('MyObjectStore'); 

        let deleteRequest = objectStore.delete(1);

        deleteRequest.onsuccess =  (event) => {
          console.log('删除成功');
        };

        deleteRequest.onerror = (event) => {
          console.error('删除失败');
        };
      };

image.png 刷新下数据库你就会发现已被删除

读取和更新数据: 读取数据通常使用get()方法,根据主键检索数据。更新数据则可以使用put()方法,如果记录存在则更新,不存在则新增。

读取:

request.onsuccess = (event) => {
        let db = event.target.result;

        let transaction = db.transaction(['MyObjectStore'], 'readwrite');
        let objectStore = transaction.objectStore('MyObjectStore');
        let table = objectStore.get(2);

        table.onsuccess = (e) => {
          console.log(table)
          if (table.result) {
            // 数据已存在,可以选择更新或不做任何操作
            console.log('邮件已存在:', table.result);
          } else {
            let data = { name: '张三', age: 25, email: 'san@example.com' };
            objectStore.add(data);
          }
        };
      };

image.png

更新:

request.onsuccess = (event) => {
        let db = event.target.result;

        let transaction = db.transaction(['MyObjectStore'], 'readwrite');
        let objectStore = transaction.objectStore('MyObjectStore');
        let newData = {
          id: 3,
          name: '李四',
          age: 30,
          email: 'li@example.com',
        };
        let putRequest = objectStore.put(newData);

        putRequest.onsuccess = (event) => {
          console.log('数据更新成功');
        };

        putRequest.onerror = (event) => {
          console.error('数据更新失败!');
        };
      };

image.png 看到这个就证明插入一条数据成功啦!

异步操作 拿下面这段查询语句来说

request.onsuccess = (event) => {
        let db = event.target.result;

        let transaction = db.transaction(['MyObjectStore'], 'readwrite');
        let objectStore = transaction.objectStore('MyObjectStore');
        let table = objectStore.get(2);

        table.onsuccess = (e) => {
          if (table.result) {
            // 数据已存在,可以选择更新或不做任何操作
            console.log('邮件已存在:', table.result);
          } else {
            let data = { name: '张三', age: 25, email: 'san@example.com' };
            objectStore.add(data);
          }
        };

        console.log('我在后,但是我先');
      };

image.png 证明操作是异步的,不会阻塞我们主线程

实际应用场景

1、例如一个庞大的表单,上百个字段(存在着图片的上传)需要用户去填写,那么当用户误操作的时候就会丢失已填写数据,那么我们就可以设计一个indexedDB专门去存储,并将用户上传的图片转为base64存储。

代码如下:

export function fileToBase64(file) {
  const reader = new FileReader()
  reader.onload = function (e) {
    storeImage(e.target.result)
  }
  reader.readAsDataURL(file)
}

function openDB() {
  return new Promise((resolve, reject) => {
    const request = indexedDB.open('FormDatabase', 1)
    request.onerror = (event) => {

      console.error('Database error: ' + event.target.errorCode)
      reject(event.target.errorCode)
    }
    request.onupgradeneeded = (event) => {
      const db = event.target.result

      if (!db.objectStoreNames.contains('FormData')) {
        db.createObjectStore('FormData', { keyPath: 'id', autoIncrement: true })
      }

      if (!db.objectStoreNames.contains('Images')) {
        db.createObjectStore('Images', { keyPath: 'id', autoIncrement: true })
      }
    }

    request.onsuccess = (event) => {
      resolve(event.target.result)
    }
  })
}

const handleDataBase = (
  name,
  event
) => {
  return async (
    id,  // 不同表单对应的不同唯一ID
    value
  ) => {
    const db = await openDB()
    const tx = db.transaction(name, event)

    const store = tx.objectStore(name)

    store.put({ id, value: JSON.stringify(value) }) // 存储表单大字段

    return tx.complete
  }
}

export const storeFormData = handleDataBase(
  'FormData',
  'readwrite'
)

export const storeImage = handleDataBase(
  'Images',
  'readwrite'
)

export const getImageAndDataById = async (id) => {

  const db = await openDB()

  const requestToPromise = (request) => {
    return new Promise((resolve, reject) => {
      request.onsuccess = event => resolve(event.target.result);
      request.onerror = event => reject(event.target.error);
    });
  }

  const images = db.transaction('Images', 'readonly')
  const tIamge = images.objectStore('Images')

  const formData = db.transaction('FormData', 'readonly')
  const tFormData = formData.objectStore('FormData')

  return new Promise(async (resolve, reject) => {

    try {
      const [imgs, data] = await Promise.all([
        requestToPromise(tIamge.get(id)),
        requestToPromise(tFormData.get(id))
      ]);

      resolve({
        imgs,
        data
      })

    } catch (e) {
      reject(e)
    }

  })
}

我们测验下:

storeFormData(1, { name: '张三', age: 18 })

getImageAndDataById(1).then((data) => {
  console.log(data)
})  

image.png 2、当我们编写一个款设计器的时候,用户的节点信息,生成的图片预览、皆可存储至indexedDB。 代码操作类似上面例子。

IndexedDB因其大量数据存储能力、结构化数据存储、高效的查询能力、事务支持、异步操作、持久化存储以及强大的灵活性和可扩展性等优点,成为我们存储大量结构化数据的首选解决方案。