23 июля 2010 г.

XSLT Templater - шаблонизация на стороне клиента

В начале июля мне удалось съездить на Я.Субботник, на котором меня вдохновил Степан Резников своим докладом про клиентскую шаблонизацию...

Настолько вдохновил, что захотелось написать собственное решение, чем я собственно и занялся. Сразу оговорюсь, что я не любитель изобретать велосипеды (хотя нет... на самом деле любитель), но варианты, которые существовали на тот момент меня не очень привлекали. И написаны они были хорошо, и работали быстро и надежно, но лично мне работать с ними было неудобно - неудобно вызывать, неудобно обращаться к результатам обработки и т.д. и т.п.

В качестве технологической базы выбрал XSLT, хотя JS шаблонизация во многих случая предпочтительней, но уж больно давно мне полюбилась эта технология (благодаря, кстати, Mozart Framework). В результате этого моего увлечения появился плагин для jQuery:


XSLT Templater - плагин для фреймворка jQuery, реализующий следующие задачи:
  1. XML/XSLT процессинг

    Плагин позволяет произвести XSLT преобразования для предаваемых в качестве аргументов xml и xsl. Причем xml и xsl могут предоставляться в трех формах:

    • Строковое представление
      В этом случае плагин самостоятельно распарсит строковое представление XML, и произведет необходимые вычисления. Такой вариант представления удобен прежде всего в тех ситуациях, когда необходим самостоятельный контроль за доставкой или генерацией исходного xml или xsl шаблона.

    • Ссылка на файл/ресурс
      Плагин с помощью AJAX загрузит необходимые данные из внешнего источника (в пределах домена) и произведит необходимые преобразования. Все просто - указываем путь к xml и xsl на сервере, а получаем результат преобразований.

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

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

  2. Кэширование XML/XSLT на двух уровнях

    Кэширование позволяет организовать хранение xml/xsl для повторного использования, что и реализует базовые возможности для шаблонизации с вычислительными возможностями XSLT. Кэширование организуется на двух уровнях:

    • Пользовательский уровень
      Пользователь самостоятельно управляет кэшированием с доступом только к результатам последнего преобразования. Это очень удобно, когда при ручной обработке нужно повторно использовать предыдущие аргументы. А так как кэшируется сразу XMLDocument, то наглядно получаем некоторую прибавку в скорости... Хотя нужно отметить, что самой тяжелой операцией по-прежнему является сама трансформация.

    • Базовый уровень
      Внутреннее кэширование плагина, включенное по умолчанию на кэширование только xsl. Можно переключать внутренний кэш между четырьмя различными уровнями, но на практике из четырех режимов понадобятся всего 2-3.
    Кэширование ведется, только для аргументов со строковым типом – строковое представление xml или ссылка на ресурс - это необходимость ассоциативных массивов. Оба уровня кэширования можно использовать совместно, но при этом они являются полностью независимыми.

    Внутренне кэширование предполагает, что пользователь контролирует порядок вызовов нескольких преобразований над одинаковыми входными данными. Иными словами, два идентичных последовательных вызова преобразования будут работать (в силу асинхронности JavaScript) с идентичным состоянием кэша, что означает, что одинаковые ресурсы могут быть загружены несколько раз. При этом, существует механизм позволяющий организовывать цепочки преобразований, на основе обратных вызовов. Этот механизм позволяет установить четкую очередь обработки вызовов, что в свою очередь позволяет использовать актуальное состояние кэша при каждом отдельном преобразовании.
Плагин поддерживает следующие браузеры: IE 6+, Firefox, Opera, Chrome, Safari


Различные способы вызова


