Производительный и читабельный XSLT: сборник советов

В моей практике чаще всего в качестве шаблонизатора используется именно XSLT. Я не буду рассуждать о том, почему так происходит — о преимуществах данной технологии написано вполне достаточно. Но ещё больше написано о её недостатках. Считается, что XSLT является слишком многословным и тяжёлым для чтения, а также не самым производительным. В этой статье я постараюсь собрать несколько советов по улучшению качества XSLT-кода с точки зрения читабельности и выразительности. Некоторые из них также позволят XSLT работать несколько быстрее.

Именованные шаблоны

Многие «проблемы» XSLT связаны с тем, что мы слишком часто пытаемся писать на нём в процедурном стиле. Мы постоянно пытаемся сделать из него Smarty, но упираемся в один простой факт — XSLT является декларативным языком, как бы необычно это для нас не выглядело.
Например, мы пытаемся использовать именованные шаблоны, воспринимая их как процедуры, выводящие данные в определённом формате:

<xsl:template name="CreateItemLink">
    <xsl:param name="item"/>
    <a href="/item/?id={$item/id}">
        <xsl:value-of select="$item/name"/>
    </a><br/>
</xsl:template>


* This source code was highlighted with Source Code Highlighter.

Наверное, многие программисты именно так написали свой первый шаблон. И он неплохо решает свою задачу. Декларативный XSLT предлагает немного другой подход:

<xsl:template match="item">
    <a href="/item/?id={id}">
        <xsl:value-of select="name"/>
    </a><br/>
</xsl:template>


* This source code was highlighted with Source Code Highlighter.

Разница совсем не велика. Дело вкуса и стиля программирования. Давайте посмотрим, как шаблон будет использоваться в дальнейшем.


Xsl:for-each и xsl:apply-templates

«Императивную» версию нашего шаблона мы бы стали использовать примерно так:

<xsl:template match="/">
    <h1>My market</h1>
    <xsl:for-each select="/root/market/item">
        <xsl:call-template name="CreateItemLink">
            <xsl:with-param name="item" select="."/>
        </xsl:call-template>
    </xsl:for-each>
</xsl:template>


* This source code was highlighted with Source Code Highlighter.

А «декларативную» — так:

<xsl:template match="/">
    <h1>My market</h1>
    <xsl:apply-templates select="/root/market/item"/>
</xsl:template>


* This source code was highlighted with Source Code Highlighter.

Теперь разница стала куда заметнее:
  • Больше нет «нагромождения тегов», которым так славится XSLT.
  • Больше нет цикла xsl:for-each — его делает за нас XSLT-процессор, который может сделать существенную оптимизацию.
  • Наш код сильно уменьшился по объёму — не критично, но приятно.

Xsl:Choose

Xsl:choose, пожалуй, одна из самых многословных конструкций в XSLT. Очень часто она используется примерно так:

<xsl:template match="product">
    ...
    <xsl:choose>
        <xsl:when test="currencyCode = 'eur'">
            <xsl:value-of select="'Euros'"/>
        </xsl:when>
        <xsl:when test="currencyCode = 'usd'">
            <xsl:value-of select="'Dollars'"/>
        </xsl:when>
        <xsl:when test="currencyCode = 'cad'">
            <xsl:value-of select="'Canadian dollars'"/>
        </xsl:when>
        ...
    </xsl:choose>
    ...
</xsl:template>


* This source code was highlighted with Source Code Highlighter.

Бывают случаи, когда её использовать просто необходимо. Но в данном примере, гораздо правильнее использовать внешний документ со словарём:

<?xml version="1.0"?>
<currencies>
    <currency>
        <code>eur</code>
        <name>Euros</name>
    </currency>
    <currency>
        <code>usd</code>
        <name>Dollars</name>
    </currency>
    <currency>
        <code>cad</code>
        <name>Canadian dollars</name>
    </currency>
    ...
</currencies>


* This source code was highlighted with Source Code Highlighter.

Обратиться к нему из шаблона можно с помощью функции document() и XPath:

<xsl:variable name="currencies"
    select="document('cur.xml')/currencies"/>

<xsl:template match="product">
    ...
    <xsl:value-of select="$currencies/currency[code=currencyCode]/name"/>
    ...
</xsl:template>


