Опубликован: 10.12.2007 | Уровень: специалист | Доступ: платный
Лекция 5:

Скрипты

5.6. Практика: динамическое содержимое NoteTaker

В этом разделе мы добавим в окно редактирования ( Edit ) NoteTaker несколько скриптов. Эти скрипты будут менять способ работы XUL- документа после его отображения платформой. Во время работы мы будем манипулировать несколькими типами объектов, описанных в этой лекции.

5.6.1. Работа с <deck> с помощью DOM

Первое изменение NoteTaker будет касаться содержимого окна с ключевыми словами. Это окно на самом деле поддерживает две отдельные панели. Каждая панель представляется одним из тегов <toolbarbutton>: "Edit" и "Keywords". Мы бы хотели отображать окно редактирования так, чтобы видимой была только одна из этих панелей. Так как содержимое панели с ключевыми словами еще не определено, мы используем "заполнитель".

Чтобы добавить поддержку двухпанельной системы, воспользуемся тегом <deck>. Содержимое панели "Edit" будет появляться как одна карта колоды, а содержимое панели "Keyword" будет другой картой. Ранее содержимое, скрывающееся под двумя тегами <toolbarbutton>, было для кнопки "Edit". Теперь мы хотим поместить это содержимое на одну карту, а для другой используем что-нибудь вроде:

<hbox flex="1">
  <description>Добавим потом.</description>
</hbox>

На рисунке 5.3. показана структура XUL-документа до и после изменения.

Добавление <deck> в NoteTaker

увеличить изображение
Рис. 5.3. Добавление <deck> в NoteTaker

Все, что мы сделали - добавили в нужные места <deck> и новое содержимое. Это можно было сделать и в "Верстка с XUL" , "Проектирование с XUL", однако переключаться между двумя картами <deck> без скрипта невозможно. Теперь у нас есть JavaScript, и мы можем реализовать переключение. Слегка забегая вперед (в "События" , "События"), мы отметим, что теги <toolbarbutton> поддерживают обработчик событий onclick. Мы добавим скрипт так, чтобы с ним было интересно повозиться.

Существует множество способов решить нашу задачу, но все они сводятся к добавлению тегам атрибутов id ; написанию функции, которую мы назовем action(), и добавлению обработчика onclick. Обработчики onclick выглядят примерно так:

<toolbarbutton label="Edit" onclick="action('edit')"/>
<toolbarbutton label="Edit" onclick="action('keywords')"/>

Новые идентификаторы будут такие:

<deck flex="1" id="dialog.deck">
<hbox flex="1" id="dialog.edit">
<hbox flex="1" id="dialog.keywords">

Функция action() ; приведена в листинге 5.9.

function action(task) {
  var card = document.getElementById("dialog." + task);
  if (!card || ( task != "edit" && task != "keywords") )
  throw("Unknown Edit Task");
  var deck = card.parentNode;
  var index = 0;
  if ( task == "edit" ) index = 0;
  if ( task == "keywords") index = 1;
  deck.setAttribute("selectedIndex",index);
}
Листинг 5.9. Простая функция NoteTaker, принимающая команды как аргументы

Мы хотим научиться передавать функциям аргументы, похожие на команды, потому что это общая практика для приложения Mozilla. Если мы включим этот код в наш XUL-файл, с тегом <script>, мы получим непонятные ошибки, описанные в разделе "Отладка" этой лекции, если только не будем осторожны и не воспользуемся содержимым <![CDATA[]]>. Лучше всего поместить наш скрипт в отдельный файл с самого начала и включить его примерно так:

<script src="editDialog.js"/>

Функция action() вызывает метод window.document.getElementById(). Этот метод - самая распространенная входная точка для доступа к DOM из скриптов. Ей передается значение HTML- или XUL-атрибута id, и она возвращает объект для тега с этим идентификатором. Далее мы можем работать с этим объектом через его свойства и методы.

Оставшаяся часть функции проходит через тег <deck> с помощью свойства DOM parentNode и устанавливает там значение атрибута selectedIndex с помощью свойства DOM setAttribute(). Так как это свойство имеет смысл для тега <deck>, система отображения XUL отреагирует на изменения автоматически, и отобразится нужная карта.

Если мы не будем пользоваться аргументами, похожими на команды, можно достигнуть того же эффекта быстрее, но не очень аккуратно. Эквивалентные обработчики onclick, которые по-прежнему используют интерфейсы DOM, выглядят так:

