前端的資料庫:IndexedDB入門

cucr發表於2014-12-27

應用程式需要資料。對大多數Web應用程式來說,資料在伺服器端組織和管理,客戶端通過網路請求獲取。隨著瀏覽器變得越來越有能力,因此可選擇在瀏覽器儲存和操縱應用程式資料。

本文向你介紹名為IndexedDB的瀏覽器端文件資料庫。使用lndexedDB,你可以通過慣於在伺服器端資料庫幾乎相同的方式建立、讀取、更新和刪除大量的記錄。請使用本文中可工作的程式碼版本去體驗,完整的原始碼可以通過GitHub庫找到。

讀到本教程的結尾時,你將熟悉IndexedDB的基本概念以及如何實現一個使用IndexedDB執行完整的CRUD操作的模組化JavaScript應用程式。讓我們稍微親近IndexedDB並開始吧。

什麼是IndexedDB

一般來說,有兩種不同型別的資料庫:關係型和文件型(也稱為NoSQL或物件)。關聯式資料庫如SQL Server,MySQL,Oracle的資料儲存在表中。文件資料庫如MongoDB,CouchDB,Redis將資料集作為個體物件儲存。IndexedDB是一個文件資料庫,它在完全內建於瀏覽器中的一個沙盒環境中(強制依照(瀏覽器)同源策略)。圖1顯示了IndexedDB的資料,展示了資料庫的結構

圖1:開發者工具檢視一個object store

全部的IndexedDB API請參考完整文件

設計典範

IndexedDB的架構很像在一些流行的伺服器端NOSQL資料庫實現中的設計典範型別。物件導向資料通過object stores(物件倉庫)進行持久化,所有操作基於請求同時在事務範圍內執行。事件生命週期使你能夠控制資料庫的配置,錯誤通過錯誤冒泡來使用API管理。

物件倉庫

object store是IndexedDB資料庫的基礎。如果你使用過關聯式資料庫,通常可以將object store等價於一個資料庫表。Object stores包括一個或多個索引,在store中按照一對鍵/值操作,這提供一種快速定位資料的方法。

當你配置一個object store,你必須為store選擇一個鍵。鍵在store中可以以“in-line”或“out-of-line”的方式存在。in-line鍵通過在資料物件上引用path來保障它在object store的唯一性。為了說明這一點,想想一個包括電子郵件地址屬性Person物件。您可以配置你的store使用in-line鍵emailAddress,它能保證store(持久化物件中的資料)的唯一性。另外,out-of-line鍵通過獨立於資料的值識別唯一性。在這種情況下,你可以把out-of-line鍵比作一個整數值,它(整數值)在關聯式資料庫中充當記錄的主鍵。

圖1顯示了任務資料儲存在任務的object store,它使用in-line鍵。在這個案例中,鍵對應於物件的ID值。

基於事務

不同於一些傳統的關聯式資料庫的實現,每一個對資料庫操作是在一個事務的上下文中執行的。事務範圍一次影響一個或多個object stores,你通過傳入一個object store名字的陣列到建立事務範圍的函式來定義。

建立事務的第二個引數是事務模式。當請求一個事務時,必須決定是按照只讀還是讀寫模式請求訪問。事務是資源密集型的,所以如果你不需要更改data store中的資料,你只需要以只讀模式對object stores集合進行請求訪問。

清單2演示瞭如何使用適當的模式建立一個事務,並在這片文章的 Implementing Database-Specific Code 部分進行了詳細討論。

基於請求

直到這裡,有一個反覆出現的主題,您可能已經注意到。對資料庫的每次操作,描述為通過一個請求開啟資料庫,訪問一個object store,再繼續。IndexedDB API天生是基於請求的,這也是API非同步本性指示。對於你在資料庫執行的每次操作,你必須首先為這個操作建立一個請求。當請求完成,你可以響應由請求結果產生的事件和錯誤。

本文實現的程式碼,演示瞭如何使用請求開啟資料庫,建立一個事務,讀取object store的內容,寫入object store,清空object store。

開啟資料庫的請求生命週期

