Разработка веб-страниц с помощью google gears. Часть 4

March 4, 2008Comments Off on Разработка веб-страниц с помощью google gears. Часть 4

Я завершаю рассказ о разработке веб-приложений в стиле google gears и хранящих часть данных не на сервере в internet, а на компьютере клиента. Кроме сухой теории я не забываю и про практику: мы создаем приложение “записная книжка”. От всех прочих записных книжек она отличается возможностью хранить информацию и изменять ее без прямого подключения к интернету: загрузив базу записей на свой компьютер, вы правите заметки, а после восстановления соединения с internet они “пачкой” сохраняются на сервер.

В прошлый раз я остановился на том, что был создан php-скрипт, отбирающий данные из sqlite-базы (размещенной на сервере) и формирующий json-поток данных. Эти сведения, в свою очередь, должны быть загружены в браузер клиента и отображены в виде html-таблицы. Но перед тем как я приведу пример кода, визуализирующего загруженную информацию, необходимо подготовить “окружение” для веб-приложения. Под “окружением” я понимаю набор вспомогательных функций и переменных, которые позволят писать меньше кода и, главное, позволят плавно переключать стиль работы приложения с gears-стиля на классический стиль (ведь не у всех пользователей пока установлен gears-плагин). И первым шагом в создании подобной “среды” будет написание кода, который определяет, а есть ли в данном конкретном браузере поддержка gears или нет? Как именно это сделать я рассказывал еще в первой статье серии. В том случае, если поддержки gears нет, то следует выполнить загрузку данных в таблицу из internet и на этом все. Действия, которые срабатывают при наличии gears, гораздо сложнее.

Прежде всего, необходимо провести анализ того, в первый ли раз (самый-самый первый раз) пользователь открыл веб-страницу с нашей записной книжкой. Если это так, то необходимо создать в локальном хранилище gears две таблицы. Одна из них будет хранить значения элементов записной книжки (notes), а вторая – конфигурационные переменные приложения. Зачем, скажите Вы, еще одна таблица, и какие такие конфигурационные переменные? Смотрите, gears приложение работает либо с подключением в internet, либо без. Когда клиент открывает браузер, то мы должны проанализировать то, какой режим был активным в прошлый раз. Если активен был режим “online”, то следует загрузить информацию из internet, а если режим “offline”, то из локального хранилища gears. Определение последнего активного режима не представляет сложности: сохранить одно из этих двух слов можно где угодно, например, в cookie, которые я так критиковал в первой статье этой серии. Но мы пойдем другим путем: по мере роста приложения, добавления к нему всяких удобностей и полезностей нам все равно придется делать механизм хранения пользовательских настроек (например, цветовую палитру оформления внешнего вида, количество одновременно отображаемых на странице заметок из записной книжки и т.д.). Так почему бы не создать сейчас в дополнение к таблице notes (содержание записной книжки), так же и таблицу config (хранилище всевозможных опций настройки и конфигурационных переменных)? Плюс на этом легко показать методы чтения и записи информации в gears таблицы.
  1. var db = null;
  2.  var tab = null;
  3.  var glob_jsonnotes = null;
  4.  var ICON_OFFLINE = {'background-image' : 'url(disconnect24.png)'};
  5.  var ICON_ONLINE = {'background-image' : 'url(connect24.png)'};
  6.  var msg_Offline = 'Данные загружены из локального хранилища';
  7.  var msg_Online = 'Данные загружены из internet';
  8.  
  9.  $(document).ready(init);
  10.  
  11.  function init (){
  12.    tab = $('#rows')[0]; 
  13.    if (!window.google || !google.gears){
  14.      $('#hint_switch').html('google gears не доступен');
  15.      $('#hint_mode').html('google gears не доступен');
  16.      loadFromInet();
  17.    }
  18.    else{ 
  19.      setup (); 
  20.      if (getConfig('mode') == 'offline'){
  21.         $('#hint_mode').html (msg_Offline);
  22.         $('#hint_switch').css (ICON_OFFLINE);
  23.         loadFromLocal();
  24.      }
  25.      else{ 
  26.         $('#hint_mode').html (msg_Online);
  27.         $('#hint_switch').css (ICON_ONLINE);
  28.         loadFromInet(); }
  29.         $('#hint_switch').click (doSwitchMode);
  30.     }
  31.  }
