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

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

Сегодня я продолжаю рассказ о google gears. В первой статье серии я рассказал о новой идее организации веб-приложений, легшей в основу технологии google gears. Тогда же в качестве примера я решил показать, как создать небольшое приложение “записная книжка”. Во второй статье мне пришлось сделать небольшое отступление от, собственно, gears и рассказать об sqlite (базе данных, где gears хранит пользовательскую информацию). Сегодня пришло время собрать эти кусочки воедино.

С чего начинается разработка любого gears-приложения? Учитывая, что не у всех пользователей сети есть установленный в браузере плагин google gears, начать нужно с создания такого же по функциональности веб-приложения. Вот только, данные оно будет брать не из gears-хранилища, а из базы данных размещенной на веб-сервере. Gears должны не заменить привычные нам веб-приложения, а расширить их функциональность. Сайт должен работать всегда: даже, если у клиента нет поддержки gears. С другой стороны, мы понимаем, что необходимо заложить такой каркас приложения, чтобы при добавлении gears-функциональности нам не пришлось переделывать все с нуля. И тем более мы хотим избежать ситуации, когда возникнут две параллельные версии сайта (одна в стиле gears, другая в стиле classic). Акцент традиционного веб-приложения в том, что данные и внешний вид связаны - и это очень плохо. Ведь когда мы будем работать в режиме offline (без подключения к интернету), то данные должны браться не из Интернета, а из локального gears-хранилища. В первой статье, когда я говорил об архитектуре gears, то упомянул, что частью его функциональность является возможность сохранять во внутреннем КЭШе не только табличную информацию (за это отвечает sqlite), но элементы оформления (html, javascript, css). Значит, наше приложение будет размещено, по сути, в четырех местах: локальное и удаленное хранилище ресурсов (html, js, картинки), локальное и удаленно хранилище данных (sqlite). И если выбор источника ресурсов большей частью - задача gears, то выбор источника данных и загрузка информации из него лежит целиком на нас. Хочу сразу напугать: сделать хорошее gears-приложение без использования ajax-практически не возможно. Также, для работы с html (ведь страница будет динамически формироваться на стороне клиента) мне потребуется какая-нибудь хорошая javascript-библиотека. Естественно, я буду часто комментировать свои действия, но все же рекомендую найти и прочитать мою летнюю серию статей про ajax и jquery: именно эти инструменты нам сегодня понадобятся. Кроме того, методики веб-разработки, которые я сегодня покажу, могут быть применимы не только, когда вы пишите gears-приложение, но и при разработке (чуть не сказал традиционных) приложений асинхронно загружающих данные (ajax).

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



В таблице будут перечислены заметки; а при “клике” по какой-либо из строк, внизу должна открываться форма редактирования текущей записи. Над таблицей расположен индикатор режима работы (offline или online), а также кнопка переключения между этими режимами. При переходе в режим offline данные с сервера будут загружаться в локальное хранилище, а при переходе в режим online данные следует закачать обратно на сервер.

Ниже приведен код создающий страницу-прототип.
  1. <html> 
  2.   <head>
  3.    <link rel="stylesheet" href="core.css" type="text/css" />
  4.    <script src="jquery.js"> </script>
  5.   </head> 
  6.   <body>
  7.     <table id="header" border="1"> 
  8.          <tr>
  9.             <td id="hint_mode">Сообщаем какой режим активен</td>
  10.             <td id="hint_switch">Заготовка для кнопки переключения режимов</td>
  11.         </tr>
  12.     </table>
  13.     <table id="rows" border="1" cellspacing="0"> 
  14.         <tr>
  15.             <th>Категория</th>
  16.             <th>Дата</th>
  17.             <th>Заголовок</th>
  18.         </tr>
  19.     </table>
  20.     <table border="1" id="editor"> 
  21.         <tr>
  22.             <td>Категория:</td>
  23.             <td><input id="txt_category" /></td>
  24.         </tr>
  25.         <tr>
  26.             <td>Дата:</td>
  27.             <td><input id="txt_date" /></td>
  28.         </tr> 
  29.         <tr>
  30.             <td>Заголовок:</td>
  31.             <td><input id="txt_title" /></td>
  32.         </tr> 
  33.         <tr>
  34.             <td>Сообщение:</td>
  35.             <td><textarea id="txt_message" cols="30" rows="3"></textarea></td>
  36.         </tr>
  37.     </table>
  38.   </body>
  39. </html>
