前端的數據庫:IndexedDB 。 ps:入門

應用程序須要數據。對大多數Web應用程序來講,數據在服務器端組織和管理,客戶端經過網絡請求獲取。隨着瀏覽器變得愈來愈有能力,所以可選擇在瀏覽器存儲和操縱應用程序數據。javascript

本文向你介紹名爲IndexedDB的瀏覽器端文檔數據庫。使用lndexedDB,你能夠經過慣於在服務器端數據庫幾乎相同的方式建立、讀取、更新和刪除大量的記錄。請使用本文中可工做的代碼版本去體驗,完整的源代碼能夠經過GitHub庫找到。css

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

什麼是IndexedDBjava

通常來講,有兩種不一樣類型的數據庫:關係型和文檔型(也稱爲NoSQL或對象)。關係數據庫如SQL Server,MySQL,Oracle的數據存儲在表中。文檔數據庫如MongoDB,CouchDB,Redis將數據集做爲個體對象存儲。IndexedDB是一個文檔數據庫,它在徹底內置於瀏覽器中的一個沙盒環境中(強制依照(瀏覽器)同源策略)。圖1顯示了IndexedDB的數據,展現了數據庫的結構

圖1:開發者工具查看一個object storejquery

所有的IndexedDB API請參考完整文檔git

設計典範

IndexedDB的架構很像在一些流行的服務器端NOSQL數據庫實現中的設計典範類型。面向對象數據經過object stores(對象倉庫)進行持久化,全部操做基於請求同時在事務範圍內執行。事件生命週期使你可以控制數據庫的配置,錯誤經過錯誤冒泡來使用API管理。github

對象倉庫

object store是IndexedDB數據庫的基礎。若是你使用過關係數據庫,一般能夠將object store等價於一個數據庫表。Object stores包括一個或多個索引,在store中按照一對鍵/值操做,這提供一種快速定位數據的方法。web

當你配置一個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鍵比做一個整數值,它(整數值)在關係數據庫中充當記錄的主鍵。ajax

圖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)
<!doctype html>
<html lang="en-US">
    <head>
        <meta charset="utf-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <title>Introduction to IndexedDB</title>
        <meta name="description"
              content="Introduction to IndexedDB">
        <meta name="viewport"
              content="width=device-width, initial-scale=1">
        <link rel="stylesheet"
              href="//maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap.min.css">
        <link rel="stylesheet"
              href="//cdnjs.cloudflare.com/ajax/libs
/font-awesome/4.1.0/css/font-awesome.min.css" >
        <link rel="stylesheet"
              href="//cdnjs.cloudflare.com/ajax/libs
/font-awesome/4.1.0/fonts/FontAwesome.otf" >
        <link rel="stylesheet"
              href="//cdnjs.cloudflare.com/ajax/libs
/font-awesome/4.1.0/fonts/fontawesome-webfont.eot" >
        <link rel="stylesheet"
              href="//cdnjs.cloudflare.com/ajax/libs
/font-awesome/4.1.0/fonts/fontawesome-webfont.svg" >
        <link rel="stylesheet"
              href="//cdnjs.cloudflare.com/ajax/libs
/font-awesome/4.1.0/fonts/fontawesome-webfont.ttf" >
        <link rel="stylesheet"
              href="//cdnjs.cloudflare.com/ajax/libs
/font-awesome/4.1.0/fonts/fontawesome-webfont.woff" >
        <style>
            h1 {
                text-align: center;
                color:#999;
            }
 
            ul li {
                font-size: 1.35em;
                margin-top: 1em;
                margin-bottom: 1em;
            }
 
            ul li.small {
                font-style: italic;
            }
 
            footer {
                margin-top: 25px;
                border-top: 1px solid #eee;
                padding-top: 25px;
            }
 
            i[data-id] {
                cursor: pointer;
                color: #eee;
            }
 
            i[data-id]:hover {
                color: #c75a6d;
            }
 
            .push-down {
                margin-top: 25px;
            }
 
            #save-button {
                margin-left: 10px;
            }
        </style>
        <script src="//cdnjs.cloudflare.com/ajax/libs/modernizr
/2.8.2/modernizr.min.js" ></script>
    </head>
    <body class="container">
        <h1>Tasks</h1>
        <div id="unsupported-message"
             class="alert alert-warning"
             style="display:none;">
            <b>Aww snap!</b> Your browser does not support indexedDB.
        </div>
        <div id="ui-container" class="row">
            <div class="col-sm-3">
 
                <a href="#" id="delete-all-btn" class="btn-xs">
                    <i class="fa fa-trash-o"></i> Delete All</a>
 
                <hr/>
 
                <ul id="list-container" class="list-unstyled"></ul>
 
            </div>
            <div class="col-sm-8 push-down">
 
                <input type="hidden" id="id-hidden" />
 
                <input
                       id="title-text"
                       type="text"
                       class="form-control"
                       tabindex="1"
                       placeholder="title"
                       autofocus /><br />
 
                <textarea
                          id="notes-text"
                          class="form-control"
                          tabindex="2"
                          placeholder="text"></textarea>
 
                <div class="pull-right push-down">
 
                    <a href="#" id="clear-button" tabindex="4">Clear</a>
 
                    <button id="save-button"
                            tabindex="3"
                            class="btn btn-default btn-primary">
                                <i class="fa fa-save"></i> Save</button>
                </div>
            </div>
        </div>
        <footer class="small text-muted text-center">by
            <a href="http://craigshoemaker.net" target="_blank">Craig Shoemaker</a>
            <a href="http://twitter.com/craigshoemaker" target="_blank">
                <i class="fa fa-twitter"></i></a>
        </footer>
        <script id="note-template" type="text/template">
            <li>
                <i data-id="{ID}" class="fa fa-minus-circle"></i>
                <a href="#" data-id="{ID}">{TITLE}</a>
            </li>
        </script>
        <script id="empty-note" type="text/template">
            <li class="text-muted small">No tasks</li>
        </script>
        <script src="//ajax.googleapis.com/ajax/libs
/jquery/1.11.1/jquery.min.js"></script>
        <script src="index.db.js" type="text/javascript"></script>
        <script src="index.ui.js" type="text/javascript"></script>
    </body>
</html>
相關文章
相關標籤/搜索