Статьи

Близко и Лично с HTML5 IndexedDB

За прошедшие годы Интернет все больше превращается из хранилища контента в рынок полноценных функциональных приложений. Набор технологий, которые попадают под баннер «HTML5», имеют в качестве основной цели возможности для создания в этом новом поколении программного обеспечения. В этой статье я рассмотрю технологию, которая решает важную часть головоломки приложения — управление хранением и извлечением пользовательских данных на стороне клиента — под названием «IndexedDB».

Что такое IndexedDB?

IndexedDB — это в основном постоянное хранилище данных в браузере — база данных на стороне клиента. Как и обычные реляционные базы данных, он поддерживает индексы для записей, которые он хранит, и разработчики используют API-интерфейс IndexedDB JavaScript для поиска записей по ключу или при поиске индекса. Каждая база данных ограничена «источником», то есть доменом сайта, который создает базу данных.

IndexedDB также является отличным примером того, как развиваются веб-стандарты. Через рабочие группы по стандартизации и HTML5 Labs (сайт, который публикует реализации прототипов различных спецификаций HTML5, чтобы вы могли их опробовать и предоставлять отзывы), IndexedDB скоро будет готов к использованию сайта в прайм-тайм.

Если вы новичок в IndexedDB, начните здесь:

  1. Демонстрация поваренной книги на IETestDrive
  2. Руководство для разработчиков по MSDN
  3. Спекуляция на W3C

Теперь давайте подойдем ближе и создадим собственное приложение.

Настройка вашей среды разработки

Начните с установки:

  1. Загрузите прототип, нажав на ссылку «Загрузить Protoype сейчас!» Отсюда .
  2. Распакуйте загруженный файл.
  3. Если вы используете 32-разрядную версию Windows, запустите vcredist_x86.exe .
  4. Зарегистрируйте «sqlcejse40.dll», выполнив в командной строке с повышенными правами следующую команду:

regsvr32 sqlcejse40.dll

Если все идет хорошо, вы должны увидеть этот экран:

What the screen should look like

Internet Explorer 10 Platform Preview поставляется с поддержкой IndexedDB. Или вы можете получить последнюю версию Google Chrome или Firefox, и все должно быть готово.

Создание автономного приложения для создания заметок

Мы собираемся создать слой данных на стороне клиента для веб-приложения для создания заметок:

С точки зрения модели данных, это настолько просто, насколько это возможно. Приложение позволяет пользователям писать текстовые заметки и помечать их конкретными ключевыми словами. Каждая заметка будет иметь уникальный идентификатор, который будет служить ее ключом, и помимо текста заметки она будет связана с набором строк тегов.

Вот пример объекта примечания, представленного в буквальной нотации объекта JavaScript:

  var note = {
   id: 1,
   текст: «Текст примечания»,
   теги: ["образец", "тест"]
 }; 

Мы NotesStore объект NotesStore который имеет следующий интерфейс:

  var NotesStore = {
     init: function (callback) {
     },

     addNote: функция (текст, теги, обратный вызов) {
     },

     listNotes: функция (обратный вызов) {
     }
 }; 

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

Тестирование для IndexedDB

Корневой объект, с которым вы имеете дело при общении с IndexedDB API, называется indexedDB. Вы можете проверить наличие этого объекта, чтобы увидеть, поддерживает ли текущий браузер IndexedDB или нет. Вот так:

  if (window [“indexedDB”] === undefined) {
   // Нет, нет IndexedDB!
 } еще {
   // да мы в порядке!
 } 

Кроме того, вы можете использовать библиотеку JavaScript Modernizr для проверки поддержки IndexedDB следующим образом:

  if (Modernizr.indexeddb) {
     // да, иди indexeddb!
 } еще {
     // блеф!  Нет радости!
 } 

Асинхронные запросы

Асинхронные вызовы API работают через так называемые объекты «запроса». Когда выполняется асинхронный вызов API, он возвращает ссылку на объект «запрос», который выставляет два onsuccessonsuccess и onerror .

Вот как выглядит типичный звонок:

  var req = someAsyncCall ();
 req.onsuccess = function () {
     // обрабатывать случай успеха
 };
 req.onerror = function () {
     // обработать ошибку
 }; 

