Материалы с мастер-класса PHPCONF-2007
Архитектурно — система представляет собой HTML документ с разметкой, содержащей множество блоков – окон. Для каждого окна в БД существует запись, описывающая его размеры, состояние, местоположение, тип приложения и т.д. Содержимым окна управляет клиентская часть модуля или плагина (JavaScript), которая общается с серверной частью через AJAX механизм. Каждое окно живет своей собственной жизнью, не пересекаясь по пространству имен переменных и функций с другими окнами.
Окно представляет собой таблицу с контейнером в виде элемента DIV внутри. Окно может изменять размеры во всех направлениях, может сворачиваться в виде заголовка, может перезагружаться. Практически у всех окон есть диалог настроек.
<table name="window" style="position:absolute;top:px;left:px;z-index:;">
<tr>
<td>
<div class="windowContainer" style="width:px;height:px; overflow:auto;">
HTML
</div>
</td>
</tr>
</table>
Плагин представляет собой совокупность титульной страницы в формате xml (описан ниже) и серверных скриптов, физически находящихся или на наших серверах, или в любом другом доступном месте. Функция титульной страницы - сообщить загрузчику стартовые данные, загрузить начальный контент, подключить JavaScript и CSS библиотеки. Далее плагин начинает жить своей жизнью в общем пространстве документа. Для реализации динамического функционала плагин может обращаться к своей серверной части через AJAX интерфейс, используя функции библиотеки ajax или srvapi.invoke. В случае использования srvapi.invoke, скрипты на сервере должны формирвать ответ в виде XML документа, в случае прямого обращения через ajax ответ может быть в произвольном виде, но его обработка полностью возлагается на JS библиотеки плагина на клиенте. Для реализации кросс-доменного доступа через AJAX используется прокси сервер, все URL должны оборачиваться вызовами/
newURL = getProxy(url), где url – обычного вида http://somehost.ru/somefile.php?somequery
newURL уже можно передавать через ajax
Прокси сервер полностью передает GET запрос и POST данные, соответственно поддерживая оба метода запроса. Сервер полностью транслирует все заголовки от браузера до точки назначения, но обратные заголовки могут приходить не все. Компоненты не должны напрямую ссылаться на другие документы через теги A, FORM и др. Все запросы должны отправляться или в некий блок IFRAME или через механизм AJAX.
При разработке плагинов необходимо жестко следовать следующим требованиям:
<?xml version="1.0" encoding="UTF-8"?>
<plugin>
<description>
<application class="my_application_classname" title="Название приложения" version="0.1" lmtime="Tue, 17 Oct 2006 13:05:00 +0400" expires="Tue, 17 Oct 2006 13:05:00 +0400"/>
<author name="Иванов Иван" mail="ivanov#mail.com" home="http://mypage.com"/>
<window width="400" height="300" title="Заголовок окна" url="http://someaddr.com"/>
</description>
<config>
<!—- Предварительный формат описан ниже -->
</config>
<content>
<link rel="stylesheet" href="absolute_path_and_url/file.css"/>
<style>
<![CDATA[
.myApp A {color: red;}
.myApp B {color: green;}
]]>
</style>
<script src="someurl/somefile.js" charset="UTF-8"/>
<script language="javascript">
<![CDATA[
if (!window.my_application_classname)
window.my_application_classname = function(win_id, parameters)
{
this.instance = win_id;
this.appName = 'vd_application_'+this.instance;
this.appWindow = document.getElementById('window_'+win_id);
this.appWindowContainer = document.getElementById('windowFrame_'+win_id);
this.parameters = parameters;
this.init = function()
{
alert('Load ok!');
}
this.go = function()
{
var p = document.getElementById('message_'+this.instance);
p.innerHTML = 'Preved, krosavcheg!';
}
this.unload = function()
{
alert('Unloading...');
}
}
]]>
</script>
<body>
<div>
<b>Hello world!</b><br/>
<button onclick="vd_application___VDINSTANCE__.go();">Go!</button>
<p id="message___VDINSTANCE__"></p>
</div>
</body>
</content>
</plugin>
<tab title="Настройки RSS">
<fieldset>
<legend>
Основные параметры
</legend>
<description>
Описание группы 1
</description>
<input type="text" label="URL:" name="url" default="" required="1" validate="url" maxlength="64" readonly="0" />
<input type="select" label="Выводить по:" name="num" default="10" values="5,10,20" labels="5,10,20" multiple="0" />
<input type="select" label="Открывать ссылки в:" name="target" default="_blank" values="_blank,_self" labels="Новом окне,Этом фрэйме" multiple="0" />
<input type="select" label="Выводить описания:" name="noteformat" default="1" values="0,1,2" labels="Не выводить,Скрывать,Показывать" multiple="0" />
<input type="text" label="Формат даты:" name="timeformat" default="d/m/Y H:i" required="1" validate="none" maxlength="16" readonly="0" />
<input type="text" label="Частота обновления(сек):" name="rld_time" default="300" required="1" validate="none" maxlength="4" readonly="0" />
</fieldset>
</tab>
Время, на которое будет скеширован плагин нашим сервером, определяется атрибутом expires в блоке описания плагина. Обратите внимание: если expires будет в прошлом, либо невалидным, будет применено дефолтное значение времени кеширования – 1 час.
Для авторизации пользователя предлагается использовать следующую схему:
MD5(SecretCode + window_id + user_id) Таким образом, javascript при запросе на внешний сервер, который не имеет доступа к базе, может передать в открытом виде предполагаемый user_id, window_id и вышеупомянутый md5 ключ. На сервере скрипт знает секретный код, может воспроизвести операцию и сравнить ключи. Если ключ совпадает, значит пользователь пришел действительно от нас.
С учетом того, что проксирующий скрипт транслирует полностью все заголовки, можно реализовать полноценную авторизацию через AJAX через отправку формы с логином и паролем, а сервер может поставить cookie в домен нашей среды. В дальнейшем по этим cookies можно полноценно работать. Но этот метод не приветствуется, поскольку наша основная задача - предоставить пользователю без лишних усилий сразу же доступ к нужным сервисам. Этот метод целесообразно применять для почтового клиента, хранилища файлов и т.д.
При создании окна, в случае подключения и успешной инициализации класса, создается его экземпляр, под именем vd_application_xxxx, где хххх это идентификатор окна. Для плагинов происходит попытка инициализировать класс, объявленный в блоке description. События и методы
init()
Вызывается после создания экземпляра класса. Подразумевается, что уже загружен весь HTML блок внутрь окна, поставлены на загрузку css файлы. В методе init рекомендуется произвести подстройку контента, установку таймеров, обработчиков и т.д. Как правило при инициализации задается команда на подгрузку контента. В метод init передаются в виде js-объекта параметры, заданные пользователем в настройках плагина.
unload()
Метод вызывается менеджером окон при закрытии окна. На момент вызова окно еще существует, и есть возможность произвести некие полезные действия, например снять обработчики событий, отключить таймеры, сбросить переменные и т.д. Например, для компонента «Заметка» целесообразно установить на unload обращение к серверу за удалением заметки.
onresize()
Метод вызывается во время ручного или автоматического изменения размеров окна. Отлавливая изменение размера можно динамически менять содержимое окна, масштабировать картинки и т.д. Основное применение этого события сводится к автоподстройке контейнера (DIV), который должен содержать скроллбар и должен подгоняться автоматически под размер окна.
onbeforeresize(), onafterresize()
Методы, вызываемые соответственно до начала изменения размеров окна и после изменения. В отличие от метода onresize(), который срабатывает при каждом движении мыши в процессе ресайза, данные методы вызываются по одному разу.
Всплывающие окна используются для вывода большого объема информации, которая не уместится в окно или не вписывается в его дизайн. Всплывающее окно внешне не отличается от обычного окна, но оно не имеет функций изменения размера и всегда занимает столько пространства, сколько занимает его блок контента.
После закрытия окна, все его дочерние окна автоматически уничтожаются.
Отладчик предназначен для вывода сообщений об ошибках, текстовой информации и пр. Рекомендуется для использования вместо функции alert().
При добавлении на рабочий стол компонента Отладчик инициализируется вывод сообщений, путем вызова функции debugInit(). Все сообщения, которые отправлялись несуществующему отладчику будут переданы сразу же после инициализации автоматически. Чтобы вывести отладочную информацию необходимо вызвать одну из функций: debugError, debugResult, debugNotice в главном окне (на десктопе)
Глобальные переменные хранятся как атрибуты window
Предназначены в основном для получения различных параметров.
Возможные значения параметра status:
2 | error - при выполнении запроса произошла ошибка
var mHash = {
'winid': this.plugin.instance,
'login': this.login,
'passwd':this.passwd
}
srvapi.invoke( this.urlAuth + pf.hash.serialize(mHash) );
В случае необходимости написания полноценного JavaScript функционала рекомендуется использовать следующий образец:
if (!window.DesktopBrowser_application)
window.DesktopBrowser_application = function(win_id)
{
this.instance = win_id;
this.appName = 'vd_application_'+this.instance;
this.appWindow = document.getElementById('window_'+win_id);
this.appWindowContainer = document.getElementById('windowFrame_'+win_id);
this.timer = null;
this.rld = 300;
this.url = '';
this.getContent = function()
{
window.clearTimeout(this.timer);
if (this.rld > 0)
this.timer = window.setTimeout(this.appName+".getContent()", this.rld*1000);
document.getElementById('contentFrame_'+this.instance).src = document.getElementById('contentFrame_'+this.instance).src;
}
this.goHome = function()
{
window.clearTimeout(this.timer);
if (this.rld > 0)
this.timer = window.setTimeout(this.appName+".getContent()", this.rld*1000);
document.getElementById('contentFrame_'+this.instance).src = this.url;
}
this.init = function(params)
{
this.parameters = params;
this.url = params.url;
window.clearTimeout(this.timer);
if (this.rld > 0)
this.timer = window.setTimeout(this.appName+".getContent()", this.rld*1000);
if (!document.getElementById('reload_menu_'+this.instance))
{
windowMenuAddDivider(this.instance);
windowMenuAddItem(this.instance, 'reload_menu_'+this.instance, 'обновить страницу',
"window.vd_application_"+this.instance+".getContent()", null, true);
windowMenuAddItem(this.instance, 'home_menu_'+this.instance, 'домой', "window.vd_application_"+
this.instance+".goHome()", null, true);
}
debugResult('Application '+this.appName+' init OK!');
}
this.unload = function()
{
debugResult('Unloading application '+this.appName );
window.clearTimeout(this.timer);
}
}
Для того, чтобы свободно использовать в своих скриптах обращения к элементам через document.getElementById() в HTML коде используйте зарезервированную константу VDINSTANCE, она будет заменена на ID окна во время парсинга на клиентской стороне. А в скриптах используйте обращение вида $(’name_’+this.instance). Настоятельно не рекомендуется объявлять глобальные переменные и функции, поскольку они не смогут быть автоматически выгружены в случае закрытия окна.
Переменная VDINSTANCE предназначена для автоматического разделения экземпляров одного и того же модуля или плагина на стороне клиента. В случае использования для передачи данных от сервера к клиенту нашего xml транспорта, в блоке html достаточно указать атрибут instance=”xxx” и в html коде автоматически будет произведена замена всех встречающихся переменных VDINSTANCE на реальный числовой идентификатор окна.
Нужно вписать текст, пришедший от сервера в некоторый контейнер (DIV). При этом на рабочем столе присутствует 2 или более идентичных окна с плагином.
<a href="#"
onclick="document.getElementById('targetDiv').innerHTML = prompt('Введите что-нибудь'); return false;">
Изменить текст</a>
<div id="targetDiv">Начальный текст тут</div>
В одном экземпляре этот код бы сработал прекрасно, но если открыто 2 одинаковых окна с абсолютно одинаковым HTML кодом внутри или просто кто-то другой написал плагин и использовал в нем элемент с ID targetDiv, то скорее всего текст попадет не туда куда требовалось. Правильнее было бы, при передаче контента, использовать VDINSTANCE и избежать конфликта имен.
<a href="#" onclick="document.getElementById('targetDiv___VDINSTANCE__').innerHTML = prompt('Введите что-нибудь'); return false;">
Изменить текст</a>
<div id="targetDiv___VDINSTANCE__">Начальный текст тут</div>
Заметим, указанный пример будет корректно работать в одном экземпляре в любом случае, даже если его загрузить локально. Для одновременной работы нескольких экземпляров, контент должен приходить через xml транспорт или на сервере необходимо самостоятельно реализовать подстановку значений.
Переменная VDBASEURL предназначена для удобства адресации в клиентской части плагина. Вычисляется из базового URL плагина. Пример использования:
<img src="__VDBASEURL__/images/logo.gif">
Классы и идентификаторы, относящиеся к одному объекту нужно объединять в неймспейсы по имени объекта, который явялется первой лексемой в названии.
.componentName .navigation
.componentName .menu
.navigation <- неправильно
.menu <- неправильно
TABLE.window
TABLE.window TD
TABLE.window .windowControlButton
TABLE.window B.button_red <- неправильно, подчеркивание запрещено
DIV.Window <- неправильно, имя класса начинается с большой буквы
Ответ сервера приходит в виде xml, парсится контент, содержащийся в тэге response. Парсер распознает следующие элементы:
Все тэги кроме result могут присутствовать в документе любое число раз, все информационные блоки должны содержать только текстовые ноды или CDATA контейнеры. Порядок выполнения кода следующий:
<?xml version="1.0" encoding="windows-1251"?>
<!-- Sample server response file -->
<response>
<result code="1" />
<execute>
<![CDATA[
parent.debugNotice("Пользователь:36");
parent.debugNotice("Работаю с окном");
]]>
</execute>
<html id="code123" instance="123">
<![CDATA[
некий html код
]]>
</html>
<execute>
<![CDATA[
document.getElementById('someID').innerHTML = html.code123;
serverSetStatus(1);
]]>
</execute>
<execute>
alert('Пришел ответ от сервера');
</execute>
<jscript src="myscript.js" />
<style id="someid">
<![CDATA[
b {color: red}
]]>
</style>
<stylesheet id="someOtherID" href="someurl/somefile.css"/>
</response>
Для упрощения и ускорения процесса разработки плагинов, нами был написан ряд полезных утилит, вызываемых из модуля DevTools:
Загрузчик плагинов предназначен для ручного запуска плагинов, расположенных на внешних серверах. Введите в поле ввода путь к плагину и нажмите «Поехали».
Дебаггер позволяет, вместо выдачи сообщений при помощи команды alert(), выводить их в специальное окно для сообщений. Поддерживаются следующие команды:
В дебаггер также выводится информация об ошибках, отловленных блоком try-catch при попытке исполнения кода плагина.

При просмотре текста в дебаггере, можно выделить его мышью и отправить в VarDump.
VarDump предназначен для просмотра свойств и методов объектов. Позволяет рекурсивно ходить по свойствам, загружая все дочерние объекты. Незаменим при отладке ООП на JavaScript.
