使用idb操作IndexedDB

news2024/11/24 14:22:00

使用idb操作IndexedDB

译自:https://www.hackernoon.tech/use-indexeddb-with-idb-a-1kb-library-that-makes-it-easy-8p1f3yqq

GitHub地址:https://github.com/jakearchibald/idb

文章目录

  • 使用idb操作IndexedDB
    • 前置条件
    • 本文承诺
    • 上手
      • demo1:创建db和store
    • 先易后难:如何插入和获取数据?
      • demo2:插入数据
      • demo3:错误处理
      • 关于``db.close()``
      • demo4:自动生成键
      • demo5:获取值
      • demo6:设置值
    • 事务
      • demo7:将多个操作放在同一个事务中
      • demo8:单store的事务以及错误处理
    • 数据库版本与store的创建
      • demo9:创建db和store(2)
      • demo10:同时处理版本0->2和1->2的升级
      • demo11:没有schema变更的版本升级
      • 将``upgrade``回调写在哪里?
      • ``blocked()``与``blocking()``回调
    • 索引
      • demo12:创建索引
      • demo13:按照索引键从索引中取值
      • demo14:用单操作事务包装方法从索引中取值
    • 简单区间查找
      • demo15:使用range对象查找满足某些条件的记录
    • 使用游标进行遍历查找和复杂查找
      • demo16:使用游标进行遍历
      • demo17:在索引和区间上使用游标
    • 与Typescript一起使用
    • 在``web workers`` / ``service workers``里使用
    • 继续阅读

前置条件

本文假设您

  1. 从事前端工作,需要类似于localStorage但更强大的东西。
  2. 您找到了IndexedDB,觉得它能满足您的需求(bingo!);然后进行了初步调研,大体知道怎么用。
  3. 同时却发现IndexedDB并不简单易用——它的原生API很不友好。
  4. 后来听说了idb——最热门的IndexedDB包装库(下图为近一年的流行趋势,来自npmcharts.com)。

在这里插入图片描述

本文承诺

如果认真读完本文,您将

  1. 不再需要其它教程。
  2. 只需通过idb使用IndexedDB,不必再用它的原生API。
  3. 明白IndexedDB里所有重要的概念,用起IndexedDB来得心应手。

上手

下面是demo1。可以拷到您本地运行(建议将每个demo做成一个按钮来执行)。

demo1:创建db和store

import { openDB } from 'idb';

// demo1: Getting started
export function demo1() {
  openDB('db1', 1, {
    upgrade(db) {
      db.createObjectStore('store1');
      db.createObjectStore('store2');
    },
  });
  openDB('db2', 1, {
    upgrade(db) {
      db.createObjectStore('store3', { keyPath: 'id' });
      db.createObjectStore('store4', { autoIncrement: true });
    },
  });
}

先不用急着读代码,只要运行它就好。然后,打开浏览器的调试工具(如Chrome的DevTools)找到localStorageIndexedDB就在它的下面;可以看到我们创建了两个DB,4个store。

在这里插入图片描述
上图就是典型的应用场景——数据被存在不同DB下的不同store里。

每个store就如同一个加强版的localStorage,用来存储键值对。如果您只需要一个db下的一个加强版localStorage,可以看看同作者的另一个库:idb-keyval,可能就不需继续阅读本文了。

先易后难:如何插入和获取数据?

demo2:插入数据

有了store就可以插入一些数据了。其实创建db和store的demo1内容更复杂一些,所以我们放到后面再说。先看demo2:

import { openDB } from 'idb';

// demo2: add some data into db1/store1/
export async function demo2() {
  const db1 = await openDB('db1', 1);
  db1.add('store1', 'hello world', 'message');
  db1.add('store1', true, 'delivered');
  db1.close();
}

运行后,在DevTools里刷新一下,看看变化。

不出所料,我们的数据放进去了!(注意看:true排在'hello world'之前,这是因为store总是按照键排序的,不管插入顺序如何。)

看一下demo2的代码:当使用store时,首先要通过openDB()来获取一个“数据库连接”,该方法返回db对象,然后再去调用这个对象的方法。现代IDE如VSCode可以在您键入db.时提示方法与参数。

您现在一定有两个疑问:1)openDB的那个参数1到底是做什么用的?2)add方法里keyName为什么放在最后?

我们将在后面回答这两个问题。现在让我们接着看demo3:

demo3:错误处理

// demo3: error handling
export async function demo3() {
  const db1 = await openDB('db1', 1);
  db1
    .add('store1', 'hello again!!', 'new message')
    .then(result => {
      console.log('success!', result);
    })
    .catch(err => {
      console.error('error: ', err);
    });
  db1.close();
}

