1. 程式人生 > >HTML5 indexedDB前端本地儲存資料庫例項教程

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的,從目前瀏覽器的相容性來看,也表明了這種結果:

indexedDB相容性表

web sql資料庫相容性

可以看到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的輕型的關係型資料庫管理系統)
  • 允許物件的快速索引和搜尋,因此在Web應用程式場景中,您可以非常快速地管理資料以及讀取/寫入資料。
  • 由於是NoSQL資料庫,因此我們可以根據實際需求設定我們的JavaScript物件和索引。
  • 在非同步模式下工作,每個事務具有適度的粒狀鎖。這允許您在JavaScript的事件驅動模組內工作。
不足
  • 規範不支援啦
  • 由於使用SQL語言,因此我們需要掌握和轉換我們的JavaScript物件為對應的查詢語句。
  • 非物件驅動。
如果你的世界觀裡面只有關係型資料庫,恐怕不太容易理解。
位置 包含行和列的表。 包含JavaScript物件和鍵的儲存物件。
查詢機制 SQL Cursor APIsKey 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個基本特徵:

  1. 原子性(Atomicity):事務中的所有操作作為一個整體提交或回滾。
  2. 一致性(Consistemcy):事物完成時,資料必須是一致的,以保證資料的無損。
  3. 隔離性(Isolation):對資料進行修改的多個事務是彼此隔離的。
  4. 永續性(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 APIsKey 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資料庫快取資料,兩者一結合而就可以實現百分百的離線開發,聽上去還是很厲害的。