IndexedDB 用法介绍

本指南介绍 IndexedDB API 的基础知识。我们使用的是 Jake Archibald 的 IndexedDB Promised 库,该库与 IndexedDB API 非常相似,但使用了 promise,您可以通过 await 来实现更简洁的语法。这样可以简化 API,同时保持其结构。

IndexedDB 是一个大规模的 NoSQL 存储系统,几乎可以存储用户浏览器中的任何内容。除了常见的搜索、获取和放置操作之外,IndexedDB 还支持事务,非常适合存储大量结构化数据。

每个 IndexedDB 数据库都是唯一的来源(通常是网站网域或子网域),这意味着任何其他来源无法访问或访问该数据库。其数据存储限制通常很大(如果存在),但不同的浏览器处理限制和数据逐出的方式不同。如需了解详情,请参阅深入阅读部分。

数据库
最高级别的 IndexedDB。它包含对象存储,而对象存储又包含要保留的数据。您可使用任意名称创建多个数据库。
对象存储
用于存储数据的各个存储分区,类似于关系型数据库中的表。通常,您要存储的每种类型(而不是 JavaScript 数据类型)的数据都有一个对象存储。与数据库表不同,存储区中数据的 JavaScript 数据类型不需要一致。例如,如果某个应用的 people 对象存储区包含有关三个人的信息,则这些人的年龄属性可以是 53'twenty-five' 和 unknown
索引
一种对象存储,用于按数据的各个属性来组织另一个对象存储(称为引用对象存储)中的数据。索引用于按此属性检索对象存储区中的记录。例如,如果您要存储用户,以后可能希望按用户的姓名、年龄或喜欢的动物来提取用户。
操作
与数据库的交互。
事务
用于确保数据库完整性的一个或一组操作的封装容器。如果事务中的某个操作失败,则系统不会应用任何操作,并且数据库会恢复到事务开始前的状态。IndexedDB 中的所有读取或写入操作都必须是事务的一部分。这样可以实现原子读取-修改-写入操作,而不会面临与其他同时对数据库执行操作的线程冲突的风险。
Cursor
一种用于遍历数据库中多条记录的机制。

IndexedDB 几乎受到广泛支持。但是,如果您使用的是旧版浏览器,最好不要为了以防万一而进行功能检测支持。最简单的方法是检查 window 对象:

function indexedDBStuff () {
  // Check for IndexedDB support:
  if (!('indexedDB' in window)) {
    // Can't use IndexedDB
    console.log("This browser doesn't support IndexedDB");
    return;
  } else {
    // Do IndexedDB stuff here:
    // ...
  }
}

// Run IndexedDB code:
indexedDBStuff();

借助 IndexedDB,您可以创建多个采用任何名称的数据库。当您尝试打开某个数据库时,如果它不存在,系统会自动创建一个该数据库。 如需打开数据库,请使用 idb 库中的 openDB() 方法:

import {openDB} from 'idb';

async function useDB () {
  // Returns a promise, which makes `idb` usable with async-await.
  const dbPromise = await openDB('example-database', version, events);
}

useDB();

此方法会返回一个解析为数据库对象的 promise。使用 openDB() 方法时,请提供名称、版本号和事件对象以设置数据库。

以下是上下文中的 openDB() 方法示例:

import {openDB} from 'idb';

async function useDB () {
  // Opens the first version of the 'test-db1' database.
  // If the database does not exist, it will be created.
  const dbPromise = await openDB('test-db1', 1);
}

useDB();

在匿名函数的顶部检查是否支持 IndexedDB。如果浏览器不支持 IndexedDB,这将退出函数。如果函数可以继续运行,它会调用 openDB() 方法来打开名为 'test-db1' 的数据库。为简单起见,此示例中删除了可选事件对象,但您需要指定该对象,才能使用 IndexedDB 执行任何有意义的工作。

IndexedDB 数据库包含一个或多个对象存储,其中每个存储都有一列用于键,另一列用于存放与该键关联的数据。

结构合理的 IndexedDB 数据库应该为需要持久保留的每种数据类型分别有一个对象存储。例如,一个保留用户个人资料和备注的网站可能具有一个包含 person 对象的 people 对象存储区,以及一个包含 note 对象的 notes 对象存储区。