IndexedDB使用事件生命週期管理資料庫的開啟和配置操作。圖2演示了一個開啟的請求在一定的環境下產生upgrade need事件。

圖2:IndexedDB開啟請求的生命週期

所有與資料庫的互動開始於一個開啟的請求。試圖開啟資料庫時,您必須傳遞一個被請求資料庫的版本號的整數值。在開啟請求時,瀏覽器對比你傳入的用於開啟請求的版本號與實際資料庫的版本號。如果所請求的版本號高於瀏覽器中當前的版本號(或者現在沒有存在的資料庫),upgrade needed事件觸發。在uprade need事件期間,你有機會通過新增或移除stores,鍵和索引來操縱object stores。

如果所請求的資料庫版本號和瀏覽器的當前版本號一致,或者升級過程完成,一個開啟的資料庫將返回給呼叫者。

錯誤冒泡

當然,有時候,請求可能不會按預期完成。IndexedDB API通過錯誤冒泡功能來幫助跟蹤和管理錯誤。如果一個特定的請求遇到錯誤,你可以嘗試在請求物件上處理錯誤,或者你可以允許錯誤通過呼叫棧冒泡向上傳遞。這個冒泡天性,使得你不需要為每個請求實現特定錯誤處理操作,而是可以選擇只在一個更高階別上新增錯誤處理,它給你一個機會,保持你的錯誤處理程式碼簡潔。本文中實現的例子,是在一個高階別處理錯誤,以便更細粒度操作產生的任何錯誤冒泡到通用的錯誤處理邏輯。

瀏覽器支援

也許在開發Web應用程式最重要的問題是:“瀏覽器是否支援我想要做的?“儘管瀏覽器對IndexedDB的支援在繼續增長,採用率並不是我們所希望的那樣普遍。圖3顯示了caniuse.com網站的報告,支援IndexedDB的為66%多一點點。最新版本的火狐,Chrome,Opera,Safar,iOS Safari,和Android完全支援IndexedDB,Internet Explorer和黑莓部分支援。雖然這個列表的支持者是令人鼓舞的,但它沒有告訴整個故事。

圖3:瀏覽器對IndexedDB的支援,來自caniuse.com

只有非常新版本的Safari和iOS Safari 支援IndexedDB。據caniuse.com顯示,這隻佔大約0.01%的全球瀏覽器使用。IndexedDB不是一個你認為能夠理所當然得到支援的現代Web API,但是你將很快會這樣認為。

另一種選擇

瀏覽器支援本地資料庫並不是從IndexedDB才開始實現,它是在WebSQL實現之後的一種新方法。類似IndexedDB,WebSQL是一個客戶端資料庫,但它作為一個關聯式資料庫的實現,使用結構化查詢語言(SQL)與資料庫通訊。WebSQL的歷史充滿了曲折,但底線是沒有主流的瀏覽器廠商對WebSQL繼續支援。

如果WebSQL實際上是一個廢棄的技術,為什麼還要提它呢?有趣的是,WebSQL在瀏覽器裡得到穩固的支援。Chrome, Safari, iOS Safari, and Android 瀏覽器都支援。另外,並不是這些瀏覽器的最新版本才提供支援,許多這些最新最好的瀏覽器之前的版本也可以支援。有趣的是,如果你為WebSQL新增支援來支援IndexedDB,你突然發現,許多瀏覽器廠商和版本成為支援瀏覽器內建資料庫的某種化身。

因此,如果您的應用程式真正需要一個客戶端資料庫,你想要達到的最高階別的採用可能,當IndexedDB不可用時,也許您的應用程式可能看起來需要選擇使用WebSQL來支援客戶端資料架構。雖然文件資料庫和關聯式資料庫管理資料有鮮明的差別,但只要你有正確的抽象,就可以使用本地資料庫構建一個應用程式。

IndexedDB是否適合我的應用程式?

