大数据存储方案,浏览器还可以这样玩!!!

478 阅读8分钟

前言

已知咱们前端的存储有cookie、 localStorage、 sessionStorage,但是这几个存储都有各种各样的限制,并且有一个统一的限制就是存储不行最大的localStorage也就5M,如果大数据储存应该使用什么方案, 来请咱们重量级选手 浏览器indexedDb

来上概念,讲特点:

IndexedDB 是一个用于在浏览器中储存较大数据结构的 Web [API], 并提供索引功能以实现高性能查找。像其他基于 [SQL] 的  [关系型数据库管理系统 (RDBMS)] 一样,IndexedDB 是一个事务型的数据库系统。然而,它是使用 [JavaScript] 对象而非列数固定的表格来储存数据的,由于它是浏览器提供的本地 [数据库] ,可以被网页脚本创建和操作,允许存贮大量数据,提供查找接口,能建立索引,用于在客户端存储大量的 [结构化数据](也包括文件/二进制大型对象(blobs)。

  • 数据库(IDBDatabase 对象)数据库是一系列相关数据的容器。每个域名(严格的说,是协议 + 域名 + 端口)都可以新建任意多个数据库。但是它版本的概念。同一个时刻,只能有一个版本的数据库存在。如果要修改数据库结构(新增或删除表、索引或者主键),只能通过升级数据库版本完成。
  • 对象仓库(IDBObjectStore 对象)每个数据库包含若干个对象仓库(object store)。它类似于关系型数据库的表格。
  • 索引(IDBIndex 对象)为了加速数据的检索,可以在对象仓库里面,为不同的属性建立索引。
  • 事务(IDBTransaction 对象)数据记录的读写和删改,都要通过事务完成。事务对象提供error、abort和complete三个事件监听操作结果。
  • 操作请求(IDBRequest 对象)。
  • 指针 (IDBCursor 对象) 。
  • 主键集合 (IDBKeyRange 对象)。
IndexedDB 的主要特点:

1、IndexedDB 遵守 [同源策略],每一个数据库对应创建它的域名,网页只能访问自身域名下的数据库,而不能访问跨域的数据库。

2、IndexedDB 执行的操作是异步执行的,不会影响用户进行其他操作。这样也可以防止进行大量数据的读写时,拖慢网页的现象。

3、IndexedDB是采用对象仓库(类似关系型数据库中的表)存储数据的,所有类型的数据都可以直接存入,比如js对象,二进制流等。对象仓库中的数据是以键值对的形式保存的,其中键必须是唯一的,不能重复,否则会抛出错误。

4、IndexedDB支持事务(transaction),即操作要么全部执行,要么全部不执行。因此,在执行操作的过程中,只要有一步失败,整个事务就会被取消,数据库会进行回滚,回到事务发生前的状态。

5、 一般来说不少于 250MB,甚至没有上限。储 存 在 电 脑 上 中 的 位 置 为 C:\Users\当 前 的 登 录 用 户\AppData\Local\Google\Chrome\User Data\Default\IndexedDB。

6、不支持DOM操作,不能跨域。

先了解Api
indexedDB.open()
  • onsuccess: 数据库打开成功触发该事件
  • onerror: 数据库打开失败触发该事件
  • onupgradeneeded: 第一打开该数据库或者数据库版本发生变化时触发该事件
  • onblocked: 上一次的数据连接还未关闭时触发该事件
let request = window.indexedDB.open('dbName');
let db ;

request.onupgradeneeded = function(e) {
    console.log("onupgradeneeded");
}

request.onsuccess = function(e) {
    console.log("onsuccess");
    db = e.target.result;
}

request.onerror = function(e) {
    console.log("onerror");
    console.dir(e);
}

上面代码有两个地方需要注意。首先,open方法返回的是一个对象(IDBOpenDBRequest),回调函数定义在这个对象上面。其次,回调函数接受一个事件对象event作为参数,它的target.result属性就指向打开的IndexedDB数据库。

createObjectStore方法

createObjectStore方法用于创建存放数据的“对象仓库”(object store),类似于传统关系型数据库的表格

db.createObjectStore('dbObjectStore')

该代码创建了dbObjectStore的对象仓库, 如果该仓库已经存在就会抛出一个错误。

objectStoreNames

可以通过 objectStoreNames 来检测当前仓库

  if(!db.objectStoreNames.contains("dbObjectStore")) { 
    db.createObjectStore("dbObjectStore"); 
  }

下面代码中的keyPath属性表示,所存入对象的keys属性用作每条记录的键名(由于键名不能重复,所以存入之前必须保证数据的keys属性值都是不一样的),默认值为null。autoIncrement属性表示,是否使用自动递增的整数作为键名(第一个数据为1,第二个数据为2,以此类推),默认为false。一般来说,keyPath和autoIncrement属性只要使用一个就够了,如果两个同时使用,表示键名为递增的整数,且对象不得缺少指定属性。

  db.createObjectStore("test", { keyPath: "keys" }); 
  db.createObjectStore("test1", { autoIncrement: true });
transaction方法

transaction方法用于创建一个数据库事务。向数据库添加数据之前,必须先创建数据库事务。

let dbObj = db.transaction(["dbObjectStore"],"readwrite");

transaction方法接受两个参数:第一个参数是一个数组,里面是所涉及的对象仓库,通常是只有一个,第二个参数是一个表示操作类型的字符串。目前,操作类型只有两种:readonly(只读)和readwrite(读写)。添加数据使用readwrite,读取数据使用readonly。

let store = dbObj.objectStore('dbObjectStore')

transaction方法返回一个事务对象,该对象的objectStore方法用于获取指定的对象仓库。

transaction方法有三个事件,可以用来定义回调函数。

  • abort:事务中断。
  • complete:事务完成。
  • error:事务出错。
add方法

add方法的第一个参数是所要添加的数据,第二个参数是这条数据对应的键名(key),上面代码将对象o的键名设为keys。如果在创建数据仓库时,对键名做了设置,这里也可以不指定键名。

注意 add方法是异步的,有自己的success和error事件

store.add({'name': '测试数据'}, keys)

put 方法 跟add方法接近

store.put({'name': '测试数据'}, keys)

get 方法
注意get 方法也是异步回调同add一样

store.get(x)

delete方法
注意 delete方法也是异步回调同add一样

store.delete(id)

openCursor 遍历数据
注意 openCursor方法也是异步回调同add一样
// 回调函数接受一个事件对象作为参数,该对象的target.result属性指向当前数据对象。
// 当前数据对象的key和value分别返回键名和键值(即实际存入的数据)。
// continue方法将光标移到下一个数据对象,如果当前数据对象已经是最后一个数据了,则光标指向null。

// openCursor方法还可以接受第二个参数,表示遍历方向,默认值为next,其他可能的值为prev、nextunique和prevunique。后两个值表示如果遇到重复值,会自动跳过。
  const cursor = store.openCursor()
  cursor.onsuccess = function(e) {
    var res = e.target.result;
    if(res) {
        console.log("Key", res.key);
        console.dir("Data", res.value);
        res.continue();
    }
}


有了以上的功能的Api 咱们来自己搞一下

封装indexedDb

支持功能 增删改查


class IndexedDB {
  constructor(dbName, storeName, version = 1, key) {
    this.dbName = dbName;
    this.storeName = storeName;
    this.version = version;
    this.key = key
    this.db = null;
  }
// 初始化需要 调用openDB 创建仓库以及关联关系
  openDB() {
    return new Promise((resolve, reject) => {
      const request = window.indexedDB.open(this.dbName, this.version);

      // 数据仓库打开成功
      request.onsuccess = (event) => {
        this.db = event.target.result;
        resolve(this.db);
      };
      // 数据仓库打开失败
      request.onerror = (event) => {
        reject(event);
      };
      // 数据仓库升级事件(第一次新建库是也会触发)
      request.onupgradeneeded = (event) => {
        // 生成db实例
        this.db = event.target.result;
        if (!this.db.objectStoreNames.contains(this.storeName)) {
          this.db.createObjectStore(this.storeName, { keyPath: this.key });
        }
      };
    });
  }

  executeRequest(request) {
    return new Promise((resolve, reject) => {
      request.onsuccess = (event) => {
        resolve(event.target.result);
      };

      request.onerror = (event) => {
        reject(event);
        throw new Error(event.target.error);
      };
    });
  }

  addData(data) {
    const transaction = this.db.transaction([this.storeName], 'readwrite');
    const objectStore = transaction.objectStore(this.storeName);
    const request = objectStore.add(data);
    return this.executeRequest(request);
  }

  getDataByKey(key) {
    const transaction = this.db.transaction([this.storeName]);
    const objectStore = transaction.objectStore(this.storeName);
    const request = objectStore.get(key);
    return this.executeRequest(request);
  }

  cursorGetData() {
    const list = [];
    const store = this.db.transaction(this.storeName, 'readwrite').objectStore(this.storeName);
    const request = store.openCursor();
    return new Promise((resolve, reject) => {
      request.onsuccess = (event) => {
        const cursor = event.target.result;
        if (cursor) {
          list.push(cursor.value);
          cursor.continue();
        } else {
          resolve(list);
        }
      };

      request.onerror = (event) => {
        reject(event);
      };
    });
  }

  getDataByIndex(indexName, indexValue) {
    const store = this.db.transaction(this.storeName, 'readwrite').objectStore(this.storeName);
    const request = store.index(indexName).get(indexValue);
    return this.executeRequest(request);
  }

  cursorGetDataByIndex(indexName, indexValue) {
    const list = [];
    const store = this.db.transaction(this.storeName, 'readwrite').objectStore(this.storeName);
    const request = store.index(indexName).openCursor(IDBKeyRange.only(indexValue));
    return new Promise((resolve, reject) => {
      request.onsuccess = (event) => {
        const cursor = event.target.result;
        if (cursor) {
          list.push(cursor.value);
          cursor.continue();
        } else {
          resolve(list);
        }
      };

      request.onerror = (event) => {
        reject(event);
      };
    });
  }

  updateDB(data) {
    const request = this.db.transaction([this.storeName], 'readwrite').objectStore(this.storeName).put(data);
    return this.executeRequest(request);
  }

  deleteDB(id) {
    const request = this.db.transaction([this.storeName], 'readwrite').objectStore(this.storeName).delete(id);
    return this.executeRequest(request);
  }

  static deleteDBAll(dbName) {
    const deleteRequest = window.indexedDB.deleteDatabase(dbName);
    return new Promise((resolve, reject) => {
      deleteRequest.onerror = (event) => {
        console.log('删除失败');
      };

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

  static closeDB(db) {
    db.close();
    console.log('数据库已关闭');
  }
}

为什么要用 indexedDB

脱开需求讲技术 那不是纸上谈兵么, 有这样一个场景, 渲染某一个页面, 当前页面接口请求服务端,服务端还会调用自己的服务,或者多个服务,服务之间的连接也是需要消耗时间,同时返回的数据也超过5m,为了相对好的体验,咱们就可以用到indexedDb的存储了 ,这个时候要考虑一个小细节,如果数据储存本地了,服务端的数据发生了新的变化,如何通知前端呢,毕竟数据存储在浏览器,前端不知道的话,那么每次都是取indexedDb的数据?

  1. 首先跟服务端约定一个查询版本的接口,以版本号作为key
  2. 初始化请求来数据之后储存在indexedDb中 以版本号为key 数据 为value 当版本号发生变化时替换仓库中的数据。
  3. 根据当前的返回的key来判定是否取当前indexedDb中的数据

模拟下业务场景

 const response = await fetch('/listTag', {
    method: 'get',
  })
  // 生成基本信息
  const indexDb = new IndexedDB('DetailDb', 'detailStore', '1.0', 'tag')
  // 连接仓库
  const openDB = await indexDb.openDB()
  if (openDB) {
    // 查询具体的key
    const info = await indexDb.getDataByKey(response.tag)
    if (info) {
      this.data = info
    }else {
      const res =  await fetch('/list', {
        method: 'get',
      })
      // 仓库添加数据
      indexDb.addData(res)
    }
  }
  // 当然了增删改查 根据业务场景调用对应的方法即好。