* This source code was highlighted with Source Code Highlighter.

Мне кажется, что такой код гораздо легче поддерживать и он является идеологически более правильным — данные должны оставаться данными, а не превращаться в код, который сложно прочесть.

Встроенные преобразования

Многие не знают, что в XSLT есть несколько «встроенных» преобразований, которые делают за программиста часть работы. Эти преобразования делаются на уровне XSLT-процессора, т.ч. в некоторых реализациях могут работать значительно производительнее «ручных».
Например, за счёт встроенных преобразований осуществляется рекурсивное применение шаблона к потомкам текущего узла. Это равносильно такому шаблону:

<xsl:template match="*|/">
    <xsl:apply-templates/>
</xsl:template>


* This source code was highlighted with Source Code Highlighter.

Причём, если в apply-templates используется mode, то дочерние узлы тоже будут преобразовываться с этим mode. Другой пример, это автоматический вывод текстовых узлов и атрибутов:

<xsl:template match="text()|@*">
    <xsl:value-of select="."/>
</xsl:template>


* This source code was highlighted with Source Code Highlighter.

Это значит, что вместо

<xsl:template match="name">
    <name><xsl:value-of select="."/></name>
</xsl:template>


* This source code was highlighted with Source Code Highlighter.

можно использовать

<xsl:template match="name">
    <name><xsl:apply-templates/></name>
</xsl:template>


* This source code was highlighted with Source Code Highlighter.

Это (возможно) будет работать быстрее и точно сделает наши шаблоны более гибкими и расширяемыми.
Раздел в стандарте.

Функция concat()

Больше всего при чтении шаблонов меня раздражает подобный код:

<xsl:value-of select="$string1" />
<xsl:text> - </xsl:text>
<xsl:value-of select="$string2" />
<xsl:text>...</xsl:text>


* This source code was highlighted with Source Code Highlighter.

В таких случаях гораздо удобнее использовать один <xsl:value-of/>, а всё остальное форматирование производить с помощью concat().

<xsl:value-of select="concat($string1, ' - ', $string2, '...')" />

* This source code was highlighted with Source Code Highlighter.

Вообще в XPath есть много встроенных функции, которые можно использовать для упрощения кода. Неплохую документацию с примерами можно найти здесь. Особенно стоит обратить внимание на substring(), translate() и т.п. Эти функции не стоит реализовывать на PHP или другом внешнем языке — XSLT-процессор всё равно сделает это лучше.

Используем xsl:element по назначению

Xsl:element нужен исключительно в случаях, когда название элемента определяется динамически.

<xsl:variable name="s">hello</xsl:variable>
<xsl:element name="{$s}">
    <xsl:attribute name="site">habr.ru</xsl:attribute>
    <xsl:value-of select="{concat('Привет ', $username)}"/>
</xsl:element>


* This source code was highlighted with Source Code Highlighter.

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

<hello site="http://habr.ru/">
    <xsl:value-of select="{concat('Привет ', $username)}"/>
</hello>


* This source code was highlighted with Source Code Highlighter.

Я всё ещё собираю подобные советы, т.ч. если у вас есть, что добавить, обязательно отпишитесь в комментах.
Share post

Similar posts

