IndexedDB - ECMAScript 绑定
使用 Promises 和 Async/Await 现代化 IndexedDB
Section titled “使用 Promises 和 Async/Await 现代化 IndexedDB”原生 IndexedDB API 功能强大,但其基于事件的回调风格可能过于冗长,并导致代码深度嵌套,通常被称为“回调地狱”(callback hell)。现代 JavaScript (ECMAScript 2017+) 提供了 Promises 和 async/await 等特性,可以极大地简化异步代码,使其更具可读性和可维护性。
挑战:基于回调的 API
Section titled “挑战:基于回调的 API”考虑一个简单的任务:打开数据库,添加一个项目,然后将其读回。使用原生 API,这需要嵌套 onsuccess 处理程序,并且在每个层级都有单独的 onerror 处理程序。
const request = indexedDB.open('myDB', 1);request.onsuccess = (e) => { const db = e.target.result; const tx = db.transaction('store', 'readwrite'); const store = tx.objectStore('store'); const addRequest = store.add({ id: 1, name: 'test' }); addRequest.onsuccess = () => { console.log('Added!'); // 现在我们来读取它... const getRequest = store.get(1); getRequest.onsuccess = (e) => { console.log('Got item:', e.target.result); } getRequest.onerror = (e) => { /* 处理获取错误 */ }; } addRequest.onerror = (e) => { /* 处理添加错误 */ };};这种代码难以阅读,对于更复杂的序列也更难管理。
解决方案:用 Promises 封装请求
Section titled “解决方案:用 Promises 封装请求”Promise 是一种表示异步操作最终完成(或失败)的对象。我们可以创建一个辅助函数,它接受一个 IDBRequest 并返回一个 Promise。
function promisifyRequest(request) { return new Promise((resolve, reject) => { request.onsuccess = (event) => { resolve(event.target.result); }; request.onerror = (event) => { reject(event.target.error); }; });}这个简单的包装器允许我们使用 .then() 处理成功,使用 .catch() 处理错误,这是一个巨大的改进。
终极便利:Async/Await
Section titled “终极便利:Async/Await”async/await 语法建立在 Promises 之上,它允许我们编写看起来和行为都像同步代码的异步代码。这是 JavaScript 中处理异步操作的现代标准。
示例:使用 Async/Await 重构
Section titled “示例:使用 Async/Await 重构”让我们将“添加和获取”任务重构为一个清晰的 async 函数。请注意 try...catch 如何提供统一的错误处理。
// 上面的辅助函数function promisifyRequest(request) { return new Promise((resolve, reject) => { request.onsuccess = () => resolve(request.result); request.onerror = () => reject(request.error); });}
// 我们还需要 Promise 化事务function promisifyTransaction(tx) { return new Promise((resolve, reject) => { tx.oncomplete = resolve; tx.onerror = () => reject(tx.error); });}
async function addAndGetItem(db, item) { try { const tx = db.transaction('store', 'readwrite'); const store = tx.objectStore('store');
await promisifyRequest(store.add(item)); console.log('Item added!');
const retrievedItem = await promisifyRequest(store.get(item.id)); console.log('Retrieved item:', retrievedItem);
await promisifyTransaction(tx); console.log('Transaction complete.'); return retrievedItem;
} catch (error) { console.error('An error occurred:', error); throw error; // 重新抛出错误以供调用者处理 }}
// 用法:// const db = await promisifyRequest(indexedDB.open('myDB', 1));// await addAndGetItem(db, { id: 1, name: 'test' });最佳实践:使用包装库
Section titled “最佳实践:使用包装库”尽管编写自己的 Promise 包装器是一个很好的学习练习,但对于实际项目,强烈建议使用经过良好测试的包装库。最流行的是 Jake Archibald 的 idb 库。
它提供了一个轻量级的、基于 Promise 的 IndexedDB 封装层,使所有操作变得异常简单,同时在需要时仍能完全访问底层 API。你可以通过 npm 将其添加到你的项目中:npm install idb。
示例:使用 idb 库完成相同任务
Section titled “示例:使用 idb 库完成相同任务”比较前面示例的冗长与使用 idb 的此版本的优雅和清晰。
import { openDB } from 'idb';
async function addAndGetItemWithIdb(item) { try { const db = await openDB('myDB', 1, { upgrade(db) { db.createObjectStore('store', { keyPath: 'id' }); }, });
// 该库为你处理事务! await db.add('store', item); console.log('Item added!');
const retrievedItem = await db.get('store', item.id); console.log('Retrieved item:', retrievedItem);
return retrievedItem;
} catch (error) { console.error('An error occurred:', error); }}
// 用法:// await addAndGetItemWithIdb({ id: 1, name: 'test' });对于任何认真的 IndexedDB 开发,采用 async/await 等现代实践并使用像 idb 这样的辅助库不仅仅是建议——它对于构建健壮且可维护的应用程序至关重要。