現在最關鍵的問題:“IndexedDB是否適合我的應用程式?“像往常一樣,答案是肯定的:“視情況而定。“首先當你試圖在客戶端儲存資料時,你會考慮HTML5本地儲存。本地儲存得到廣泛瀏覽器的支援,有非常易於使用的API。簡單有其優勢,但其劣勢是無法支援複雜的搜尋策略,儲存大量的資料,並提供事務支援。

IndexedDB是一個資料庫。所以,當你想為客戶端做出決定,考慮你如何在服務端選擇一個持久化介質的資料庫。你可能會問自己一些問題來幫助決定客戶端資料庫是否適合您的應用程式,包括:

  • 你的使用者通過瀏覽器訪問您的應用程式,(瀏覽器)支援IndexedDB API嗎 ?
  • 你需要儲存大量的資料在客戶端?
  • 你需要在一個大型的資料集合中快速定位單個資料點?
  • 你的架構在客戶端需要事務支援嗎?

如果你對其中的任何問題回答了“是的”,很有可能,IndexedDB是你的應用程式的一個很好的候選。

使用IndexedDB

現在,你已經有機會熟悉了一些的整體概念,下一步是開始實現基於IndexedDB的應用程式。第一個步驟需要統一IndexedDB在不同瀏覽器的實現。您可以很容易地新增各種廠商特性的選項的檢查,同時在window物件上把它們設定為官方物件相同的名稱。下面的清單展示了window.indexedDB,window.IDBTransaction,window.IDBKeyRange的最終結果是如何都被更新,它們被設定為相應的瀏覽器的特定實現。

現在,每個資料庫相關的全域性物件持有正確的版本,應用程式可以準備使用IndexedDB開始工作。

應用概述

在本教程中,您將學習如何建立一個使用IndexedDB儲存資料的模組化JavaScript應用程式。為了瞭解應用程式是如何工作的,參考圖4,它描述了任務應用程式處於空白狀態。從這裡您可以為列表新增新任務。圖5顯示了錄入了幾個任務到系統的畫面。圖6顯示如何刪除一個任務,圖7顯示了正在編輯任務時的應用程式。

圖4:空白的任務應用程式

圖5:任務列表
圖6:刪除任務
圖7:編輯任務
現在你熟悉的應用程式的功能,下一步是開始為網站鋪設基礎。

鋪設基礎

這個例子從實現這樣一個模組開始,它負責從資料庫讀取資料,插入新的物件,更新現有物件,刪除單個物件和提供在一個object store刪除所有物件的選項。這個例子實現的程式碼是通用的資料訪問程式碼,您可以在任何object store上使用。

這個模組是通過一個立即執行函式表示式(IIFE)實現,它使用物件字面量來提供結構。下面的程式碼是模組的摘要,說明了它的基本結構。

用這樣的結構,可以使這個應用程式的所有邏輯封裝在一個名為app的單物件上。此外,資料庫相關的程式碼在一個叫做db的app子物件上。

這個模組的程式碼使用IIFE,通過傳遞window物件來確保模組的適當範圍。使用use strict確保這個函式的程式碼函式是按照(javascript嚴格模式)嚴格編譯規則。db物件作為與資料庫互動的所有函式的主要容器。最後,window物件檢查app的例項是否存在,如果存在,模組使用當前例項,如果不存在,則建立一個新物件。一旦app物件成功返回或建立,db物件附加到app物件。

本文的其餘部分將程式碼新增到db物件內(在implementation here會評論),為應用程式提供特定於資料庫的邏輯。因此,如你所見本文後面的部分中定義的函式,想想父db物件移動,但所有其他功能都是db物件的成員。完整的資料庫模組列表見清單2。

Implementing Database-Specific Code

對資料庫的每個操作關聯著一個先決條件,即有一個開啟的資料庫。當資料庫正在被開啟時,通過檢查資料庫版本來判斷資料庫是否需要任何更改。下面的程式碼顯示了模組如何跟蹤當前版本,object store名、某成員(儲存了一旦資料庫開啟請求完成後的資料庫當前例項)。

在這裡,資料庫開啟請求發生時,模組請求版本1資料庫。如果資料庫不存在,或者版本小於1,upgrade needed事件在開啟請求完成前觸發。這個模組被設定為只使用一個object store,所以名字直接定義在這裡。最後,例項成員被建立,它用於儲存一旦開啟請求完成後的資料庫當前例項。