db1.add()返回一个promise,因此我们可以实现自己的错误处理函数。运行demo3时,控制台会打出"success!“,但如果再运行一次,就会打出"error:”,因为同一个store里面不能有重复的键。

在DevTools中,有两个按钮,分别用来删除一个store内的一条记录和所有记录,可使用这两个按钮配合运行demo3来测试错误处理:

在这里插入图片描述
而idb中能实现这两个按钮功能的,是db.clear(storeName)db.delete(storeName, keyName)

关于db.close()

问:是否需要每次操作都打开和关闭数据库?

答:本文为了演示,每次都调用openDB()获取一个数据库连接,最后再调用db.close()。但在现实中,典型的用法是只获取并使用同一个连接,且根本不关闭,如:

import { openDB } from "idb";

export const idb = {
  db1: openDB("db1", 1),
  db2: openDB("db2", 1)
};

然后使用之:

import { idb } from "../idb";

export async function addToStore1(key, value) {
  (await idb.db1).add("store1", value, key);
}

即不需每次都打开再关闭。

问:对同一个db,可以打开多个连接吗?

答:是的。如果在程序的不同地方多次调用openDB(),就会建立多个连接,不过这没关系。你甚至不需要费心去想着关闭它们,除了让人感觉不太舒服外没有其它副作用。

问:在demo3中,db.add()是异步的。db.close()会不会在事情完成前先执行了?

答:调用db.close()不会马上关闭db。它会等待当前队列中所有的操作都执行完后再关闭。

demo4:自动生成键

现在让我们来回答前面的一个问题:为什么键名是最后一个参数?答案是:因为它可以省略。

回顾demo1,我们在创建store3和store4的时候,分别指定了选项{keyPath: 'id'}{autoIncrement: true}。现在让我们为这两个store添加一些猫猫:

// demo4: auto generate keys:
export async function demo4() {
  const db2 = await openDB('db2', 1);
  db2.add('store3', { id: 'cat001', strength: 10, speed: 10 });
  db2.add('store3', { id: 'cat002', strength: 11, speed: 9 });
  db2.add('store4', { id: 'cat003', strength: 8, speed: 12 });
  db2.add('store4', { id: 'cat004', strength: 12, speed: 13 });
  db2.close();
}

在这个demo里,我们省略了最后的参数。运行它你会发现,在store3中,键是id属性;而在store4中,键是自动生成的递增整数。

在IndexedDB中,除了数字、字符串外,date、binary、array都可以作key。

有了自动生成的键,store3和store4就不太像加强版localStorage而更像传统的数据库了。(译注:通常将这种键称为primary key,即主键,以区别于后面要讲的索引键,主键不能重复,索引键可以。)

demo5:获取值

获取值的方式很直观,您可以运行下面的程序并留意控制台的输出:

// demo5: retrieve values:
export async function demo5() {
  const db2 = await openDB('db2', 1);
  // retrieve by key:
  db2.get('store3', 'cat001').then(console.log);
  // retrieve all:
  db2.getAll('store3').then(console.log);
  // count the total number of items in a store:
  db2.count('store3').then(console.log);
  // get all keys:
  db2.getAllKeys('store3').then(console.log);
  db2.close();
}

demo6:设置值

如果您想更新/覆盖某个值,可使用db.put()而非db.add()。如果该键不存在,则会如add()一样创建。

// demo6: overwrite values with the same key
export async function demo6() {
  // set db1/store1/delivered to be false:
  const db1 = await openDB('db1', 1);
  db1.put('store1', false, 'delivered');
  db1.close();
  // replace cat001 with a supercat:
  const db2 = await openDB('db2', 1);
  db2.put('store3', { id: 'cat001', strength: 99, speed: 99 });
  db2.close();
}

在RESTful API里,PUT是所谓“幂等”的操作,意思是执行多次结果也不会变,每次都只替换其自身;而POST则每次都创建一个新对象。

IndexedDB里,put具有同样的语义。因此,demo6可以执行任意多次。如果换成add()就会报错,因为主键重复。

事务

在数据库术语中,一个“事务”代表一组不可分割的操作,若都执行成功,则整个事务成功;若任何一个失败,则整个事务退出(回滚到事务启动之前的状态)。经典的例子是从一个银行取1000块钱(-1000)存入另一个银行(+1000),两个操作必须都成功或都失败。

IndexedDB中,任何一个操作都必须属于某个事务。

在上面的所有demo中,我们其实都在使用事务,但它们都是单操作事务。例如,在demo4中我们添加了4只猫,实际上起了4个事务。

若要将多个操作归入一个事务,让它们要么都成功,要么都失败,则需显式调用事务API。

demo7:将多个操作放在同一个事务中

现在让我们把1号猫(超猫)从store3移到store4,这就是说,从store3中删除,添加进store4。这两个操作要么都成功,要么都失败:

