離線存儲應用程序數據已成爲現代Web開發中的必要條件。內置的瀏覽器 localStorage
能夠用做簡單輕量數據的數據存儲,可是在結構化數據或存儲大量數據方面卻不足。html
最重要的是,咱們只能將字符串數據存儲在受XSS攻擊的 localStorage
中,而且它沒有提供不少查詢數據的功能。react
這就是IndexedDB的亮點。使用IndexedDB,咱們能夠在瀏覽器中建立結構化的數據庫,將幾乎全部內容存儲在這些數據庫中,並對數據執行各類類型的查詢。git
在本文中,咱們將瞭解IndexedDB的所有含義,以及如何使用Dexie.js(用於IndexedDB的簡約包裝)處理Web應用程序中的離線數據存儲。github
IndexedDB是用於瀏覽器的內置非關係數據庫。它使開發人員可以將數據持久存儲在瀏覽器中,即便在脫機時也能夠無縫使用Web應用程序。使用IndexedDB時,您會常常看到兩個術語:數據庫存儲和對象存儲。讓咱們在下面進行探討。shell
IndexedDB數據庫對每一個Web應用程序來講都是惟一的。這意味着一個應用程序只能從與本身運行在同一域或子域的 IndexedDB 數據庫中訪問數據。數據庫是容納對象存儲的地方,而對象存儲又包含存儲的數據。要使用IndexedDB數據庫,咱們須要打開(或鏈接到)它們:數據庫
const initializeDb = indexedDB.open('name_of_database', version)
複製代碼
indexedDb.open()
方法中的 name_of_database
參數將用做正在建立的數據庫的名稱,而 version
參數是一個表明數據庫版本的數字。npm
在IndexedDB中,咱們使用對象存儲來構建數據庫的結構,而且每當要更新數據庫結構時,都須要將版本升級到更高的值。這意味着,若是咱們從版本1開始,則下次要更新數據庫的結構時,咱們須要將 indexedDb.open()
方法中的版本更改成2或更高版本。編程
對象存儲相似於關係數據庫(如PostgreSQL)中的表和文檔數據庫(如MongoDB)中的集合。要在IndexedDB中建立對象存儲,咱們須要從以前聲明的 initializeDb
變量中調用 onupgradeneeded()
方法:bootstrap
initializeDb.onupgradeneeded = () => {
const database = initializeDb.result
database.createObjectStore('name_of_object_store', {autoIncrement: true})
}
複製代碼
在上面的代碼塊中,咱們從 initializeDb.result
屬性獲取數據庫,而後使用其 createObjectStore()
方法建立對象存儲。第二個參數 {autoIncrement:true}
告訴IndexedDB自動提供/增長對象存儲中項目的ID。
我省略了諸如事務和遊標之類的其餘術語,由於使用低級IndexedDB API須要進行大量工做。這就是爲何咱們須要Dexie.js,它是IndexedDB的簡約包裝。讓咱們看看Dexie如何簡化建立數據庫,對象存儲,存儲數據以及從數據庫查詢數據的整個過程。
使用Dexie,建立IndexedDB數據庫和對象存儲很是容易:
const db = new Dexie('exampleDatabase')
db.version(1).stores({
name_of_object_store: '++id, name, price',
name_of_another_object_store: '++id, title'
})
複製代碼
在上面的代碼塊中,咱們建立了一個名爲 exampleDatabase
的新數據庫,並將其做爲值分配給 db
變量。咱們使用 db.version(version_number).stores()
方法爲數據庫建立對象存儲。每一個對象存儲的值表明了它的結構。例如,當在第一個對象存儲中存儲數據時,咱們須要提供一個具備屬性 name
和 price
的對象。++id
選項的做用就像咱們在建立對象存儲區時使用的 {autoIncrement:true}
參數同樣。
請注意,在咱們的應用程序中使用dexie包以前,咱們須要安裝並導入它。當咱們開始構建咱們的演示項目時,咱們將看到如何作到這一點。
對於咱們的演示項目,咱們將使用Dexie.js和React構建一個市場列表應用程序。咱們的用戶將可以在市場列表中添加他們打算購買的商品,刪除這些商品或將其標記爲已購買。
咱們將看到如何使用Dexie useLiveQuery
hook來監視IndexedDB數據庫中的更改以及在數據庫更新時從新呈現React組件。這是咱們的應用程序的外觀:
首先,咱們將使用爲應用程序的結構和設計建立的GitHub模板。這裏有一個模板的連接。點擊**Use this template(使用此模板)**按鈕,就會用現有的模板爲你建立一個新的資源庫,而後你就能夠克隆和使用這個模板。
或者,在計算機上安裝了GitHub CLI的狀況下,您能夠運行如下命令從市場列表GitHub模板建立名爲 market-list-app
的存儲庫:
gh repo create market-list-app --template ebenezerdon/market-list-template
複製代碼
完成此操做後,您能夠繼續在代碼編輯器中克隆並打開您的新應用程序。使用終端在應用程序目錄中運行如下命令應安裝npm依賴項並啓動新應用程序:
npm install && npm start
複製代碼
導航到成功消息中的本地URL(一般爲http://localhost:3000)時,您應該可以看到新的React應用程序。您的新應用應以下所示:
當您打開 ./src/App.js
文件時,您會注意到咱們的應用程序組件僅包含市場列表應用程序的JSX代碼。咱們正在使用Materialize框架中的類進行樣式設置,並將其CDN連接包含在 ./public/index.html
文件中。接下來,咱們將看到如何使用Dexie建立和管理數據。
要在咱們的React應用程序中使用Dexie.js進行離線存儲,咱們將從在終端中運行如下命令開始,以安裝 dexie
和 dexie-react-hooks
軟件包:
npm i -s dexie dexie-react-hooks
複製代碼
咱們將使用 dexie-react-hooks
包中的 useLiveQuery
hook來監視更改,並在對IndexedDB數據庫進行更新時從新渲染咱們的React組件。
讓咱們將如下導入語句添加到咱們的 ./src/App.js
文件中。這將導入 Dexie
和 useLiveQuery
hook:
import Dexie from 'dexie'
import { useLiveQuery } from "dexie-react-hooks";
複製代碼
接下來,咱們將建立一個名爲 MarketList
的新數據庫,而後聲明咱們的對象存儲 items
:
const db = new Dexie('MarketList');
db.version(1).stores(
{ items: "++id,name,price,itemHasBeenPurchased" }
)
複製代碼
咱們的 items
對象存儲將期待一個具備屬性name
、price
和 itemHasBeenPurchased
的對象,而 id
將由 Dexie 提供。在將新數據添加到對象存儲中時,咱們將爲 itemHasBeenPurchased
屬性使用默認布爾值 false
,而後在咱們從市場清單中購買商品時將其更新爲 true
。
讓咱們建立一個變量來存儲咱們全部的項目。咱們將使用 useLiveQuery
鉤子從 items
對象存儲中獲取數據,並觀察其中的變化,這樣當 items
對象存儲有更新時,咱們的 allItems
變量將被更新,咱們的組件將用新的數據從新渲染。咱們將在 App
組件內部進行:
const App = () => {
const allItems = useLiveQuery(() => db.items.toArray(), []);
if (!allItems) return null
...
}
複製代碼
在上面的代碼塊中,咱們建立了一個名爲 allItems
的變量,並將 useLiveQuery
鉤子做爲其值。useLiveQuery
鉤子的語法相似於React的useEffect鉤子,它指望一個函數及其依賴項數組做爲參數。咱們的函數參數返回數據庫查詢。
在這裏,咱們以數組格式獲取 items
對象存儲中的全部數據。在下一行中,咱們使用一個條件來告訴咱們的組件,若是 allItems
變量是undefined,則意味着查詢仍在加載中。
仍在App組件中,讓咱們建立一個名爲 addItemToDb
的函數,咱們將使用該函數向數據庫中添加項目。每當咱們點擊「ADD ITEM(添加項目)」按鈕時,咱們都會調用此函數。請記住,每次更新數據庫時,咱們的組件都會從新渲染。
...
const addItemToDb = async event => {
event.preventDefault()
const name = document.querySelector('.item-name').value
const price = document.querySelector('.item-price').value
await db.items.add({
name,
price: Number(price),
itemHasBeenPurchased: false
})
}
...
複製代碼
在 addItemToDb
函數中,咱們從表單輸入字段中獲取商品名稱和價格值,而後使用 db.[name_of_object_store].add
方法將新商品數據添加到商品對象存儲中。咱們還將 itemHasBeenPurchased
屬性的默認值設置爲 false
。
如今咱們有了 addItemToDb
函數,讓咱們建立一個名爲 removeItemFromDb
的函數以從咱們的商品對象存儲中刪除數據:
...
const removeItemFromDb = async id => {
await db.items.delete(id)
}
...
複製代碼
接下來,咱們將建立一個名爲 markAsPurchased
的函數,用於將商品標記爲已購買。咱們的函數在調用時,會將物品的主鍵做爲第一個參數——在本例中是 id
,它將使用這個主鍵來查詢咱們想要標記爲購買的物品的數據庫。取得商品後,它將其 markAsPurchased
屬性更新爲 true
:
...
const markAsPurchased = async (id, event) => {
if (event.target.checked) {
await db.items.update(id, {itemHasBeenPurchased: true})
}
else {
await db.items.update(id, {itemHasBeenPurchased: false})
}
}
...
複製代碼
在 markAsPurchased
函數中,咱們使用 event
參數來獲取用戶單擊的特定輸入元素。若是選中其值,咱們將itemHasBeenPurchased
屬性更新爲 true
,不然更新爲 false
。db.[name_of_object_store] .update()
方法指望該項目的主鍵做爲其第一個參數,而新對象數據做爲其第二個參數。
下面是咱們的 App
組件在這個階段應該是什麼樣子。
...
const App = () => {
const allItems = useLiveQuery(() => db.items.toArray(), []);
if (!allItems) return null
const addItemToDb = async event => {
event.preventDefault()
const name = document.querySelector('.item-name').value
const price = document.querySelector('.item-price').value
await db.items.add({ name, price, itemHasBeenPurchased: false })
}
const removeItemFromDb = async id => {
await db.items.delete(id)
}
const markAsPurchased = async (id, event) => {
if (event.target.checked) {
await db.items.update(id, {itemHasBeenPurchased: true})
}
else {
await db.items.update(id, {itemHasBeenPurchased: false})
}
}
...
}
複製代碼
如今,咱們建立一個名爲 itemData
的變量,以容納咱們全部商品數據的JSX代碼:
...
const itemData = allItems.map(({ id, name, price, itemHasBeenPurchased }) => (
<div className="row" key={id}> <p className="col s5"> <label> <input type="checkbox" checked={itemHasBeenPurchased} onChange={event => markAsPurchased(id, event)} /> <span className="black-text">{name}</span> </label> </p> <p className="col s5">${price}</p> <i onClick={() => removeItemFromDb(id)} className="col s2 material-icons delete-button"> delete </i> </div>
))
...
複製代碼
在 itemData
變量中,咱們映射了 allItems
數據數組中的全部項目,而後從每一個 item
對象獲取屬性 id
、name
、price
和 itemHasBeenPurchased
。而後,咱們繼續進行操做,並用數據庫中的新動態值替換了之前的靜態數據。
注意,咱們還使用了 markAsPurchased
和 removeItemFromDb
方法做爲相應按鈕的單擊事件偵聽器。咱們將在下一個代碼塊中將 addItemToDb
方法添加到表單的 onSubmit
事件中。
準備好 itemData
後,讓咱們將 App
組件的return語句更新爲如下JSX代碼:
...
return (
<div className="container"> <h3 className="green-text center-align">Market List App</h3> <form className="add-item-form" onSubmit={event => addItemToDb(event)} > <input type="text" className="item-name" placeholder="Name of item" required/> <input type="number" step=".01" className="item-price" placeholder="Price in USD" required/> <button type="submit" className="waves-effect waves-light btn right">Add item</button> </form> {allItems.length > 0 && <div className="card white darken-1"> <div className="card-content"> <form action="#"> { itemData } </form> </div> </div> } </div>
)
...
複製代碼
在return語句中,咱們已將 itemData
變量添加到咱們的項目列表中(items list)。咱們還使用 addItemToDb
方法做爲 add-item-form
的 onsubmit
值。
爲了測試咱們的應用程序,咱們能夠返回到咱們先前打開的React網頁。請記住,您的React應用必須正在運行,若是不是,請在終端上運行命令 npm start
。您的應用應該可以像下面的演示同樣運行:
咱們還可使用條件用Dexie查詢咱們的IndexedDB數據庫。例如,若是咱們要獲取價格高於10美圓的全部商品,則能夠執行如下操做:
const items = await db.friends
.where('price').above(10)
.toArray();
複製代碼
您能夠在Dexie文檔中查看其餘查詢方法。
在本文中,咱們學習瞭如何使用IndexedDB進行離線存儲以及Dexie.js如何簡化該過程。咱們還了解了如何使用Dexie useLiveQuery
鉤子來監視更改並在每次更新數據庫時從新渲染React組件。
因爲IndexedDB是瀏覽器原生的,從數據庫中查詢和檢索數據比每次須要在應用中處理數據時都要發送服務器端API請求要快得多,並且咱們幾乎能夠在IndexedDB數據庫中存儲任何東西。
過去使用IndexedDB可能對瀏覽器的支持是一個大問題,可是如今全部主流瀏覽器都支持它。在Web應用中使用IndexedDB進行離線存儲的諸多優點大於劣勢,將Dexie.js與IndexedDB一塊兒使用,使得Web開發變得史無前例的有趣。
這是咱們的演示應用程序的GitHub庫的連接。