document.getElementById("dialog.deck").setAttribute("selectedIndex",0);
document.getElementById("dialog.deck").setAttribute("selectedIndex",1);

Независимо от того, какой из двух подходов выбрать, для решения большей части задачи потребуются всего лишь три интерфейса стандарта DOM 1 Core. Эти три интерфейса - Document, Element и Node.

5.6.2. Альтернатива: изменение стилей с использованием DOM

Использование <deck> - лишь один из многих способов "оживить" XUL-документ скриптами. Другой, не менее правильный способ - менять CSS-стили после загрузки документа.

Начнем с примера <deck>, удалив его открывающие и закрывающие теги. Теперь каждый набор содержимого, заключенный в <hbox>, можно интерпретировать как HTML-тег <div>. В листинге 5.10 показана другая версия метода action().

function action(task) {
  var card = document.getElementById("dialog." + task);
  if (!card || ( task != "edit" && task != "keywords") )
  throw("Unknown Edit Task");
  var oldcard; // the content to remove
  if (task == "edit")
  oldcard = document.getElementById("dialog.keywords");
  else
  oldcard = document.getElementById("dialog.edit");
  oldcard.setAttribute("style","visibility:collapse");
  card.removeAttribute("style");
}
Листинг 5.10. Простая функция NoteTaker, принимающая команды как аргументы

Здесь используются те же интерфейсы DOM, что и в листинге 5.9, но меняются атрибуты в DOM-иерархии окна. При изменении правил стиля содержимого система отображения внутри платформы автоматически обновляет отображаемый документ. Использовать атрибут style не очень удобно. В XUL есть более удобный атрибут collapsed, который можно менять, не трогая при этом никакие другие атрибуты или встроенные стили, которые может добавлять скрипт. Две последние инструкции скрипта нужно заменить следующими строчками:

oldcard.setAttribute("collapsed","true");
card.removeAttribute("collapsed");

Атрибут XUL hidden не должен обновляться автоматически, поскольку он может нанести вред XBL-связям (см. "XBL-связки" , "XBL-связки").

Кроме использования <deck> и стилей, можно использовать DOM-интерфейсы и для физического удаления/вставки частей в иерархию DOM документа. Это третий подход, использующийся для скрытия и отображения содержимого, но он слишком сложен для столь простой задачи.

5.6.2. Альтернатива: изменение <deck> с использованием DOM

При преобразовании XUL-документа в иерархию DOM предоставляется больше информации, чем предусмотрено стандартными интерфейсами DOM. Существует еще и набор интерфейсов XBL-системы Mozilla. Эти дополнительные интерфейсы добавляют свойства и методы специфичным для XUL DOM-объектам. Эти свойства и методы чрезвычайно удобны и упрощают написание скриптов. Иногда использование только стандартов DOM делает код менее простым и понятным.

Давайте посмотрим, можно ли с помощью одного из этих интерфейсов улучшить наше решение с использованием тега <deck>. Вероятнее всего, тег, с которым мы свяжем наш скрипт, будет <deck>. Для начала заглянем в файл xul.css. Этот файл хранится в архиве toolkit.jar в chrome. Желательно хранить распакованную версию этого архива где-нибудь под рукой. Просматривая нужный файл в обычном текстовом редакторе, зададим поиск "deck", и вот что мы найдем:

