使用idb操作IndexedDB

使用idb操作IndexedDB

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

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

前置条件

本文假设您

  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

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值