介绍 引用 MDN 的介绍:
IndexedDB 是一个事务型数据库系统,类似于基于 SQL 的 RDBMS。 然而不同的是它使用固定列表,IndexedDB 是一个基于 JavaScript 的面向对象的数据库。 IndexedDB 允许您存储和检索用键索引的对象; 可以存储 structured clone algorithm 支持的任何对象。 您只需要指定数据库模式,打开与数据库的连接,然后检索和更新一系列事务中的数据。
IndexedDB 是一种低级 API,用于客户端存储大量结构化数据(包括, 文件/ blobs)。该 API 使用索引来实现对该数据的高性能搜索。虽然 Web Storage 对于存储较少量的数据很有用,但对于存储更大量的结构化数据来说,这种方法不太有用。IndexedDB 提供了一个解决方案。
区别 这是我整理的 WebStorage 和 indexedDB 的之间区别,有问题的地方还请指出。
用法 1. 打开数据库
1 2 3 4 5 6 7 8 9 10 11 12 const DB_NAME = "Netease" ;const DB_VERSION = 1 ;const OB_NAMES = { UseKeyPath : "UseKeyPath" , UseKeyGenerator : "UseKeyGenerator" , }; const request = window .indexedDB .open (DB_NAME , DB_VERSION );
indexedDB.open
接收两个参数,分别为数据库名称和版本,返回的是一个 IDBOpenDBRequest
对象。可以以 DOM 事件的方式监听它的 success 和 error 来获取到它的结果。几乎所有对 indexedDB 的异步操作都是这种以事件的方式进行,返回一个拥有结果或错误的 IDBRequest
对象。在这里,open
方法得到的结果是一个 IDBDatabase
的实例。
第二个参数是数据库的版本。版本决定了数据库的模式:存储在里面的 object store 和它们的结构。当第一次通过 open
方法打开数据库时,会触发一个 onupgradeneeded
事件,我们可以也只能在这里设置数据库模式。当数据库已经存在,而我们打开一个更高版本时,同样会触发 onupgradeneeded
事件,用来更新数据库模式。
添加处理方法 我们可以通过监听它的 success
, error
以及 upgradeneeded
事件来做相应的操作。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 request.onerror = function (event ) { console .error ("open request failed" , event.target .error ); }; request.onsuccess = function (event ) { var db = event.target .result ; db.onerror = function (e ) { console .error ("Database error: " , e.target .error ); }; db.onclose = (e ) => { console .error ("Database close:" , e.target .error ); }; };
可以在 success
事件里面拿到 db 对象,这个是后续操作的主体。
错误处理 由于是基于 DOM 事件模式的,所以所有的错误是会冒泡的。也就是说在一个特定 request 的上引发的错误会依次冒泡到事务,然后到 db 对象。 如果为了简化错误处理,可以直接在 db 对象上添加错误处理:
1 2 3 4 db.onerror = function (e ) { console .error ("Database error: " , e.target .error ); };
2. 创建或更新数据库版本 前面已经说过,当创建或者增大数据库版本的时候,会触发 onupgradeneeded
事件。在事件内部,可以拿到 db 对象来创建或更新 object store , 具体如下。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 request.onupgradeneeded = function (event ) { console .log ("onupgradeneeded" , event); var db = event.target .result ; const objectStore = db.createObjectStore (OB_NAMES .UseKeyPath , { keyPath : "time" , }); };
3. 构建数据库 indexedDB 是以对象存储(object store)而不是以表结构存储的,一个数据库可以存储任意多个存储对象。每当有一个值存储在 object store 里面,就必须和一个 key 关联起来。有几种提供 key 的方法,取决于 object store 使用 key path 还是 key generator .
它们之间区别,借用 MDN 的一个表格来看一下:
Key Path (keyPath)
Key Generator (autoIncrement)
Description
No
No
This object store can hold any kind of value, even primitive values like numbers and strings. You must supply a separate key argument whenever you want to add a new value.
Yes
No
This object store can only hold JavaScript objects. The objects must have a property with the same name as the key path.
No
Yes
This object store can hold any kind of value. The key is generated for you automatically, or you can supply a separate key argument if you want to use a specific key.
Yes
Yes
This object store can only hold JavaScript objects. Usually a key is generated and the value of the generated key is stored in the object in a property with the same name as the key path. However, if such a property already exists, the value of that property is used as key rather than generating a new key.
来自 https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API/Using_IndexedDB
当存储的是不是基础类型而是 js 对象的时候,我们还可以给 object store 创建索引,这样就可以通过索引属性来查找某一个具体的对象。而且, 索引还能再一定程度上约束要存储的对象。当创建索引的时候通过设置唯一标识,可以确保不会存储拥有两个相同索引值的对象。 看一个例子,假设我们有如下日志数据需要存储:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 const TestData = [ { event : "NE-TEST1" , level : "warning" , errorCode : 200 , url : "http://www.example.com" , time : "2017/11/8 下午4:53:039" , isUploaded : false , }, { event : "NE-TEST2" , msg : "测试2" , level : "error" , errorCode : 1000 , url : "http://www.example.com" , time : "2017/11/8 下午4:53:042" , isUploaded : false , }, { event : "NE-TEST3" , msg : "测试3" , level : "info" , errorCode : 3000 , url : "http://www.example.com" , time : "2017/11/8 下午4:53:043" , isUploaded : false , }, { event : "NE-TEST4" , mgs : "测试4" , level : "info" , url : "http://www.example.com" , time : "2017/11/8 下午4:53:0423" , isUploaded : false , }, ];
这里有两种存储方式,分别是通过 key path 和 key generator.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 function obUseKeypath (db ) { const objectStore = db.createObjectStore (OB_NAMES .UseKeyPath , { keyPath : "time" , }); objectStore.createIndex ("errorCode" , "errorCode" , { unique : false }); objectStore.createIndex ("level" , "level" , { unique : false }); } function obUseKeyGenerator (db ) { const objectStore = db.createObjectStore (OB_NAMES .UseKeyGenerator , { autoIncrement : true , }); objectStore.createIndex ("errorCode" , "errorCode" , { unique : false }); objectStore.createIndex ("time" , "time" , { unique : true }); objectStore.createIndex ("level" , "level" , { unique : false }); }
4. 增删改查 为了方便介绍这部分的内容,我们先把上一节的代码包装一下,为了方便后续例子的讲解:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 function openindexedDB ( ) { return new Promise ((resolve, reject ) => { const request = window .indexedDB .open (DB_NAME , DB_VERSION ); request.onerror = function (event ) { console .log ("open request failed" , event); console .error (event.target .error ); }; request.onsuccess = function (event ) { var db = event.target .result ; db.onerror = function (e ) { console .error ("Database error: " , e.target .error ); reject (e.target .error ); }; db.onclose = (e ) => { console .error ("Database close:" , e.target .error ); reject (e.target .error ); }; resolve (db); }; request.onupgradeneeded = function (event ) { console .log ("onupgradeneeded" , event); var db = event.target .result ; obUseKeypath (db); obUseKeyGenerator (db); db.transaction .oncomplete = function (e ) { console .log ("obj create success" , e); }; }; }); } function obUseKeypath (db ) { const objectStore = db.createObjectStore (OB_NAMES .UseKeyPath , { keyPath : "time" , }); objectStore.createIndex ("errorCode" , "errorCode" , { unique : false }); objectStore.createIndex ("level" , "level" , { unique : false }); } function obUseKeyGenerator (db ) { const objectStore = db.createObjectStore (OB_NAMES .UseKeyGenerator , { autoIncrement : true , }); objectStore.createIndex ("errorCode" , "errorCode" , { unique : false }); objectStore.createIndex ("time" , "time" , { unique : true }); objectStore.createIndex ("level" , "level" , { unique : false }); }
这样每次我们需要对数据库做操作的话只需要调用 openindexedDB
方法就可以了。
事务 所有对数据库的操作都是建立在事务(transaction)上的,有三种模式(mode):readonly
, readewrite
, versionchange
. 要修改数据库结构的 schema ,必须在 versionchange
模式下。读取和修改对应另外两种模式。 通过 IDBDatabase.transaction
打开一个 transaction
, 接收两个参数:storeNames
, mode
. 所有对数据库的操作都遵循以下流程:
Get database object
Open transaction on database
Open object store on transaction
Perform operation on object store
加速事务操作:
当定义作用域时(scope), 只定义需要的 object stores. 这样,就可以在不重叠的作用域上并行的执行多个事务。
只有在需要的时候才开启一个 readwrite 事务。因为在重叠的作用域上可以并发执行多个 readonly 事务,但只能有一个 readwrite 事务。
增 方法如下:
首先拿到 db 对象
然后打开一个 readwrite
事务
通过事务拿到 object store 对象
执行添加数据操作
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 function addData (docs, objName ) { if (!(docs && docs.length )) { throw new Error ("docs must be a array!" ); } return openindexedDB ().then ((db ) => { const tx = db.transaction ([objName], "readwrite" ); tx.oncomplete = (e ) => { console .log ("tx:addData onsuccess" , e); return Promise .resolve (docs); }; tx.onerror = (e ) => { e.stopPropagation (); console .error ("tx:addData onerror" , e.target .error ); return Promise .reject (e.target .error ); }; tx.onabort = (e ) => { console .warn ("tx:addData abort" , e.target ); return Promise .reject (e.target .error ); }; const obj = tx.objectStore (objName); docs.forEach ((doc ) => { const req = obj.add (doc); req.onerror = (e ) => { console .error ("obj:addData onerror" , e.target .error ); }; }); }); }
如果要把上面的 TestData 同时使用 key generator 和 key path 方式保存到数据库中,那么方法:
1 2 3 addData (TestData , OB_NAMES .UseKeyGenerator ).then (() => addData (TestData , OB_NAMES .UseKeyPath ) );
删 流程和添加一样:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 function deleteData (objName, key ) { return openindexedDB ().then ((db ) => { const tx = db.transaction ([objName], "readwrite" ); const obj = tx.objectStore (objName); const req = obj.delete (key); req.onsuccess = (e ) => { console .log (`readData success. key:${key} ,result:` , e.target .result ); return Promise .resolve (e.target .result ); }; req.onerror = (e ) => { console .error (`readData error. key:${key} ,error: ${e.target.errorCode} ` ); return Promise .reject (e.target .error ); }; }); }
假如要删除 UserKeyGenerator
里 key 为 1 值,那么:
1 deleteData (OB_NAMES .UseKeyGenerator , 1 ).then ((doc ) => console .log (doc));
查 方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 function readData (objName, key ) { return openindexedDB ().then ((db ) => { const tx = db.transaction ([objName]); const obj = tx.objectStore (objName); const req = obj.get (key); req.onsuccess = (e ) => { console .log (`readData success. key:${key} ,result:` , e.target .result ); return Promise .resolve (e.target .result ); }; req.onerror = (e ) => { console .error (`readData error. key:${key} ,error: ${e.target.errorCode} ` ); return Promise .reject (e.target .error ); }; }); }
例子:
1 readData (OB_NAMES .UseKeyGenerator , 1 );
改 方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 function updateData (objName, key, changes ) { return openindexedDB ().then ((db ) => { return new Promise ((resolve, reject ) => { const tx = db.transaction ([objName], "readwrite" ); const obj = tx.objectStore (objName); const req = obj.get (key); req.onsuccess = (e ) => { let doc = e.target .result ; let newDoc = Object .assign (doc, changes); const req = obj.put (newDoc); req.onsuccess = (e ) => { console .log (`updateData success, newDoc:` , newDoc, e); resolve (e.target .result ); }; req.onerror = (e ) => { resolve (e.target .result ); }; }; req.onerror = (e ) => { reject (e.target .error ); }; }); }); }
例子:
1 2 3 updateData (OB_NAMES .UseKeyGenerator , 1 , { time : "123" }) .then ((doc ) => console .log (doc)) .catch (console .error );
使用游标 (cursor) 使用 get 方法需要预先知道一个 key. 如果需要步进整个 object store 的值,那么可以使用 cursor.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 function getAllByCursor (objName, cb ) { return openindexedDB ().then ((db ) => { const arr = []; const tx = db.transaction ([objName]); const obj = tx.objectStore (objName); return new Promise ((resolve ) => { obj.openCursor ().onsuccess = (e ) => { const cursor = e.target .result ; if (cursor) { arr.push (cursor.value ); cb && cb (cursor); cursor.continue (); } else { return resolve (arr); } }; }); }); }
openCursor()
方法接收多个参数。
第一个参数,可以传入一个 key range 对象来限制需要获取值的范围。
第二个参数,可以设置迭代的方向。
成功回调函数的 result
值就是 cursor 本身,当前遍历值可以通过 cursor 对象的 key
和 value
值获取。如果要继续往下遍历,那么调用 cursor 的 continue()
方法,当遍历结束时, cursor 也就是 event.target.result
的值为 undefined
. 如果要获取 UseKeyGenerator 仓库下所有的值,那么可以这样:
1 getAllByCursor (OB_NAMES .UseKeyGenerator ).then (console .log );
除了 openCursor()
外,还可以使用 openKeyCursor()
来获取所有存储对象的主键值,使用方法和 openCursor 一样,
使用索引 (index) 在建立 object store 时,如果我们给它创建了索引。这时,就可以使用索引来查找某个特定属性的值:
1 2 3 4 5 6 7 8 9 10 11 function getByIndex (objName, indexName, key ) { return openindexedDB ().then ((db ) => { const index = db.transaction (objName).objectStore (objName).index (indexName); return new Promise ((resolve ) => { index.get (key).onsuccess = (e ) => { console .log ("getByIndex" , e.target .result ); return resolve (e.target .result ); }; }); }); }
比如,我们要查找 level
为 info
的日志,那么可以这样:
1 2 3 getByIndex (OB_NAMES .UseKeyGenerator , "level" , "info" ).then ((doc ) => console .log (doc) );
设置范围 (range) 和游标 (cursors) 的方向 如果需要设置 cursor 遍历的范围,可以使用 IDBKeyRange
对象并把作为第一个参数给 openCursor()
或者 openKeyCursor()
. 一个 key range 的值,可以认为是一个区间,区间的类型可以是开区间也可以是闭区间。看一些例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 getWithRangeByCursor ( OB_NAMES .UseKeyGenerator , "errorCode" , IDBKeyRange .only (1000 ) ) .then (console .log ) .catch (console .error ); getWithRangeByCursor ( OB_NAMES .UseKeyGenerator , "errorCode" , IDBKeyRange .lowerBound (1000 ) ) .then (console .log ) .catch (console .error ); getWithRangeByCursor ( OB_NAMES .UseKeyGenerator , "errorCode" , IDBKeyRange .lowerBound (1000 , true ) ) .then (console .log ) .catch (console .error ); getWithRangeByCursor ( OB_NAMES .UseKeyGenerator , "errorCode" , IDBKeyRange .bound (1000 , 2000 ) ) .then (console .log ) .catch (console .error ); getWithRangeByCursor ( OB_NAMES .UseKeyGenerator , "time" , IDBKeyRange .bound ( "2017/11/8 下午4:53:042" , "2017/11/8 下午4:53:043" , false , true ) ) .then (console .log ) .catch (console .error );
降序迭代 默认情况下,迭代的方向是升序的。如果需要按照降序规则迭代,那么只要把 prev
作为 openCursor()
的第二个参数就可以了:
1 2 3 getWithDescendingByCursor (OB_NAMES .UseKeyGenerator , "time" ) .then (console .log ) .catch (console .error );