Comments 50

    0
    Только один раз сталкивался с XSL, правда, в тяжелом случае, но на декларативный вид переписывать уже не буду.
    В следующий раз обязательно вспомню и учту :)

    Хотелось бы уточнить на тему apply-templates. А как быть, если item нужно вывести несколько раз на странице из одного набора, причем в каждом случае по-своему?
    В «обычном» виде это решается через CreateItemLink, CreateItemSubmenuLink, CreateItemDescriptionLink например.
      +2
      Посмотрите в сторону атрибута mode.
        +2
        столкнулся с xslt при работе для Head Hunter. И скажу, что декларативный стиль на очень больших проектах очень помогает упрощать работу.
        0
        Вот насчет читабельности не надо. XQuery и то более читабельный, хотя вообще говоря, оба языка в плане читабельности «не фонтан».
        • UFO just landed and posted this here
            +1
            Скорость вполне хорошая, если конечно понимать как писать шаблоны и сколько данных подавать на обработку, есть и более тормозные шаблонизаторы, например Smarty. На практике XSLT отлично применяется мною уже много лет, ищу чем бы заменить, вдруг что-то лучшее придумали, но пока не нашел ничего лучше. После декларативности XSLT совершенно не хочется возвращаться в процедурщину.

            Прелесть XSLT как раз в том, что он есть везде, при смене языка программирования не придется изучать еще и какой-нибудь новый шаблонизатор только из-за того, что прежний был реализован только на одном языке программирования, да еще и с ошибками (в XSLT процессорах ошибок уже давно нет).
            • UFO just landed and posted this here
                +1
                Я имел ввиду не переносимость шаблонов при смене основы сайта, что редко и никому не нужно, а про то, что нет нужды изучать совершенно новый язык шаблонов при работе на различных платформах.

                Насчет сложного не совсем согласен, нужно просто выделить что XSLT умеет делать отлично (например работа с деревьями), а что вообще не умеет, и переложить эту работу на другой язык программирования.

                Прелесть XSLT еще и в том, что если он запустился, то с огромной вероятностью проблем с шаблонами в будущем вообще не будет. В других шаблонизаторах с этим всё довольно плохо, хотя у меня не большой опыт работы с другими шаблонизаторами, но и этого хватило, чтобы оставаться на XSLT :)
                Не согласен, что код получается только для записи, поддерживать его легко, если конечно привык к XSLT коду, даже через долгое время правка кода не вызывает проблем, и главное, что всё тут же работает без ошибок, имхо такая надежность у всех декларативных языков.
                • UFO just landed and posted this here
              0
              Со скоростью обычно проблемы возникают есть при большом количестве данных использовать не полные xpath (//foo/@bar). xslt-процессору приходится проходить по всему дереву, по одному разу на каждый подобный xpath.
                +1
                Смешной скоростью? xslt в libxslt-реализации очень быстр.

                Дольше всего занимает парсинг больших xml и компиляция xsl. Если хранить скомпилированные стили и часто используемые распарсенные xml в памяти, то можно очень быстро гонять шаблонизацию.
                  0
                  Когда начинают говорить про скорость XSLT, хочется всегда сказать, что XSLT — это далеко не только шаблонизатор для сайтов…
                  А быстрее нативного PHP ничего не будет работать. А для XSLT нужно еще данные в XML собрать внутри PHP, а это тоже не особо быстро будет по сравнению с обычными массивами.
                  Но, тем не менее, у Яндекса все хорошо работать на XSLT.
                  Собственно, думаю, что наверняка Яндексу проще купить еще серверов, чтобы сэкономить время программистов, которым намного быстрее и удобнее поддерживать такую шаблонизацию.
                    0
                    Так в яндексе и не на PHP пишут ведь)
                      0
                      Ну тут я имел ввиду, что у них шаблонизатор XSLT и он работает неплохо у них.
                  0
                  А есть процессоры которые могут держать шаблоны в ОЗУ (shared memory к примеру) в уже распарсеном виде? Т.е. что бы при получении данных он сразу же мог выполнять трансформацию не тратя на каждый запрос время на загрузку и парсинг шаблона.
                    0
                    Есть, например, такой проект. Честно говоря, не пробовал.
                      0
                      Я использовал xslcache в реальных проектах.

                      Идея действительно такая как было сказано выше.
                      Прирост производительности — порядка 20%(зависит от XSLT)
                      Жаль только что похоже проект остановился в развитии еще несколько лет назад
                        0
                        Пробовал в своих проектах, но к сожалению большого прироста производительности не получил, вместо этого на порядок выросла нагрузка на CPU, с чем это было связано я так и не докопался тогда. Возможно, если бы проект развивался дальше — это решилось бы, а так последняя ревизия была 4 года назад ((
                        0
                        nginx так умеет, получать от backend xml и трансформировать его xslt
                          0
                          Насчёт shared memory не знаю, но просто в памяти процесса держать скомпилированные xsl позволяет libxslt. Очень шустро получается.
                            0
                            Кроме PHP наверное любой распространенный язык может) Для PHP есть xslcache но он не развивается. Хотя пару лет назад на одном проекте его использовали — все работало.
                            +3
                            Ваша статья не раскрывает такие важные темы, как организация всего этого декларативного безобразия. Часто именно императивный шаблон легче сопровождать, чем выискивать «затерявшуюся» декларацию. Я пытался донести эту мысль в своей статье по XSLT.
                            Написать хороший шаблон на XSLT намного сложнее, чем плохой и часто среднестатистический Razor-код бывает понятнее среднестатистического XSLT.
                            XSLT даёт возможность удобно работать с иерархическими данными (отображение структуры сайта в навигацию), но когда отображение становится очень далеко от данных (отображение 2-3х чисел в пэйджинг новостей), то вам приходится городить структуры данных именно в угоду XSLT.
                            Правильный план: это всегда разумная достаточность, комбинирование шаблонизаторов и чувство простоты — однако это искусство и приходит оно с опытом.
                              0
                              Полностью согласен.
                              Я так и не нашёл идеального варианта «организации безобразия». Поэтому и не включил эту тему в статью.
                              Как найду — отпишусь =) А Razor — это и правда круто.
                                0
                                Согласен, насчет организации,
                                но for-each тоже не может выступать в качестве универсального решения, поскольку в таком случае теряется возможность переопределения правил для специфичных случаев
                                  0
                                  Чтобы не городить плохой XSLT, нужно просто напросто перенести этот огород туда, где формируются XML данные. Ну и в этой статье написано как лучше делать, и как лучше не делать.
                                    0
                                    Вот как раз переносить логику отображения в данные — плохо: поскольку так мы получаем узкоспециализированную ViewModel, которая не совсем годится для генерации другого HTML из тех же данных.
                                    0
                                    Насчет поиска декларации: иногда очень помогает некоторое подобие call-стека: на отладочной странице выводится стек подгруженных шаблонов, показана информация кто из какого файла, сразу видно кто кого перекрывает. Отладочная страница естественно может быть сгенерирована для каждого конкретного URL путем добавления GET-параметра, либо другим удобным, для вашего URL-маршрутизатора, способом.
                                      0
                                      Я всем советую использовать mode, чтобы не париться с колстэками. Кстати отладку сделать и колстэк можно посмотреть в Visual Studio
                                      Ибо если у вас уже шаблон дошёл до такой степени, что без отладки в тяжело, то это хороший повод для рефакторинга.
                                    +1
                                    Спасибо, хорошая статья.
                                    Правда, смутила фраза

                                    «Причём, если для шаблона используется mode, то дочерние узлы тоже будут преобразовываться с этим mode»


                                    mode не наследуется его необходимо принудительно прописать для apply-templates, вне зависимости от mode самого шаблона. Может поведение в разных парсерах и отличается, но для libxslt это так.
                                      0
                                      Да уж, намудрил. Имелось в виду именно то, что вы сказали. Fixed.
                                      +2
                                      Очень хороший цикл статей
                                      по практическому применению XSLT (временные деревья) здесь

                                      В целом он конечно больше ориентирован на опытных разработчиков.
                                        0
                                        На моей практике часто встречался вопрос: Как разложить список в N-колоночную таблицу.
                                        Я всегда отвечаю примером:
                                          <xsl:template match="items" mode="table">
                                            <xsl:variable name="$cols" />
                                            <table>
                                              <xsl:for-each select="item[position() mod $cols] = 1">
                                                <tr>
                                                  <xsl:for-each select=".|following-sibling::items[position() < $cols]">
                                                    <td>
                                                      <xsl:apply-templates select="." mode="table-cell" />
                                                    </td>
                                                  </xsl:for-each>
                                                </tr>
                                              </xsl:for-each>
                                            </table>
                                          </xsl:template>

                                        Я даю «православный» совет?
                                          0
                                          Мои заповеди:
                                          • если представление зависит от позиции элемента в наборе (position()) — правильно использовать for-each
                                          • если шаблон — вспомогательный (как в вашем примере) и/или ему всё равно, что выводить — это должен быть именованный шаблон
                                          Т.о. я почти согласен с вашим решением.
                                            –2
                                            нет. что если у тебя данных 13 штук, а ширина таблицы 5 ячеек? вместо пустых ячеек будут дырки, что не всегда приемлемо. что, если у пользователя узкий/широкий экран? будут либо слишком большие поля, либо скроллинг.
                                            лучше заверстать без таблички и чтобы количество столбцов бинамически подстраивалось под размер экрана
                                              +1
                                              довод понятен, но сабж не про верстку
                                              0
                                              Почти, ни к чему использовать for-each здесь, просто унести td в вызываемый шаблон.

                                              for-each имеет смысл использовать если внутри цикла есть условия, сортировка или для уменьшения раздробленности.
                                                0
                                                Ну вот я воспользовался for-each именно для уменьшения раздробленности структуры таблицы.
                                                Сортировка — не аргумент. xsl:sort применим как для for-each, так и для apply-templates
                                                  0
                                                  так тут всё равно вызывается
                                                  <xsl:apply-templates select="." mode="table-cell" />

                                                  так, что раздробленность наличествует
                                                0
                                                Да. Вот когда он разрастётся свичами, ифами и HTML-кодом, тогда можно будет его разделять, а пока лучше не трогать.
                                                0
                                                Дело вкуса и стиля программирования.

                                                Не совсем так в части именованных шаблонов. Есть еще такой критерий как реюзинг. И когда в десяти разных шаблонов осуществляется, например, рендеринг таблицы, а стиль которой един для всего сайта, то такие шаблоны просто необходимо выносить в качестве именованных. Собственно говоря, тот самый «процедурный» подход в программировании был придуман как раз с целью реюзинга. Так что я бы добавил условие, «если вы используете такой шаблон только один раз». Иначе вы можете сэкономить несколько строк и выиграть битву в рамках отдельно взятого xslt, но проигрываете войну в рамках всего проекта в целом))

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

                                                Самое веселое, что мне доводилось делать в XSLT — это реализация циклов от X до Y. Можно показать их рекурсивную реализацию. Так же можно посоветовать сразу обратить внимание на возможности расширений XSLT. Как правило, многие трансформеры имеют возможность подключать внешние библиотеки с реализацией тех или иных функций. В некоторых их можно писать на JS прямо в теле стиля. Просто по моему наблюдению многие отказываются от XSLT как только возникает необходимость в каком-то даже не очень сложном вычислении, а сделать его представляется проблематичным, в силу очень скудного набора базовых функций.
                                                  0
                                                  не надо программировать на xslt. рекурсивные алгоритмы — это громоздко, тормозно и геморройно
                                                    0
                                                    Иногда приходиться. Хорошо, когда это простая задачка в виде шаблонизатора для сайта и ты можешь подкрутить выдаваемый xml. Но xslt также часто используется для обеспечения интерфейса между двумя системами. У одной — свой xml-выхлоп, у другой — свой входной xml. И никак на это повлиять нельзя. XSLT, как правило, справляется гораздо лучше, чем ручной разбор и трансформация, но очень часто находятся некоторые мелкие и неприятные затычки. Например, тебе на входе идет строка +7(322)223-33-22, а на выходе />. Это тоже трансформация, но к ней XSLT не готов.
                                                      0
                                                      Тут нужны экстеншены. Umbraco вся построена на экстеншенах.
                                                    0
                                                    Вы правы. Нужно использовать все возможности языка по назначению. Чуть выше я привёл свои правила по выбору стиля.
                                                      0
                                                      Самое веселое, что мне доводилось делать в XSLT — это реализация циклов от X до Y. Можно показать их рекурсивную реализацию

                                                      habrahabr.ru/blogs/xslt/47545/
                                                      Собственно, с этой статьи и создан был блог, но дальше руки не дошли что-то писать. Вот теперь неплохую статью автор выкатил, я тоже хотел тогда давно об этом написать.
                                                    • UFO just landed and posted this here
                                                        0
                                                        Я в шаблонах раньше всегда такой добавлял:
                                                        <xsl:template match="@*|node()">
                                                        <xsl:copy>
                                                        <xsl:apply-templates select="@*|node()"/>
                                                        </xsl:copy>
                                                        </xsl:template>


                                                        Сейчас на XSLT почти не приходится писать…
                                                        • UFO just landed and posted this here
                                                            0
                                                            Я тоже не уверен, поэтому привел свой вариант :)
                                                        0
                                                        У libxslt не очень удачная реализация document().
                                                        Каждый раз при вызове функции, файл открывается и парсится заново. Кеширования нет. Поэтому имеет смысл встроить свои справочники либо в сам xslt, либо во входной xml.

                                                        Only users with full accounts can post comments. Log in, please.