Разработка веб-сайтов
14 апреля 2011

IndexedDB: пробуем готовить

IndexedDB – стандарт хранения больших объемов структурированных данных на клиенте – был ожидаем также как и WebSocket (ну может самую малость меньше). В свете выхода FireFox 4 я нашёл время и силы всё-таки разобраться, как им пользоваться, и попытаться написать что-то больше, чем пример с адресной книгой, гуляющий по интернетам (в процессе поиска информации у меня сложилось впечатление, что это был единственный пример).

Несколько вводных слов


IndexedDB служит для хранения больших объемов структурированных данных, с возможностью индексации. Потребность в таком инструментарии назрела давно, что и привело к его появлению в спецификациях HTML5. Краткую предысторию можно прочесть здесь.

В тексте будут встречаться лирические отступления, требующиеся для выхода эмоций. Посыпать статью ссылками вики-стайл не хочется, все источники сложены в конце.

На чём будем тренироваться


Так случилось, что я в последнее время занимаюсь созданием чатиков. Вот и будем строить чатик, со следующими требованиями:
  • все сообщения чата хранятся локально, от сервера принимаются не более 100 последних сообщений;
  • также локально хранятся пользовательские настройки, в нашем случае — имя пользователя.

Поехали


Первым делом необходимо проверить, есть ли поддержка IndexedDB в браузере. Делается это так:
if ("webkitIndexedDB" in window){
 var idb=window.webkitIndexedDB;
} else if ("mozIndexedDB" in window) {
 var idb=window.mozIndexedDB;
} else {
 //тут объясняем, что этот конь здесь не ходит или делаем что-то альтернативное и умное
};


Лирическое отступление:
Не могу понять, зачем разработчики браузеров используют модификаторы движков для поддерживаемых функций. Даже если наполнение отличается друг от друга, определить, что за браузер пришёл (про параноиков речь не идёт), труда не составляет, а так приходится писать, в общем-то, лишний код.


Далее необходимо создать объект типа IDBRequest, который будет предоставлять асинхронный доступ к объектам базы данных. Синхронный доступ к IndexedDB в драфте спецификации присутствует, но пока не реализован.
var idbRequest=idb.open(dbName,dbDescription);
//dbName – имя базы данных, dbDescription – её описание (опционально)
//И навесим на него обработчики
idbRequest.onsuccess=function (e) {…};
idbRequest.onerror=function (e) {…};


Обработчки onerror:
Если верить спецификации, то в качестве аргумента должен прилететь объект типа IDBErrorEvent, имеющий два свойства – code и message. На практике, прилетает просто событие, к счастью имеющее в свойствах объект IDBRequest, из которого можно вытащить код ошибки и, в случае вебкита, собственно сообщение. Итоговый обработчик имеет следующую структуру:
function idbRequestError(err){
 idbRequest=err.target;
 //код ошибки idbRequest.errorCode
 //если webkit, описание ошибки idbRequest.webkitErrorMessage;
}


Вызвать эту ошибку довольно просто:
  • запретить сохранение локальных данных в настройках браузера;
  • долго думать над сообщением FireFox «Этот сайт пытается записать данные локально. Разрешить?»

Если подключение прошло успешно, можно продолжить и проверить, есть ли нужная база данных у пользователя и той ли она версии. Структура обработчика успешного подключения:
function idbRequestSuccess(e){
  var db=e.target.result;
  if (db.version===’’){
    //базы данных нет
  }else if (db.version!=’3.14’){
    //база данных не той версии
  } else {
    //всё хорошо
  };
}


В случае отсутствия базы данных или неверной её версии, надо бы её создать или модифицировать. Для этого нам потребуется метод setVersion. Здесь, я вынужден отвлечься от написания кода, и рассказать о механизме трансакций в IndexedDB.

Трансакции в IndexedDB


Драфт W3C определяет четыре типа трансакций: READ_ONLY, READ_WRITE, SNAPSHOT_READ и VERSION_CHANGED.