Преобразования осуществляются контекстно - относительно элемента контейнера для результирующего представления. И исходный xml и xsl могут представляться в трех различных формах.
  • Строковые представления

    Пусть существуют следующие строковые представления:
    "<xml-message>"+
          "<message>It working with string representation of xml</message>"+
    "</xml-message>"
    и
    '<?xml version="1.0"?>'+
    '<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">'+
            '<xsl:template match="xml-message">'+
                    '<p>Result: <xsl:value-of select="message/text()"/>!</p>'+
            '</xsl:template>'+
    '</xsl:stylesheet>'
    Тогда вызов xml/xsl трансформации будет выглядеть следующим образом:
    $('#test1').xslt("<xml-message>"+
                                   "<message>It working with string representation of xml</message>"+
                            "</xml-message>",
                            '<?xml version="1.0"?>'+
                            '<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">'+
                                 '<xsl:template match="xml-message">'+
                                        '<p>Result: <xsl:value-of select="message/text()"/>!</p>'+
                                 '</xsl:template>'+
                            '</xsl:stylesheet>'
    );
    или
    var xmlString = "<xml-message>"+
                                   "<message>It working with string representation of xml</message>"+
                             "</xml-message>";
    var xslString = '<?xml version="1.0"?>'+
                             '<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">'+
                                   '<xsl:template match="xml-message">'+
                                          '<p>Result: <xsl:value-of select="message/text()"/>!</p>'+
                                   '</xsl:template>'+
                             '</xsl:stylesheet>';

    $('#test1').xslt(xmlString,xslString);

  • Ссылка на ресурс

    Пусть существует следующий файл или скрипт возвращающий следующую структуру:
    /tests/data/message.xml
    <?xml version="1.0"?>
    <xml-message>
        <message>It working with loading xml file</message>
    </xml-message>
    и
    /tests/xslt/message-template.xsl
    <?xml version="1.0"?>
    <xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
    <xsl:template match="xml-message">
         <p>Result: <xsl:value-of select="message/text()"/> and loading xslt file!</p>
        </xsl:template>
    </xsl:stylesheet>
    В этом случае вызов будет осуществляться следующим образом:
    $('#test2').xslt("/tests/data/message.xml","/tests/xslt/message-template.xsl");

  • В виде XMLDocument (см. Работа с пользовательским кешем)

Комплексный пример:
var xslString = '<?xml version="1.0"?>'+
                        '<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">'+
                              '<xsl:template match="xml-message">'+
                                    '<p>Result: <xsl:value-of select="message/text()"/>!</p>'+
                              '</xsl:template>'+
                        '</xsl:stylesheet>';

$('#test2').xslt("/tests/data/message.xml",xslString);


Обратные вызовы


Для синхронизации работы нескольких вызовов и организации работы с кэшом существует механизм обратных вызовов:
$('#test2').xslt("/tests/data/message.xml","/tests/xslt/message-template.xsl", function(){
         console.debug("Text message after transformation!");
});
Этот механизм позволяет организовывать цепочки вызовов, причем каждый обратный вызов уже будет иметь доступ к состоянию кеша после предыдущего вызова:
var xslString = '<?xml version="1.0"?>'+
                        '<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">'+
                              '<xsl:template match="xml-message">'+
                                     '<p>Result: <xsl:value-of select="message/text()"/>!</p>'+
                              '</xsl:template>'+
                        '</xsl:stylesheet>';

$('#test2').xslt("/tests/data/message.xml",xslString, function() {
            $('#test3').xslt("/tests/data/message.xml","/tests/xslt/message-template.xsl",  function() {
                   $('#test4').xslt("/tests/data/message.xml","/tests/xslt/message-template.xsl",function(){
                              console.debug("Text message after all transformations ");
                   });
            });
});


Работа с пользовательским кэшем


Пользовательский (локальный) кэш необходим для самостоятельного контроля кэширования внутри шаблонизатора. Локальный кэш передается непосредственно в функцию преобразования, поэтому с его помощью существует доступ только к результатам кэширования последнего выполненного преобразования. Если локальный кэш передается в качестве аргумента, то необходимо получить экземпляр локального кэша:
var xsltCache = $.getXSLTLocalCache({
        onUnlock: function(state) {
                console.debug ('New value in cache');
        }
});
Теперь этот экземпляр можно использовать в вызовах:
$('#test2').xslt("/tests/data/message.xml","/tests/xslt/message-template.xsl", function() {

         $('#test3').xslt(xsltCache.getXmlDoc(),"/tests/xslt/message-template.xsl",null,xsltCache,false); 
// using result of caching xml representation in callback function


},xsltCache, false);// insert local cache to transformation function and switch off base cache
В приведенном примере стоит обратить внимание на следующие особенности:
  • xsltCache.getXmlDoc() – получение XMLDocument исходного xml из предыдущего вызова
  • …null,xsltCache,false… во внутреннем вызове - null вместо функции обратного вызова, повторное использование локального кэша, полное отключение внутреннего (базового) кэширования плагина