// demo7: move supercat: 2 operations in 1 transaction:
export async function demo7() {
  const db2 = await openDB('db2', 1);
  // open a new transaction, declare which stores are involved:
  let transaction = db2.transaction(['store3', 'store4'], 'readwrite');
  // do multiple things inside the transaction, if one fails all fail:
  let superCat = await transaction.objectStore('store3').get('cat001');
  transaction.objectStore('store3').delete('cat001');
  transaction.objectStore('store4').add(superCat);
  db2.close();
}

我们首先使用db.transaction()启动了一个事务,并指定有哪些store参与本事务(store3和store4,IndexedDB术语叫“范围”:scope)。第二个参数’readwrite’代表本事务有读写权限。如果是只读,则可以用’readonly’或者不写(默认)。

开启事务后,就不能用前面那些方法了,因为它们都是单操作事务的包装。此时,我们需要用transaction.objectStore(storeName).methodName(..)这种形式。参数都是一样的,除了第一个(storeName)被单独拎出来放到.objectStore(storeName)调用中。

objectStore是store的学名。

Readonly的事务比readwrite的快。同一个store同时只能执行一个readwrite的事务,期间store会被锁定;而readonly事务无此限制。

demo8:单store的事务以及错误处理

如果您的事务只涉及一个store,则可以简写如下:

// demo8: transaction on a single store, and error handling:
export async function demo8() {
  // we'll only operate on one store this time:
  const db1 = await openDB('db1', 1);
  // ↓ this is equal to db1.transaction(['store2'], 'readwrite'):
  let transaction = db1.transaction('store2', 'readwrite');
  // ↓ this is equal to transaction.objectStore('store2').add(..)
  transaction.store.add('foo', 'foo');
  transaction.store.add('bar', 'bar');
  // monitor if the transaction was successful:
  transaction.done
    .then(() => {
      console.log('All steps succeeded, changes committed!');
    })
    .catch(() => {
      console.error('Something went wrong, transaction aborted');
    });
  db1.close();
}

注意在程序的最后我们监听了transaction.done这个promise,它会告诉我们事务执行是否成功。demo8给store2添加了一些数据,您可以运行两遍,第一遍会成功,第二遍会失败(主键重复)。

一个事务如果完成了所有的操作,就会自动提交它自己;监视transaction.done是不错的实践,但不是必须的。

数据库版本与store的创建

现在是时候回答前面那个问题了:那个参数1是什么?

想象这样一个场景:您启动了一个web应用,一个用户访问了它,那么他的浏览器中就创建了相应的DB和store,存储了数据。不久,您的应用升级了,DB和store的结构改变了;于是面临这样的问题:先前那个用户再连上来的时候,您希望将其库中旧的schema转成新的,而数据不能丢。:

为解决这类问题,IndexedDB引入了版本的概念。每个db都有个版本号。在DevTools中可以看到我们的db1和db2的版本都是1。当调用openDB()时,必须提供一个正整数型的版本号,同时可以提供一个叫做upgrade的回调函数,如果提供的版本号大于当前浏览器中的版本号,就会触发该函数。如果当前浏览器中没有这个db,则默认版本为0,该函数仍旧会触发。

demo9:创建db和store(2)

让我们看一下demo9:

// demo9: very explicitly create a new db and new store
export async function demo9() {
  const db3 = await openDB('db3', 1, {
    upgrade: (db, oldVersion, newVersion, transaction) => {
      if (oldVersion === 0) upgradeDB3fromV0toV1();

      function upgradeDB3fromV0toV1() {
        db.createObjectStore('moreCats', { keyPath: 'id' });
        generate100cats().forEach(cat => {
          transaction.objectStore('moreCats').add(cat);
        });
      }
    },
  });
  db3.close();
}

function generate100cats() {
  return new Array(100).fill().map((item, index) => {
    let id = 'cat' + index.toString().padStart(3, '0');
    let strength = Math.round(Math.random() * 100);
    let speed = Math.round(Math.random() * 100);
    return { id, strength, speed };
  });
}

demo9创建了一个新的db3,然后创建了个store,名叫moreCats,里面有100只猫。先看一下DevTools里的输出,然后再来看代码。

upgrade回调函数是创建/删除store的唯一途径。

upgrade回调函数本身在一个事务里。但它既不是readonly也不是readwrite,而是一个更高的事务类型:versionchange。这个事务有权做任何事,包括读写任何store和创建/删除store。由于它自身在一个事务中,所以里面不要用单操作事务包装方法如db.add(),而要用作为参数传进来的transaction对象。

demo10:同时处理版本0->2和1->2的升级

现在让我们来看demo10是如何将老用户的版本升级到2的:

