发布于 2026-01-06 0 阅读
0

如何使用 IndexedDB 在客户端存储数据?DEV 的全球展示挑战赛,由 Mux 呈现:展示你的项目!

如何使用 IndexedDB 在客户端存储数据

由 Mux 赞助的 DEV 全球展示挑战赛:展示你的项目!

想象一下,如果微积分考试要求你所有计算都在心算中完成,那会是什么情况?理论上可行,但完全没有必要这么做。同样的道理也适用于在浏览器中存储数据。

如今,客户端存储技术种类繁多,应用广泛。例如,Cookie、Web Storage APIIndexedDB。虽然完全可以编写一个功能齐全的 Web 应用程序而无需考虑这些技术,但你不应该这样做。那么,应该如何使用它们呢?其实,每种技术都有其最适合的应用场景。

浏览器存储概览

曲奇饼

Cookie 几乎在每个请求中都会发送,最适合用于传输短小的数据片段。Cookie 的最大优势在于服务器可以直接通过Set-Cookie请求头设置它们,无需 JavaScript。在后续请求中,客户端会发送一个Cookie包含所有先前设置的 Cookie 的请求头。但缺点是,较大的 Cookie 会严重拖慢请求速度。这就需要接下来介绍的两种技术了。

网络存储

Web Storage API 由两个类似的存储组成——localStoragesessionStorage。它们拥有相同的接口,但后者仅在浏览会话期间有效。前者则只要有可用内存就会一直存在。这种内存限制既是它最大的优势,也是它最大的劣势。

由于这些值并非随每个请求一起发送,因此可以在其中存储大量数据而不会影响性能。但是,“大量”是相对的,不同浏览器的存储限制可能差异很大。一个好的经验法则是,整个网站存储的数据量不要超过 5 MB。这个限制并不理想,如果需要存储更多数据,则可能需要使用第三个也是最后一个 API。

IndexedDB

有人可能会说,IndexedDB 被严重低估了。尽管几乎所有浏览器都支持它,但它的普及程度远不及其他两种存储方式。它不像 cookie 那样随每个请求一起发送,也没有 Web Storage 那样任意的限制。那么,这是为什么呢?

IndexedDB 不流行的原因其实是它用起来极其麻烦。你不能直接使用Promises`get` 或 `get` async/await,而是需要手动定义成功和错误处理程序。虽然很多库封装了这些功能,但它们往往过于复杂。如果你只需要保存和加载数据,完全可以自己编写所需的一切。

对 IndexedDB 进行简洁的封装

虽然有很多方法可以与 IndexedDB 交互,但我将要介绍的是我个人偏好的方法。这段代码适用于一个数据库和一个表,但应该很容易修改以适应其他用例。在开始编写代码之前,让我们先快速列出我们需要哪些功能。

1. 理想情况下,它应该是某种可以导入和导出的类或对象。

2. 每个“对象”应该只代表一个 database 对象 table

3. 与 CRUD API 类似,我们需要读取、保存和删除键值对的方法。

这看起来很简单。顺便提一下——class这里我们会使用 ES6 语法,但你可以根据需要进行修改。如果你只在一个文件中使用它,甚至不需要使用类。现在让我们开始吧。

一些样板文字

我们基本知道需要哪些方法,所以我们可以先列出这些方法,并确保所有函数都合理。这样一来,编写代码和测试就更容易了(我当时没做测试,因为这是个人项目,但我真的应该着手做了)。

嘿,看来你用的屏幕有点窄。下面的代码块可能显示得不太清楚,但文章的其他部分应该没问题。如果你想跟着做,可以换个宽点的屏幕。我不会离开的(保证)。

     class DB {
        constructor(dbName="testDb", storeName="testStore", version=1) {
          this._config = {
            dbName,
            storeName,
            version
          };
        }

        set _config(obj) {
          console.error("Only one config per DB please");
        }

        read(key) {
          // TODO
        }

        delete(key) {
          // TODO
        }

        save(key, value) {
          // TODO
        }
      }