为了确保数据库的完整性,您只能通过 openDB() 调用在事件对象中创建或移除对象存储。事件对象公开了 upgrade() 方法,可用于创建对象存储。调用 upgrade() 方法中的 createObjectStore() 方法来创建对象存储:

import {openDB} from 'idb';

async function createStoreInDB () {
  const dbPromise = await openDB('example-database', 1, {
    upgrade (db) {
      // Creates an object store:
      db.createObjectStore('storeName', options);
    }
  });
}

createStoreInDB();

此方法会获取对象存储的名称和一个可选的配置对象,该对象允许您为对象存储定义各种属性。

以下示例展示了如何使用 createObjectStore()

import {openDB} from 'idb';

async function createStoreInDB () {
  const dbPromise = await openDB('test-db1', 1, {
    upgrade (db) {
      console.log('Creating a new object store...');

      // Checks if the object store exists:
      if (!db.objectStoreNames.contains('people')) {
        // If the object store does not exist, create it:
        db.createObjectStore('people');
      }
    }
  });
}

createStoreInDB();

在此示例中,将事件对象传递给 openDB() 方法以创建对象存储,并且与之前一样,创建对象存储的工作是在事件对象的 upgrade() 方法中完成的。但是,由于如果您尝试创建已存在的对象存储区,浏览器会抛出错误,因此我们建议您将 createObjectStore() 方法封装在 if 语句中,用于检查对象存储区是否存在。在 if 代码块内,调用 createObjectStore() 以创建名为 'firstOS' 的对象存储。

定义对象存储时,您可以定义如何使用主键在存储区中唯一标识数据。您可以通过定义键路径或使用密钥生成器来定义主键。

键路径是始终存在且包含唯一值的属性。例如,如果是 people 对象存储,您可以选择电子邮件地址作为密钥路径:

import {openDB} from 'idb';

async function createStoreInDB () {
  const dbPromise = await openDB('test-db2', 1, {
    upgrade (db) {
      if (!db.objectStoreNames.contains('people')) {
        db.createObjectStore('people', { keyPath: 'email' });
      }
    }
  });
}

createStoreInDB();

此示例会创建一个名为 'people' 的对象存储,并将 email 属性指定为 keyPath 选项中的主键。

您还可以使用密钥生成器,例如 autoIncrement。密钥生成器会为添加到对象存储的每个对象创建一个唯一值。默认情况下,如果您未指定键,IndexedDB 会创建一个键,并将其与数据分开存储。

以下示例会创建一个名为 'notes' 的对象存储,并将主键设置为自动递增数字:

import {openDB} from 'idb';

async function createStoreInDB () {
  const dbPromise = await openDB('test-db2', 1, {
    upgrade (db) {
      if (!db.objectStoreNames.contains('notes')) {
        db.createObjectStore('notes', { autoIncrement: true });
      }
    }
  });
}

createStoreInDB();

以下示例与上一个示例类似,但这次自动递增值被明确分配给名为 'id' 的属性。

import {openDB} from 'idb';

async function createStoreInDB () {
  const dbPromise = await openDB('test-db2', 1, {
    upgrade (db) {
      if (!db.objectStoreNames.contains('logs')) {
        db.createObjectStore('logs', { keyPath: 'id', autoIncrement: true });
      }
    }
  });
}

createStoreInDB();

选择使用哪种方法来定义键取决于您的数据。如果您的数据具有始终唯一的属性,您可以将其设为 keyPath 以强制执行此唯一性。否则,请使用自动递增值。

以下代码会创建三个对象存储,演示在对象存储中定义主键的各种方法:

import {openDB} from 'idb';

async function createStoresInDB () {
  const dbPromise = await openDB('test-db2', 1, {
    upgrade (db) {
      if (!db.objectStoreNames.contains('people')) {
        db.createObjectStore('people', { keyPath: 'email' });
      }

      if (!db.objectStoreNames.contains('notes')) {
        db.createObjectStore('notes', { autoIncrement: true });
      }

      if (!db.objectStoreNames.contains('logs')) {
        db.createObjectStore('logs', { keyPath: 'id', autoIncrement: true });
      }
    }
  });
}

createStoresInDB();

索引是一种对象存储,用于按指定属性从引用对象存储中检索数据。索引位于引用对象存储区内且包含相同的数据,但使用指定属性作为其键路径,而不是引用存储区的主键。您必须在创建对象存储时创建索引,索引可用于定义针对数据的唯一限制条件。