// demo10: handle both upgrade: 0->2 and 1->2
export async function demo10() {
  const db3 = await openDB('db3', 2, {
    upgrade: (db, oldVersion, newVersion, transaction) => {
      switch (oldVersion) {
        case 0:
          upgradeDB3fromV0toV1();
        // falls through
        case 1:
          upgradeDB3fromV1toV2();
          break;
        default:
          console.error('unknown db version');
      }

      function upgradeDB3fromV0toV1() {
        db.createObjectStore('moreCats', { keyPath: 'id' });
        generate100cats().forEach(cat => {
          transaction.objectStore('moreCats').add(cat);
        });
      }

      function upgradeDB3fromV1toV2() {
        db.createObjectStore('userPreference');
        transaction.objectStore('userPreference').add(false, 'useDarkMode');
        transaction.objectStore('userPreference').add(25, 'resultsPerPage');
      }
    },
  });
  db3.close();
}

function generate100cats() {
  return new Array(100).fill().map((item, index) => {
    let id = 'cat' + index.toString().padStart(3, '0');
    let strength = Math.round(Math.random() * 100);
    let speed = Math.round(Math.random() * 100);
    return { id, strength, speed };
  });
}

运行demo10,老用户的db3中会新增一个名叫userPreference的store;而新用户(db3的版本为0)的浏览器中会同时创建moreCates和userPreference。

// falls through的意思是“不要中断”。这行注释会告诉eslint这里不需要写break语句。

您可以用DevTools的工具按钮删除db3,然后先运行demo9再运行demo10来模拟老用户;或直接运行demo10来模拟新用户。

demo11:没有schema变更的版本升级

很多人把版本升级当做一个“schema变更”事件。虽说升级回调函数是创建和删除store的唯一途径,但仍有其它场景可以利用该函数。

译注:即,只有db和store的增删被视为schema变更,store里面的实体结构(属性)变更不是schema变更。

在demo10中,我们新增了一个store:userPreference,并提供了一些初值如’useDarkMode’: false, ‘resultsPerPage’: 25,来模拟用户偏好设置。现在假设我们新加了一项设置,语言,默认为’English’;此外又实现了无限滚动,因此’resultsPerPage’不需要了;最后,还把’useDarkMode’从布尔型改成了字符串类型,支持’light’ | ‘dark’ | 'automatic’几个选项。这样,该如何在支持新用户的同时兼容老用户已有的设置呢?

这是web开发者经常面对的问题。如果用localStorage来保存用户设置,你可能会用left-merge之类的包。这里,让我们用IndexedDB的版本升级来解决:

// demo11: upgrade db version even when no schema change is needed:
export async function demo11() {
  const db3 = await openDB('db3', 3, {
    upgrade: async (db, oldVersion, newVersion, transaction) => {
      switch (oldVersion) {
        case 0:
          upgradeDB3fromV0toV1();
        // falls through
        case 1:
          upgradeDB3fromV1toV2();
        // falls through
        case 2:
          await upgradeDB3fromV2toV3();
          break;
        default:
          console.error('unknown db version');
      }

      function upgradeDB3fromV0toV1() {
        db.createObjectStore('moreCats', { keyPath: 'id' });
        generate100cats().forEach(cat => {
          transaction.objectStore('moreCats').add(cat);
        });
      }
      function upgradeDB3fromV1toV2() {
        db.createObjectStore('userPreference');
        transaction.objectStore('userPreference').add(false, 'useDarkMode');
        transaction.objectStore('userPreference').add(25, 'resultsPerPage');
      }
      async function upgradeDB3fromV2toV3() {
        const store = transaction.objectStore('userPreference');
        store.put('English', 'language');
        store.delete('resultsPerPage');
        let colorTheme = 'automatic';
        let useDarkMode = await store.get('useDarkMode');
        if (oldVersion === 2 && useDarkMode === false) colorTheme = 'light';
        if (oldVersion === 2 && useDarkMode === true) colorTheme = 'dark';
        store.put(colorTheme, 'colorTheme');
        store.delete('useDarkMode');
      }
    },
  });
  db3.close();
}

function generate100cats() {
  return new Array(100).fill().map((item, index) => {
    let id = 'cat' + index.toString().padStart(3, '0');
    let strength = Math.round(Math.random() * 10);
    let speed = Math.round(Math.random() * 10);
    return { id, strength, speed };
  });
}

在这里我们没有新增或删除任何store,所以即使不在版本升级回调里也能实现;但版本升级使得这项任务更条理、更不容易出错。配合DevTools的功能按钮,您可以模拟所有的场景,如依次运行demo9、10、11;9、11;10、11;或仅11。

upgrade回调写在哪里?

如果您在代码里建立了多个通向同一个db的连接,您会希望在打开应用页面时进行版本升级(此时尚未建立任何连接);此后其它地方调用openDB()时,直接省略第三个参数即可。

