并不总是需要将用户数据发送到服务器:您可以选择在浏览器中存储一些信息。它非常适合特定于设备的设置,例如接口配置(例如亮/暗模式)或不应通过网络传输的私有数据(例如加密密钥)。
考虑用于计时网络和页面事件的性能 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) // ...
- 数据库名称,和
- 可选版本整数。
const dbOpen = indexedDB.open('performance', 1);
必须定义三个重要的事件处理函数:
-
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 database
constructor(dbName, dbVersion, dbUpgrade) {
return new Promise((resolve, reject) => {
// connection object
this.db = null;
// no support
if (!('indexedDB' in window)) reject('not supported');
// open database
const dbOpen = indexedDB.open(dbName, dbVersion);
if (dbUpgrade) {
// database upgrade event
dbOpen.onupgradeneeded = e => {
dbUpgrade(dbOpen.result, e.oldVersion, e.newVersion);
};
}
// success event handler
dbOpen.onsuccess = () => {
this.db = dbOpen.result;
resolve( this );
};
// failure event handler
dbOpen.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 connection
const perfDB = await new IndexedDB(
'performance',
1,
(db, oldVersion, newVersion) => {
console.log(`upgrading database from ${ oldVersion } to ${ newVersion }`);
switch (oldVersion) {
case 0: {
const
navigation = 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 创建一个名为的索引:durationIdx duration resource
case 1: {
const resource = db.transaction.objectStore('resource');
resource.createIndex('durationIdx', 'duration', { unique: false });
}
省略break 每个块末尾的通常。case 当有人第一次访问应用程序时,该case 0 块将运行,然后是case 1 所有后续块。任何已经在版本上1 的人都将从case 1 .?IndexedDB 模式更新方法包括:
加载页面的每个人都将使用相同的版本——除非他们的应用程序在两个或更多选项卡中运行。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() 方法:IndexedDB indexeddb.js
// store item
update(storeName, value, overwrite = false) {
return new Promise((resolve, reject) => {
// new transaction
const
transaction = this.db.transaction(storeName, 'readwrite'),
store = transaction.objectStore(storeName);
// ensure values are in array
value = Array.isArray(value) ? value : [ value ];
// write all values
value.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 information
const
date = 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 transaction
transaction = db.transaction('resource', 'readonly'),
// get resource object store
resource = transaction.objectStore('resource'),
// fetch record 1
request = resource.get(1);
// request complete
request.onsuccess = () => {
console.log('result:', request.result);
};
// request failed
request.onerror = () => {
console.log('failed:', request.error);
};
类似的方法包括:
查询也可以是KeyRange参数来查找范围内的记录,例如IDBKeyRange.bound(1, 10) 返回键在 1 到 10 之间的所有记录:
request = resource.getAll( IDBKeyRange.bound(1, 10) );
键范围选项:
lower、upper 和 bound 方法有一个可选的独占标志,例如IDBKeyRange.bound(1, 10, true, false) - 大于1 (但不是1 自身)且小于或等于 的键10 。
随着数据库变得越来越大,将整个数据集读入数组变得不可能。IndexedDB 提供了可以一次遍历每条记录的游标。该.openCursor()方法传递一个 KeyRange 和可选的方向字符串("next" 、"nextunique" 、"prev" 或"preunique" )。
向类中添加一个新fetch() 方法,以使用传递给游标的回调函数搜索具有上限和下限的对象存储或索引。还需要另外两种方法:IndexedDB indexeddb.js
index(storeName, indexName) — 返回对象存储或该存储上的索引,并且bound(lowerBound, upperBound) — 返回一个适当的 KeyRange 对象。
// get items using cursor
fetch(storeName, indexName, lowerBound = null, upperBound = null, callback) {
const
request = this.index(storeName, indexName)
.openCursor( this.bound(lowerBound, upperBound) );
// pass cursor to callback function
request.onsuccess = () => {
if (callback) callback(request.result);
};
request.onerror = () => {
return(request.error); // failure
};
}
// start a new read transaction on object store or index
index(storeName, indexName) {
const
transaction = this.db.transaction(storeName),
store = transaction.objectStore(storeName);
return indexName ? store.index(indexName) : store;
}
// get bounding object
bound(lowerBound, upperBound) {
if (lowerBound && upperBound) return IDBKeyRange.bound(lowerBound, upperBound);
else if (lowerBound) return IDBKeyRange.lowerBound(lowerBound);
else if (upperBound) return IDBKeyRange.upperBound(upperBound);
}
该performance.js 脚本现在可以检索页面导航指标,例如domContentLoadedEventEnd 在 2021 年 6 月期间全部返回:
// fetch page navigation objects in June 2021
perfDB.fetch(
'navigation',
null, // not an index
new Date(2021,5,1,10,40,0,0), // lower
new Date(2021,6,1,10,40,0,0), // upper
cursor => { // callback function
if (cursor) {
console.log(cursor.value.domContentLoadedEventEnd);
cursor.continue();
}
}
);
同样,您可以计算特定文件的平均下载时间并将其报告回OpenReplay:
// calculate average download time using index
let
filename = 'http://mysite.com/main.css',
count = 0,
total = 0;
perfDB.fetch(
'resource', // object store
'nameIdx', // index
filename, // matching file
filename,
cursor => { // callback
if (cursor) {
count++;
total += cursor.value.duration;
cursor.continue();
}
else {
// all records processed
if (count) {
const avgDuration = total / count;
console.log(`average duration for ${ filename }: ${ avgDuration } ms`);
// report to OpenReplay
if (asayer) asayer.event(`${ filename }`, { avgDuration });
}
}
});
在这两种情况下,cursor 对象都被传递给回调函数,它可以:
- 获取记录值
cursor.value - 前进到下一个记录cursor.continue()
- 向前移动
N 记录cursor.advance(N) - 更新记录cursor.update(data),或
- 删除记录cursor.delete()
cursor 是null 当所有匹配的记录都已处理完毕。
检查剩余存储空间
浏览器会为 IndexedDB 分配大量存储空间,但最终会用完。新的基于 Promise 的StorageManager API可以计算域的剩余空间:
(async () => {
if (!navigator.storage) return;
const
estimate = await navigator.storage.estimate(),
// calculate remaining storage in MB
available = Math.floor((estimate.quota - estimate.usage) / 1024 / 1024);
console.log(`${ available } MB remaining`);
})();
IE 或 Safari 不支持 API。当接近限制时,您可以选择删除较旧的记录。
结论
IndexedDB 是较旧且更复杂的浏览器 API 之一,但您可以添加包装器方法以采用 Promises 和async /?await 。如果您不想自己这样做,诸如idb之类的预构建库会有所帮助。
尽管存在缺点和一些不寻常的设计决策,IndexedDB 仍然是最快和最大的基于浏览器的数据存储。
|