Таким образом, после обработки первого вызова экземпляр кэша будет содержать XMLDocument для xml и xsl, которые можно использовать повторно.


Методы пользовательского кэша


Методы Описание
checkXmlDoc() Проверяет наличие в xml кэше значения. Возвращает true, если значение установлено, false – в обратном случае
checkXslDoc() Проверяет наличие в xsl кэше значения. Возвращает true, если значение установлено, false – в обратном случае
getXmlDoc() Возвращает значение xml кэша. Если значение не было установлено, то возвращает false
getXslDoc() Возвращает значение xsl кэша. Если значение не было установлено, то возвращает false
lock() Сервисный метод для проверки возможности работы с кэшом. Имеет только информативный смысл.
setXmlDoc(xml) Устанавливает новое значение xml кэша. Используется внутри плагина.
setXslDoc(xml) Устанавливает новое значение xsl кэша. Используется внутри плагина.
reset Сбрасывает все значения кэша
unlock Сервисный метод для проверки возможности работы с кэшом. Имеет только информативный смысл.


Работа с базовым кэшом


Базовый кэш используется внутри плагина для оптимизации преобразований. Кэширование происходит на основе строкового ключа (строковое представление xml или ссылка на ресурс) Возможны четыре состояния внутреннего кэша:
  • Кэшируется только xsl – режим по умолчанию
    Этот режим предполагает ситуацию, когда шаблон загружается только один раз, а данные периодически обновляются. В этом случае подготовка шаблона к преобразованиям будет произведена только один раз.

  • Кэшируется только xml
    В этом режиме предполагается, что для одних, статических данных меняется только форма представления (шаблон)

  • Кешируется и xml, и xsl
    В этом случае предполагается, что и данные и представление являются статическим. Как правило, данный вариант используется, когда существует неопределенность о возможности повторной загрузки статического контента.

  • Кеширование не происходит


Настройка базового кэша


Для четырех различных случаев кэширование настраивается по-разному:
  • Кэшируется только xsl – режим по умолчанию
    $('#test1').xslt(xmlString,xslString);
    или
    var cache = {
            "xml":false,
            "xsl":true
    }
    $('#test1').xslt(xmlString,xslString,null,null,cache);

  • Кэшируется только xml
    var cache = {
            "xml":true,
            "xsl":false
    }
    $('#test1').xslt(xmlString,xslString,null,null,cache);

  • Кешируется и xml, и xsl
    $('#test1').xslt(xmlString,xslString,null,null,true);
    или
    var cache = {
            "xml":true,
            "xsl":true
    }
    $('#test1').xslt(xmlString,xslString,null,null,cache);

  • Кеширование не происходит
    $('#test1').xslt(xmlString,xslString,null,null,false);
    или
    var cache = {
            "xml":false,
            "xsl":false
    }
    $('#test1').xslt(xmlString,xslString,null,null,cache);


Комплексный пример

$(document).ready(function() {
        var time = new Date();
        var xmlString = "<xml-message>"+
                                         "<message>It working with string representation of xml</message>"+
                                 "</xml-message>";
        var xslString = '<?xml version="1.0"?>'+
                                 '<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">'+
                                        '<xsl:template match="xml-message">'+
                                             '<p>Result: <xsl:value-of select="message/text()"/>!</p>'+
                                        '</xsl:template>'+
                                 '</xsl:stylesheet>';

     $('#test1').xslt(xmlString,xslString);

        var xsltCache = $.getXSLTLocalCache({
                onUnlock: function(state) {
                        console.debug("Text message after setting new value in local cache!");
                }
        });

     $('#test2').xslt("/tests/data/message.xml",xslString, function() {
                  $('#test3').xslt(xsltCache.getXmlDoc(),"/tests/xslt/message-template.xsl",  function() {
                            $('#test4').xslt(xsltCache.getXmlDoc(),xsltCache.getXslDoc(),function () {
                                var time2 = new Date();
                                $("#time").text("Calculating time:"+(time2-time));
                        },xsltCache,false);
                  },xsltCache);
        },xsltCache, false);
});