如果您像我们在demo3和demo4之间讲的那样,重用一个固定的连接,那就只需将回调函数写在那里即可。记住,只有当用户浏览器中的db版本低于openDB()中指定的版本时才会触发。

blocked()blocking()回调

localStorage类似,IndexedDB也使用同源策略。当用户依次在两个tab页签打开同一个应用时,他访问的是同一个db。这通常没问题,但假如某用户在一个tab页打开我们的应用,恰在此时我们更新了一版代码,而他又在第二个tab页再次打开这个应用——这就有问题了:因为同一个db不能同时在不同tab页里打开不同的版本。

为解决此问题,idb提供了另外两个回调函数:blockedblocking

const db = await openDB(dbName, version, {
  blocked: () => {
    // seems an older version of this app is running in another tab
    console.log(`Please close this app opened in other browser tabs.`);
  },
  upgrade: (db, oldVersion, newVersion, transaction) => {
    // …
  },
  blocking: () => {
    // seems the user just opened this app again in a new tab
    // which happens to have gotten a version change
    console.log(`App is outdated, please close this tab`);
  }
});

当发生上述双tab页问题时,旧openDB()的连接会触发blocking回调,该回调会阻止upgrade的触发;而新openDB()的连接会触发blocked回调,新连接的upgrade会一直等到旧连接db.close()之后或旧tab关闭后才触发。

如果您觉得考虑这类问题很烦人,那么我完全赞同。幸运的是还有个更好的办法:使用service worker预缓存你的js文件,这样无论用户打开多少个tab页,都使用同一个js,从而用同一个版本的db;但这是另一个话题了。

索引

store支持索引。不管在其它数据库中索引的含义是什么,在IndexedDB中,索引是指store的一个重新排序后的副本。可以看做原store的一个"影子store",且永远与原store保持同步。

demo12:创建索引

// demo12: create an index on the 100 cats' strength:
export async function demo12() {
  const db3 = await openDB('db3', 4, {
    upgrade: (db, oldVersion, newVersion, transaction) => {
      // upgrade to v4 in a less careful manner:
      const store = transaction.objectStore('moreCats');
      store.createIndex('strengthIndex', 'strength');
    },
  });
  db3.close();
}

demo12在力气(strength)属性上创建了一个索引。运行它,在DevTools中可以看到moreCats下面出现了一个名叫strengthIndex的"影子store"。注意:

  1. upgrade事件是创建索引的唯一途径,因此我们将不得不把db3升级到版本4。
  2. 升级并不一定会按照0->1、1->2、2->3的顺序。如何升级没有一定之规,决定权在开发者。不过,在这里,如果你删除db3后运行demo12就会出错,即将一个新用户直接升级到版本4是个bug(新用户没有这个store,因此无法创建索引)。
  3. 在DevTools中可以看到strengthIndex与原store一样有100只猫,只不过key不同——这恰恰是索引的实质:用另一个字段作为键的同一个store。您可以通过该键获取值,但不能修改,因为它只是一个影子。当主store变化时,它自动随着变化。

加一个索引相当于把store以不同的’keyPath’复制一次。该副本是按这个key排序的,如同原store按主键排序一样。

从索引中取值:

demo13:按照索引键从索引中取值

// demo13: get values from index by key
export async function demo13() {
  const db3 = await openDB('db3', 4);
  const transaction = db3.transaction('moreCats');
  const strengthIndex = transaction.store.index('strengthIndex');
  // get all entries where the key is 10:
  let strongestCats = await strengthIndex.getAll(10);
  console.log('strongest cats: ', strongestCats);
  // get the first entry where the key is 10:
  let oneStrongCat = await strengthIndex.get(10);
  console.log('a strong cat: ', oneStrongCat);
  db3.close();
}

运行demo13并查看控制台的输出,我们会发现,对于非主键索引(这里是strength)而言,key不是唯一的,故而get()方法只返回第一个结果;要想获得全部结果,需用getAll()

demo13在一个事务里执行了两个操作。您也可以用单操作事务包装方法:db.getFromIndex()db.getAllFromIndex(),省得使用transaction对象去操作。

demo14:用单操作事务包装方法从索引中取值

// demo14: get values from index by key using shortcuts:
export async function demo14() {
  const db3 = await openDB('db3', 4);
  // do similar things as demo13, but use single-action transaction shortcuts:
  let weakestCats = await db3.getAllFromIndex('moreCats', 'strengthIndex', 0);
  console.log('weakest cats: ', weakestCats);
  let oneWeakCat = await db3.getFromIndex('moreCats', 'strengthIndex', 0);
  console.log('a weak cat: ', oneWeakCat);
  db3.close();
}