Лирическое отступление
Честно говоря, я не понял отличий READ_ONLY от SNAPSHOT_READ, видимо, разработчики браузеров тоже этот момент не осознали потому, она не реализована.


READ_ONLY – служит, как следует из названия, для чтения. Блокирует трансакции других типов.
READ_WRITE – служит для изменения данных, дожидается завершения всех конкурирующих трансакций над выбранным объектом, блокирует все прочие трансакции и выполняется.
VERSION_CHANGE – трансакция, которая дожидается завершения всех прочих трансакций, блокирует доступ к объектам данных для всех и выполняется. Только в этой трансакции можно создавать, удалять или изменять объекты данных.

Лирическое отступление
Все трансакции имеют числовые коды. По спецификации W3C READ_WRITE=0, READ_ONLY=1, SNAPSHOT_READ=2, VERSION_CHANGE=3. Писать конструкции типа “webkitIDBTransaction.READ_ONLY” мне было, конечно же, лень и я задавал трансакции кодами. То, что VERSION_CHANGE трансакция имеет код 2, я выяснил довольно быстро. Но выяснение того, что в FireFox READ_ONLY=0, а READ_WRITE=1 слоило мне многих закоротивших нервных клеток.


Создание объектов данных


Как уже было сказано, совершать манипуляции с объектами данных можно только из трансакции VERSION_CHANGE. Подключиться к ней мы можем из обработчика успешной смены версии.
var setVersion=db.setVersion('3.14');
setVersion.onsuccess=function (e) { 
  var db=e.target.transaction.db;
  //действия над объектами данных
};


Что можно сделать с объектами данных:
Создать – createObjectStore()
Удалить – deleteObjectStore()
Назначить трансакцию – transaction()

Разберемся с удалением


В качестве аргумента метод удаления принимает имя объекта данных, который следует удалить. Метод выполняется асинхронно, и по идее, должен возвращать объект типа IDBRequest, к которому можно прицепить обработчик onsuccess. Но ни Webkit, ни Mozilla не считают нужным что-либо вернуть. Работает метод, тем не менее, асинхронно и блокирует доступ конкурирующим методам. Потому использование конструкции
for (var i=0; i<db.objectStoreNames.length; i++){
 db.deleteObjectStore(db.objectStoreNames.length[i]);
};

приводит к непрогнозируемым результатам. Что-то удалиться, что-то нет. Узнать нормальным способом не получается. Структура костыля, в принципе, понятна, но я решил этим не заморачиваться, т.к. удалаять объекты нужно, по большому счету, только в случае изменения структуры базы.

Создание объектов


В примере, с адресной книгой, создавался всего один объект базы данных. Но, когда я решил создать два объекта, FireFox начала обкладывать меня матом на тему NON_TRANSIENT_ERR, что примерно означает: «не то, и не в той трансакции делаешь».

Изначально конструкция была следующей, и нормально работала в Chrome:
var setVersion=db.setVersion(dbVersion);
setVersion.onsuccess=idbCreateStore;
function idbCreateStore(e){
 //получим объект базы данных, ассоциированный с VERSION_CHANGE трансакцией
 var db=e.target.transaction.db;
 if (!db.objectStoreNames.contains('chat')){
  //объект для хранения записей из чата
  soChat=db.createObjectStore('chat', ‘id’);
  soChat.createIndex('itime','time');
 };
 if (!db.objectStoreNames.contains('iam')){
  //объект для хранения настроек пользователя
  soIam=db.createObjectStore('iam');
 };
}


