使用 IndexedDB 进行大数据存储
并不總是需要將用戶數據發送到服務器:您可以選擇在瀏覽器中存儲一些信息。它非常適合特定于設備的設置,例如接口配置(例如亮/暗模式)或不應通過網絡傳輸的私有數據(例如加密密鑰)。
考慮用于計時網絡和頁面事件的性能 API 。您可以在所有數據可用的時候上傳所有數據,但具有諷刺意味的是,這是一個相當大的信息量,會影響頁面性能。更好的選擇是將其存儲在本地,使用Web Worker處理統計信息,并在瀏覽器不那么忙時上傳結果。
有兩個跨瀏覽器客戶端存儲 API 可用:
網絡存儲
同步名稱-值對存儲永久保存 (?localStore) 或當前會話 (?sessionStore)。瀏覽器允許每個域最多 5MB 的 Web 存儲。
索引數據庫
一種異步的類似 NoSQL 的名稱-值存儲,可以保存數據、文件和 blob。每個域至少應該有 1GB 可用空間,并且最多可以達到剩余磁盤空間的 60%。
另一種存儲選項WebSQL在 Chrome 和 Safari 的某些版本中可用。但是,它有 5MB 的限制,不一致,并在 2010 年被棄用。
在本教程中,我們將存儲頁面和所有資產的時間數據。Web 存儲空間可能很慢而且太有限,所以 IndexedDB 是最好的選擇。
如果您想在自己的網站上試用 IndexedDB,所有示例代碼都可以在 Github上找到。
什么是索引數據庫?
IndexedDB 于 2011 年首次實現,并于 2015 年 1 月成為 W3C 標準。它具有良好的瀏覽器支持,盡管它的回調和基于事件的 API 在我們擁有 ES2015+ 時顯得笨拙。本文演示了如何編寫基于 Promise 的包裝器,以便您可以使用鏈接和async/?await。
請注意以下 IndexedDB 術語:
-
數據庫——頂級存儲。一個域可以創建任意數量的 IndexedDB 數據庫,但看到多個數據庫是不常見的。只有同一域內的頁面才能訪問數據庫。
-
對象存儲——相關數據項的名稱/值存儲。它類似于 MongoDB 中的集合或關系數據庫中的表。
-
key?— 用于引用對象存儲中每條記錄(值)的唯一名稱。它可以使用自動增量數字生成,也可以設置為記錄中的任何唯一值。
-
索引——在對象存儲中組織數據的另一種方式。搜索查詢只能檢查鍵或索引。
-
schema?— 對象存儲、鍵和索引的定義。
-
version?— 分配給模式的版本號(整數)。IndexedDB 提供自動版本控制,因此您可以將數據庫更新到最新模式。
-
操作——數據庫活動,例如創建、讀取、更新或刪除記錄。
-
事務——一組一個或多個操作。一個事務保證它的所有操作要么成功要么失敗。它不能讓一些人失敗,也不能讓其他人失敗。
-
cursor?— 一種迭代記錄的方法,而不必一次將所有記錄加載到內存中。
開發和調試數據庫
在本教程中,您將創建一個名為performance.?它包含兩個對象存儲:
1.?navigation
這存儲頁面導航時間信息(重定向、DNS 查找、頁面加載、文件大小、加載事件等)。將添加一個日期以用作鍵。
2.?resource
這會存儲資源計時信息(其他資源的計時,例如圖像、樣式表、腳本、Ajax 調用等)。將添加一個日期,但可以同時加載兩個或更多資產,因此將使用自動遞增的 ID 作為關鍵。_?將為日期和名稱(資源的 URL)創建索引。
所有基于 Chrome 的瀏覽器都有一個應用程序選項卡,您可以在其中檢查存儲空間、人為限制容量以及擦除所有數據。Storage 樹中的IndexedDB條目允許您查看、更新和刪除對象存儲、索引和單個記錄。Firefox 的面板名為Storage。
您還可以在隱身模式下運行您的應用程序,以便在關閉瀏覽器窗口后刪除所有數據。
連接到 IndexedDB 數據庫
indexeddb.js在檢查 IndexedDB 支持時創建的包裝類使用:
if ('indexedDB' in window) // ... 然后它通過以下方式打開一個數據庫連接indexedDB.open():dbOpen.onerror當無法建立 IndexedDB 連接時運行。
dbOpen.onupgradeneeded當所需版本 (?1) 大于當前版本0時運行(未定義數據庫時)。處理函數必須運行 IndexedDB 方法,例如createObjectStore()和createIndex()來創建存儲結構。
dbOpen.onsuccess在建立連接并且完成任何升級時運行。中的連接對象dbOpen.result用于所有后續的數據操作。它被分配到this.db包裝類中。
包裝構造函數代碼:
// IndexedDB wrapper class: indexeddb.js export class IndexedDB {// connect to IndexedDB databaseconstructor(dbName, dbVersion, dbUpgrade) {return new Promise((resolve, reject) => {// connection objectthis.db = null;// no supportif (!('indexedDB' in window)) reject('not supported');// open databaseconst dbOpen = indexedDB.open(dbName, dbVersion);if (dbUpgrade) {// database upgrade eventdbOpen.onupgradeneeded = e => {dbUpgrade(dbOpen.result, e.oldVersion, e.newVersion);};}// success event handlerdbOpen.onsuccess = () => {this.db = dbOpen.result;resolve( this );};// failure event handlerdbOpen.onerror = e => {reject(`IndexedDB error: ${ e.target.errorCode }`);};});}// more methods coming later...} 一個performance.js腳本加載這個模塊并實例化一個以perfDB頁面加載后命名的新 IndexedDB 對象。它傳遞數據庫名稱 (?performance)、版本 (?1) 和升級函數。構造indexeddb.js函數使用數據庫連接對象、當前數據庫版本和新版本調用升級函數: // performance.js import { IndexedDB } from './indexeddb.js';window.addEventListener('load', async () => {// IndexedDB connectionconst perfDB = await new IndexedDB('performance',1,(db, oldVersion, newVersion) => {console.log(`upgrading database from ${ oldVersion } to ${ newVersion }`);switch (oldVersion) {case 0: {constnavigation = db.createObjectStore('navigation', { keyPath: 'date' }),resource = db.createObjectStore('resource', { keyPath: 'id', autoIncrement: true });resource.createIndex('dateIdx', 'date', { unique: false });resource.createIndex('nameIdx', 'name', { unique: false });}}});// more code coming later...});在某些時候,有必要更改數據庫模式——可能是添加新的對象存儲、索引或數據更新。在這種情況下,您必須增加版本(從1到2)。下一頁加載將再次觸發升級處理程序,因此您可以在語句中添加更多塊,例如在對象存儲中的屬性上switch創建一個名為的索引:durationIdxdurationresource
case 1: {const resource = db.transaction.objectStore('resource');resource.createIndex('durationIdx', 'duration', { unique: false }); }省略break每個塊末尾的通常。case當有人第一次訪問應用程序時,該case 0塊將運行,然后是case 1所有后續塊。任何已經在版本上1的人都將從case 1.?IndexedDB 模式更新方法包括:
- createIndex()
- deleteIndex()
- createObjectStore()
- deleteObjectStore()
- deleteDatabase()
加載頁面的每個人都將使用相同的版本——除非他們的應用程序在兩個或更多選項卡中運行。onversionchange為了避免沖突,可以添加數據庫連接處理程序indexeddb.js,提示用戶重新加載頁面:
// version change handler dbOpen.onversionchange = () => {dbOpen.close();alert('Database upgrade required - reloading...');location.reload();};您現在可以將performance.js腳本添加到頁面并運行它以檢查是否創建了對象存儲和索引(DevTools應用程序或存儲面板):
<script type="module" src="./performance.js"></script>記錄性能統計
所有 IndexedDB 操作都包裝在一個事務中。使用以下過程:
創建數據庫事務對象。這定義了一個或多個對象存儲(單個字符串或字符串數??組)和訪問類型:"readonly"用于獲取數據,或"readwrite"用于插入和更新。
objectStore()在事務范圍內創建對 an 的引用。
運行任意數量的add()(僅插入)或put()方法(插入和更新)。
在類中添加一個新update()方法:IndexedDBindexeddb.js
// store itemupdate(storeName, value, overwrite = false) {return new Promise((resolve, reject) => {// new transactionconsttransaction = this.db.transaction(storeName, 'readwrite'),store = transaction.objectStore(storeName);// ensure values are in arrayvalue = Array.isArray(value) ? value : [ value ];// write all valuesvalue.forEach(v => {if (overwrite) store.put(v);else store.add(v);});transaction.oncomplete = () => {resolve(true); // success};transaction.onerror = () => {reject(transaction.error); // failure};});}這會在命名存儲中添加或更新(如果overwrite參數是true)一個或多個值,并將整個事務包裝在 Promise 中。當transaction.oncomplete事務在函數結束時自動提交并且所有數據庫操作都完成時,事件處理程序將運行。處理程序transaction.onerror報告錯誤。
IndexedDB 事件從操作冒泡到事務、存儲和數據庫。onerror您可以在接收所有錯誤的數據庫上創建一個處理程序。像 DOM 事件一樣,傳播可以用event.stopPropagation().
該performance.js腳本現在可以報告頁面導航指標:
// record page navigation informationconstdate = new Date(),nav = Object.assign({ date },performance.getEntriesByType('navigation')[0].toJSON());await perfDB.update('navigation', nav);和資源指標:
const res = performance.getEntriesByType('resource').map(r => Object.assign({ date }, r.toJSON()));await perfDB.update('resource', res);在這兩種情況下,都會將一個date屬性添加到克隆的計時對象中,這樣就可以在特定時間段內搜索數據。
閱讀成績記錄
與其他數據庫相比,IndexedDB 搜索是初級的。您只能通過鍵或索引值獲取記錄。您不能使用 SQL 的等效項JOIN或函數,例如AVERAGE()和SUM()。所有記錄處理都必須使用 JavaScript 代碼處理;后臺Web Worker線程可能是一個實用的選擇。
.get()您可以通過將其鍵傳遞給對象存儲或索引的方法并定義處理程序來檢索單個記錄onsuccess:
// EXAMPLE CODE const// new readonly transactiontransaction = db.transaction('resource', 'readonly'),// get resource object storeresource = transaction.objectStore('resource'),// fetch record 1request = resource.get(1);// request complete request.onsuccess = () => {console.log('result:', request.result); };// request failed request.onerror = () => {console.log('failed:', request.error); };類似的方法包括:
- .count(query)— 匹配記錄數
- .getAll(query)— 匹配記錄的數組
- .getKey(query)— 匹配的鍵(而不是分配給該鍵的值)
- .getAllKeys(query)— 匹配鍵的數組
查詢也可以是KeyRange參數來查找范圍內的記錄,例如IDBKeyRange.bound(1, 10)返回鍵在 1 到 10 之間的所有記錄:
request = resource.getAll( IDBKeyRange.bound(1, 10) );鍵范圍選項:
- IDBKeyRange.lowerBound(X)— 大于或等于X
- IDBKeyRange.upperBound(X)— 小于或等于Y
- IDBKeyRange.bound(X,Y)——介于X兩者Y之間
- IDBKeyRange.only(X)— 單鍵匹配X。
lower、upper 和 bound 方法有一個可選的獨占標志,例如IDBKeyRange.bound(1, 10, true, false)- 大于1(但不是1自身)且小于或等于 的鍵10。
隨著數據庫變得越來越大,將整個數據集讀入數組變得不可能。IndexedDB 提供了可以一次遍歷每條記錄的游標。該.openCursor()方法傳遞一個 KeyRange 和可選的方向字符串("next"、"nextunique"、"prev"或"preunique")。
向類中添加一個新fetch()方法,以使用傳遞給游標的回調函數搜索具有上限和下限的對象存儲或索引。還需要另外兩種方法:IndexedDBindexeddb.js
該performance.js腳本現在可以檢索頁面導航指標,例如domContentLoadedEventEnd在 2021 年 6 月期間全部返回:
// fetch page navigation objects in June 2021perfDB.fetch('navigation',null, // not an indexnew Date(2021,5,1,10,40,0,0), // lowernew Date(2021,6,1,10,40,0,0), // uppercursor => { // callback functionif (cursor) {console.log(cursor.value.domContentLoadedEventEnd);cursor.continue();}});同樣,您可以計算特定文件的平均下載時間并將其報告回OpenReplay:
// calculate average download time using indexletfilename = 'http://mysite.com/main.css',count = 0,total = 0;perfDB.fetch('resource', // object store'nameIdx', // indexfilename, // matching filefilename,cursor => { // callbackif (cursor) {count++;total += cursor.value.duration;cursor.continue();}else {// all records processedif (count) {const avgDuration = total / count;console.log(`average duration for ${ filename }: ${ avgDuration } ms`);// report to OpenReplayif (asayer) asayer.event(`${ filename }`, { avgDuration });}}});在這兩種情況下,cursor對象都被傳遞給回調函數,它可以:
cursor是null當所有匹配的記錄都已處理完畢。
檢查剩余存儲空間
瀏覽器會為 IndexedDB 分配大量存儲空間,但最終會用完。新的基于 Promise 的StorageManager API可以計算域的剩余空間:
(async () => {if (!navigator.storage) return;constestimate = await navigator.storage.estimate(),// calculate remaining storage in MBavailable = Math.floor((estimate.quota - estimate.usage) / 1024 / 1024);console.log(`${ available } MB remaining`);})();IE 或 Safari 不支持 API。當接近限制時,您可以選擇刪除較舊的記錄。
結論
IndexedDB 是較舊且更復雜的瀏覽器 API 之一,但您可以添加包裝器方法以采用 Promises 和async/?await。如果您不想自己這樣做,諸如idb之類的預構建庫會有所幫助。
盡管存在缺點和一些不尋常的設計決策,IndexedDB 仍然是最快和最大的基于瀏覽器的數據存儲。
總結
以上是生活随笔為你收集整理的使用 IndexedDB 进行大数据存储的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 安装RocketChat报错:npm W
- 下一篇: 彩翼系列-彩票分析软件源代码(双色球,排