Официальный сайт: http://www.xslt-templater.com
Страница проекта: http://code.google.com/p/xslt-templater/
Страница на jQuery.com: http://plugins.jquery.com/project/XSLTTemplater

Вот так, интересно ваше мнение...

Progg it

10 комментариев:

  1. в свое время (2005-ый год) для внутреннего приложения сделал похожий шаблонизатор на основе XML (помесь html и собственных тэгов) + XSLT. Собственные тэги конвертировались в контролы типа полей ввода, таблиц и т.п - ессно в виде web2.0 . Работало очень быстро - по сравнению со всеми на тот момент имеющимися web 2.0 вариантами (GWT тогда еще не было и в помине). Поддерживался ИЕ и файрфокс

    Жаль, проект так и остался внутренним...

    ОтветитьУдалить
  2. Это всё здорово до тех пор пока не упрёшься в "особенности" реализации XSLT в разных браузерах, а они там есть и весьма заковыристые порой. Одна из таких проблем это то что все браузеры на базе webkit не обрабатывают xsl:include, xsl:import и document(). Тема не новая правда, но малоизученная и зачастую о разного рода подводных камнях там узнаешь лишь когда сам спотыкаешься о них.

    ОтветитьУдалить
  3. Привет!

    Вдохновение это большая сила )

    В тему xslt я таки не вижу аргументов в пользу использования на клиенте. На клиенте рулит json. В коде минимум лишнего и повторяющихся строк. В этом смысле xml не самое лучшее решение, имхо.

    Т.е. конкуренты этого решения на мой взгляд не другие xslt шаблонизаторы, а например:

    http://github.com/jquery/jquery-tmpl

    с кучей очевидных преимуществ, хотя бы в меньшем количестве кода.

    PS: Надеюсь не очень далеко от темы.

    ОтветитьУдалить
  4. Уже вышел новый релиз, в котором сделаны значительные изменения, поэтому пост описывает не актуальную версию! Желательно сверяться с документацией на официальном сайте...

    ОтветитьУдалить
  5. Олег, спасибо! Удобный плагин и хорошая реализация.
    Единственное - столкнулся с проблемой в Google Chrome (5.0.375.125). Если в подгружаемом xsl есть include или import второго xsl-файла, то рендера не происходит. Не сталкивались с подобной проблемой?

    ОтветитьУдалить
  6. Рад, что понравилось) Скоро будет готова новая версия.

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

    С xslt на стороне клиента, к сожалению, есть проблемы кроссбраузерности,но в целом они обходимы в самом xsl).

    ОтветитьУдалить
  7. Хорошо, что проект развивается! Буду ждать обновленную версию)

    С Chrome смутило то, что если документ рендерится "обычным" способом, то все инклюды работают. Но если я пытаюсь динамически через ява-скрипт отрендерить шаблон, то возникает проблема с импортом.

    Под "обычным" способом я подразумеваю следующее: в строке запроса браузера я обращаюсь к некоему URL на сайте, который мне возвращает XML и в заголовке указан основной XSL-файл которым нужно преобразовать дерево. Браузер подгружает основной XSL, а также все XSL которые указаны как импортируемые.

    ОтветитьУдалить
  8. Действительно, странное поведение... Постараюсь разобраться в этом вопросе)

    ОтветитьУдалить
  9. Олег, привет.
    Столкнулся с необходимостью получать результаты преобразования, не вкладывая их в какой-либо контейнер. Хотелось бы, чтоб $.xslt(xml,xsl) возвращал не пусто, а результаты рендеринга, как текст. В некоторых момента действительно необходимо - например .append()

    Заранее спасибо :)

    ОтветитьУдалить
  10. Такая возможность есть, $.xslt(xml,xsl) возвращает пустоту не с проста, так как наложение происходит асинхронно. Ох уж этот JavaScript!)

    В данной ситуации необходимо использовать пользовательский кэш, через который вернется результат преобразования:
    var xsltCache = $.getXSLTLocalCache();
    $.xslt('data.xml','view.xsl',function() {
    alert('Получаем строку:'+xsltCache.getResultDoc());
    // и что-то с ней делаем
    },xsltCache);

    ОтветитьУдалить