廢話很少說,讓咱們直接用一個實際的例子來看 IndexedDB 如何使用。(PS:這一部分算是對 IndexedDB 的簡介和科普,本文真正的核心在後面,若是不想看科普能夠直接跳到後面)html
數據庫: IDBDatabase 對象倉庫:IDBObjectStore (我更願意稱之爲:表) 索引: IDBIndex 事務: IDBTransaction 操做請求:IDBRequest 指針: IDBCursor
實際項目中通常正常的流程爲:web
開庫 → 建表 → 建立索引 → 存入/刪除數據 → 獲取數據數據庫
這裏咱們先使用文檔中的一個例子(後面再來講哪裏存在問題)數組
const dbName = "the_name"; const customerData = [{ ssn: "444-44-4444", name: "Bill", age: 35, email: "bill@company.com" }, { ssn: "555-55-5555", name: "Donna", age: 32, email: "donna@home.org" }]; var request = indexedDB.open(dbName, 2); request.onsuccess = function(event) { var db = event.target.result; // todo }; request.onupgradeneeded = function(event) { var db = event.target.result; var objectStore = db.createObjectStore("customers", { keyPath: "ssn" }); objectStore.createIndex("name", "name", { unique: false }); objectStore.createIndex("email", "email", { unique: true }); objectStore.transaction.oncomplete = function(event) { var customerObjectStore = db.transaction("customers", "readwrite").objectStore("customers"); customerData.forEach(function(customer) { customerObjectStore.add(customer); }); }; };
indexedDB.open(庫名,數據庫版本)
一般來講咱們常常會用到的函數 onsuccess : 成功回調,通俗的講就是:你能夠開始頁面的其餘操做了。 onupgradeneeded :升級數據庫回調,通俗的講就是:穩一手,再操做。
當僅當數據庫版本號 發生變化的時候觸發 onupgradeneeded 。換句話說,若是當前版本號爲2。promise
event.target.result.createObjectStore('myList',{ keyPath: 'id', autoIncrement: true })
這個對象就是一般意義上的數據庫自己,咱們能夠經過這個對象進行表的增、刪,以及事物 IDBTransaction 。
在IndexedDB中所作的全部事情老是發生在事務的上下文中,表示與數據庫中的數據的交互。 IndexedDB中的全部對象——包括對象存儲、索引和遊標等都與特定事務綁定。 所以,在事務以外不能執行命令、訪問數據或打開任何東西。 (PS: 通俗的意義上講就是...此路是我開,此樹是我栽,要想讀寫數據,請過我這關  ̄□ ̄ )
objectStore.createIndex("name", "name", { unique: false });
這個就是表了,它所包含的方法不少都是實際項目中常常用到的好比: add() 寫入數據 createIndex() 建立索引 delete() 刪除鍵 index() 獲取索引 get() 檢索值 getAll() 檢索全部的值 不作過多敘述,詳見文檔。
還記得 IDBTransaction 和 IDBObjectStore 嗎?此時繞不開這倆貨 雖然說真正執行數據操做的函數是 objectStore.add() 等等,但請在事物IDBTransaction中獲取IDBObjectStore對象。
同上,原諒我,懶得寫 :) 了
若是光看上面的例子,其實 IndexedDB 並不複雜。然而在實際項目中卻會遇到大量的問題,主要集中在1個問題所引起更多的小問題。瀏覽器
這個問題就是:多庫或多表的同時操做。 這也是本文真正的想要表達的東西cookie
在實際項目中,不太可能一張表就寫完全部數據,有過數據庫操做經驗的老哥應該明白。一般咱們須要關聯兩張甚至多張表,即一張表的鍵值,是另外一張表的鍵或主鍵,因此咱們能夠關聯這兩張表,而沒必要要也不須要在一張表裏寫完全部數據。session
因爲 IndexedDB 是異步實現,因此首先要明確咱們究竟在操做哪張表,創建了哪一個事物,這個連接完成了嗎?等等。異步
明確上述問題才能解決:爲什麼索引變更會蛋疼到難以言喻?爲何首次進入瀏覽器建立兩張表再寫入數據會失效?等一系列問題。async
話很少說,先上代碼,下面是我對 IndexedDB 的簡單封裝用做講解。
class localDB { constructor(openRequest = {}, db = {}, objectStore = {}) { this.openRequest = openRequest; this.db = db; this.objectStore = objectStore; Object.getOwnPropertyNames(this.__proto__).map(fn => { if (this.__proto__[fn] === 'function') { this[fn] = this[fn].bind(this); } }) } openDB(ops, version) { let db = Object.assign(new defaultVaule('db'), ops); this.openRequest = !!version ? window.indexedDB.open(db.name, version) : window.indexedDB.open(db.name); } onupgradeneeded() { const upgradeneed = new Promise((resolve, reject) => { this.openRequest.onupgradeneeded = (event) => { this.db = event.target.result; resolve(this); } }) return upgradeneed; } onsuccess() { const success = new Promise((resolve, reject) => { this.openRequest.onsuccess = (event) => { this.db = event.target.result; resolve(this); } }) return success; } createObjectStore(ops) { let list = Object.assign(new defaultVaule('list'), ops); const store = new Promise((resolve, reject) => { this.objectStore = this.db.createObjectStore(list.name, { keyPath: list.keyPath, autoIncrement: list.auto }); resolve(this); }) return store; } createIndex(ops, save) { const store = new Promise((resolve, reject) => { ops.map(data => { let o = Object.assign(new defaultVaule('idx'), data); this.objectStore.createIndex(o.name, o.name, { unique: o.unique }) }) resolve(this); }) return store; } saveData(type = {}, savedata) { let save = Object.assign(new defaultVaule('save'), type); const transAction = new Promise((resolve, reject) => { let preStore = this.objectStore = this.getObjectStore(save); preStore.transaction.oncomplete = (event) => { let f = 0; let store = this.objectStore = this.getObjectStore(save); savedata.map(data => { let request = store.add(data); request.onsuccess = (event) => { // todo 這裏至關於每一個存儲完成後的回調,能夠作點其餘事,也能夠啥都不幹,反正留出來吧 :) } f++; }) if (f == savedata.length) { resolve(this); } } }) return transAction; } getData(ops, name, value) { let store = this.getObjectStore(ops); let data = new Promise((resolve, reject) => { store.index(name).get(value).onsuccess = (event) => { event.target.result ? resolve(event.target.result) : resolve('暫無相關數據') } }) return data; } getAllData(ops) { let store = this.getObjectStore(ops); let data = new Promise((resolve, reject) => { store.getAll().onsuccess = (event) => { event.target.result ? resolve(event.target.result) : resolve('暫無相關數據') }; }) return data; } deleteData(ops,name) { // 主鍵名 let store = this.getObjectStore(ops); store.delete(name).onsuccess = (event) => { console.log(event); console.log(this); } } updateData(ops, index, lastValue, newValue) { // index 索引名 lastValue 須要修改的值 newValue 修改後的值 let store = this.getObjectStore(ops); let data = new Promise((resolve, reject) => { store.openCursor().onsuccess = (event) => { const cursor = event.target.result; if (cursor) { if (cursor.value[index] == lastValue) { let updateData = cursor.value; updateData[index] = newValue; let updateDataRequest = cursor.update(updateData) updateDataRequest.onsuccess = () => { resolve('更新完成'); }; } cursor.continue(); } else { resolve('找不到指定的值'); } } }) return data; } getObjectStore(ops) { return this.db.transaction(ops.name, ops.type).objectStore(ops.name); } clear(ops) { let clear = new Promise((resolve, reject) => { this.getObjectStore(ops).clear(); resolve(this); }) return clear } deleteStore(name) { let store = new Promise((resolve, reject) => { this.db.deleteObjectStore(name); resolve(this); }) return store; } updateDB() { let version = this.db.version; let name = this.db.name; let update = new Promise((resolve, reject) => { this.closeDB(); this.openDB({ name: name }, ++version); resolve(this); }) return update; } closeDB() { this.db.close(); this.objectStore = this.db = this.request = {}; } } class defaultVaule { constructor(fn) { if (typeof this.__proto__[fn] === 'function') { return this.__proto__[fn](); } } db() { return { name: 'myDB', } } list() { return { name: 'myList', keyPath: 'id', auto: false, } } idx() { return { name: 'myIndex', unique: false, } } save() { return { name: 'myList', type: 'readwrite' } } }
模擬一下用戶在使用的時候遇到的場景:
一、打開瀏覽器 → 由於是首次進入瀏覽器,這時必然觸發 onsuccess 與 onupgradeneeded 。此時咱們在 onupgradeneeded 中建表創建索引,存入或者不存入初始數據之類的操做,固然仍是根據具體的業務邏輯來。
let db = new localDB(); db.openDB(DB); db.onsuccess().then(data => { console.log('onsuccess'); // todo }) db.onupgradeneeded().then(data => { console.log('onupgradeneeded'); // todo })
此處,若是隻創建一張表,再存入數據,那寫法多是多樣的,例如
db.onupgradeneeded().then(data => { data.createObjectStore(MAINKEY).then(data => { data.createIndex(ITEMKEY).then(data => { console.log('表和索引建立完畢') }) }) }) db.onsuccess().then(data=>{ data.saveData(SAVETYPE, person).then(data => { console.log('數據寫入完畢'); }) })
這樣作,不是不能夠,可是沒有必要,既然用了promise就不要搞成無限嵌套 推薦使用 async/await 看上去更美滋滋。
async function showDB(db) { try { await db.createObjectStore(MAINKEY); await db.createIndex(ITEMKEY); return db; } catch (err) { console.log(err); } } db.onupgradeneeded().then(data => { console.log('onupgradeneeded'); showDB(data).then(data=>{ console.log('表以及索引建立完畢') }) })
用同步寫異步,邏輯層面更清晰一點。上述代碼其實迴歸本質依然是
var localIDB = function() { this.request = this.db = this.objectStore = {}; } localIDB.prototype = { openDB: function(ops, callback) { var ops = this.extend(ops, this.defaultDB()); this.request = window.indexedDB.open(ops.name, ops.version); return this; }, onupgradeneeded: function(callback) { var _this = this; this.request.onupgradeneeded = function(event) { _this.db = event.target.result; callback && callback(event, _this); } return this; }, onsuccess: function(callback) { var _this = this; this.request.onsuccess = function(event) { _this.db = event.target.result; callback && callback(event, _this); } return this; } } var db = new localDB(); db.open().onupgradeneeded(function(event,data){ // todo event是這個事件,data指向對象自己 }).onsuccess(function(event,data){ // todo 同上 })
其實看上去差很少對不對,但若是創建兩張表,並分別寫入數據呢? async/await 就顯得更清晰了
async function showDB(db) { try { await db.createObjectStore(MAINKEY); await db.createIndex(ITEMKEY); let success = await db.onsuccess(); // 第一次 觸發 onsuccess await success.saveData(SAVETYPE, person); // 第一次 寫入數據 await success.updateDB(); // 升級數據庫 await success.onupgradeneeded(); // 第二次 觸發 onupgradeneeded await success.createObjectStore(MAINKEY1); await success.createIndex(ITEMKEY1); let success1 = await success.onsuccess(); // 第二次 觸發 onsuccess await success1.saveData(SAVETYPE1, personDetail); // 第二次 寫入數據 return success1; } catch (err) { console.log(err); } } db.onupgradeneeded().then(data => { console.log('onupgradeneeded'); showDB(data).then(data => { console.log('兩張表,分別寫入數據完成'); }) }) db.onsuccess().then(data=>{ console.log('數據庫加載完畢'); })
當用戶第一次進入時開庫建表觸發的是 onupgradeneeded 以及完成開庫建表操做的 onsuccess 。實際狀況也確實如此,但咱們在 onupgradeneeded 裏面執行了函數 showDB(),因而問題來了:
爲何最外層的
db.onsuccess().then(data=>{ console.log('數據庫加載完畢'); })
沒有被觸發呢?
按照上文,咱們已經有一個數據庫
表1:
表2:
假設:咱們須要從表1中拿到秀兒的uid,而後用uid去表2中獲取秀兒的具體信息。
// html部分代碼 <button onclick="getXiuer()"></button> // js 部分 // 能夠以下嵌套的方式 function getXiuer() { let uid; let obj; db.getData({ name: 'person', type: 'readonly', }, 'name', '秀兒').then(data => { console.log(data) uid = data.uid; db.getData({ name: 'detail', type: 'readonly', }, 'uid', uid).then(data => { console.log(data); }); }); } // 也能夠以下async/await的方式 funtion getXiuer() { getXiuerWait(db).then(data => { console.log(data); }) } async function getXiuerWait(db) { try { let uid; let data = await db.getData({ name: 'person', type: 'readonly', }, 'name', '秀兒'); let result = await db.getData({ name: 'detail', type: 'readonly', }, 'uid', data.uid); return result; } catch (err) { console.log(err); } }
結果如圖所示:
獲取全部數據的返回值是一個數組
db.getAllData({ name: 'detail', type: 'readonly' }).then(data => { console.log(data) })
如圖所示:
想必聰明的你已經發現,其實存入數據庫的值能夠是多種多樣的,字符串、數字、布爾值、數組都是能夠的。長度其實也沒有特別的限制(反正我連base64的本地圖片都存了 o(╥﹏╥)o )
假設:咱們須要修改一個已經存在的值(把索引爲 age 的值由 60 改成 17)
db.updateData(SAVETYPE1, 'age', 60, 17).then(data => { console.log(data) })
結果如圖所示:
IndexedDB只要理清楚開篇的幾個概念即:
以及異步返回的時機,此時此刻在操做哪張表,能夠觸發哪幾個函數,實際上是一個蠻好用的工具。
如今再來回答 索引的修改應該如何進行?
答:
若是無可避免,那麼能夠備份當前索引(getAllData裏應有盡有)。再經過升級數據庫版本觸發 onupgradeneeded 刪除之前的表,建立新的表。然而這裏又有一個隱晦的坑 o(╥﹏╥)o
var objectStore = db.createObjectStore("customers", { keyPath: "ssn" }); objectStore.createIndex("email", "email", { unique: true });
因此咱們的代碼可能看上去可能應該是這樣
// 懶得寫 async/await 版本的了 !!(╯' - ')╯︵ ┻━┻ 好累!反正就這意思 db.updateDB().then(data => { data.onupgradeneeded().then(data => { data.deleteStore('detail').then(data => { console.log(data); // 建表 建包含新索引的索引 再存入數據 }) }) })
看到了嗎?這是人乾的事兒嗎?第一次開庫建表的時候就能夠弄好的事情,不要搞成這樣...
差很少就是這樣了,當只有1張表的時候,事情很輕鬆,可是多張表的時候笑容漸漸變態...
好了,有啥不清楚的,能夠留言,若是看到了,並且我會的話,確定會回答。
最後附上用做存儲的測試數據 (能夠忽略)
const DB = { name: 'student', version: 1 } const MAINKEY = { name: 'person', keyPath: 'id', auto: true, } const ITEMKEY = [{ name: 'name', unique: false, }, { name: 'uid', unique: true, }] const person = [{ name: '秀兒', uid: '100', }, { name: '張三', uid: '101', }, { name: '李敏', uid: '102', }, { name: '日天', uid: '103', }] const SAVETYPE = { name: 'person', type: 'readwrite', } const MAINKEY1 = { name: 'detail', keyPath: 'uid', auto: false, } const ITEMKEY1 = [{ name: 'uid', unique: false, }, { name: 'age', unique: false, }, { name: 'sex', unique: false, }, { name: 'desc', unique: false, }, { name: 'address', unique: false, }] const personDetail = [{ uid: '102', age: '18', sex: '♀', desc: '女裝大佬', address: ["遙遠的地方"], }, { uid: '103', age: '18', sex: 'man', desc: 'rua!', address: '{"test":"123","more":"asd"}', }, { uid: '100', age: 'unknown', sex: 'unknown', desc: '666', address: true, }, { uid: '101', age: 60, sex: 'man', desc: '路人甲', address: true, }] const SAVETYPE1 = { name: 'detail', type: 'readwrite', }