Enter fullscreen mode Exit fullscreen mode

这里我们设置了一些样板代码,包含了所有函数和一个简洁的常量配置。这个setter循环_config确保配置在任何时候都无法更改。这既有助于调试错误,也能从源头上防止错误发生。

样板工作全部完成,现在是时候进入有趣的部分了。让我们看看 IndexedDB 能做什么。

从数据库读取数据

尽管 IndexedDB 本身不使用异步函数Promises,但为了实现异步操作,我们将把所有函数都封装在异步函数中。从某种意义上说,我们编写的代码将有助于弥合 IndexedDB 与更现代的 JavaScript 编写方式之间的差距。在我们的read函数中,让我们把所有内容都封装在一个新的异步函数中Promise

      read(key) {
        return new Promise((resolve, reject) => {
          // TODO
        });
      }
Enter fullscreen mode Exit fullscreen mode

当我们从数据库中获取到值后,我们将使用该resolve参数将其沿链传递Promise。这意味着我们可以在代码的其他地方执行类似这样的操作:

      db = new DB();

      db.read('testKey')
        .then(value => { console.log(value) })
        .catch(err => { console.error(err) });` 
Enter fullscreen mode Exit fullscreen mode

现在我们已经完成了设置,接下来看看如何建立连接。要打开数据库,我们只需要调用对象open的相应方法window.indexedDB。我们还需要处理三种不同的情况:出现错误、操作成功以及需要升级。目前我们先用占位符代替这些情况。目前的代码如下所示:

      read(key) {
        return new Promise((resolve, reject) => {
          let dbRequest = window.indexedDB.open(dbConfig.dbName);

          dbRequest.onerror = (e) => {
            // TODO
          };

          dbRequest.onupgradeneeded = (e) => {
            // TODO
          };

          dbRequest.onsuccess = (e) => {
            // TODO
          };
        });
      }
Enter fullscreen mode Exit fullscreen mode

如果open出现错误,我们可以reject用一条清晰易懂的错误信息来简化它:

      dbRequest.onerror = (e) => {
        reject(Error("Couldn't open database."));
      };
Enter fullscreen mode Exit fullscreen mode

对于第二个处理程序onupgradeneeded,我们不需要做太多。这个处理程序仅version在构造函数中提供的数据库版本不存在时才会调用。如果数据库版本不存在,则没有数据可供读取。因此,我们只需中止事务并拒绝该请求即可Promise

      dbRequest.onupgradeneeded = (e) => {
        e.target.transaction.abort();
        reject(Error("Database version not found."));
      };
Enter fullscreen mode Exit fullscreen mode

这样就剩下第三个也是最后一个处理程序,用于处理成功状态。我们将在这里进行实际的读取操作。我在前一个处理程序中简要地提到了事务,但现在值得花点时间详细讲解一下。由于 IndexedDB 是一个 NoSQL 数据库,读写操作都是在事务中执行的。这些事务记录了数据库上正在执行的不同操作,并且可以以不同的方式撤销或重新排序。当我们在上文中止事务时,我们所做的只是告诉计算机取消所有待处理的更改。

现在我们有了数据库,接下来还需要对事务进行更多操作。首先,让我们获取实际的数据库:

      let database = e.target.result;
Enter fullscreen mode Exit fullscreen mode

现在我们有了数据库,就可以依次获取交易和存储信息了。

      let transaction = database.transaction([ _config.storeName ]);
      let objectStore = transaction.objectStore(_config.storeName);
Enter fullscreen mode Exit fullscreen mode

第一行代码创建了一个新事务并声明了其作用域。也就是说,它告诉数据库它只会操作一个存储(或表)。第二行代码获取存储并将其赋值给一个变量。

有了这个变量,我们终于可以实现目标了。我们可以调用get该存储的方法来获取与该键关联的值。

      let objectRequest = objectStore.get(key);
Enter fullscreen mode Exit fullscreen mode

我们差不多完成了。剩下的就是处理错误和成功事件了。需要注意的一点是,我们会检查实际结果是否存在。如果不存在,我们也会抛出一个错误:

      objectRequest.onerror = (e) => {
        reject(Error("Error while getting."));
      };

      objectRequest.onsuccess = (e) => {
        if (objectRequest.result) {
          resolve(objectRequest.result);
        } else reject(Error("Key not found."));
      };
Enter fullscreen mode Exit fullscreen mode

至此,我们的read函数完整代码如下:

      read(key) {
        return new Promise((resolve, reject) => {
          let dbRequest = window.indexedDB.open(_config.dbName);

          dbRequest.onerror = (e) => {
            reject(Error("Couldn't open database."));
          };

          dbRequest.onupgradeneeded = (e) => {
            e.target.transaction.abort();
            reject(Error("Database version not found."));
          };

          dbRequest.onsuccess = (e) => {
            let database = e.target.result;
            let transaction = database.transaction([ _config.storeName ], 'readwrite');
            let objectStore = transaction.objectStore(_config.storeName);
            let objectRequest = objectStore.get(key);

            objectRequest.onerror = (e) => {
              reject(Error("Error while getting."));
            };

            objectRequest.onsuccess = (e) => {
              if (objectRequest.result) {
                resolve(objectRequest.result);
              } else reject(Error("Key not found."));
            };
          };
        });
      }
Enter fullscreen mode Exit fullscreen mode
从数据库中删除

delete函数执行的步骤有很多相同之处。以下是完整的函数:

      delete(key) {
        return new Promise((resolve, reject) => {
          let dbRequest = indexedDB.open(_config.dbName);

          dbRequest.onerror = (e) => {
            reject(Error("Couldn't open database."));
          };

          dbRequest.onupgradeneeded = (e) => {
            e.target.transaction.abort();
            reject(Error("Database version not found."));
          };

          dbRequest.onsuccess = (e) => {
            let database = e.target.result;
            let transaction = database.transaction([ _config.storeName ], 'readwrite');
            let objectStore = transaction.objectStore(_config.storeName);
            let objectRequest = objectStore.delete(key);

            objectRequest.onerror = (e) => {
              reject(Error("Couldn't delete key."));
            };

            objectRequest.onsuccess = (e) => {
              resolve("Deleted key successfully.");
            };
          };
        });
      }
Enter fullscreen mode Exit fullscreen mode

你会注意到这里有两点不同。首先,我们调用的是delete` objectStore.`。其次,成功处理程序会立即解析。除了这两点之外,代码基本相同。第三个也是最后一个函数也是如此。

保存到数据库

再次强调,由于两者非常相似,以下是完整的save函数代码:

      save(key, value) {
        return new Promise((resolve, reject) => {
          let dbRequest = indexedDB.open(dbConfig.dbName);

          dbRequest.onerror = (e) => {
            reject(Error("Couldn't open database."));
          };

          dbRequest.onupgradeneeded = (e) => {
            let database = e.target.result;
            let objectStore = database.createObjectStore(_config.storeName);
          };

          dbRequest.onsuccess = (e) => {
            let database = e.target.result;
            let transaction = database.transaction([ _config.storeName ], 'readwrite');
            let objectStore = transaction.objectStore(_config.storeName);
            let objectRequest = objectStore.put(value, key); // Overwrite if exists

            objectRequest.onerror = (e) => {
              reject(Error("Error while saving."));
            };

            objectRequest.onsuccess = (e) => {
              resolve("Saved data successfully.");
            };
          };
        });
      }
Enter fullscreen mode Exit fullscreen mode

这里有三点不同。首先,onupgradeneeded需要填写处理程序。这很合理,因为应该支持在新版本的数据库中设置值。在这里,我们只需objectStore使用名称恰当的createObjectStore方法创建即可。其次,我们使用put的方法objectStore来保存值,而不是读取或删除它。最后,与delete方法一样,成功处理程序会立即解析。

完成所有这些步骤后,最终效果如下:

      class DB {
        constructor(dbName="testDb", storeName="testStore", version=1) {
          this._config = {
            dbName,
            storeName,
            version
          };
        }

        set _config(obj) {
          console.error("Only one config per DB please");
        }

        read(key) {
          return new Promise((resolve, reject) => {
            let dbRequest = window.indexedDB.open(_config.dbName);

            dbRequest.onerror = (e) => {
              reject(Error("Couldn't open database."));
            };

            dbRequest.onupgradeneeded = (e) => {
              e.target.transaction.abort();
              reject(Error("Database version not found."));
            };

            dbRequest.onsuccess = (e) => {
              let database = e.target.result;
              let transaction = database.transaction([ _config.storeName ], 'readwrite');
              let objectStore = transaction.objectStore(_config.storeName);
              let objectRequest = objectStore.get(key);

              objectRequest.onerror = (e) => {
                reject(Error("Error while getting."));
              };

              objectRequest.onsuccess = (e) => {
                if (objectRequest.result) {
                  resolve(objectRequest.result);
                } else reject(Error("Key not found."));
              };
            };
          });
        }

        delete(key) {
          return new Promise((resolve, reject) => {
            let dbRequest = indexedDB.open(_config.dbName);

            dbRequest.onerror = (e) => {
              reject(Error("Couldn't open database."));
            };

            dbRequest.onupgradeneeded = (e) => {
              e.target.transaction.abort();
              reject(Error("Database version not found."));
            };

            dbRequest.onsuccess = (e) => {
              let database = e.target.result;
              let transaction = database.transaction([ _config.storeName ], 'readwrite');
              let objectStore = transaction.objectStore(_config.storeName);
              let objectRequest = objectStore.delete(key);

              objectRequest.onerror = (e) => {
                reject(Error("Couldn't delete key."));
              };

              objectRequest.onsuccess = (e) => {
                resolve("Deleted key successfully.");
              };
            };
          });
        }

        save(key, value) {
          return new Promise((resolve, reject) => {
            let dbRequest = indexedDB.open(dbConfig.dbName);

            dbRequest.onerror = (e) => {
              reject(Error("Couldn't open database."));
            };

            dbRequest.onupgradeneeded = (e) => {
              let database = e.target.result;
              let objectStore = database.createObjectStore(_config.storeName);
            };

            dbRequest.onsuccess = (e) => {
              let database = e.target.result;
              let transaction = database.transaction([ _config.storeName ], 'readwrite');
              let objectStore = transaction.objectStore(_config.storeName);
              let objectRequest = objectStore.put(value, key); // Overwrite if exists

              objectRequest.onerror = (e) => {
                reject(Error("Error while saving."));
              };

              objectRequest.onsuccess = (e) => {
                resolve("Saved data successfully.");
              };
            };
          });
        }
      }
Enter fullscreen mode Exit fullscreen mode

使用方法很简单,只需创建一个新DB对象并调用指定的方法即可。例如:

      const db = new DB();

      db.save('testKey', 12)
        .then(() => {
          db.get('testKey').then(console.log); // -> prints "12"
        })
Enter fullscreen mode Exit fullscreen mode

一些收尾工作

如果想在另一个文件中使用它,只需在文件末尾添加一个导出语句即可:

      export default DB;
Enter fullscreen mode Exit fullscreen mode

然后,将其导入到新脚本中(确保所有内容都支持模块),并调用它:

      import DB from './db';
Enter fullscreen mode Exit fullscreen mode

然后,直接使用它。

和往常一样,别忘了关注我,获取更多类似内容。我目前在dev.toMedium上写作,非常感谢您在这两个平台上的支持。我还设立了会员制度,您可以提前预览文章并获得大量专属资源。如果您特别喜欢这篇文章,不妨请我喝杯咖啡以示支持。下次见!

文章来源:https://dev.to/shaileshcodes/how-to-store-data-client-side-with-indexeddb-4482