如需创建索引,请对对象存储实例调用 createIndex() 方法:

import {openDB} from 'idb';

async function createIndexInStore() {
  const dbPromise = await openDB('storeName', 1, {
    upgrade (db) {
      const objectStore = db.createObjectStore('storeName');

      objectStore.createIndex('indexName', 'property', options);
    }
  });
}

createIndexInStore();

此方法会创建并返回索引对象。对象存储实例上的 createIndex() 方法将新索引的名称作为第一个参数,第二个参数引用您要编入索引的数据的属性。最后一个参数可让您定义两个用于确定索引运行方式的选项:unique 和 multiEntry。如果 unique 设置为 true,则索引不允许单个键出现重复值。接下来,multiEntry 确定当编入索引的属性为数组时 createIndex() 的行为方式。如果设置为 true,则 createIndex() 会为每个数组元素在索引中添加一个条目。否则,它会添加一个包含数组的条目。

示例如下:

import {openDB} from 'idb';

async function createIndexesInStores () {
  const dbPromise = await openDB('test-db3', 1, {
    upgrade (db) {
      if (!db.objectStoreNames.contains('people')) {
        const peopleObjectStore = db.createObjectStore('people', { keyPath: 'email' });

        peopleObjectStore.createIndex('gender', 'gender', { unique: false });
        peopleObjectStore.createIndex('ssn', 'ssn', { unique: true });
      }

      if (!db.objectStoreNames.contains('notes')) {
        const notesObjectStore = db.createObjectStore('notes', { autoIncrement: true });

        notesObjectStore.createIndex('title', 'title', { unique: false });
      }

      if (!db.objectStoreNames.contains('logs')) {
        const logsObjectStore = db.createObjectStore('logs', { keyPath: 'id', autoIncrement: true });
      }
    }
  });
}

createIndexesInStores();

在此示例中,'people' 和 'notes' 对象存储具有索引。要创建索引,请先将 createObjectStore()(对象存储对象)的结果分配给变量,以便对其调用 createIndex()

本部分介绍如何创建、读取、更新和删除数据。这些操作都是异步的,并且 IndexedDB API 使用请求的位置使用 promise。这样可以简化 API。您可以对 openDB() 方法返回的数据库对象调用 .then() 以开始与数据库交互,或 await 其创建过程,而不是监听请求触发的事件。

IndexedDB 中的所有数据操作都在事务内执行。每项操作都采用以下格式:

  1. 获取数据库对象。
  2. 打开数据库上的事务。
  3. 在事务上打开对象存储。
  4. 对对象存储执行操作。

事务可以视为一个操作或一组操作的安全封装容器。如果事务中的某个操作失败,则所有操作都会回滚。事务特定于一个或多个对象存储,您可以在打开事务时定义这些对象存储。它们可以是只读的,也可以是读写的。它指示事务内的操作是读取数据还是对数据库进行更改。

如需创建数据,请对数据库实例调用 add() 方法,并传入要添加的数据。add() 方法的第一个参数是要向其中添加数据的对象存储,第二个参数是包含要添加的字段和关联数据的对象。下面是最简单的示例,其中添加了一行数据:

import {openDB} from 'idb';

async function addItemToStore () {
  const db = await openDB('example-database', 1);

  await db.add('storeName', {
    field: 'data'
  });
}

addItemToStore();

每次 add() 调用都发生在事务内,因此,即使 promise 成功解析,也不一定表示操作可以执行。为了确保添加操作已执行,您需要使用 transaction.done() 方法检查整个事务是否已完成。这是一个 promise,会在事务自行完成时解析,并在出现事务错误时拒绝。您必须对所有“写入”操作执行此检查,因为这是了解数据库更改实际发生的唯一方式。

以下代码展示了如何在事务中使用 add() 方法:

import {openDB} from 'idb';

async function addItemsToStore () {
  const db = await openDB('test-db4', 1, {
    upgrade (db) {
      if (!db.objectStoreNames.contains('foods')) {
        db.createObjectStore('foods', { keyPath: 'name' });
      }
    }
  });
  
  // Create a transaction on the 'foods' store in read/write mode:
  const tx = db.transaction('foods', 'readwrite');

  // Add multiple items to the 'foods' store in a single transaction:
  await Promise.all([
    tx.store.add({
      name: 'Sandwich',
      price: 4.99,
      description: 'A very tasty sandwich!',
      created: new Date().getTime(),
    }),
    tx.store.add({
      name: 'Eggs',
      price: 2.99,
      description: 'Some nice eggs you can cook up!',
      created: new Date().getTime(),
    }),
    tx.done
  ]);
}

