« Сложные интерфейсы на javascript вместе Yahoo UI. Часть 13 | Сложные интерфейсы на javascript вместе Yahoo UI. Часть 15 » |
Сложные интерфейсы на javascript вместе Yahoo UI. Часть 14
Сегодняшняя статья лишь формально продолжает серию, рассказывающую о библиотеке javascript компонентов Yahoo UI. Разработка сложного интерфейса веб-страницы активно использующего идеи ajax, поднимает вопрос о том, как визуализировать данные, загруженные с сервера. В отдельных ситуациях можно обойтись подходом, когда на стороне сервера формируется полный фрагмент html-представления страницы. В других случаях YUI компоненты диктуют правила как должны выглядеть отображаемые в них данные. Я расскажу о том, как быть когда ни один из этих двух подходов нам не подходит.Чем плох подход, когда серверный скрипт в ответ на ajax-запрос от браузера клиента формирует фрагмент html-кода, который можно сразу же поместить внутрь какого-либо из существующих элементов html-страницы, да хоть присвоив новое значение свойству innerHtml. Первый недостаток очевиден – это лишние затраты на трафик. Т.е. по сети передается не только информация, но и теги html, фрагменты css-стилей, которые управляют внешним видом этих данных. В разных ситуациях, процентное соотношение между “чистыми” данными и их форматированием меняется в широких диапазонах. Причем фрагменты тегов форматирования склонны к повторению: например, каждый элемент списка с перечнем имен сотрудников имеет одинаковую структуру: “li”, “a”, “img”, “span”. Возможным решением, чтобы уменьшить объем трафика, а, следовательно, скорость отклика приложения на запрос клиента будет выполнять gzip¬-сжатие html-фрагмента, который сформировал http-сервер. Естественно, что это повлечет за собой дополнительную нагрузку на сервер: работа шаблонификатора и последующее сжатие данных не бесплатно. Можно было бы переложить эту работу на клиента. Т.е. серверный скрипт отдает “чистые” данные, в виде xml или json. А javascript-код на стороне клиента эти данные интерпретирует и строит dom-дерево. Простого ответа на вопрос какой подход выбрать нет. Здесь участвуют факторы технические, экономические, организационные и многое другое. Например, вы выбираете то, куда “переложить нагрузку”: на сервер или на машину клиента. С одной стороны сервера легче масштабировать, т.е. когда серверная часть не станет справляться с нагрузкой, то вы добавляете в стойку пару серверов. Клиентскую часть вы не контролируете, не знаете каким “железом” пользуется клиент, насколько у него тормозит, и не можете заставить его сделать апгрейд (хотя производители игр только этим и занимаются). С другой стороны, может быть так, что для сохранения приемлемой производительности по мере роста количества клиентов, увеличением количества серверов уже не обойтись: затраты на покупку или аренду серверов, их обслуживание становятся большими чем получаемая прибыль с каждого подписчика. Более того, выбираемый подход может меняться во времени. Так мы можем начать с того, что сделаем “быстрый и грязный” прототип, который выходит на рынок, опережая своих конкурентов. А затем по мере появления достаточных ресурсов и уточнения понимания того, как проект должен работать и приносить прибыль, мы начинаем его переделывать. Больше в философию “как быть” я вдаваться не буду, и перехожу к практическому вопросу “как сделать”.
Предположим, что нужно разработать приложение календарь. Это приложение получает данные с сервера (в формате xml или json). Затем эти данные трансформируются и визуализируются. Почему визуализация будет идти на стороне клиента? Предполагается, что календарь может иметь несколько представлений одних и тех же данных. Например, после загрузки сведений о запланированных делах на месяц, вы можете захотеть увидеть эту информацию сразу целиком, или по отдельным неделям, или дням. Можно придумать задачу анализа расписания, например, в таблице по строкам расположить названия запланированных работ, а по столбцам вывести дни недели с подбивкой итогов, сколько суммарно мы тратим времени на каждый из видов работ. Здесь очевидно, что нет никакой необходимости обращаться к серверу при переключении “видов” календаря: данные одни и те же – меняется только правила их визуализации. Обращаясь к паттерну MVC, мы говорим, что меняется V и C (внешний вид и контроллер, обрабатывающий действия пользователя). Что касается данных, то они неизменны. Более того, я советовал бы вам обратиться к моим статьям, посвященным google gears. С помощью gears мы можем хранить содержимое календаря не только на сервере, но и на вашей локальной машине в форме реляционной базы данных sqlite. Относительно выбора между json и xml, я могу сказать, что хотя мне очень нравится xml и xslt, у меня есть опыт работы с этими технологиями уже несколько лет. Но применительно к веб-разработке, а точнее задачам трансформации xml с помощью xsl на стороне именно клиента (браузера), xsl лучше не использовать. Т.к. нет единого поведения у всех основных браузеров, плюс написание правил трансформации достаточно сложно. Например, когда я рассказывал своим знакомым о том, как с помощью xslt сделать цикл на пять повторений (с помощью рекурсии), то они смотрели на меня непонимающими глазами и спрашивали: “почему так сложно?”. С другой стороны, если вам нужно делать запросы на отбор именно информации, циклы по существующим данным, то можно компактно записать с помощью xpath выражение вроде “найти и вывести все отделы, у которых количество сотрудников мужского пола составляет менее половины общего числа”. Сложности составляет и процесс отладки xsl-выражений, хотя есть отличный инструментарий вроде altova xmlxpy или intellij idea с плагином xslt debugger, но пользоваться ими (по своему опыту общения с начинающим веб-разработчиками) сложно. Так, что я выбираю как основной формат данных загружаемых с сервера именно json. Теперь полагая, что у меня есть следующий входной массив, задумаемся над тем как эти данные визуализировать:
var kadry = {
title : "Рога и копыта",
main_departments : [ {title : "Отдел продаж", employees: [
{fio: "Jim", age: 12, sex : "male"},
{fio: "Janet", age: 13, sex : "female"} ],
subdepartments: [
{title: "Рекламщики"},
{title: "PR"}
] } , { -- описание еще одного отдела -- } ]
};
var main_deps = document.createElement( "div" );
main_deps.className = "style_for_deps";
for (var i = 0; i < kadry.main_departments.length; i++){
var h1 = document.createElement("h1");
h1.appendChild (document.createTextNode( kadry.main_departments[i].title ));
h1.setAttribute ('class', 'heading_department');
main_deps.appendChild (h1);
}
document.body.appendChild (main_deps);
var main_deps = '<div class="style_for_deps">';
for (var i = 0; i < kadry.main_departments.length; i++)
main_deps += '<h1 class="heading_department">' + kadry.main_departments[i].title + '</h1>' ;
main_deps += '</div>'
document.body.innerHTML = main_deps;
var e = html (
{ tagName : 'div',
style: {color: 'red', fontSize: '16px'},
'text': 'Hello from javascript',
onclick : 'alert(this.innerHTML)',
'class' : 'class_a',
children: [
{
tagName: 'span',
text: 'Hello from HTML',
children:
{tagName: 'b', text: 'bold text'}
}
]
}
);
document.body.appendChild (e.tag);
alert ('второй тег внутри div равен ' + e.children[1].tag.innerHTML);
function html( attrs ) {
var children = [];
// создаем элемент
var e = document.createElement( attrs.tagName);
for ( var atName in attrs ) {
var atValue = attrs[atName];
if (atName == 'tagName') continue;
// если один из атрибутов это массив стилей
if (atName == 'style')
for (var stName in atValue)
e.style[stName] = atValue[stName];
else if (atName == 'text')
// если текстовое содержимое элемента
e.appendChild( document.createTextNode( atValue ) );
else if (atName == 'children'){
if (! atValue.length) atValue = [atValue];
// если список дочерних тегов
for (var i = 0; i < atValue.length; i++){
var innerEl = html (atValue[i]);
children.push (innerEl);
e.appendChild (innerEl.tag); }
}
else
// если обычный атрибут
e.setAttribute (atName, atValue);
}// конец цикла
return { tag: e, children: children};
}
function doSomeThing (){
alert (this);
}
$('<div>')
.addClass('class_a')
.css({color: 'red', fontSize: '24px'})
.click (doSomeThing)
.append( $('<span>')
.append('Hello From HTML')
.append( $('<b>')
.append('bold text')
)
).appendTo('#box');
Завершающим сегодняшнюю статью будет рассказ о template engines в javascript. Шаблонные движки широко известны и применяются на стороне сервера для внедрения информации внутрь шаблона html-страницы. Так обязательным требованием при приеме на работу php программиста является знание smarty. Для javascript выбор поменьше и какого-то де-факто стандарта нет. Я расскажу о trimpath (сайт проекта http://code.google.com/p/trimpath/). Код самой javascript-библиотеки порядка 20 кб. Итак, полагаю, что вы загрузили и подключили к своему html-файлу библиотеку trimpath-template-1.0.38.js. В качестве исходных данных для template engine я буду использовать описанную ранее переменную kadry. Осталось только определить шаблон. Разработчики trimpath рекомендуют хранить текст шаблона внутри тега textarea (естественно, что сама textarea должна быть невидимой). Это удобно т.к. textarea может содержать как значение произвольный фрагмент html-кода, даже не корректный, перемешанный со специфическими для trimpath командами проверки условий, организации циклов. Для того, чтобы подставить в шаблон значение поля из переменной kadry, например, title делаем так (синтаксис похож и на smarty и на jstl):
<textarea id="template" style="display:none;">
${main_departments[0].title}
</textarea>
var compiled = TrimPath.parseDOMTemplate("template");
YAHOO.util.Dom.get('box').innerHTML = compiled.process(kadry);
Hello ${title2|default:'substitution value'|escape|capitalize}
function lpad (d, numDigits){
d = ""+d;
while (d.length < numDigits) d = "0"+d;
return d;
}
TrimPath.parseTemplate_etc.modifierDef.lpad = lpad;
// И пример использования:
Hello ${title|lpad:20|capitalize}
{for dep in main_departments}
${dep.title}
{forelse}
Список отделов пуст
{/for}
{if title == ‘Менеджеры’}
Наш отдел
{else} Их отдел
{/if}
var template = "Company ${title}";
var s = "Hello ${title}";
YAHOO.util.Dom.get('box_1').innerHTML = s.process(kadry);
var compiled = TrimPath.parseTemplate(s);
YAHOO.util.Dom.get('box_2').innerHTML = compiled.process(kadry);
« Сложные интерфейсы на javascript вместе Yahoo UI. Часть 13 | Сложные интерфейсы на javascript вместе Yahoo UI. Часть 15 » |