demo14中,从strengthIndex中取值的两个操作各在各的事务中。

简单区间查找

对任何数据库来说,进行某种区间查找都是很常见的任务。比如,我们想找到“所有力气值大于7的猫”;在IndexedDB中,我们依然可以用getAll()方法来达成目的,但给的参数是一个取值区间(range)。

要获取区间对象(Range Object),需调用一个叫做IDBKeyRange的浏览器原生API:

demo15:使用range对象查找满足某些条件的记录

// demo15: find items matching a condition by using range
export async function demo15() {
  const db3 = await openDB('db3', 4);
  // create some ranges. note that IDBKeyRange is a native browser API,
  // it's not imported from idb, just use it:
  const strongRange = IDBKeyRange.lowerBound(8);
  const midRange = IDBKeyRange.bound(3, 7);
  const weakRange = IDBKeyRange.upperBound(2);
  let [strongCats, ordinaryCats, weakCats] = [
    await db3.getAllFromIndex('moreCats', 'strengthIndex', strongRange),
    await db3.getAllFromIndex('moreCats', 'strengthIndex', midRange),
    await db3.getAllFromIndex('moreCats', 'strengthIndex', weakRange),
  ];
  console.log('strong cats (strength >= 8): ', strongCats);
  console.log('ordinary cats (strength from 3 to 7): ', ordinaryCats);
  console.log('weak cats (strength <=2): ', weakCats);
  db3.close();
}

运行demo15,猫猫们就被分成了三组:大力猫、一般猫、黛玉猫。

任何时候调用get()getAll(),都可以传入range对象而不传具体的键(主键或索引键)。

字符串也可以作range,因为字符串可以作键,而键是自动排序的。比如您可以写:IDBKeyRange.bound('cat042', 'cat077')

创建各种range的方法可参考MDN。

使用游标进行遍历查找和复杂查找

IndexedDB并不支持用SQL这样的声明式语言来进行查找(“声明式”的意思是“为我查找xxx,我并不关心用什么算法,只要给我结果就行”),因此我们经常需要自己动手,用JavaScript来写循环。

您可能会想:“是啊,为什么我要去学数据库查询,为什么不能直接用getAll(),然后在结果中筛选我想要的记录呢?”

这样做不是不行,但是有个问题:IndexedDB是个数据库,意味着人们可能往里存上百万条记录。如果用getAll(),就会先将这上百万的记录读入内存,然后在其上进行遍历。

为避免消耗太多内存,IndexedDB提供一种叫做游标(cursor)的工具,可直接在store上遍历。游标就好比一个指针,指向store里的某个位置,您可以读取这个位置的记录,然后位置向前移一格,再读下一条记录,以此类推。让我们来看demo16:

demo16:使用游标进行遍历

// demo16: loop over the store with a cursor
export async function demo16() {
  const db3 = await openDB('db3', 4);
  // open a 'readonly' transaction:
  let store = db3.transaction('moreCats').store;
  // create a cursor, inspect where it's pointing at:
  let cursor = await store.openCursor();
  console.log('cursor.key: ', cursor.key);
  console.log('cursor.value: ', cursor.value);
  // move to next position:
  cursor = await cursor.continue();
  // inspect the new position:
  console.log('cursor.key: ', cursor.key);
  console.log('cursor.value: ', cursor.value);

  // keep moving until the end of the store
  // look for cats with strength and speed both greater than 8
  while (true) {
    const { strength, speed } = cursor.value;
    if (strength >= 8 && speed >= 8) {
      console.log('found a good cat! ', cursor.value);
    }
    cursor = await cursor.continue();
    if (!cursor) break;
  }
  db3.close();
}

看一下控制台的输出,程序不难理解。我们创建了一个游标,从位置0开始,然后通过调用continue()一步步向后移动,同时通过cursor.keycursor.value读取数据。

您还可以在索引和区间上使用游标。

demo17:在索引和区间上使用游标

// demo17: use cursor on a range and/or on an index
export async function demo17() {
  const db3 = await openDB('db3', 4);
  let store = db3.transaction('moreCats').store;
  // create a cursor on a very small range:
  const range = IDBKeyRange.bound('cat042', 'cat045');
  let cursor1 = await store.openCursor(range);
  // loop over the range:
  while (true) {
    console.log('cursor1.key: ', cursor1.key);
    cursor1 = await cursor1.continue();
    if (!cursor1) break;
  }
  console.log('------------');
  // create a cursor on an index:
  let index = db3.transaction('moreCats').store.index('strengthIndex');
  let cursor2 = await index.openCursor();
  // cursor.key will be the key of the index:
  console.log('cursor2.key:', cursor2.key);
  // the primary key will be located in cursor.primaryKey:
  console.log('cursor2.primaryKey:', cursor2.primaryKey);
  // it's the first item in the index, so it's a cat with strength 0
  console.log('cursor2.value:', cursor2.value);
  db3.close();
}