addItemsToStore();

打开数据库(并根据需要创建对象存储)后,您需要调用事务的 transaction() 方法以将其打开。此方法会接受您要进行交易的商店及模式的参数。在本例中,我们希望向存储区写入数据,因此本示例指定了 'readwrite'

下一步是开始将商品作为交易的一部分添加到商店。 在前面的示例中,我们在 'foods' 存储区上处理了三项操作,每项操作都返回一个 promise:

  1. 再添一份美味三明治的记录。
  2. 正在为一些鸡蛋添加记录。
  3. 表明交易已完成 (tx.done)。

由于所有这些操作均基于 promise,我们需要等待所有操作完成。将这些 promise 传递给 Promise.all 是一种很符合工效学的好方法。Promise.all 接受 promise 数组,并在传递给它的所有 promise 都已解析后完成。

对于添加的两条记录,事务实例的 store 接口会调用 add() 并将数据传递给它。您可以 await Promise.all 调用,使其在事务完成时完成。

如需读取数据,请使用 openDB() 方法对数据库实例调用 get() 方法。get() 接受存储区的名称以及要检索的对象的主键值。下面是一个基本示例:

import {openDB} from 'idb';

async function getItemFromStore () {
  const db = await openDB('example-database', 1);

  // Get a value from the object store by its primary key value:
  const value = await db.get('storeName', 'unique-primary-key-value');
}

getItemFromStore();

与 add() 一样,get() 方法会返回一个 promise,因此您可以根据需要将其 await,或使用 promise 的 .then() 回调。

以下示例对 'test-db4' 数据库的 'foods' 对象存储使用 get() 方法,按 'name' 主键获取单个行:

import {openDB} from 'idb';

async function getItemFromStore () {
  const db = await openDB('test-db4', 1);
  const value = await db.get('foods', 'Sandwich');

  console.dir(value);
}

getItemFromStore();

从数据库检索单个行非常简单:打开数据库并指定要从中获取数据的行的对象存储区和主键值。由于 get() 方法会返回一个 promise,因此可以对其执行 await 操作。

如需更新数据,请对对象存储调用 put() 方法。put() 方法与 add() 方法类似,也可取代 add() 来创建数据。下面是一个基本示例,展示了如何使用 put() 按主键值更新对象存储中的行:

import {openDB} from 'idb';

async function updateItemInStore () {
  const db = await openDB('example-database', 1);

  // Update a value from in an object store with an inline key:
  await db.put('storeName', { inlineKeyName: 'newValue' });

  // Update a value from in an object store with an out-of-line key.
  // In this case, the out-of-line key value is 1, which is the
  // auto-incremented value.
  await db.put('otherStoreName', { field: 'value' }, 1);
}

updateItemInStore();

与其他方法一样,此方法会返回 promise。您还可以在事务中使用 put()。以下示例使用之前的 'foods' 商店更新了三明治和鸡蛋的价格:

import {openDB} from 'idb';

async function updateItemsInStore () {
  const db = await openDB('test-db4', 1);
  
  // Create a transaction on the 'foods' store in read/write mode:
  const tx = db.transaction('foods', 'readwrite');

  // Update multiple items in the 'foods' store in a single transaction:
  await Promise.all([
    tx.store.put({
      name: 'Sandwich',
      price: 5.99,
      description: 'A MORE tasty sandwich!',
      updated: new Date().getTime() // This creates a new field
    }),
    tx.store.put({
      name: 'Eggs',
      price: 3.99,
      description: 'Some even NICER eggs you can cook up!',
      updated: new Date().getTime() // This creates a new field
    }),
    tx.done
  ]);
}

updateItemsInStore();

项的更新方式取决于您如何设置键。如果您设置了 keyPath,则对象存储中的每一行都与一个内嵌键相关联。上述示例会根据此键更新行,在这种情况下,当您更新行时,您需要指定该键以更新对象存储区中的相应项。您还可以通过将 autoIncrement 设置为主键来创建外行键

如需删除数据,请对对象存储调用 delete() 方法:

import {openDB} from 'idb';

