HTML5 indexedDB前端本地儲存資料庫例項教程
by zhangxinxu from http://www.zhangxinxu.com/wordpress/?p=6289
本文可全文轉載,但需得到原作者書面許可,同時保留原作者和出處,摘要引流則隨意。
// zxx: 本文內容較多,有一定的深度,建議預留足夠的時間閱讀,或者可以先馬後看~
一、indexedDB為何替代了Web SQL Database?
跟小朋友的教育從來沒有什麼“贏在起跑線”這種說法一樣,在前端領域,也不是哪來先出來哪個就在日後引領風騷的。
HTML5 indexedDB和Web SQL Database都是本地資料庫資料儲存,Web SQL Database資料庫要出來的更早,然並卵。從2010年11月18日W3C宣佈捨棄Web SQL database草案開始,就已經註定Web SQL Database資料庫是明日黃花。
未來一定是indexedDB的,從目前瀏覽器的相容性來看,也表明了這種結果:
可以看到IE和Firefox並不支援Web SQL Database,基本上可以斷定永遠也不會支援,規範都不認可,實在沒有浪費精力去支援的理由。
好,現在我們知道了indexedDB取代Web SQL Database大局已定,那大家知道為何Web SQL Database會被捨棄嗎?
下面是一段Web SQL Database程式碼:
database.transaction(function(tx) { tx.executeSql("CREATE TABLE IF NOT EXISTS tasks (id REAL UNIQUE, text TEXT)", []); }
可以看到直接把SQL語句弄到JS中了,主流的關係型資料庫即視感,這麼設計看上去似乎無可厚非,但恰恰這個設計成為了Web SQL Database被捨棄的重要原因之一:一是學習成本高了很多,SQL雖然本身並不複雜,但跨度較大,例如我司玩JS的工程師和玩SQL的工程師中間還隔了個玩php的工程師;二是本身使用很不方便,需要把JS物件轉換成關係型的字串語句,很囉嗦的。
而indexedDB直接JS物件入庫,無縫對接。
下表為更詳細的indexedDB和Web SQL Database對比內容:
WebSQL | IndexedDB | |
---|---|---|
優點 | 真正意義上的關係型資料庫,類似SQLite(SQLite是遵守ACID的輕型的關係型資料庫管理系統) |
|
不足 |
|
如果你的世界觀裡面只有關係型資料庫,恐怕不太容易理解。 |
位置 | 包含行和列的表。 | 包含JavaScript物件和鍵的儲存物件。 |
查詢機制 | SQL | Cursor APIs,Key Range APIs,應用程式程式碼 |
事務 | 鎖可以發生在資料庫,表,行的“讀寫”時候。 | 鎖可以發生在資料庫版本變更事務,或是儲存物件“只讀”和“讀寫”事務時候。 |
事務提交 | 事務建立是顯式的。預設是回滾,除非我們呼叫提交。 | 事務建立是顯式的。預設是提交,除非我們呼叫中止或有一個錯誤沒有被捕獲。 |
STOP!
不能再繼續說下去了,估計看完上面對比表中的內容,已經有很多同學已經有些不知所以然了,因為其中涉及到了很多資料庫相關的概念,要想輕鬆理解上面內容以及更輕鬆掌握indexedDB的相關知識,這些概念還是需要掌握的。
二、務必先了解資料庫的一些概念
對於前端同學,資料庫概念沒必要理解十分精準,只需要知道大概怎麼回事就好了,因此,下面的一些解釋會其意,勿嚼字。
1. 關係型資料庫和非關係型資料庫
“關係型資料庫”是歷史悠久,已經有半個世紀,佔據主流江山的資料庫模型,估計諸位公司的網站多半都是使用的這種資料庫模型。而“非關係型資料庫”(NoSQL資料庫)則要年輕很多,根據資料顯示是Carlo Strozzi在1998年提出來的,20年不到,使用鍵值對儲存資料,且結構不固定,非常類似JavaScript中的純物件。
var obj = { key1: 'value1', key2: 'value2', key3: 'value3', ... };
“關係型資料庫”對一致性要求非常嚴格,例如要寫入100個數據,前99個成功了,結果第100個不合法,此時事務會回滾到最初狀態。這樣保證事務結束和開始時候的儲存資料處於一致狀態。非常適合銀行這種對資料一致性要求非常高的場景。但是,這種一致性保證是犧牲了一部分效能的。
但是,對於微博,QQ空間這類web2.0應用,一致性卻不是顯得那麼重要,比方說朋友A看我的主頁和朋友B看我的主頁資訊有不一樣,沒什麼大不了的,有個幾秒差異很OK的。雖然這類應用對一致性要求不高,但對效能要求卻很高,因為讀寫實在太頻繁了。如果使用“關係型資料庫”,一致性的好處沒怎麼收益,反而效能問題比較明顯,此時,不保證一致性但效能更優的“非關係型資料庫”則更合適。同時,由於“非關係型資料庫”的資料結構不固定,非常容易擴充套件。由於對於社交網站,需求變動那是一日三餐常有的事,新增新欄位在所難免,“非關係型資料庫”輕鬆上陣,如果使用“關係型資料庫”,多半要資料大變動,要好好琢磨琢磨了。
從氣質上講,“關係型資料庫”穩重持久,“非關係型資料庫”迅速靈動。
在前端領域,Web SQL Database是“關係型資料庫”,indexedDB是“非關係型資料庫”。
2. 瞭解資料庫中的事務-transaction
資料庫的事務(英文為’transaction’),我們可以理解為對資料庫的操作,而且專指一個序列上的操作。舉個例子,銀行轉賬,一個賬號錢少了然後另一個賬號錢多了,這兩個操作要麼都執行,要麼都不執行。像這種操作就可以看成一個事務。
事務的提出主要是為了保證併發情況下保持資料一致性。關係型資料庫中的事務具有下面4個基本特徵:
- 原子性(Atomicity):事務中的所有操作作為一個整體提交或回滾。
- 一致性(Consistemcy):事物完成時,資料必須是一致的,以保證資料的無損。
- 隔離性(Isolation):對資料進行修改的多個事務是彼此隔離的。
- 永續性(Durability):事務完成之後,它對於系統的影響是永久的,該修改即使出現系統故障也將一直保留。
通常專業描述中會抽取這4個基本特性的首字母,統稱為“ACID特性”。事務執行過程可以粗淺地理解為:開始事務,巴拉巴拉操作,如果錯誤,回滾(rollback),如果沒問題,提交(commit),結束事務。
3. 瞭解資料庫中的遊標-cursor
現實世界中的“遊標”相關聯的常見事物就是“遊標卡尺”,有刻度有區域:
資料庫中的遊標其實與之有共同之處。記憶體條本質上就像一把尺子,我們可以想象上面有很多刻度,然後記憶體大小就是由這些刻度一個一個堆砌起來的。資料庫的事務為了保證資料可以回滾,顯然需要有一片記憶體區域放置那些即將受影響是資料,這個記憶體區域中的虛表就是資料庫的“遊標”。
和現實世界的“遊標卡尺”相對映就是:一個刻度表示一行資料,遊標就是尺子上的一片區域,想要獲得資料庫一行一行的資料,我們可以遍歷這個遊標就好了。
4. 資料庫中的“鎖”-lock
資料庫中的“鎖”是保證資料庫資料高併發時候資料一致性的一種機制。舉個例子:現有兩處火車票售票點,同時讀取某發現上海到背景車票餘額為5
。此時兩處售票點同時賣出一張車票,同時修改餘額為5-1
也就是4
寫回資料庫,這樣就造成了實際賣出兩張火車票而資料庫中的記錄卻只少了1
張。為了避免發生這種狀況,就有了鎖機制,也就是執行多執行緒時用於強行限制資源訪問。
三、從簡單實用的例項開始學習indexedDB
要想系統學習indexedDB相關知識,可以去MDN文件啃API,假以時日就可以成為前端indexedDB方面的專家。但是這種學習方法週期長,過於痛苦,無法立竿見影,因為API實在太多,天天花個把小時,估計也需要數週時間才能全部通透。
一想到投入產出比這麼低,於是我決定從例項入手開始學習,能夠實現基本的資料庫增刪改查功能就好,基本上80%+的相關需求都能從容搞定;等日後有機會再慢慢深入。
花了幾個晚上折騰,一個具有增刪改無查的例項頁面終於弄出來了,您可以狠狠地點選這裡:HTMl5 indexedDB儲存編輯和刪除資料demo
我們第一次進去是沒有資料的,顯示如下:
1. 此時,我們填寫表單,建立一條資料,如下:
點選“確定建立”按鈕後,就得到如下圖所示的結果:
此時我們重新整理頁面,依然是上圖所示的結果。
開啟控制檯,在Application→indexedDB中,我們可以看到當前儲存的資料欄位和值等資訊:
2. 我們可以直接對建立的資料進行修改,例如修改備註資訊:
失焦後直接就修改了本地資料庫資訊了,我們重新整理一下,可以看到備註文案已經變成“是個帥哥~”
3. 我們還可以對資料庫資料進行刪除,例如點選刪除按鈕:
結果這條資料庫資料就被刪掉了,介面重新顯示為“暫無資料”。
以上就是demo頁面所實現的indexedDB的增加資料,編輯資料和刪除資料功能。
如果您當前的需求急用,可以把demo頁面上的JS原始碼直接複製過去改改;如果你希望對indexedDB相關API及其使用有所瞭解,就繼續往下看。
1. 首先開啟indexedDB資料庫
indexedDB資料庫開啟非常簡單,語法就是字面意思:
window.indexedDB.open(dbName, version);
dbName
就是資料庫名稱,例如demo中使用的是'project'
;version
表示資料庫的版本,根據我的理解,當我們對資料庫的欄位進行增加或修改時候,需要增加個版本。
通常,開啟indexedDB資料庫是和一些回撥方法一起出現的,程式碼套路比較固定,基本上如果大家在實際專案中使用的話,都可以使用類似的程式碼:
// 資料庫資料結果 var db; // 開啟資料庫 var DBOpenRequest = window.indexedDB.open('project', 1); // 資料庫開啟成功後 DBOpenRequest.onsuccess = function(event) { // 儲存資料結果 db = DBOpenRequest.result; // 做其他事情... }; // 下面事情執行於:資料庫首次建立版本,或者window.indexedDB.open傳遞的新版本(版本數值要比現在的高) DBOpenRequest.onupgradeneeded = function(event) { // 通常對主鍵,欄位等進行重定義,具體參見demo };
其中,最重要就是紅色高亮的db
變數,我們後面所有的資料庫操作都離不開它。
2. 建立indexedDB資料庫的主鍵和欄位
在我們開始資料庫的增加刪除操作之前,首先要把我們資料庫的主鍵和一些欄位先建好。是在onupgradeneeded
這個回撥方法中設定的,這個回撥方法執行於資料庫首次建立版本或者資料庫indexedDB.open()
方法中傳遞的版本號比當前版本號要高。
我們對資料庫某一行資料進行增加刪除操作,我們是沒有必要對資料庫的版本號進行修改的。但是對於欄位修改就不一樣了,比方說原來是5
列資料,我們現在改成6
列,由於相關設定是在onupgradeneeded
回撥中,因此,我們需要增加版本號來觸發欄位修改。demo頁面這部分程式碼如下:
DBOpenRequest.onupgradeneeded = function(event) { var db = event.target.result; // 建立一個數據庫儲存物件 var objectStore = db.createObjectStore(dbName, { keyPath: 'id', autoIncrement: true }); // 定義儲存物件的資料項 objectStore.createIndex('id', 'id', { unique: true }); objectStore.createIndex('name', 'name'); objectStore.createIndex('begin', 'begin'); objectStore.createIndex('end', 'end'); objectStore.createIndex('person', 'person'); objectStore.createIndex('remark', 'remark'); };
這裡出現了一個比較重要的概念,叫做objectStore
,這是indexedDB可以替代Web SQL Database的重要優勢所在,我稱之為“儲存物件”。
objectStore.add()
可以向資料庫新增資料,objectStore.delete()
可以刪除資料,objectStore.clear()
可以清空資料庫,objectStore.put()
可以替換資料等還有很多很多操作API。
在這裡,我們使用objectStore
來建立資料庫的主鍵和普通欄位。
db.createObjectStore
在建立主鍵同時返回“儲存物件”,本演示中使用自動遞增的id
欄位作為關鍵路徑。createIndex()
方法可以用來建立索引,可以理解為建立欄位,語法為:
objectStore.createIndex(indexName, keyPath, objectParameters)
其中:
indexName
建立的索引名稱,可以使用空名稱作為索引。
keyPath
索引使用的關鍵路徑,可以使用空的keyPath
, 或者keyPath
傳為陣列keyPath
也是可以的。
objectParameters
可選引數。常用引數之一是unique
,表示該欄位值是否唯一,不能重複。例如,本demo中id
是不能重複的,於是有設定:
objectStore.createIndex('id', 'id', { unique: true });
3. indexedDB資料庫新增資料
欄位建好了之後,我們就可以給indexedDB資料庫新增資料了。
由於資料庫的操作都是基於事務(transaction)來進行,於是,無論是新增編輯還是刪除資料庫,我們都要先建立一個事務(transaction),然後才能繼續下面的操作。語法就是名稱:
var transaction = db.transaction(dbName, "readwrite");
dbName
就是資料庫的名稱,於是我們的資料庫新增資料操作變得很簡單:
// 新建一個事務 var transaction = db.transaction('project', "readwrite"); // 開啟儲存物件 var objectStore = transaction.objectStore('project'); // 新增到資料物件中 objectStore.add(newItem);
使用一行語句表示就是:
db.transaction('project', "readwrite").objectStore('project').add(newItem);
這裡的newItem
直接就是一個原生的純粹的JavaScript物件,在本demo中,newItem
資料類似下面這樣:
{ "name": "第一個專案", "begin": "2017-07-16", "end": "2057-07-16", "person": "張鑫旭", "remark": "測試測試" }
可以發現,當我們使用indexedDB資料庫新增資料的時候,根本就不用去額外學習SQL語句,原始的JavaScript物件直接無縫對接,,
4. indexedDB資料庫的編輯
原理為,先根據id
獲得對應行的儲存物件,方法為objectStore.get(id)
,然後在原儲存物件上進行替換,再使用objectStore.put(record)
進行資料庫資料替換。
程式碼示意:
function edit(id, data) { // 新建事務 var transaction = db.transaction('project', "readwrite"); // 開啟已經儲存的資料物件 var objectStore = transaction.objectStore(project); // 獲取儲存的對應鍵的儲存物件 var objectStoreRequest = objectStore.get(id); // 獲取成功後替換當前資料 objectStoreRequest.onsuccess = function(event) { // 當前資料 var myRecord = objectStoreRequest.result; // 遍歷替換 for (var key in data) { if (typeof myRecord[key] != 'undefined') { myRecord[key] = data[key]; } } // 更新資料庫儲存資料 objectStore.put(myRecord); }; }
其中,id
就是要替換的資料庫行的id
欄位值,因為唯一的主鍵,可以保證準確和高效能;data
則是需要替換的資料物件,例如從“測試測試”修改為“是個帥哥~”,則呼叫:
edit(1, { remark: '是個帥哥~' });
就可以了,跟我們平常寫JS程式碼就沒有什麼區別。
5. indexedDB資料庫的刪除
和新增操作正好相反,但程式碼結構卻是類似的,一行表示法:
db.transaction('project', "readwrite").objectStore('project').delete(id);
id
表示需要刪除行id
欄位對應的值。
5. indexedDB資料庫的獲取
indexedDB資料庫的獲取使用Cursor APIs和Key Range APIs。
也就是使用“遊標API”和“範圍API”。在本文的演示頁面中,只使用了“遊標API”,直接顯示全部資料,對於“範圍API”並沒有使用,但這裡也會簡單介紹下。
“遊標API”可以讓我們一行一行讀取資料庫資料,程式碼示意:
var objectStore = db.transaction(dbName).objectStore(dbName); objectStore.openCursor().onsuccess = function(event) { var cursor = event.target.result; if (cursor) { // cursor.value就是資料物件 // 遊標沒有遍歷完,繼續 cursor.continue(); } else { // 如果全部遍歷完畢... } }
可以看到,我們使用儲存物件的openCursor()
開啟遊標,在onsuccess
回撥中就可以遍歷我們的遊標物件了。其中cursor.value
就是完整的資料物件,純JS物件,就像下面這樣:
{ "id": 1, "name": "第一個專案", "begin": "2017-07-16", "end": "2057-07-16", "person": "張鑫旭", "remark": "測試測試" }
我們就可以按照需求隨意顯示我們的資料了。
“範圍API”是和“遊標API”一起使用的,看下面這個例子,只打開id
從4~10之間的資料,則有:
// 確定開啟的遊標的主鍵範圍 var keyRangeValue = IDBKeyRange.bound(4, 10); // 開啟對應範圍的遊標 var objectStore = db.transaction(dbName).objectStore(dbName); objectStore.openCursor(keyRangeValue).onsuccess = function(event) { var cursor = event.target.result; // ... }
其中,有bound()
, only()
, lowerBound()和upperBound()
這幾個方法,意思就是方法名字面意思,“範圍內”,“僅僅是”,“小於某值”和“大於某值”。
方法最後還支援兩個布林值引數,例如:
IDBKeyRange.bound(4, 10, true, true)
則表示範圍3~9
,也就是為true
的時候不能和範圍邊界相等。
四、indexedDB儲存和localStorage儲存對比
- indexedDB儲存IE10+支援,localStorage儲存IE8+支援,後者相容性更好;
- indexedDB儲存比較適合鍵值對較多的資料,我之前不少專案需要儲存多個欄位,使用的是localStorage儲存,結果每次寫入和寫出都要字串化和物件化,很麻煩,如果使用indexedDB會輕鬆很多,因為無需資料轉換。
- indexedDB儲存可以在workers中使用,localStorage貌似不可以。這就使得在進行PWA開發的時候,資料儲存的技術選型落在了indexedDB儲存上面。
總結下就是,如果是瀏覽器主窗體執行緒開發,同時儲存資料結構簡單,例如,就存個true/false
,顯然localStorage
上上選;如果資料結構比較複雜,同時對瀏覽器相容性沒什麼要求,可以考慮使用indexedDB;如果是在Service Workers中開發應用,只能使用indexedDB資料儲存。
五、結束語
indexedDB資料庫的使用目前可以直接在http協議下使用,這個和cacheStorage
快取儲存必須使用https協議不一樣。所以就應用場景來講,indexedDB資料庫還是挺廣的,考慮到IE10也支援,所以基本可以確定在實際專案中應用是絕對不成問題的。
例如,頁面中一些不常變動的結構化資料,我們就可以使用indexedDB資料庫儲存在本地,有助於增強頁面的互動效能。cacheStorage
快取頁面,indexedDB資料庫快取資料,兩者一結合而就可以實現百分百的離線開發,聽上去還是很厲害的。