可见,在索引上打开的游标,cursor.key就会变成索引键,而主键可以用cursor.primaryKey获得。

与Typescript一起使用

如果您使用Typescript,别忘了类型化您的store,使生活更美好。

web workers / service workers里使用

您可以在service worker中使用IndexedDB。它可以用来存储worker的状态(worker应该是无状态的,因为可能随时被kill),也可以用来在worker和您的app间传递数据。同时它还非常适合做PWA(Progressive Web App),因为IndexedDB本就是用来存储大量离线数据的。

(译注:事实上IndexedDB常常和service worker结合使用,因为客户端DB本质上是缓存,而service worker充当客户端和服务端之间的代理,根本目的是模拟离线应用,提升用户体验。)

大多数人用workbox来写service worker,环境都支持npm包引入。但假如您的环境不支持模块引入,则可以用这种方法将idb引入您的serviceWorker。

以上就是本文的全部内容。我在写一个谷歌任务(Google Task)的桌面应用时用到了idb,它对我帮助很大,希望也能够帮到您。

继续阅读

https://javascript.info/indexeddb

https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API/Browser_storage_limits_and_eviction_criteria

https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API/Basic_Terminology

https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API/Using_IndexedDB

https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/114781.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

python中的encode()和decode()函数

前言&#xff1a; 我们知道&#xff0c;计算机是以二进制为单位的&#xff0c;也就是说计算机只识别0和1,也就是我们平时在电脑上看到的文字&#xff0c;只有先变成0和1&#xff0c;计算机才会识别它的意思。这种数据和二进制的转换规则就是编码。计算机的发展中&#xff0c;有…

【SpringCloud Alibaba】Sentinel流控规则

概念 流控规则 直接&#xff08;默认&#xff09; QPS快速失败 线程数直接控制 QPSWarming up QPS排队等待 关联 链路 具体启动Sentinel的步骤可以参考我的上一篇文章。 概念 资源名&#xff1a;唯一名称&#xff0c;默认请求路径 针对来源&#xff1a;Sentinel可以针…

微服务系列之远程服务调用

随笔 对自己不满是任何有才能的人的根本特征 参考书籍&#xff1a; “凤凰架构”“微服务架构设计模式” 本篇文章开始之前提示一下&#xff0c;读者带着“IPC与RPC的有什么区别”疑惑读效果更好 引言 从架构师的角度来看&#xff0c;微服务架构的落地实现第一个需要解决问…

改良型新药之详细分类

随着一类新药开发越来越困难、仿制药竞争激烈&#xff0c;改良型新药被认为符合我国医药企业转型升级的方向&#xff0c;吸引了更多企业切入&#xff0c;本文也将针对改良型新药的6个常见共性问题给予解答&#xff0c;涉及科普、专利、分类、临床价值、立项、注册申请、数据统计…

windows@网络防火墙@软件联网控制

文章目录ref打开防火墙控制面板常用部分限制某个软件联网文档参考具体操作取消控制/禁用第三方软件控制ref (Windows) 创建出站端口规则 | Microsoft LearnWindows Defender Firewall with Advanced Security (Windows) | Microsoft Learn组策略 Windows) 高级安全性的 Window…

你可能不知道的DOM断点调试技巧

前言 作为一个前端&#xff0c;DOM断点应该是我们非常熟悉的&#xff0c;也是我们日常工作中经常要用到的一种调试技巧&#xff1b;但是下面这些DOM断点调试技巧你可能不知道&#xff0c;且听我一一道来。 监听元素 有这样一种场景&#xff0c;当DOM中某个元素移除或者元素属…

再学C语言14:基本运算符

C使用运算符&#xff08;operator&#xff09;代表算数运算 一、赋值运算符&#xff08;assignment operator&#xff09;&#xff1a; 在C中&#xff0c;符号并不表示“相等”&#xff0c;而是一个赋值运算符 year 2022; 符号左边是一个变量名&#xff0c;右边是赋给该变…

arraybuffer的应用,下载图片/文件等

在这篇文章中&#xff0c;我们了解了js中arraybuffer是用来存储二进制缓存的&#xff0c;但是都是理论知识&#xff0c;本篇文章来介绍一个arraybuffer应用的场景。 主要应用场景是下载文件&#xff0c;在ajax请求中&#xff0c;设置responseType arraybuffer 得到一个二进制…

零刻 SEi12 Pro,ALL IN ONE搭建教程

一台mini的NUC能做什么&#xff1f;当然每个人的心里都会有着不同的答案&#xff0c;既然是一台Mini主机那就肯定少不了部署一个All-In-One来榨干他的性能。今天我就大家带来一个部署All-In-One的详细教程&#xff0c;希望能够对大家有帮助。 我这台机器配置是i5-1240P 16G内存…