Когда вы работаете с API indexedDB, со временем становится трудно отслеживать все обратные вызовы. Чтобы сделать его несколько проще, я определю и использую небольшую служебную подпрограмму, которая абстрагирует шаблон «запроса»:

  var Utils = {
     errorHandler: function (cb) {
         функция возврата (e) {
             if (cb) {
                 CB (е);
             } еще {
                 бросить е;
             }
         };
     },

     request: function (req, callback, err_callback) {
         if (callback) {
             req.onsuccess = function (e) {
                 обратный вызов (е);
             };
         }
         req.onerror = errorHandler (err_callback);
     }
 }; 

Теперь я могу написать свои асинхронные вызовы так:

  Utils.request (someAsyncCall (), function (e) {
     // обрабатывать завершение вызова
 }); 

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

Создание / открытие базы данных выполняется путем вызова метода indexedDB объекта indexedDB .

Вот реализация метода init объекта NotesStore :

  var NotesStore = {
     название: «notes-db»,
     дБ: ноль,
     вер: «1.0»,
     init: function (callback) {
         var self = this;
         callback = callback ||  function () {};
         Utils.request (window.indexedDB.open («open», this.name), function (e) {
             self.db = e.result;
             Перезвони();
         });
     },

 ... 

Метод open открывает базу данных, если она уже существует. Это не так, это создаст новый. Вы можете думать об этом как об объекте, который представляет соединение с базой данных. Когда этот объект уничтожается, соединение с базой данных прерывается.

Теперь, когда база данных существует, давайте создадим остальные объекты базы данных. Но сначала вам нужно познакомиться с некоторыми важными конструкциями IndexedDB.

Объектные магазины

Хранилища объектов являются эквивалентом IndexedDB «таблиц» из мира реляционных баз данных. Все данные хранятся в хранилищах объектов и служат основной единицей хранения.

База данных может содержать несколько хранилищ объектов, и каждое хранилище представляет собой набор записей. Каждая запись представляет собой простую пару ключ / значение. Ключи должны однозначно идентифицировать конкретную запись и могут генерироваться автоматически. Записи в хранилище объектов автоматически сортируются в порядке возрастания по ключам. И, наконец, хранилища объектов могут создаваться и удаляться только в контексте транзакций «изменение версии». (Подробнее об этом позже.)

Ключи и ценности

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

Ключи могут быть встроенными или нет. Под «in-line» мы указываем IndexedDB, что ключ для конкретной записи фактически является частью самого объекта значения. Например, в нашем примере хранилища заметок каждый объект заметки имеет свойство id которое содержит уникальный идентификатор для конкретной заметки. Это пример встроенного ключа — ключ является частью объекта значения.

Когда ключи являются «встроенными», мы также должны указать «путь к ключу» — строку, которая указывает, как значение ключа может быть извлечено из объекта значения.

Ключевым путем для объектов «примечаний», например, является строка «id», поскольку ключ может быть извлечен из экземпляров примечаний путем доступа к свойству «id». Но эта схема позволяет хранить значение ключа на произвольной глубине в иерархии элементов объекта значения. Рассмотрим следующий пример объекта значения:

  var product = {
   Информация: {
     название: «Полотенце»,
     тип: «Незаменимая вещь автостопщика»,
   },
   личность: {
     сервер: {
       значение: «T01»
     },
     клиент: {
       значение: «TC01»
     },
   },
   цена: «бесценный»
 }; 

Здесь можно использовать следующий ключевой путь:

  identity.client.value 

Управление версиями базы данных

С базами данных IndexedDB связана строка версии. Это может использоваться веб-приложениями для определения, имеет ли база данных на конкретном клиенте самую последнюю структуру.

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

Изменения номера версии должны выполняться в контексте транзакции «изменение версии». Прежде чем мы перейдем к этому, давайте быстро рассмотрим, что такое «транзакции».

операции

Как и реляционные базы данных, IndexedDB также выполняет все свои операции ввода-вывода в контексте транзакций. Транзакции создаются через объекты соединений и обеспечивают атомарный, надежный доступ к данным и мутации. Есть два ключевых атрибута для объектов транзакции:

  1. Сфера

    Область действия определяет, какие части базы данных могут быть затронуты транзакцией. Это в основном помогает реализации IndexedDB определить, какой уровень изоляции следует применять в течение срока действия транзакции. Представьте себе область видимости просто как список таблиц (известных как «хранилища объектов»), которые станут частью транзакции.

  2. Режим

    Режим транзакции определяет, какая операция ввода-вывода разрешена в транзакции. Режим может быть:

    1. Только для чтения

      Разрешает только операции «чтения» над объектами, которые являются частью области транзакции.

    2. Читай пиши

      Позволяет операции «чтения» и «записи» над объектами, которые являются частью области транзакции.

    3. Изменение версии

      Режим «изменения версии» позволяет выполнять операции «чтения» и «записи», а также позволяет создавать и удалять хранилища и индексы объектов.

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

  • когда они завершат
  • когда они прерывают и
  • когда они тайм-аут

Создание хранилища объектов

Наша база данных хранилища заметок будет содержать только одно хранилище объектов для записи списка заметок. Как обсуждалось ранее, хранилища объектов должны создаваться в контексте транзакции «изменение версии».

Давайте продолжим и расширим метод init объекта NotesStore чтобы включить создание хранилища объектов. Я выделил измененные биты жирным шрифтом.

  var NotesStore = {
     название: «notes-db»,
     store_name: «магазин заметок»,
     store_key_path: «id»,
     дБ: ноль,
     вер: «1.0»,
     init: function (callback) {
         var self = this;
         callback = callback ||  function () {};
         Utils.request (window.indexedDB.open («open», this.name), function (e) {
             self.db = e.result;

             // если версия этого БД не равна
             // self.version затем меняем версию
             if (self.db.version! == self.version) {
                 Utils.request (self.db.setVersion (self.ver), function (e2) {
                     var txn = e2.result;

                     // создать хранилище объектов
                     self.db.createObjectStore (self.store_name,
                                               self.store_key_path,
                                               правда);
                     txn.commit ();
                     Перезвони();
                 });
             } еще {
                 Перезвони();
             }
         });
     },

 ... 

createObjectStore объектов создаются путем вызова метода createObjectStore для объекта базы данных. Первый параметр — это имя хранилища объектов. Затем следует строка, идентифицирующая путь ключа, и, наконец, логический флаг, указывающий, должно ли значение ключа автоматически генерироваться базой данных при добавлении новых записей.

Добавление данных в хранилища объектов

Новые записи можно добавить в хранилище объектов, вызвав метод put для хранилища объектов. Ссылка на экземпляр хранилища объектов может быть получена через объект транзакции. Давайте реализуем метод addNote нашего объекта NotesStore и посмотрим, как мы можем добавить новую запись:

  ...
     addNote: функция (текст, теги, обратный вызов) {
         var self = this;
         callback = callback ||  function () {};
         var txn = self.db.transaction (null, TransactionMode.ReadWrite);
         var store = txn.objectStore (self.store_name);
         Utils.request (store.put ({
             текст: текст,
             теги: теги
         }), функция (е) {
             txn.commit ();
             Перезвони();
         });
     },

     ... 

Этот метод можно разбить на следующие шаги:

  1. Вызовите метод transaction для объекта базы данных, чтобы начать новую транзакцию. Первый параметр — это имена хранилищ объектов, которые станут частью транзакции. Передача null делает все хранилища объектов в базе данных частью области. Второй параметр указывает режим транзакции. Это в основном числовая константа, которую мы объявили так:
      // Константы режима транзакции IndexedDB
     var TransactionMode = {
         ReadWrite: 0,
         ReadOnly: 1,
         Изменение версии: 2
     }; 
  2. Как только транзакция создана, мы получаем ссылку на хранилище объектов с objectStore метода objectStore объекта objectStore .
  3. Как только у нас появится хранилище объектов, добавление новой записи — это всего лишь вопрос выполнения асинхронного вызова API для метода put хранилища объектов, передавая новый объект, который будет добавлен в хранилище. Обратите внимание, что мы не передаем значение для поля id нового объекта примечания. Поскольку мы передали значение true для параметра auto-generate при создании хранилища объектов, реализация IndexedDB должна позаботиться об автоматическом назначении уникального идентификатора для новой записи.
  4. Как только асинхронный вызов put завершается успешно, мы фиксируем транзакцию.

Выполнение запросов с курсорами

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

  1. Диапазон записей в индексе или хранилище объектов.
  2. Источник , ссылающийся на индекс или хранилище объектов, по которому перебирает курсор.
  3. Позиция, указывающая текущую позицию курсора в заданном диапазоне записей.

Хотя концепция курсора довольно проста, написание кода для фактической итерации по хранилищу объектов несколько сложно, учитывая асинхронный характер всех вызовов API. Давайте реализуем метод listNotes нашего объекта NotesStore и посмотрим, как выглядит код.

  listNotes: функция (обратный вызов) {
         var self = this,
             txn = self.db.transaction (null, TransactionMode.ReadOnly),
             заметки = [],
             store = txn.objectStore (self.store_name);

         Utils.request (store.openCursor (), function (e) {
             var cursor = e.result,
                 iterate = function () {
                     Utils.request (cursor.move (), function (e2) {
                         // если «result» равно true, у нас есть данные
                         // мы достигли конца строки
                         if (e2.result) {
                             notes.push (cursor.value);

                             // рекурсивно получаем следующую запись
                             итерация ();
                         }
                         еще {
                             // мы закончили извлечение строк;  вызвать обратный вызов
                             txn.commit ();
                             Обратный вызов (заметки);
                         }
                     });
                 };

             // установить вращение шара, вызывая итерацию для первого ряда
             итерация ();
         });
     }, 

Давайте разберем эту реализацию:

  1. Сначала мы получаем объект транзакции, вызывая метод транзакции объекта базы данных. Обратите внимание, что на этот раз мы указываем, что нам требуется транзакция «только для чтения».
  2. Затем мы получаем ссылку на хранилище объектов с помощью метода objectStore объекта транзакции.
  3. Затем мы выполняем асинхронный вызов API openCursor в хранилище объектов. Сложность в том, что каждая итерация записи курсора сама по себе является асинхронной операцией! Чтобы предотвратить утопление кода в море обратных вызовов, мы определяем локальную функцию с именем iterate чтобы инкапсулировать логику итерации по каждой записи в курсоре.
  4. Эта iterate функция выполняет асинхронный вызов метода перемещения объекта курсора и рекурсивно вызывает себя снова в обратном вызове, если обнаруживает, что есть еще строки, которые нужно извлечь. После того, как все строки в курсоре были получены, мы, наконец, вызываем метод обратного вызова, переданный вызывающей стороной, передавая извлеченные данные в качестве параметра.

Погружайся еще глубже!

Это ни в коем случае не всесторонний охват API, несмотря на то, что вы думаете! Я только покрыл:

  1. Доступные варианты реализации хранилища на стороне клиента сегодня
  2. Различные ключевые аспекты API IndexedDB, в том числе:
    1. Проверка поддержки браузером
    2. Управление асинхронными вызовами API
    3. Создание / открытие баз данных
    4. Ключевые части API, включая хранилища объектов, ключи / значения, управление версиями и транзакции
  3. Создание хранилищ объектов
  4. Добавление записей в хранилища объектов
  5. Перечисление хранилищ объектов с использованием курсоров

Надеюсь, это было близко и достаточно лично!

Теперь, если вы готовы к большему, документ спецификации W3C является хорошим справочником и достаточно короток, чтобы его можно было прочитать! Я бы посоветовал вам поэкспериментировать — доступ к функциональной базе данных на стороне клиента открывает ряд новых сценариев для веб-приложений.

Другим хорошим ресурсом является пример IndexedDB / AppCache на сайте IE Test Drive . В этом примере рассматривается сценарий, в котором две спецификации дополняют друг друга, предоставляя пользователю богатый опыт… даже когда он не подключен к Интернету. Образец также демонстрирует использование новых функций в IE10, таких как 3D-преобразования CSS3 и переходы CSS3.

Веселиться!