接下來的操作是實現upgrade needed事件的事件處理程式。在這裡,檢查當前object store的名字來判斷請求的object store名是否存在,如果不存在,建立object store。

在這個事件處理程式裡,通過事件引數e.target.result來訪問資料庫。當前的object store名稱的列表在_db.objectStoreName的字串陣列上。現在,如果object store不存在,它是通過傳遞object store名稱和store的鍵的定義(自增,關聯到資料的ID成員)來建立。

模組的下一個功能是用來捕獲錯誤,錯誤在模組不同的請求建立時冒泡。

在這裡,errorHandler在一個警告框顯示任何錯誤。這個函式是故意保持簡單,對開發友好,當你學習使用IndexedDB,您可以很容易地看到任何錯誤(當他們發生時)。當你準備在生產環境使用這個模組,您需要在這個函式中實現一些錯誤處理程式碼來和你的應用程式的上下文打交道。

現在基礎實現了,這一節的其餘部分將演示如何實現對資料庫執行特定操作。第一個需要檢查的函式是open函式。

open函式試圖開啟資料庫,然後執行回撥函式,告知資料庫成功開啟可以準備使用。通過訪問window.indexedDB呼叫open函式來建立開啟請求。這個函式接受你想開啟的object store的名稱和你想使用的資料庫版本號。

一旦請求的例項可用,第一步要進行的工作是設定錯誤處理程式和升級函式。記住,當資料庫被開啟時,如果指令碼請求比瀏覽器裡更高版本的資料庫(或者如果資料庫不存在),升級函式執行。然而,如果請求的資料庫版本匹配當前資料庫版本同時沒有錯誤,success事件觸發。

如果一切成功,開啟資料庫的例項可以從請求例項的result屬性獲得,這個例項也快取到模組的例項屬性。然後,onerror事件設定到模組的errorHandler,作為將來任何請求的錯誤捕捉處理程式。最後,回撥被執行來告知呼叫者,資料庫已經開啟並且正確地配置,可以使用了。

下一個要實現的函式是helper函式,它返回所請求的object store。

在這裡,getObjectStore接受mode引數,允許您控制store是以只讀還是讀寫模式請求。對於這個函式,預設mode是隻讀的。

每個針對object store的操作都是在一個事物的上下文中執行的。事務請求接受一個object store名字的陣列。這個函式這次被配置為只使用一個object store,但是如果你需要在事務中操作多個object store,你需要傳遞多個object store的名字到陣列中。事務函式的第二個引數是一個模式。

一旦事務請求可用,您就可以通過傳遞需要的object store名字來呼叫objectStore函式以獲得object store例項的訪問權。這個模組的其餘函式使用getObjectStore來獲得object store的訪問權。

下一個實現的函式是save函式,執行插入或更新操作,它根據傳入的資料是否有一個ID值。

save函式的兩個引數分別是需要儲存的資料物件例項和操作成功後需要執行的回撥。讀寫模式用於將資料寫入資料庫,它被傳入到getObjectStore來獲取object store的一個可寫例項。然後,檢查資料物件的ID成員是否存在。如果存在ID值,資料必須更新,put函式被呼叫,它建立持久化請求。否則,如果ID不存在,這是新資料,add請求返回。最後,不管put或者add 請求是否執行了,success事件處理程式需要設定在回撥函式上,來告訴呼叫指令碼,一切進展順利。

下一節的程式碼在清單1所示。getAll函式首先開啟資料庫和訪問object store,它為store和cursor(遊標)分別設定值。為資料庫遊標設定遊標變數允許迭代object store中的資料。data變數設定為一個空陣列,充當資料的容器,它返回給呼叫程式碼。