async function deleteItemFromStore () {
  const db = await openDB('example-database', 1);

  // Delete a value 
  await db.delete('storeName', 'primary-key-value');
}

deleteItemFromStore();

与 add() 和 put() 一样,您可以在事务中使用此函数:

import {openDB} from 'idb';

async function deleteItemsFromStore () {
  const db = await openDB('test-db4', 1);
  
  // Create a transaction on the 'foods' store in read/write mode:
  const tx = db.transaction('foods', 'readwrite');

  // Delete multiple items from the 'foods' store in a single transaction:
  await Promise.all([
    tx.store.delete('Sandwich'),
    tx.store.delete('Eggs'),
    tx.done
  ]);
}

deleteItemsFromStore();

数据库交互的结构与其他操作相同。请记得在传递给 Promise.all 的数组中加入 tx.done 方法,以检查整个事务是否已完成。

到目前为止,您一次只从存储区中检索了一个对象。您还可以使用 getAll() 方法或游标从对象存储或索引中检索所有数据或数据子集。

要检索对象存储的所有数据,最简单的方法是对对象存储或索引调用 getAll(),如下所示:

import {openDB} from 'idb';

async function getAllItemsFromStore () {
  const db = await openDB('test-db4', 1);

  // Get all values from the designated object store:
  const allValues = await db.getAll('storeName');

  console.dir(allValues);
}

getAllItemsFromStore();

此方法会返回对象存储区中的所有对象,没有任何限制。它是从对象存储中获取所有值的最直接方法,但灵活性也最不理想。

import {openDB} from 'idb';

async function getAllItemsFromStore () {
  const db = await openDB('test-db4', 1);

  // Get all values from the designated object store:
  const allValues = await db.getAll('foods');

  console.dir(allValues);
}

getAllItemsFromStore();

此示例对 'foods' 对象存储区调用了 getAll()。这将返回 'foods' 中的所有对象,按主键排序。

游标是一种更灵活的检索多个对象的方式。游标会逐个选择对象存储或索引中的每个对象,以便您在数据被选中后对其进行某些操作。与其他数据库操作一样,游标也在事务中运行。

如需创建游标,请作为事务的一部分对对象存储调用 openCursor()。使用前面示例中的 'foods' 存储,可以使游标遍历对象存储中的所有数据行:

import {openDB} from 'idb';

async function getAllItemsFromStoreWithCursor () {
  const db = await openDB('test-db4', 1);
  const tx = await db.transaction('foods', 'readonly');

  // Open a cursor on the designated object store:
  let cursor = await tx.store.openCursor();

  // Iterate on the cursor, row by row:
  while (cursor) {
    // Show the data in the row at the current cursor position:
    console.log(cursor.key, cursor.value);

    // Advance the cursor to the next row:
    cursor = await cursor.continue();
  }
}

getAllItemsFromStoreWithCursor();

在本例中,事务会在 'readonly' 模式下打开,并调用其 openCursor 方法。在随后的 while 循环中,游标当前位置的行可以读取其 key 和 value 属性,并且您可以通过最适合应用的方式对这些值执行操作。准备就绪后,您可以调用 cursor 对象的 continue() 方法来转到下一行,当游标到达数据集末尾时,while 循环就会终止。

借助索引,您可以通过主键以外的属性提取对象存储区中的数据。您可以针对任何属性创建索引(该属性将成为索引的 keyPath),在该属性上指定一个范围,并使用 getAll() 或游标获取该范围内的数据。

您可以使用 IDBKeyRange 对象以及以下任意方法来定义您的范围:

upperBound() 和 lowerBound() 方法用于指定范围的上限和下限。

IDBKeyRange.lowerBound(indexKey);

或者:

IDBKeyRange.upperBound(indexKey);

它们各自采用一个参数:您要指定为上限或下限的项的索引 keyPath 值。

bound() 方法同时指定上限和下限:

IDBKeyRange.bound(lowerIndexKey, upperIndexKey);

默认情况下,这些函数的范围包含边界值,这意味着它包含指定为范围上限的数据。如需省去这些值,请将 true 指定为 lowerBound() 或 upperBound() 的第二个参数,或 bound() 的第三个和第四个参数(分别用于下限和上限),将该范围指定为独占模式。

