При работе с локальными базами данных браузера часто возникает необходимость хранить не только текстовую информацию и числовые значения, но и крупные бинарные объекты. К таким данным относятся:
Поскольку Dexie.js представляет собой удобную надстройку над IndexedDB, библиотека позволяет работать с бинарными объектами практически так же, как с обычными JavaScript-структурами.
Наиболее распространёнными форматами бинарных данных являются:
Все эти структуры могут сохраняться в IndexedDB без дополнительного преобразования.
Blob (Binary Large Object) представляет собой контейнер для хранения произвольного бинарного содержимого.
Создание Blob:
const blob = new Blob(
["Пример содержимого файла"],
{ type: "text/plain" }
);
Размер объекта:
console.log(blob.size);
MIME-тип:
console.log(blob.type);
Blob особенно удобен при работе с файлами, загружаемыми пользователем
через элемент <input type="file">.
Пример:
const input = document.querySelector("#file");
input.addEventListener("change", event => {
const file = event.target.files[0];
console.log(file.name);
console.log(file.size);
});
Объект File является наследником Blob,
поэтому может сохраняться в Dexie без преобразований.
Структура базы данных:
import Dexie fr om "dexie";
const db = new Dexie("StorageDB");
db.version(1).stores({
files: "++id, name"
});
Сохранение файла:
await db.files.add({
name: file.name,
data: file
});
Структура записи:
{
id: 1,
name: "image.jpg",
data: Blob
}
Dexie автоматически передаст объект в IndexedDB через механизм структурированного клонирования.
Дополнительная сериализация не требуется.
Чтение объекта:
const fileRecord = await db.files.get(1);
Доступ к бинарным данным:
const blob = fileRecord.data;
Проверка:
console.log(blob instanceof Blob);
После извлечения данные остаются полноценным объектом Blob.
Одна из самых частых задач — хранение изображений.
Получение объекта:
const imageRecord = await db.files.get(1);
const url = URL.createObjectURL(
imageRecord.data
);
Отображение:
img.src = url;
Полный пример:
const image = document.createElement("img");
image.src = URL.createObjectURL(
imageRecord.data
);
document.body.appendChild(image);
После завершения использования рекомендуется освобождать память:
URL.revokeObjectURL(url);
Особенно это важно при работе с большими изображениями.
В ряде случаев бинарные данные удобнее хранить как массив байтов.
Получение ArrayBuffer:
const buffer = await file.arrayBuffer();
Сохранение:
await db.files.add({
name: file.name,
data: buffer
});
Чтение:
const record = await db.files.get(1);
const buffer = record.data;
Проверка:
console.log(
buffer instanceof ArrayBuffer
);
Современные браузеры поддерживают прямой метод:
const buffer = await blob.arrayBuffer();
Ранее использовался FileReader:
function blobToBuffer(blob) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onl oad = () => {
resolve(reader.result);
};
reader.oner ror = reject;
reader.readAsArrayBuffer(blob);
});
}
Сегодня предпочтителен первый вариант.
Обратная операция:
const blob = new Blob(
[buffer],
{ type: "application/octet-stream" }
);
Для изображений:
const imageBlob = new Blob(
[buffer],
{ type: "image/png" }
);
Для PDF:
const pdfBlob = new Blob(
[buffer],
{ type: "application/pdf" }
);
Нередко бинарные данные обрабатываются через типизированные массивы.
Пример:
const bytes = new Uint8Array([
10,
20,
30,
40
]);
Сохранение:
await db.files.add({
data: bytes
});
Извлечение:
const record = await db.files.get(1);
console.log(record.data);
Также поддерживаются:
Int8Array
Uint8Array
Uint16Array
Uint32Array
Float32Array
Float64Array
BigInt64Array
BigUint64Array
IndexedDB сохраняет их без потери структуры.
Хотя IndexedDB поддерживает крупные объекты, существуют ограничения.
На практике размер зависит от:
Например, изображения размером в несколько мегабайт обычно не вызывают проблем.
Однако хранение тысяч файлов по сотни мегабайт требует особого подхода.
Опасная ситуация:
const buffer =
await hugeFile.arrayBuffer();
Если размер файла составляет:
500 МБ
то браузеру потребуется выделить как минимум 500 МБ оперативной памяти.
При нескольких одновременных операциях расход памяти возрастает многократно.
Для мобильных устройств это может приводить к:
Blob работает ленивее.
Сохранение:
await db.files.add({
file: blob
});
Получение:
const record =
await db.files.get(id);
Фактическое чтение содержимого происходит только при необходимости.
Поэтому для крупных файлов Blob обычно предпочтительнее.
При работе с очень большими объектами применяется стратегия chunking.
Файл разбивается на множество небольших блоков.
Пример структуры:
db.version(1).stores({
files: "id",
chunks: "[fileId+index]"
});
Метаданные файла:
{
id: "file1",
name: "video.mp4",
chunkCount: 50
}
Фрагмент:
{
fileId: "file1",
index: 0,
data: Blob
}
Размер блока:
const CHUNK_SIZE =
1024 * 1024;
Один мегабайт.
Создание частей:
const chunks = [];
for (
let offset = 0;
offset < file.size;
offset += CHUNK_SIZE
) {
chunks.push(
file.slice(
offset,
offset + CHUNK_SIZE
)
);
}
Каждый элемент массива является Blob.
for (
let i = 0;
i < chunks.length;
i++
) {
await db.chunks.add({
fileId: "file1",
index: i,
data: chunks[i]
});
}
Сохранение метаданных:
await db.files.put({
id: "file1",
name: file.name,
chunkCount: chunks.length
});
Получение частей:
const chunks =
await db.chunks
.wh ere("fileId")
.equals("file1")
.sortBy("index");
Создание Blob:
const restoredFile =
new Blob(
chunks.map(
chunk => chunk.data
)
);
Полученный объект идентичен исходному.
Современные браузеры поддерживают Streams API.
Получение потока:
const stream = blob.stream();
Чтение:
const reader =
stream.getReader();
Получение данных частями:
while (true) {
const {
done,
value
} = await reader.read();
if (done) {
break;
}
console.log(value);
}
Такой подход значительно снижает потребление памяти.
Для фотографий обычно сохраняются:
{
id,
filename,
mimeType,
width,
height,
imageBlob
}
Для аудио:
{
id,
title,
duration,
audioBlob
}
Для видео:
{
id,
title,
codec,
videoBlob
}
Дополнительные метаданные позволяют быстро выполнять поиск без чтения самого файла.
Создавать индекс непосредственно по Blob невозможно.
Неправильно:
files:
"++id, data"
Поиск по содержимому бинарного объекта IndexedDB не поддерживает.
Поэтому индексируются только метаданные:
files:
"++id, name, type, createdAt"
Например:
await db.files
.where("type")
.equals("image/png")
.toArray();
Для мониторинга размера хранилища используется Storage API.
Получение статистики:
const estimate =
await navigator.storage.estimate();
Результат:
console.log(
estimate.usage
);
Лимит:
console.log(
estimate.quota
);
Возможный вывод:
Usage: 120 MB
Quota: 2 GB
Это позволяет контролировать заполнение локального хранилища.
Удаление файла:
await db.files.delete(id);
Если используется chunking:
await db.transaction(
"rw",
db.files,
db.chunks,
async () => {
await db.files.delete(id);
await db.chunks
.where("fileId")
.equals(id)
.delete();
}
);
Транзакция гарантирует согласованность данных.
Blob предпочтительнее для крупных файлов, поскольку позволяет избежать лишнего копирования данных в память.
ArrayBuffer удобен для обработки содержимого, когда требуется анализировать байты, изменять структуру файла или выполнять криптографические операции.
TypedArray подходит для низкоуровневой работы с бинарными форматами, сетевыми протоколами и мультимедийными данными.
Метаданные должны храниться отдельно от содержимого файла, чтобы поиск и фильтрация выполнялись по индексируемым полям.
Для объектов размером в десятки и сотни мегабайт рекомендуется chunking, позволяющий уменьшить нагрузку на память и повысить устойчивость операций чтения и записи.
Контроль объёма хранилища через Storage API помогает предотвращать переполнение квоты браузера, особенно в офлайн-приложениях, где количество локально сохраняемых файлов постоянно растёт.
Транзакционное удаление файлов и их фрагментов обеспечивает целостность базы данных, предотвращая накопление неиспользуемых бинарных блоков после удаления записей.