在store訪問資料時,遊標遍歷資料庫中的每條記錄,會觸發onsuccess事件處理程式。當每條記錄訪問時,store的資料可以通過e.target.result事件引數得到。雖然實際資料從target.result的value屬性中得到,首先需要在試圖訪問value屬性前確保result是一個有效的值。如果result存在,您可以新增result的值到資料陣列,然後在result物件上呼叫continue函式來繼續迭代object store。最後,如果沒有reuslt了,對store資料的迭代結束,同時資料傳遞到回撥,回撥被執行。

現在模組能夠從data store獲得所有資料,下一個需要實現的函式是負責訪問單個記錄。

get函式執行的第一步操作是將id引數的值轉換為一個整數。取決於函式被呼叫時,字串或整數都可能傳遞給函式。這個實現跳過了對如果所給的字串不能轉換成整數該怎麼做的情況的處理。一旦一個id值準備好了,資料庫開啟了和object store可以訪問了。獲取訪問get請求出現了。請求成功時,通過傳入e.target.result來執行回撥。它(e.target.result)是通過呼叫get函式得到的單條記錄。

現在儲存和選擇操作已經出現了,該模組還需要從object store移除資料。

delete函式的名稱用單引號,因為delete是JavaScript的保留字。這可以由你來決定。您可以選擇命名函式為del或其他名稱,但是delete用在這個模組為了API儘可能好的表達。

傳遞給delete函式的引數是物件的id和一個回撥函式。為了保持這個實現簡單,delete函式約定id的值為整數。您可以選擇建立一個更健壯的實現來處理id值不能解析成整數的錯誤例子的回撥,但為了指導原因,程式碼示例是故意的。

一旦id值能確保轉換成一個整數,資料庫被開啟,一個可寫的object store獲得,delete函式傳入id值被呼叫。當請求成功時,將執行回撥函式。

在某些情況下,您可能需要刪除一個object store的所有的記錄。在這種情況下,您訪問store同時清除所有內容。

這裡deleteAll函式負責開啟資料庫和訪問object store的一個可寫例項。一旦store可用,一個新的請求通過呼叫clear函式來建立。一旦clear操作成功,回撥函式被執行。

執行使用者介面特定程式碼

現在所有特定於資料庫的程式碼被封裝在app.db模組中,使用者介面特定程式碼可以使用此模組來與資料庫互動。使用者介面特定程式碼的完整清單(index.ui.js)可以在清單3中得到,完整的(index.html)頁面的HTML原始碼可以在清單4中得到。

結論

隨著應用程式的需求的增長,你會發現在客戶端高效儲存大量的資料的優勢。IndexedDB是可以在瀏覽器中直接使用且支援非同步事務的文件資料庫實現。儘管瀏覽器的支援可能不能保障,但在合適的情況下,整合IndexedDB的Web應用程式具有強大的客戶端資料的訪問能力。

在大多數情況下,所有針對IndexedDB編寫的程式碼是天然基於請求和非同步的。官方規範有同步API,但是這種IndexedDB只適合web worker的上下文中使用。這篇文章釋出時,還沒有瀏覽器實現的同步格式的IndexedDB API。

一定要保證程式碼在任何函式域外對廠商特定的indexedDB, IDBTransaction, and IDBKeyRange例項進行了規範化且使用了嚴格模式。這允許您避免瀏覽器錯誤,當在strict mode下解析指令碼時,它不會允許你對那些物件重新賦值。

你必須確保只傳遞正整數的版本號給資料庫。傳遞到版本號的小數值會四捨五入。因此,如果您的資料庫目前版本1,您試圖訪問1.2版本,upgrade-needed事件不會觸發,因為版本號最終評估是相同的。

立即執行函式表示式(IIFE)有時叫做不同的名字。有時可以看到這樣的程式碼組織方式,它稱為self-executing anonymous functions(自執行匿名函式)或self-invoked anonymous functions(自呼叫匿名函式)。為進一步解釋這些名稱相關的意圖和含義,請閱讀Ben Alman的文章Immediately Invoked Function Expression (IIFE) 。

Listing 1: Implementing the getAll function


Listing 2: Full source for database-specific code (index.db.js)

Listing 3: Full source for user interface-specific code (index.ui.js)

Listing 3: Full HTML source (index.html)

相關文章