博客原文: https://blog.zhangbing.site/2021/03/16/dexie-js-indexeddb-react-apps-offline-data-storage/
离线存储应用程序数据已成为现代Web开发中的必要条件。内置的浏览器 localStorage
可以用作简单轻量数据的数据存储,但是在结构化数据或存储大量数据方面却不足。
最重要的是,我们只能将字符串数据存储在受XSS攻击的 localStorage
中,并且它没有提供很多查询数据的功能。
这就是IndexedDB的亮点。使用IndexedDB,我们可以在浏览器中创建结构化的数据库,将几乎所有内容存储在这些数据库中,并对数据执行各种类型的查询。
在本文中,我们将了解IndexedDB的全部含义,以及如何使用Dexie.js(用于IndexedDB的简约包装)处理Web应用程序中的离线数据存储。
IndexedDB如何工作
IndexedDB是用于浏览器的内置非关系数据库。它使开发人员能够将数据持久存储在浏览器中,即使在脱机时也可以无缝使用Web应用程序。使用IndexedDB时,您会经常看到两个术语:数据库存储和对象存储。让我们在下面进行探讨。
使用IndexedDB创建数据库
IndexedDB数据库对每个Web应用程序来说都是唯一的。这意味着一个应用程序只能从与自己运行在同一域或子域的 IndexedDB 数据库中访问数据。数据库是容纳对象存储的地方,而对象存储又包含存储的数据。要使用IndexedDB数据库,我们需要打开(或连接到)它们:
const initializeDb = indexedDB.open('name_of_database', version)
indexedDb.open()
方法中的 name_of_database
参数将用作正在创建的数据库的名称,而 version
参数是一个代表数据库版本的数字。
在IndexedDB中,我们使用对象存储来构建数据库的结构,并且每当要更新数据库结构时,都需要将版本升级到更高的值。这意味着,如果我们从版本1开始,则下次要更新数据库的结构时,我们需要将 indexedDb.open()
方法中的版本更改为2或更高版本。
使用IndexedDB创建对象存储
对象存储类似于关系数据库(如PostgreSQL)中的表和文档数据库(如MongoDB)中的集合。要在IndexedDB中创建对象存储,我们需要从之前声明的 initializeDb
变量中调用 onupgradeneeded()
方法:
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离线存储数据
使用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创建和管理数据。
用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
。
Dexie React hook
让我们创建一个变量来存储我们所有的项目。我们将使用 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,则意味着查询仍在加载中。
将items添加到我们的数据库
仍在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
。
从我们的数据库中删除items
现在我们有了 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 }) => (
${price}
removeItemFromDb(id)} className="col s2 material-icons delete-button">
delete
))
...
在 itemData
变量中,我们映射了 allItems
数据数组中的所有项目,然后从每个 item
对象获取属性 id
、name
、price
和 itemHasBeenPurchased
。然后,我们继续进行操作,并用数据库中的新动态值替换了以前的静态数据。
注意,我们还使用了 markAsPurchased
和 removeItemFromDb
方法作为相应按钮的单击事件侦听器。我们将在下一个代码块中将 addItemToDb
方法添加到表单的 onSubmit
事件中。
准备好 itemData
后,让我们将 App
组件的return语句更新为以下JSX代码:
...
return (
Market List App
{allItems.length > 0 &&
}
)
...
在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库的链接。