【mpvue】mpvue-echarts echarts动态渲染、延迟加载、双轴动态计算、双轴对齐

mpvue-echarts 双轴折线案例使用echarts双轴折线图实战项目导入一、打包结果超过小程序大小限制&#xff1f;1.下载自定义echarts.js2. 引入 echarts.js![在这里插入图片描述](https://img-blog.csdnimg.cn/ff4ad6d894404e97bceff0581fc1f736.png#pic_center)3. 项目引入二、图…

蓝桥杯备赛Day4——多维数组

二维数组初始化 p[[0 for i in range(5)] for j in range(2)] #法一 p[[0]*5 for j in range(2)] #法二 s[[1,2,3],[4,5,6]] print(s) for i in range(2):for j in range(3):print(s[i][j],end ) 三维数组初始化 a[[[0 for _ in range(2)] for __ in…

RabbitMQ总结

目录 工作模式 简单模式 工作队列模式 发布订阅模式 路由模式 通配符模式 SpringBoot整合RabbitMQ 项目搭建 配置类中创建队列和交换机 编写生产者 编写消费者 消息的可靠性传递 死信队列 延迟队列 工作模式 RabbitMQ共有六种工作模式&#xff1a;简单模式&#xff08;Si…

网络编程 select模型

目录 select模型详解 select函数解释 整体代码 select模型在代码上和c/s模型的前面一部分是一样的&#xff0c;可以去看 这个https://blog.csdn.net/weixin_62859191/article/details/128397927?spm1001.2014.3001.5501&#xff0c;相同的代码如下 #define _CRT_SECURE_NO_…

cadence SPB17.4 - orcad - WARNING(ORCAP-2354) - Wire is hanging at Point

文章目录cadence SPB17.4 - orcad - WARNING(ORCAP-2354) - Wire is hanging at Point概述普通画法, 引起的不可理解的hang wire 警告ENDcadence SPB17.4 - orcad - WARNING(ORCAP-2354) - Wire is hanging at Point 概述 在使用SPB17.4从一个PCB中反推原理图. 原理图重建的差…

Jenkins入门(一)Jenkins介绍、GitLab基础环境安装

视频学习地址&#xff1a;01-Jenkins教程简介_哔哩哔哩_bilibili 一、介绍&#xff1a; Jenkins是一个独立的开源自动化服务器&#xff0c;可用于自动化各种任务&#xff0c;如构建&#xff0c;测试和部署软件。 它替代了管理员手动集成、构建、测试&#xff0c;提交代码后自…

深度学习:ResNet从理论到代码

深度学习&#xff1a;ResNet从理论到代码面临的问题模型退化问题ResNet核心思想反向传播公式推导残差的由来残差模块为什么效果好代码实现面临的问题 模型退化问题 随着网络层数加深&#xff0c;性能逐渐降低&#xff0c;但它并不是过拟合&#xff0c;因为在test error降低的同…

多准则决策问题评估方法 | 灰云模型(含代码)

目前多准则决策问题的评估方法主要分为定性分析方法和定量分析方法两类。定性分析方法主要包括专家咨询、熵权法、案例研究和德尔菲法等&#xff1b;定量分析法主要包括层次分析法、主成分分析法、因子分析法、模糊综合评价法、灰色综合评价法以及数据包络分析法&#xff08;DE…

Apollo星火计划学习笔记——Apollo路径规划算法原理与实践

文章目录1. 路径规划算法总体介绍1.1 Task&#xff1a; LANE_CHANGE_DECIDER1.2 Task&#xff1a; PATH_REUSE_DECIDER1.3 Task&#xff1a; PATH_BORROW_DECIDER1.4 Task&#xff1a; PATH_BOUNDS_DECIDER1.5 Task&#xff1a; PIECEWISE_JERK_PATH_OPTIMIZER1.6 Task&#xf…

人脸识别经典论文Arcface解读

来源&#xff1a;投稿 作者&#xff1a;小灰灰 编辑&#xff1a;学姐 研究背景 1、在人脸识别时&#xff0c;我们需要特征的discrimination 2、之前提出到的一些方法&#xff0c;如triplet loss,center loss, L-softmax,a-softmax都有一些缺陷。 3、centerloss&#xff1a;提…

2022.12.25 学习周报

文章目录摘要文献阅读1.题目2.摘要3.问题和方案4.介绍5.Attention Transfer5.1 Activation-based Attention Transfer5.2 Gradient-based Attention Transfer6.实验7.结论深度学习Attention机制的本质Encoder to Decoder抛开encoder-decoderAttention函数工作机制Attention机制…