博客原文: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}) => (<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 库的链接。