В начале я объявляю глобальные переменные. Первая из переменных db – будет хранить ссылку на подключение к базе данных. Вторая – tab – ссылку на html-элемент таблицы, где в последующем нужно будет отображать содержимое записной книжки. Третья - glob_jsonnotes – хранит массив записей, которые были загружены либо из локального хранилища, либо из internet. Остальные переменные не представляют особого интереса и их назначение – добавить “немножко красоты”. Внутри javascript функции init (она вызывается самой первой, как только вся веб-страница была загружена) я, прежде всего, проверил, доступен ли режим gears. Если это не так, то я выполняю загрузку данных в таблицу из internet и изменяю содержимое html-элементов с идентификаторами hint_switch и hint_mode, поместив в них фразу “google gears не доступен”. Если вы посмотрите на пример схемы устройства приложения (я привел ее в прошлой статье), то увидите, что содержимое первого элемента играет роль подсказки о текущем режиме работы (есть ли подключение к internet или нет). Второй блок должен работать как кнопка по нажатию, на которую выполняется переключение двух режимов работы. Если же поддержка gears активна, то мне необходимо выполнить создание таблиц notes и config. Для этого я вызвал функцию setup, пример ее кода показан ниже:
  1. function setup (){
  2.  db = google.gears.factory.create('beta.database', '1.0');
  3.  db.open('notebook');
  4.  db.execute('CREATE TABLE IF NOT EXISTS notes 
  5.     (id INTEGER PRIMARY KEY, category varchar(100), 
  6.     dateof datetime, title varchar(100), comment TEXT)');
  7.  db.execute('CREATE TABLE IF NOT EXISTS config 
  8.     (id INTEGER PRIMARY KEY, variable varchar(100), value TEXT)'); 
  9. }
Первым шагом в ней я создаю подключение к базе данных notebook. Затем выполняю два запроса на создание таблиц notes и config. Обратите внимание на то, что текст первого запроса идентичен запросу, который я использовал при создании таблицы notes на сервере (в прошлой статье). Таблица config имеет очень простое устройство: имя переменной хранится в поле variable, а ее значение – в переменной value. Естественно, что выполнять создание таблиц нужно только в том случае, если их еще нет (за это отвечает ключевое слово IF NOT EXISTS). Для отправки запросов к СУБД используется функция execute, в качестве параметра передайте ей текст SQL-запроса.

Для работы с конфигурационным переменными я создал три функции: setConfig, getConfig, hasConfig. Их назначение это, соответственно, установка нового значения для некоторой переменной, получение значения этой переменной и проверка того существует ли такая переменная или нет. Все эти функции работают с объектом db (он был создан в функции setup). Когда мы выполняем запрос с помощью функции execute, то в качестве параметра передается не только строка SQL-запроса, но массив переменных. Каждая из этих переменных будет подставлена внутрь SQL-запроса вместо символа “?” (при этом если переменные содержат спец. символы, то они будут экранированы). Если был выполнен запрос SELECT, то отобранная информация будет возвращена в виде объекта ResultSet (в примерах выше это переменная rs). Для перемещения по записям используйте метод next объекта ResultSet. А для проверки того, что ваш цикл перебора записей все еще не до конца перебираемой таблицы, используйте isValidRow (она вернет true в случае, если текущая запись содержит информацию из таблицы). Значения полей текущей записи можно получить с помощью функций field или fieldByName. Первая из них вернет значение поля на основании его порядкового номера (задается как аргумент вызова функции). Если же порядок следования не известен, то применяйте функцию fieldByName: она принимает в качестве параметра имя того поля, значение которого нужно вернуть. И последнее: не забывайте закрыть объект ResultSet после окончания работы с ним (экономьте ресурсы).
  1. function hasConfig (v){
  2.    var rs = db.execute ('select 1 from config where variable = ?', [v]);
  3.    var rez = rs.isValidRow(); 
  4.    rs.close ();
  5.    return rez; 
  6. }
  7.  
  8. function getConfig (v){
  9.    var rs = db.execute ('select value from config where variable = ?', [v]);
  10.    var value = null;
  11.    if (rs.isValidRow())
  12.       value = rs.fieldByName ('value');
  13.    rs.close ();
  14.    return value; 
  15. }
  16.  
  17. function setConfig (k, v){
  18.    if (hasConfig(v))
  19.      db.execute ('UPDATE config set value = ? where variable = ?', [v, k]);
  20.    else
  21.      db.execute ('INSERT INTO config(variable, value) values (?,?)', [k, v]);
  22. }
Теперь вернемся назад, к рассмотрению устройства функции init. После “установки” приложения я узнал то, какой из режимов (online или offline) был активирован в последний раз и выполняю загрузку данных либо из internet (за это отвечает функция loadFromInet) либо из локального хранилища (функция loadFromLocal). Чтобы загрузить данные из локального хранилища, мне нужно выполнить запрос “SELECT * FROM notes” (отобрать все содержимое таблицы notes). Затем организовать цикл, перебирающий все найденные записи, (переход к следующей записи выполняется с помощью функции next) и каждая из записей должна быть помещена внутрь массива data. Затем этот массив поступает на вход функции визуализации информации (fillTableFromJSON).
  1. function loadFromLocal (){
  2.   var rs = db.execute ('select * from notes');
  3.   var data = [];
  4.  
  5.   while (rs.isValidRow()){
  6.      data.push ({
  7.         id:rs.fieldByName('id'),
  8.         category:rs.fieldByName('category'),
  9.         dateof : rs.fieldByName('dateof'),
  10.         title : rs.fieldByName('title'),
  11.         comment:rs.fieldByName('comment')
  12.      });
  13.      rs.next ();
  14.   }
  15.  
  16.   rs.close ();
  17.   fillTableFromJSON (data);
  18. }
Загрузка данных из internet не столь прямолинейна: нельзя просто “хватать” записи и “пихать” их внутрь таблицы, ведь так мы потеряем … Потеряем что? Снова вернемся к схеме устройства записной книжки и вспомним, зачем была нужна расположенная внизу страницы (после таблицы с записями) форма. А служила она для редактирования текущей записи. Планировалось, что по клику на строке таблицы она должна подсветиться, а в поля формы внестись значения из таблицы. После того как пользователь изменил значения указанные в этих полях формы, то следует отправить изменения либо на сервер, либо в локальное хранилище (в зависимости от текущего режима работы). Но это еще не все: если клиент в режиме offline исправил несколько записей, то обновленные их значения хранятся в локальной базе данных – не на сервере. Так что, если проявить невнимательность при написании кода и просто загрузить информацию из internet, то можно потерять все пользовательские правки. Как вывод, нужно предварительно отправить все сведения, которые хранятся в локальной базе данных на сервер, чтобы сохранить изменения и там. Подобная синхронизация – задача сложная и требующая решения каждый раз заново в зависимости от специфики вашего веб-приложения. Предупреждение: показанный далее код не оптимален, не эффективен, медленный и его следует избегать в настоящем коммерческом приложении изо всех сил. Единственная причина, по которой я его использую, - он относительно прост и занимает меньше всего места. Я “тупо” читаю все содержимое локальной базы данных, форматирую эти сведения в виде строки JSON и отправляю эту гигантскую строку на сервер к php-файлу save_json.php. Который, в свою очередь, очищает все содержимое серверной таблицы notes и наново заполняет ее пришедшими из браузера записями. Вот пример файла save_json.php:
  1. $records = json_decode ($_REQUEST['records']);
  2.  
  3.  $conn = new PDO('sqlite:notebook.db3');
  4.  
  5.  $conn->query ('DELETE FROM notes');
  6.  $stmt = $conn->prepare("INSERT INTO notes (id, category, dateof, title, comment) 
  7.       values (:id,:category, :dateof,:title,:comment)");
  8.  
  9.  for ($i = 0; $i < count($records); $i++){
  10.     $r = $records[$i];
  11.     $stmt->bindValue(':id', $r->id, PDO::PARAM_INT);
  12.     $stmt->bindValue(':category', urldecode($r->category), PDO::PARAM_STR);
  13.     $stmt->bindValue(':title', urldecode($r->title), PDO::PARAM_STR);
  14.     $stmt->bindValue(':dateof', $r->dateof, PDO::PARAM_STR);
  15.     $stmt->bindValue(':comment', urldecode($r->comment), PDO::PARAM_STR);
  16.     $stmt->execute(); 
  17.  }
  18.  
  19.  die (json_encode (array ('status'=>'true')));
Откровенно говоря, я просто скопировал приведенный в прошлой статье php-код наполняющий базу данных тестовыми записями и немного его подправил. Во-первых, пришедшие данные (это переменная $_REQUEST['records']) необходимо декодировать с помощь функции json_decode (превратить из JSON-строки в массив PHP). После очистки таблицы notes от всего содержимого я организовал цикл по всем элементам массива пришедших от клиента записей и каждую из них поместил внутрь таблицы с помощью SQL-команды INSERT. На этом я серверная часть записной книжки полностью завершена, а вот клиентская часть еще будет продолжаться долго. И сейчас мы разберем то, как были подготовлены данные для отправки на сервер.
  1. function toJSON (x){
  2.  if (x == null) 
  3.       return null;
  4.  
  5.  if(typeof x != "object") 
  6.      return '"'+encodeURIComponent(x)+'"';
  7.  
  8.  var s = [];
  9.  if (x.constructor == Array){
  10.     for (var i in x) s.push (toJSON(x[i])); 
  11.        return "["+s.join (',')+"]";
  12.  }
  13.  else{
  14.    for (var i in x) s.push ('"'+i+'":'+toJSON(x[i])); 
  15.    return "{"+s.join (',')+"}";
  16.  }
  17. }
  18.  
  19. function saveToInet (){
  20.  $.each($('tr:eq(1)', tab), function(i, n){
  21.      n.doEdit();
  22.  });
  23.  $.ajax(
  24.       { 
  25.          type: "POST", 
  26.          cache: false, 
  27.          url: "save_json.php", 
  28.          dataType : 'json', 
  29.          data : {
  30.            records : toJSON(glob_jsonnotes)
  31.          },
  32.          success : loadFromInet, 
  33.          error : function (e) {
  34.             alert ('Не возможно сохранить данные на сервер')
  35.          }
  36.       }
  37.   ); 
  38. }
Первая функция (toJSON) – это стыд и позор для разработчиков internet explorer. В прошлой статье я рассказывал, как хорошо работать с JSON вместо XML (как формат для обмена данными между браузером и сервером), рассказывал, что поддержка этого формата есть и в браузерах и в php. Я не соврал ни на йоту: просто разработчики internet explorer в очередной раз “схалявили” и не реализовали стандартную для javascript функцию преобразования массива записей в строку JSON (функция toSource). В opera и firefox эта функция есть, а в браузере от Microsoft пришлось мне написать собственную версию преобразования. Теперь внимание на код функции saveToInet. Она первым шагом выполняет сохранение текущей записи, затем отправляет с помощью ajax запрос на сервер, передавая в качестве данных переменную records, значение которой – строка содержащая в формате JSON содержимое всей таблицы с заметками. В случае если операция сохранения была неуспешна, то выводится окошко сообщения об ошибке, а если все было в порядке, то запускается функция loadFromInet. Назначение этой функции - загрузить информацию из internet и отобразить ее в виде таблицы. Но сперва, концептуальное замечание: в этом примере записной книжки подобная операция не имеет никакого смысла (после сохранения информации на сервер, содержимое серверной базы данных будет идентично локальной и загружать информацию с сервера бессмысленно). В настоящих веб-приложениях (а они по определению полагают возможность одновременной работы нескольких пользователей с информацией) возможна ситуация изменения кем-то еще содержимого записной книжки. В этом случае нужно сохранить свои правки и загрузить чужие правки – именно так я и поступаю выше.
  1. function loadFromInet (){
  2.    $.ajax(
  3.         {
  4.          type: "POST", 
  5.          cache: false, 
  6.          url: "select_json.php", 
  7.          dataType : 'json', 
  8.          success: function (e) {
  9.             fillTableFromJSON (e); 
  10.             if(db)
  11.               saveToLocal(); 
  12.          }, 
  13.          error : function (e) {
  14.             alert ('Не возможно загрузить данные из Internet.')
  15.          }
  16.        }
  17.    );
  18. }
Здесь, после того как данные были загружены (данные формирует описанный в прошлой статье скрипт select_json.php) их необходимо визуализировать и затем скопировать информацию в локальное sqlite-хранилище. За это отвечают функции fillTableFromJSON и saveToLocal, соответственно. Код второй из функций похож на приведенный выше скрипт сохранения информации в базу данных на сервере. Сначала мы очищаем все содержимое локального хранилища данных, затем с помощью команды INSERT помещаем в таблицу notes все содержимое массива с пришедшими от сервера данными.
  1. function saveToLocal (){
  2.  db.execute ('delete from notes').close();
  3.  for (var i = 0; i < glob_jsonnotes.length; i++){
  4.    db.execute ('insert into notes (id, category, dateof, title, comment) 
  5.       values(?,?,?,?,?)', [
  6.            glob_jsonnotes[i].id, 
  7.            glob_jsonnotes[i].category, 
  8.            glob_jsonnotes[i].dateof, 
  9.            glob_jsonnotes[i].title, 
  10.            glob_jsonnotes[i].comment]
  11.    ); 
  12.  }
  13. }
Теперь последний шаг: визуализация информации. Для этого функция fillTableFromJSON выполняет очистку html-таблицы от старого содержимого. Затем организуется цикл по всем элементам массива glob_jsonnotes. Для каждой записи динамически создается строка таблицы с тремя ячейками, и заполняются значениями полей записи. За редактирование текущей ячейки отвечает функция doEdit. Привязать к некоторому html-элементу обработку события “клик” можно с помощью функции click. Внутри функции обработчика (doEdit) я обращусь к активной строке таблицы, извлеку из нее значение атрибута id и выполню поиск в глобальном массиве glob_xmlnotes нужной записи, затем останется только поместить значения полей этой записи в поля формы редактирования.
  1. function fillTableFromJSON(notes){
  2.  lastSavedRow = null;
  3.  while (tab.rows.length > 1) 
  4.     ab.deleteRow (1);
  5.  
  6.  glob_jsonnotes = notes;
  7.  var oRow = null;
  8.  var oCell = null;
  9.  
  10.  for (var i = 0; i < notes.length; i++){
  11.     var n = notes [i] ;
  12.     // создаем очередную строку
  13.     oRow = tab.insertRow(i + 1);
  14.     $(oRow).attr ({id:i, id2: n.id});
  15.     // в нее помещаем три ячейки
  16.     oCell = oRow.insertCell(0);
  17.     oCell.innerHTML = n.category;
  18.     oCell = oRow.insertCell(1);
  19.     oCell.innerHTML = n.dateof;
  20.     oCell = oRow.insertCell(2);
  21.     oCell.innerHTML = n.title;
  22.     oRow.doEdit = doEdit;
  23.  
  24.     $(oRow).click (doEdit);
  25.   }
  26.  }
Естественно, что рутинная и громоздкая часть кода специально осталась за границами этого материала. Но в любом случае – я разместил специально подготовленные файлы с примерами исходного кода (равно как и рабочую “демку”) здесь:

Демо


  <iframe src="../contents/data/mediawiki/__special__/html/gears_demo_1/index.html" width="1000" height="900">
   нет поддержки iframe
  </iframe>

Примеры исходных текстов



https://github.com/study-and-dev-site-attachments/all-in-one/tree/master/php/gears_demo_1

На этом все. Надеюсь, что свою основную цель: заинтересовать читателя новой идеологией разработки веб-приложений и показать как легко (ладно, все же довольно тяжело) создавать gears-приложения, я выполнил. Еще Вас могут заинтересовать механизмы взаимодействия между google gears и flash/flex. Так, способность хранить и обновлять по требованию не только табличную информацию, но и произвольные файлы, была бы полезна для разработчиков flash-основанных игр с большим объемом графического наполнения.