使用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``里使用
- 继续阅读
前置条件
本文假设您
- 从事前端工作,需要类似于
localStorage
但更强大的东西。 - 您找到了
IndexedDB
,觉得它能满足您的需求(bingo!);然后进行了初步调研,大体知道怎么用。 - 同时却发现
IndexedDB
并不简单易用——它的原生API很不友好。 - 后来听说了
idb
——最热门的IndexedDB
包装库(下图为近一年的流行趋势,来自npmcharts.com)。
本文承诺
如果认真读完本文,您将
- 不再需要其它教程。
- 只需通过
idb
使用IndexedDB
,不必再用它的原生API。 - 明白
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)找到localStorage
,IndexedDB
就在它的下面;可以看到我们创建了两个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
提供了另外两个回调函数:blocked
和blocking
。
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"。注意:
- upgrade事件是创建索引的唯一途径,因此我们将不得不把db3升级到版本4。
- 升级并不一定会按照0->1、1->2、2->3的顺序。如何升级没有一定之规,决定权在开发者。不过,在这里,如果你删除db3后运行demo12就会出错,即将一个新用户直接升级到版本4是个bug(新用户没有这个store,因此无法创建索引)。
- 在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.key
和cursor.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