deck {
  display: -moz-deck;
  -moz-binding: url("chrome://global/content/bindings/
  general.xml#deck");
}

Строка -moz-binding сообщает нам о том, что для этого тега есть связь, так что нам есть на что посмотреть. Связь называется "deck" и описана в файле general.xml. Откроем этот файл и поищем такую строку:

<binding id="deck">

Этого достаточно, связь найдена. Вот одна из частей ее определения:

<binding id="deck">
<implementation>
<property name="selectedIndex" ...

Это все, что нам нужно. Имена свойства в XBL и имена атрибутов в XUL обычно совпадают, более того, они обычно совпадают с именами HTML- объектов. Свойство selectedIndex совпадает с атрибутом selectedIndex тега <deck>. Его мы и используем. В методе action() заменим строку

deck.setAttribute("selectedIndex",index);

на

deck.selectedIndex = index;

Это тривиальное изменение, но наш код становится достаточно коротким, чтобы мы могли выкинуть переменную index и избавиться еще от пары строк. Если бы в определении связки для <deck> был бы, например, метод setCard(), мы бы могли использовать его вместо написания action(). Последние строки нашей функции можно сократить так:

var deck = card.parentNode;
if ( task == "edit" ) deck.selectedIndex = 0;
if ( task == "keywords") deck.selectedIndex = 1;
5.6.4. Чтение наборов строк через XPCOM

Второе изменение, которое мы сделаем в NoteTaker - внесем внешние данные в отображаемое окно извне XUL-документа. То есть мы загрузим содержимое, которое будет появляться внутри прямоугольных областей. В дальнейшем мы еще изменим способ загрузки, сохранения и отображения информации несколько раз.

Чтобы получить доступ вовне, нам не нужны XPCOM-компоненты. Мы могли бы задействовать URL, указанный в "Формы и меню" , "Формы и меню". Но здесь мы воспользуемся наборами строк.

Мы можем манипулировать наборами строк (файлами properties ) из XUL или из JavaScript. Если мы выберем первый путь, будет то же, что и при написании скрипта для <deck>: мы добавим несколько тегов и поищем XBL-определение, которое предоставляет полезные интерфейсы. Но сейчас мы будем работать с наборами строк напрямую из JavaScript. Так как для этого нужен XPCOM, нам придется хранить файлы внутри chrome.

Нам нужен объект, который умел бы работать с файлами properties. Другими словами, нам требуется некий полезный XPCOM-интерфейс и компонент, реализующий этот интерфейс. Если мы будем искать его в XPIDL-файлах (или по индексу этой книги), легко заметить интерфейсы в файле nsIStringBundle.idl. В нем есть два интерфейса: nsIStringBundleService и nsIStringBundle. Так как в имени первого встретилось слово "Service" (служба), это, вероятно, XPCOM-служба; для нас это отправная точка. Вспомним, что службы создают объект с помощью getService() ; в остальных случаях используется createObject().

Мы также обратим внимание на то, что метод createBundle() этого интерфейса выглядит так:

nsIStringBundle createBundle(in string aURLSpec);

Итак, этот метод создает объект с интерфейсом nsIStringBundle из URL. У интерфейса nsIStringBundle есть метод getStringFromName(), извлекающий строку из файла с набором строк. Нам не так важно, что в XPIDL используются собственные типы wstring и string ; как нам известно, XPConnect преобразует их во что-то, что JavaScript может понять - обычное строковое значение, которое появится как объект String.

В файле интерфейса также в самом верху заявляется, что связанный с ним идентификатор контракта таков (теперь у нас получается пара из интерфейса и XPCOM-компонента):

@mozilla.org/intl/stringbundle;1 nsIStringBundleService

Нам не нужен идентификатор контракта для интерфейса nsIStringBundle, так как служба создаст объекты с этим интерфейсом для нас, когда мы вызовем createBundle(). Но нам нужно сначала получить объект службы. Так как мы нашли пару целиком, это легко:

var Cc = Components.classes;
var Ci = Components.interfaces;
var cls = Cc["@mozilla.org/intl/stringbundle;1"];
var svc = cls.getService(Ci.nsIStringBundleService);

Если в Консоли JavaScript никаких ошибок не появится (а их быть не должно), тогда теперь переменная svc содержит объект службы, который мы запросили. Теперь мы можем писать код. В листинге 5.11 показаны результаты.

var Cc = Components.classes;
var Ci = Components.interfaces;
var cls = Cc["@mozilla.org/intl/stringbundle;1"];
var svc = cls.getService(Ci.nsIStringBundleService);
var URL = "chrome://notetaker/locale/dialog.properties";
var sbundle = svc.createBundle(URL);
function load_data() {
  var names = ["summary", "details", "chop-query", "home-page", "width",
  "height", "top", "left"];
  var box, desc, value;
  for (var i = names.length-1; i>0; i--) {
    value = sbundle.GetStringFromName("dialog."+ names[i]);
    desc = document.createElement("description");
    desc.setAttribute("value",value);
    box = document.getElementById("dialog." + names[i]);
    box.appendChild(desc);
  }
}
Листинг 5.11. Код NoteTaker для чтения наборов строк

Переменная sbundle содержит XPCOM-объект, указанный нашим URL. Файл по выбранному нами URL должен соответствовать правилам файла properties. Функция load_data() считывает строки из этого файла и задействует DOM так, что тег <description value="строка"> добавляется как содержимое в конец всякого блока-заглушки <box>. Обратите внимание, что объект <description> строится и добавляется к блоку в самом конце. Это более эффективно, чем пошаговое добавление новых данных в существующую структуру DOM.

Этот код также подразумевает, что в XUL-документе в ожидаемых местах есть идентификаторы. Нам нужно добавить их туда вручную. Для этого требуется заменить каждую строку

<box class="temporary"/>

на что-то вроде

<box class="temporary" id="dialog.summary"/>

Наконец, мы вызовем функцию load_data() из другого обработчика событий, который подсмотрели в "События" , "События": атрибут onload тега <window>. Таким образом, мы будем уверены, что документ уже будет существовать к тому времени, когда мы начнем манипулировать им:

<window xmlns= "http://www.mozilla.org/keymaster/gatekeeper/
there.is.only.xul" onload="load_data()>

Так как файл хранится в части chrome, относящейся к локализации, нам нужно задать язык, как это описано в разделе "Практика" "Статическое содержимое" , "Статическое содержимое". Другими словами, необходимо создать файл contents.rdf и обновить файл installed-chrome.txt. Сам файл со строками должен выглядеть примерно так, как в листинге 5.12.

dialog.summary=My Summary
dialog.details=My Details
dialog.chop-query=true
dialog.home-page=false
dialog.width=100
dialog.height=90
dialog.top=80
dialog.left=70
Листинг 5.12. Файл dialog.properties для NoteTaker

Путь к этому файлу относительно корневого каталога chrome будет таким:

notetaker/locale/en-US/dialog.properties

Теперь диалог NoteTaker должен походить на снимок на риcунке 5.4.

Диалог NoteTaker cо строками, загружаемыми скриптом

Рис. 5.4. Диалог NoteTaker cо строками, загружаемыми скриптом

Нам удалось успешно использовать XPCOM для взаимодействия с ресурсами вне загруженного документа. Это важный шаг, даже если все, что мы сделали - чтение файла. Мы пока оставим процедуру записи в файл. Запись в файлы (и их чтение) - долгая история, ее мы расскажем в "Объекты XPCOM" , "Объекты XPCOM".

Подведем итог проделанной работы: мы работали с DOM-, AOM-, XBL- и XPCOM-объектами. JavaScript предоставляет полный доступ к загруженному содержимому и, если наше приложение установлено в chrome, позволяет делать с этим содержимым что угодно. Большая часть добавленной нами в этой лекции функциональности - лишь маленькие, но любопытные фокусы. В следующих лекциях мы заменим эти процедуры более профессиональной работой.

5.7. Отладка: отладка скриптов

JavaScript - некомпилируемый язык; следовательно, на долю программиста приходится довольно много работы по выявлению ошибок. Так как переменные, свойства и методы определяются во время выполнения, многие ошибки могут оставаться незамеченными до самой последней стадии разработки. Важно, чтобы у процесса тестирования и отладки была определенная структура и чтобы он не сводился к догадкам. В данном разделе описывается, какие инструменты можно для этого использовать.

Лучшая защита от ошибок - хороший стиль написания кода. Держите свои скрипты отдельно от документов. Не забывайте о завершающих точках с запятой и отступах, пользуйтесь содержательными именами переменных. Всегда проверяйте аргументы, передаваемые функции, и возвращайте какое-нибудь осмысленное значение. Всегда пользуйтесь блоками try, если используемые интерфейсы Mozilla могут генерировать исключения. Обратите внимание на советы о настройках в "Основные концепции" , "Основные концепции".

Метод dump() объекта окна - очень полезное средство. При его использовании можно запустить Mozilla с ключом -console, тогда вспомогательная информация будет выводиться в отдельное окно и не повлияет на собственные окна платформы. Этот вывод также можно сохранить в файл журнала. В UNIX запуск с этим ключом возможен только из командной строки. Если скрипты используют выполнение через промежутки времени, либо создаваемые либо получаемые, такой вывод - иногда единственный способ узнать, какой же на самом деле была последовательность событий во время обработки.

Mozilla также поддерживает URL javascript:. Этот URL может пригодиться для быстрой проверки состояния документа. Если документ загружен в окно Навигатора (XUL-документы могут быть загружены так наравне с web-страницами), такие URL можно использовать для проверки содержимого документа. Это наиболее эффективно, когда пользователь вводит в документ большое количество информации. Этот ввод обычно хранится как информация о состоянии, которую можно запросить инструкциями, подобными следующим:

javascript:var x=window.state.formProgress; alert(x);

Здесь alert() - небольшое окно, отображающее одну или несколько строк текста. Эту инструкцию можно поместить куда угодно в скрипт, связанный с окном Mozilla, таким образом вы получите базовую информацию о состоянии среды скрипта и ее содержимого. alert() также приостанавливает интерпретатор JavaScript; это неплохо при простой обработке данных, но неудачно, если предполагается, что информация должна обрабатываться скриптом за очень быстрое время (например, при использовании потокового аудио/видео). alert() - простейший инструмент отладки.

Контрольные точки - еще одно простое, но очень полезное средство отладки. У каждого созданного объекта JavaScript есть метод watch(). Этот метод используется для добавления к свойствам объекта одной скрытой функции. Синтаксис этой скрытой функции напоминает синтаксис "set function ()" в Mozilla. Пример приводится в листинге 5.13.

function report(prop,oldval,newval) {
  dump(prop + "old: " + oldval + "; new: " + newval);
  return newval; // гарантирует, что отслеживаемый код еще выполняется
};
var obj = { test:"before" };
obj.watch("test",report);
obj.test = "after"; // вызов report()
obj.unwatch("test");
Листинг 5.13. Отслеживание изменения свойств объекта с помощью контрольных точек

Аргументы функции report() предоставляются функцией watch(). При отслеживании свойства test каждое его изменение в качестве побочного эффекта приводит к вызову функции report().

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

Наконец, в любую часть скрипта можно вставить ключевое слово debugger. При этом откроется Отладчик JavaScript, о командах которого можно узнать, набрав /help в командной строке в нижней части окна. Отладчик основывается на этой XPCOM-паре:

@mozilla.org/js/jsd/debugger-service;1 jsdIDebuggerService

Если не хочется использовать Отладчик JavaScript, можно реализовать что-нибудь поверх этого компонента и его интерфейса jsdIDebuggerService для себя.

При возникновении непонятных проблем следует полностью закрыть Mozilla, запустить ее заново и провести проверку снова. Нужно всегда держать открытой Консоль JavaScript ( Tools | Web Development ) и всегда очищать ее журнал перед загрузкой нового документа, чтобы ошибки были заметны сразу. Время от времени необходимо проверять, не появились ли "умершие" процессы Mozilla без окон, к которым их можно отнести.

5.8. Итоги

Язык JavaScript невелик и прост в освоении. Он во многом похож на многие другие языки из семьи языков с C-подобным синтаксисом, за исключением уникальной работы с объектами. Цепочки областей видимости и прототипов - любопытные и сложные особенности языка, и на искусное создание кода, работающего с ними, можно потратить часы. В JavaScript 2.0 цепочки прототипов будут заменены классами.

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

Опытные web-разработчики заметят, что поддержка DOM-интерфейсов в Mozilla проста в использовании, знакома и функциональна. Без дополнительных усилий она применима и к XUL-приложениям. Хорошая поддержка стандартов - настоящий праздник после многих лет проверок на совместимость с различными браузерами. Начинающим XML-программистам рекомендуется внимательно изучить первую часть стандарта DOM 1, пока понятия узла, элемента, документа и фабричного метода не уложатся в голове.

Инфраструктура, состоящая из XPConnect и XPCOM - новый мир Mozilla. Имея более тысячи компонентов (и интерфейсов), нельзя получить все сразу. Возможно, некоторые интерфейсы вам не придется задействовать никогда; использование других будет во многом зависеть от того, как сильно требуется изменить платформу и насколько амбициозно ваше приложение.

XPCOM предоставляет в распоряжение пользователя целый мир объектов и позволяет сделать программирование на JavaScript таким же развитым, как и с применением языков Java, C или C++. У всех XPCOM-интерфейсов есть XPIDL-определение; отыскав нужное, вы прочитаете его без труда. Обзор этих интерфейсов может дать некоторое представление обо всех возможностях Mozilla.

Но вместо того, чтобы погрузиться в XPCOM, мы сначала вернемся к миру интерфейсов и поговорим о пользовательском вводе.

Дмитрий Гуменюк
Дмитрий Гуменюк
Россия, Звенигород
Konstantin Grishko
Konstantin Grishko
Россия, Москва, Московский финансово-промышленный университет "Синергия", Москва