В самом начале файла я подключаю javascript-библиотеку jquery (http://jquery.com/). Там же с помощью файла core.css подключается стилевое оформление страницы (я его не привожу, т.к. особенности оформления никак не влияют на дальнейшее развитие примера, к тому же хороший дизайн занимает слишком много места). Само тело страницы разбито на три таблицы. Первая из них (header) будет содержать сведения о текущем режиме работы приложения (ячейка hint_mode), в ячейке hint_switch будет создан переключатель между режимами. Вторая таблица (rows) должна будет содержать записи, но пока в ней есть только одна строка с названиями заголовков. А в третьей таблице разместились текстовые поля, которые будет использованы при редактировании записи. Я специально пропустил тег создания формы, т.к. все операции обмена данным с сервером будут выполняться асинхронно с помощью ajax, и форма просто не нужна.

Шаг 2: создать php-скрипт, выполняющий “установку” веб-приложения. Перед тем как мы начнем создавать код визуализирующий информацию из записной книжки, было бы неплохо создать саму эту записную книжку (базу данных, таблицу в ней, заполнить ее данными). Особой разницы между выбором, какую СУБД использовать на стороне веб-сервера нет. С равным успехом я мог использовать mysql, postgres, oracle. С другой стороны, раз мы в прошлый раз столько говорили об sqlite, то почему бы не использовать эту СУБД на обеих сторонах коммуникации? Предварительно проверьте, есть ли у вас на хостинге поддержка sqlite (в принципе, это стандарт, но для бесплатных и очень дешевых хостингов законы не писаны). Есть три набора функция для работы с sqlite: собственные функции расширения php_sqlite (их имена начинаются с префикса “sqlite_”), либо вы можете работать с этой базой посредством PDO или AdoDB. Естественно, что выбрать библиотеку для работы с sqlite не так просто, как кажется. Собственные функции расширения php_sqlite ориентированы на sqlite версии 2. А библиотека ADODB является “оберткой” над функциями php_sqlite и нам не поможет. Единственный способ для того, чтобы использовать средства sqlite3 (именно об этой версии я рассказывал в прошлой статье) и “обойтись малой кровью” - это воспользоваться PDO. PDO – унифицированный интерфейс работы с базами данных, который, как многие надеются, должен заменить собой все старые расширения и библиотеки в php. Далее я привожу код файла setup.php, его назначение создать sqlite базу данных, создать в ней таблицу и наполнить ее случайным набором данных (имитация заполненной записной книжки).


  1. $conn = new PDO('sqlite:notebook.db3');
  2.  
  3.   $conn->query ('DROP TABLE IF EXISTS notes');
  4.  
  5.   $conn->query ('CREATE TABLE notes (id INTEGER PRIMARY KEY, category varchar(100), 
  6.       dateof datetime, title varchar(100), comment TEXT) ');
  7.  
  8.   $stmt = $conn->prepare("INSERT INTO notes (category, dateof, title, comment) 
  9.       values (:category,:dateof,:title,:comment)");
  10.  
  11.   $categs = array ('Покупки', 'Отдых', 'Работа', 'Семья');
  12.  
  13.   for ($i = 0; $i < 50; $i++){
  14.     $stmt->bindValue(':category', iconv('windows-1251', 'utf-8',$categs[$i % 4]), PDO::PARAM_STR);
  15.     $stmt->bindValue(':dateof', '2007.'. (1+($i % 11)) . '.25', PDO::PARAM_STR);
  16.     $stmt->bindValue(':title', iconv('windows-1251', 'utf-8', 'Заметка_' . $i ), PDO::PARAM_STR);
  17.     $stmt->bindValue(':comment', iconv('windows-1251', 'utf-8', 'Информация_' . $i ), PDO::PARAM_STR);
  18.     $stmt->execute(); 
  19.   }
Для подключения к базе данных я создал (new) объект PDO, указав в качестве параметра строку конфигурации. Эта строка состоит из двух частей, разделенных двоеточием: протокол (база данных) и параметры специфические для конкретной СУБД. Так для sqlite мне нужно указать только имя файла базы данных (notebook.db3 будет создан в текущем каталоге). Далее: есть два способа отправки SQL-команд к СУБД: query и prepare/execute. Первая команда рекомендуется в том случае, если выполняемый уникальные (не повторяющиеся запросы). Например, сразу после подключения я выполнил удаление таблицы notes. При самом первом вызове скрипта настройки приложения (setup.php) такая таблица еще не существует, и чтобы не произошла ошибка, я добавил в команду DELETE TABLE опциональный параметр IF EXISTS. Это значит, что таблица будет удалена только в том случае, если она существует. Затем нужно создать таблицу для хранения заметок (помните, что типы данных в sqlite - иллюзия). В которой есть одно поле, играющее роль первичного ключа (это уникальный номер записи, и он будет генерироваться sqlite автоматически), а также четыре информационных поля (категория, дата создания, заголовок и, собственно, содержание заметки). Следующий этап – заполнить таблицу случайными данными. В отличие от предыдущих команд, выполнявшихся с помощью query, здесь я использую функции prepare/execute (ведь команда на вставку данных в таблицу будет выполняться многократно с отличиями только в данных, которые нужно сохранить в таблице). До начала цикла я создал с помощью функции prepare объект “подготовленного к выполнению” SQL-запроса (переменная $stmt). Затем внутри цикла на 50 повторений я выполняю генерацию случайных значений для каждого из полей полей записи и “привязал” (bindValue) эти величины к объекту запроса. Последний шаг – отправка запроса на выполнение, за это отвечает функция execute. Критически важно: данные, которые мы заносим внутрь sqlite таблицы, должны быть в кодировке utf-8, именно за это отвечает вызов функции iconv.

Шаг 3: создание php-скрипта, который отбирает данные из таблицы notes и возвращает этот список данных в виде … В виде чего? Нужно определить в каком из множества доступных форматов следует отправлять информацию клиенту. Традиционные приложения помещали информацию внутрь html-шаблона и отправляли клиенту готовый для “отрисовки” браузером html-документ. Нам этот способ не подойдет: визуализация должна быть проведена, именно, на стороне браузера, а не сервера (ведь приложение должно работать вне зависимости от того будут ли данные загружены с сервера или из локального хранилища). Когда я рассказывал об ajax, то говорил, что только два формата передачи данных являются общепринятыми стандартами: xml и json. Выбор одного из них кардинальным образом влияет и на то, как я буду писать код выполняющий сохранение загруженных с сервера данных в локальное хранилище gears. Влияет выбор и на то, как данные из хранилища будут отправляться на сервер для последующего сохранения. Нужно выбрать такой формат, который принесет наименьшее количество проблем. Предположим, что данные были загружены в формате xml, например, таком:
  1. <note id="1">
  2.   <category ><![CDATA[categ_0]]></category >
  3.   <dateof><![CDATA[2007.1.25]]></dateof>
  4.   <title ><![CDATA[title_0]]></title >
  5.   <comment><![CDATA[Привет]]></comment>
  6. </note>
Каждая запись таблицы notes была помещена внутрь тега note (значение атрибута id, которой хранит значение первичного ключа записи). Вложенные xml-элементы (category, dateof, title, comment) – содержат значения одноименных полей записи. Все xml-теги содержат информацию, заключенную внутрь CDATA. Если бы я так не сделал, то записная книжка перестала бы работать, как только кто-нибудь ввел бы в заголовок или текст заметки текст, содержащий специальные символы (например, символы “<” или “>” имеют особое значение в xml-е и их нужно “экранировать”).

Теперь проведем анализ дальнейшей обработки такого файла. Во-первых: xml-данные необходимо визуализировать, например, с помощью xslt-преобразования (здравствуй, еще сложная технология, которая, плюс к этому, не всегда корректно выполняются на распространенных браузерах). Либо выполнить “ручной” разбор xml-документа: с помощью множества циклов и условий мы можем “бегать” по дереву xml-документу, а найденные узлы помещать внутрь html-шаблона. И я не сказал бы, что это составит большие трудности. Несмотря на то, что “разбор” xml-я средствами DOM не всегда удобен, но ведь у нас в помощниках библиотека jquery. А с ней все становится проще. В первой статье про jquery я уделил внимание вопросу поиска элементов html-страницы с помощью xpath-нотации. Подобная функциональность применима и для xml-документов (как же иначе, ведь html – частный случай xml). Предположим, что в переменной msg находится ссылка на загруженный с сервера xml-документ. Тогда с помощью следующей строки кода я могу извлечь из входного документа массив (notes), содержащий все узлы “note”.
  1. var notes = $('note',msg);
Далее нужно организовать цикл по этому массиву, и для каждого его элемента выполнить действия по дальнейшему разбору: извлечь содержимое атрибута “id” и узлов “category”, “dateof”, “title”, “message”. С jquery это не составляет никаких трудностей:
  1. for (var i = 0; i < notes.length; i++){
  2.  var n = notes [i];
  3.  var id = $(n).attr('id');
  4.  var category = $('category', n)[0].firstChild.nodeValue;
  5.  //.. анализ остальных узлов
Что здесь происходит? Переменная n – ссылка на текущую запись (note). Для того чтобы добраться к значению атрибута для некоторого узла, я использовал функцию attr(имя_атрибута). Известно, что внутри узла “n” находится еще один узел с именем ‘category’. Для его поиска я использую вызов jquery, указав первым параметром имя искомого элемента, а вторым узел, внутри которого нужно искать. Странная запись “firstChild.nodeValue” означает то, что из узла ‘category’ необходимо извлечь еще один узел (вспомните, что я поместил текстовую информацию внутрь вложенного блока CDATA).

Так что, раз разбор xml-я так прост, значит именно этот формат мы выберем для обмена данными с сервером? Не торопитесь: ведь без анализа остались еще несколько этапов работы gears-приложения: сохранение информации внутрь gears таблицы, восстановление информации оттуда и отправка xml-на веб-сервер с последующим там его анализом. Я не буду приводить здесь детальные выкладки или прототипы кода для каждого из этих этапов (хотя сам сделал две версии записной книжки с целью оценить их трудоемкость и качество получающегося продукта). Но выводы озвучу: слишком много лишних действий, и слишком медленно все работает. Лишние действия связаны с тем, что в xml-е все сделано с избыточным запасом, так чтобы иметь возможность для хранения сложных иерархических структур данных (разве, записная книжка с 4-мя полями это сложная иерархическая структура?). Каждое из описанных выше трех действия требует значительны затрат времени (конечно, для небольшой книжки с десятком записей это не важно, но по мере роста приложения трудности будут множиться как снежный ком). Тут я немного лукавлю: фактически можно было бы сохранить весь xml-документ в одном единственном огромном текстовом поле одной единственной записи в базе данных. Но от затрат на построение DOM-дерева и его анализ мы бы все равно не смогли избежать, плюс это привело бы разрастанию количества javascript-кода. Да и вообще, эта методика пахнет каким-то иезуитством. Как вывод: xml использовать можно, но не нужно – используем JSON. JSON – сокращение от Javascript Object Notation. Проще говоря, это такой формат записи структур данных на javascript, который удобочитаем, компактен и, что самое главное, поддержка этого формата есть и на стороне браузера (не зря ведь в названии слово Javascript) и на стороне веб-сервера (PHP). Загружаемые с сервера данные будут выглядеть так:
[{"id":"1","category":"categ_0","dateof":"2007.1.25","title":"title_0","comment":"hello}, … ];
Каждая запись помещается внутрь фигурных скобок, а содержащиеся в ней поля представлены как пары: “имя поля: значение”. Так как подобных записей более одной, то мы вынуждены поместить их внутрь квадратных скобок (это означает массив записей). Теперь нужно написать код для еще одного php-файла (назовем его “select_json.php”), который бы формировал подобный json-документ:
  1. header ('Content-Type: text/javascript');
  2. $conn = new PDO('sqlite:notebook.db3');
  3. $notes = array ();
  4. foreach ($conn->query('SELECT * from notes') as $row) {
  5.  $notes [] = array ('id' => $row['id'],
  6.  'category' =>  $row['category'],
  7.  'dateof' => $row['dateof'],
  8.  'title' =>  $row['title'],
  9.  'comment' => $row['comment']);
  10. }
  11. print json_encode ($notes);
Здесь я выполнил запрос к базе данных notebook.db3, выбрав все записи из таблицы notes. Затем эти данные были помещены внутрь php-массива notes. Каждый элемент этого массива является еще одним массивом, но уже ассоциативным и хранит значения всех полей записи из таблицы notes. Преобразование php-массива в json-массив выполнено с помощью функции json_encode. Надо отметить, что столько же легко (вызовом всего одной функции) можно будет выполнить и обратное преобразование данных (когда придет время синхронизировать содержимое записной книжки на сервер).

Обратите внимание на следующие моменты. Во-первых: я явно указал тип возвращаемой информации из скрипта как “text/javascript”, заменив тем самым значение по-умолчанию (“text/html”). Во-вторых: требуется, чтобы данные, которые подлежат json-кодированию, были в формате utf-8. Вспомните, когда я создавал скрипт заполняющий БД тестовыми записями, то акцентировал на этом внимание.

В следующий раз я продолжу рассказ об gears и нам предстоит организовать анализ загруженной с сервера информации, и динамически сформировать html-документ. Также нам пора разобраться в том, как сохранить эти записи в локальном sqlite хранилище.