IndexedDB – стандарт хранения больших объемов структурированных данных на клиенте – был ожидаем также как и WebSocket (ну может самую малость меньше). В свете выхода FireFox 4 я нашёл время и силы всё-таки разобраться, как им пользоваться, и попытаться написать что-то больше, чем пример с адресной книгой, гуляющий по интернетам (в процессе поиска информации у меня сложилось впечатление, что это был единственный пример).
IndexedDB служит для хранения больших объемов структурированных данных, с возможностью индексации. Потребность в таком инструментарии назрела давно, что и привело к его появлению в спецификациях HTML5. Краткую предысторию можно прочесть здесь.
В тексте будут встречаться лирические отступления, требующиеся для выхода эмоций. Посыпать статью ссылками вики-стайл не хочется, все источники сложены в конце.
Так случилось, что я в последнее время занимаюсь созданием чатиков. Вот и будем строить чатик, со следующими требованиями:
Первым делом необходимо проверить, есть ли поддержка IndexedDB в браузере. Делается это так:
Далее необходимо создать объект типа IDBRequest, который будет предоставлять асинхронный доступ к объектам базы данных. Синхронный доступ к IndexedDB в драфте спецификации присутствует, но пока не реализован.
Обработчки onerror:
Если верить спецификации, то в качестве аргумента должен прилететь объект типа IDBErrorEvent, имеющий два свойства – code и message. На практике, прилетает просто событие, к счастью имеющее в свойствах объект IDBRequest, из которого можно вытащить код ошибки и, в случае вебкита, собственно сообщение. Итоговый обработчик имеет следующую структуру:
Вызвать эту ошибку довольно просто:
Если подключение прошло успешно, можно продолжить и проверить, есть ли нужная база данных у пользователя и той ли она версии. Структура обработчика успешного подключения:
В случае отсутствия базы данных или неверной её версии, надо бы её создать или модифицировать. Для этого нам потребуется метод setVersion. Здесь, я вынужден отвлечься от написания кода, и рассказать о механизме трансакций в IndexedDB.
Драфт W3C определяет четыре типа трансакций: READ_ONLY, READ_WRITE, SNAPSHOT_READ и VERSION_CHANGED.
READ_ONLY – служит, как следует из названия, для чтения. Блокирует трансакции других типов.
READ_WRITE – служит для изменения данных, дожидается завершения всех конкурирующих трансакций над выбранным объектом, блокирует все прочие трансакции и выполняется.
VERSION_CHANGE – трансакция, которая дожидается завершения всех прочих трансакций, блокирует доступ к объектам данных для всех и выполняется. Только в этой трансакции можно создавать, удалять или изменять объекты данных.
Как уже было сказано, совершать манипуляции с объектами данных можно только из трансакции VERSION_CHANGE. Подключиться к ней мы можем из обработчика успешной смены версии.
Что можно сделать с объектами данных:
Создать – createObjectStore()
Удалить – deleteObjectStore()
Назначить трансакцию – transaction()
В качестве аргумента метод удаления принимает имя объекта данных, который следует удалить. Метод выполняется асинхронно, и по идее, должен возвращать объект типа IDBRequest, к которому можно прицепить обработчик onsuccess. Но ни Webkit, ни Mozilla не считают нужным что-либо вернуть. Работает метод, тем не менее, асинхронно и блокирует доступ конкурирующим методам. Потому использование конструкции
приводит к непрогнозируемым результатам. Что-то удалиться, что-то нет. Узнать нормальным способом не получается. Структура костыля, в принципе, понятна, но я решил этим не заморачиваться, т.к. удалаять объекты нужно, по большому счету, только в случае изменения структуры базы.
В примере, с адресной книгой, создавался всего один объект базы данных. Но, когда я решил создать два объекта, FireFox начала обкладывать меня матом на тему NON_TRANSIENT_ERR, что примерно означает: «не то, и не в той трансакции делаешь».
Изначально конструкция была следующей, и нормально работала в Chrome:
После нескольких часов экспериментов, был создан хак и для ОгнеЛиса, который работает и в Chrome:
Как вы наверно заметили, перед создан��ем объекта идёт проверка, нет ли уже такого объекта в базе. Сделано это для того, чтобы не возиться с удалением объектов. Если попробовать создать объект, который уже присутствует в базе, то вывалится ошибка.
Опциональные аргументы создания объектов: имя ключа и флаг автоинкриментности (игнорируется браузерами).
Также из кода исчезло создание индексов: ОгнеЛис болезненно реагировал на создание индексов в трансакции смены версии.
К этому моменту база данных сформирована. Казалось бы, можно продолжить. Но это не так. Все вызовы выполняются асинхронно, потому нельзя утверждать однозначно, что все объекты были созданы. Опять-таки, если верить спецификации, в возвращаемом методом createObjectStore() объекте IDBObjectStore должно содержаться свойство IDBRequest, которому можно навесить обработчик onsuccess. Но похожего свойства в объекте нет. Потому, перед тем как запустить основной цикл работы необходимо дождаться завершения создания объектов БД, что делается следующим не хитрым кодом:
Наконец-то база создана, и в неё можно начать писать и из неё же читать, что записали.
Записывать данные можно двумя способами (и только из трансакции записи): add и put. Различия следующие: если в add передать ключ, который уже присутствует в объекте, значение не будет записано; put – заменяет данные. Операции опять же асинхронные.
Управлять начальным положением указателя не получится, но можно порулить направлением чтения, передав параметр в метод 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
Несколько вводных слов
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