После нескольких часов экспериментов, был создан хак и для ОгнеЛиса, который работает и в Chrome:
var setVersion=db.setVersion('4');
setVersion.onsuccess=idbCreateStore;
function idbCreateStore(e){
 //получим объект базы данных, ассоциированный с VERSION_CHANGE трансакцией
 var db=e.target.transaction.db;
 if (!db.objectStoreNames.contains('chat')){
  //объект для хранения записей из чата
  co=db.createObjectStore('chat',’id’);
  setVersion=db.setVersion('42')
  setVersion.onsuccess=idbCreateStore;
  return;
 };
 if (!db.objectStoreNames.contains('iam')){
  //объект для хранения настроек пользователя
  co=db.createObjectStore('iam');
 };
}


Как вы наверно заметили, перед созданием объекта идёт проверка, нет ли уже такого объекта в базе. Сделано это для того, чтобы не возиться с удалением объектов. Если попробовать создать объект, который уже присутствует в базе, то вывалится ошибка.

Опциональные аргументы создания объектов: имя ключа и флаг автоинкриментности (игнорируется браузерами).

Также из кода исчезло создание индексов: ОгнеЛис болезненно реагировал на создание индексов в трансакции смены версии.

К этому моменту база данных сформирована. Казалось бы, можно продолжить. Но это не так. Все вызовы выполняются асинхронно, потому нельзя утверждать однозначно, что все объекты были созданы. Опять-таки, если верить спецификации, в возвращаемом методом createObjectStore() объекте IDBObjectStore должно содержаться свойство IDBRequest, которому можно навесить обработчик onsuccess. Но похожего свойства в объекте нет. Потому, перед тем как запустить основной цикл работы необходимо дождаться завершения создания объектов БД, что делается следующим не хитрым кодом:
var idbObjectsWait=true;
while (idbObjectsWait){
 idbObjectsWait=!(db.objectStoreNames.contains('chat') && db.objectStoreNames.contains('iam'));
};

Наконец-то база создана, и в неё можно начать писать и из неё же читать, что записали.

Запись


Записывать данные можно двумя способами (и только из трансакции записи): add и put. Различия следующие: если в add передать ключ, который уже присутствует в объекте, значение не будет записано; put – заменяет данные. Операции опять же асинхронные.
var t=idb.transaction(['iam'], idbConst.WRITE);
var s=t.objectStore('iam');
s.put({'name':$('#name').val()},1);


Чтение


С чтением меня ждало самое большое разочарование. По идее к базе можно строить запросы используя интерфейс IDBKeyRange, но его поддержку ни в Хроме, ни в ОгнеЛисе я не обнаружил. Т.е. вся возня с индексами множится на ноль: запрашивать нечего. Собственно чтение, осуществляется весьма тривиально:
var t=idb.transaction(['chat'],idbConst.READ);
var s=t.objectStore('chat');
var r=s.openCursor();
r.onsuccess=function (e) {
 var idbEntry=e.target.result;
 if (idbEntry){
  //делаем, что нам нужно и читаем дальше
  idbEntry.continue();
 } else {
  //данные закончились
 };
};


Управлять начальным положением указателя не получится, но можно порулить направлением чтения, передав параметр в метод continue.

Заключение


Собственно, на этой не очень оптимистичной ноте можно и заканчивать. Реализация IndexedDB крайне сырая: кактус очень колючий, и текилы из него не выходит. Спектр применения крайне ограниченный, и написание кода для IndexedDB себя явно не окупает, в том числе и из-за малого количества браузеров.

Что посмотреть по теме


mikewest.org/2010/12/intro-to-indexeddb — очень хорошая презентация Майка Веста с тем самым примером адресной книги, которая хоть и содержит ряд неточностей (видимо, просто время прошло), очень хороша для начала разбирательств.
developer.mozilla.org/en/IndexedDB — документ разработки от Мозиллы.
www.w3.org/TR/IndexedDB — спецификация W3C.
www.netroxsc.ru/pub/chateg — пример для Chrome, проверенный в Chrome 11 и 12, и исходники проекта

UPD 2014-04-09. Обновлённая статья по IndexDB: Готовим IndexedDB
+43
18,5k 113
Комментарии 31