下一个示例对 'foods' 对象存储区中的 'price' 属性使用索引。商店现在还附加了一个表单,该表单包含针对范围的上限和下限的两个输入。使用以下代码查找价格介于上述限额之间的食品:

import {openDB} from 'idb';

async function searchItems (lower, upper) {
  if (!lower === '' && upper === '') {
    return;
  }

  let range;

  if (lower !== '' && upper !== '') {
    range = IDBKeyRange.bound(lower, upper);
  } else if (lower === '') {
    range = IDBKeyRange.upperBound(upper);
  } else {
    range = IDBKeyRange.lowerBound(lower);
  }

  const db = await openDB('test-db4', 1);
  const tx = await db.transaction('foods', 'readonly');
  const index = tx.store.index('price');

  // Open a cursor on the designated object store:
  let cursor = await index.openCursor(range);

  if (!cursor) {
    return;
  }

  // Iterate on the cursor, row by row:
  while (cursor) {
    // Show the data in the row at the current cursor position:
    console.log(cursor.key, cursor.value);

    // Advance the cursor to the next row:
    cursor = await cursor.continue();
  }
}

// Get items priced between one and four dollars:
searchItems(1.00, 4.00);

示例代码会先获取限制的值,并检查是否存在限制。下一个代码块会根据值决定使用哪种方法来限制范围。在数据库交互中,像往常一样打开事务上的对象存储,然后打开对象存储上的 'price' 索引。借助 'price' 索引,您可以按价格搜索商品。

然后代码会在索引上打开一个光标,并传入该范围。游标会返回一个 promise,表示范围内第一个对象;如果范围内没有任何数据,则返回 undefinedcursor.continue() 方法会返回一个表示下一个对象的游标,并一直重复该循环,直到到达该范围的末尾。

当您调用 openDB() 方法时,可以在第二个参数中指定数据库版本号。在本指南的所有示例中,版本均设置为 1,但如果您需要以某种方式对数据库进行修改,可以将其升级到新版本。如果指定的版本高于现有数据库的版本,则系统会执行事件对象中的 upgrade 回调函数,以便您向数据库添加新的对象存储和索引。

upgrade 回调中的 db 对象具有一个特殊的 oldVersion 属性,该属性指示浏览器有权访问的数据库的版本号。您可以将此版本号传递到 switch 语句,以根据现有数据库版本号在 upgrade 回调内执行代码块。示例如下:

import {openDB} from 'idb';

const db = await openDB('example-database', 2, {
  upgrade (db, oldVersion) {
    switch (oldVersion) {
      case 0:
        // Create first object store:
        db.createObjectStore('store', { keyPath: 'name' });

      case 1:
        // Get the original object store, and create an index on it:
        const tx = await db.transaction('store', 'readwrite');
        tx.store.createIndex('name', 'name');
    }
  }
});

此示例将数据库的最新版本设置为 2。首次执行此代码时,浏览器中尚不存在数据库,因此 oldVersion 为 0switch 语句从 case 0 开始。在此示例中,这会将 'store' 对象存储添加到数据库中。

要点:在 switch 语句中,每个 case 块后面通常会有一个 break,但此处特意不使用此块。这样,如果现有数据库落后几个版本或者不存在,则代码会继续运行其余的 case 块,直到处于最新状态。因此,在此示例中,浏览器会通过 case 1 继续执行,在 store 对象存储上创建一个 name 索引。

如需在 'store' 对象存储中创建 'description' 索引,请更新版本号并添加新的 case 块,如下所示:

import {openDB} from 'idb';

const db = await openDB('example-database', 3, {
  upgrade (db, oldVersion) {
    switch (oldVersion) {
      case 0:
        // Create first object store:
        db.createObjectStore('store', { keyPath: 'name' });

      case 1:
        // Get the original object store, and create an index on it:
        const tx = await db.transaction('store', 'readwrite');
        tx.store.createIndex('name', 'name');

      case 2:
        const tx = await db.transaction('store', 'readwrite');
        tx.store.createIndex('description', 'description');
    }
  }
});

如果在上一个示例中创建的数据库在浏览器中仍然存在,则执行时,oldVersion 为 2。浏览器会跳过 case 0 和 case 1,然后执行 case 2 中的代码,这会创建 description 索引。之后,浏览器具有版本 3 的数据库,其中包含具有 name 和 description 索引的 store 对象存储。

阅读余下内容
 

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注


京ICP备12002735号