Опыт использования пространств имён в клиентском XHTML


Текст Ростислава Чебыкина.


Я вам посылку принёс. Только я вам её не отдам, потому что у вас документов нету.
Почтальон Печкин

Мы вместе с Денисом Лесновым разрабатываем аудиопроигрыватель для сайта, о котором уже рассказывали здесь в 2015 году. Сейчас на подходе обновлённая версия, которая умеет играть не только отдельные треки, но и целые плейлисты.


В этой статье пойдёт речь не о самoм проигрывателе, а о неожиданных феноменах, с которыми мы столкнулись, попытавшись использовать настоящий XHTML.




Основная цель нашего проекта — получить удовольствие от совместного программирования. Поэтому мы пишем код так, как нам нравится, и используем технологии, которые нам интересны.


Мне, например, был интересен XHTML ещё с тех пор, как Консорциум W3C опубликовал первые черновики этого стандарта, и многочисленные энтузиасты бросились пропагандировать обновлённый язык. Тогда многие надеялись, что весь Интернет вот-вот перейдёт на XML-совместимую разметку, и от этого наступит всеобщее счастье.


На моём собственном сайте веб-страницы всегда соответствовали синтаксису XHTML и отдавались с типом application/xhtml+xml тем браузерам, которые его поддерживали. Несколько лет назад я вообще перестал отдавать text/html.


К нынешнему времени шумиха вокруг XHTML улеглась, соответствующая рабочая группа Консорциума закрылась после нескольких лет прострации, а тогдашние энтузиасты переключились на другие модные концепции. Мне жаль, что многие перспективные идеи XHTML 2.0 так не внедрились в широкую практику. Например, тот стандарт предлагал, чтобы любой элемент можно было превратить в гиперссылку, присвоив ему атрибут href:


<li href="/about/">О компании</li>
<li href="/contacts/">Контакты</li>

Увы, это не воплотилось в жизнь, так что для канонических идиом веб-интерфейсов типа «кликабельной» картинки приходится по сей день использовать отдельно элемент img и отдельно элемент a, как в начале 1990-х.



Может быть, XHTML «не взлетел» потому, что энтузиастам так и не удалось продемонстрировать его практические преимущества. На фанатских сайтах код XHTML носил чисто косметический характер и не содержал ничего такого, чего не обеспечивал бы обычный HTML. Наоборот, XHTML ограничивал разработчиков старой школы, не давая использовать любимый document.write и заставляя явно вставлять tbody в каждую таблицу.


В 2016 году я наконец решил попробовать эндемичные возможности XHTML, а именно пространства имён. Мне хотелось, чтобы компоненты аудиопроигрывателя были самодельными элементами, находящимися в собственном неймспейсе:


image


Как положено, пространство имён и его префикс объявлены в открывающем тэге html:


<html xmlns="http://www.w3.org/1999/xhtml" 
      xmlns:p="http://rostislav.chebykin.ru/xmlns" 
      lang="ru" xml:lang="ru">

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



Во-первых, ещё на этапе первичного тестирования идеи выяснилось, что у самодельных элементов не работает атрибут style:


<p:track style="background: #999;" />    // не работает

Соответствующий атрибут появляется в дереве DOM, но фон элемента на странице не окрашивается в указанный цвет.


Зато при подключении отдельной таблицы стилей (во внешнем файле или в элементе style) CSS успешно действует, и самодельные элементы оформляются с помощью селектора пространства имён:


p|track { background: #999; }

Правда, чтобы это работало, первым правилом в таблице стилей должно идти объявление @namespace:


@namespace p url('http://rostislav.chebykin.ru/xmlns');

Таким образом удалось поскрасить track в нужный цвет и придать элементу playhead круглую форму. Но это ещё не обеспечивало динамики, в которой состоит суть интерфейса проигрывателя. И эта динамика завела нас в дебри.



Для работы с пространствами имён в DOM есть методы createElementNS и getElementsByTagNameNS. Их поведение местами сбивает меня с толку: например, селектор p|track действует на элемент независимо от того, указан ли префикс пространства имён во втором аргументе createElementNS:


const ns = 'http://rostislav.chebykin.ru/xmlns';
document.createElementNS(ns, 'p:track');
document.createElementNS(ns, 'track');
  // на этот элемент тоже действует селектор p|track

А вот getElementsByTagNameNS хочет получить имя строго без префикса, независимо от того, каким из двух способов был создан элемент:


document.getElementsByTagNameNS(ns, 'track');
  // возвращает HTMLCollection с обоими элементами
document.getElementsByTagNameNS(ns, 'p:track');
  // возвращает пустой HTMLCollection

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



Двигать головку проигрывателя — казалось бы, что может быть проще? Получать нужную позицию, преобразовывать её в проценты и присваивать свойству left:


playhead.style.left = pos;

(Здесь переменная playhead указывает на нужный элемент в DOM, а pos — процентная строка в формате CSS, типа '12.34%').


Но нет! Оказывается, у playhead нет свойства style. Чтобы разобраться, почему так случилось, сравним цепочку прототипов playhead с цепочкой какого-нибудь стандартного элемента HTML:


playhead: Element < Node < EventTarget < Object
div: HTMLDivElement < HTMLElement < Element < Node < EventTarget < Object

Свойство style, предоставляющее доступ к свойствам CSS того или иного элемента, определено в интерфейсе HTMLElement и отсутствует у его родителя Element.


Пытаясь перенести «родной» style от HTMLElement к самодельным элементам, мы потерпели поражение. Даже если просто ввести в консоли HTMLElement.prototype.style, выпадает сообщение об ошибке, и тем более с этим свойством не получается сделать ничего содержательного.


Пришлось исхитряться через CSS Object Model: динамически подключать внешний CSS, добираться до его правил, у которых есть то самое свойство style, и гвоздями прибивать этот style к соответствующим элементам:


const link = document.createElement('link');
  // затем присваиваем link’у нужные атрибуты
  // и вставляем в head
playhead.style = link.sheet.cssRules.item(1).style

После этого конструкции вроде playhead.style.left = pos; начали работать… везде, кроме Safari. Неожиданно выяснилось, что в этом экстравагантном браузере свойство style всё-таки есть у Element.prototype, и дескрипторы этого свойства не позволяют ничего ему присваивать. Проблема решилась переопределением style персонально для наших элементов:


Object.defineProperty(playhead, 'style', { writable: true });


Наконец, отдельной неожиданностью стало то, как с пространствами имён обходятся методы querySelector, querySelectorAll и matches, принимающие в качестве аргументов селекторы CSS, например:


document.querySelectorAll('p|track');
playhead.matches('p|playhead');

Здесь каждый браузер нашёл свои собственные слова, чтобы выразить недоумение:


Edge NamespaceError
Chrome Uncaught DOMException: Failed to execute 'matches' on 'Element': 'p|playhead' is not a valid selector
Firefox SyntaxError: An invalid or illegal string was specified
Safari NamespaceError (DOM Exception 14): The operation is not allowed by Namespaces in XML

Причина в том, что перечисленные методы не могут разрулить префикс p и связать его с соответствующим пространством имён. В XHTML для объявления неймспейсов есть атрибут xmlns, в CSS — @namespace, а в JavaScript — опа! — ничего нет. Что характерно, в браузерах работает метод document.createNSResolver, однако его результат никак не прикручивается к методам типа querySelectorAll.


В Selectors API сказано, что «namespace prefix needs to be resolved», однако тут же приписано: «This specification does not provide support for resolving arbitrary namespace prefixes». Интересно, что в черновых версиях спецификации был интерфейс NSResolver и предлагался соответствующий аргумент в «селекторных» методах:


Element querySelector(in DOMString selectors, in NSResolver nsresolver);

Однако по пути к рекомендации W3C NSResolver был злодейски выпилен, и в результате спецификация ведёт себя, как почтальон Печкин: «Я вам посылку принёс, только я вам её не отдам, потому что у вас документов нету».


Я не удивлюсь, если через несколько лет Консорциум объявит язык XHTML deprecated и obsolete, а браузеры откажутся от поддержки application/xhtml+xml. Надеюсь, что до этого времени нам с Денисом удастся закончить очередную версию аудиопроигрывателя, чтобы она сохранилась в Интернете хотя бы как музейный экспонат.

Поделиться с друзьями
-->

Комментарии (17)


  1. nazarpc
    04.01.2017 21:40
    +4

    Вы смотрели на веб-компоненты и пользовательские элементы в частности? Статью читал по диагонали, но мне кажется это именно то, что вам нужно, и проблем с наследованием когда отсутствует element.style там нет.


    1. eugef
      05.01.2017 00:17
      +2

      Полностью согласен — вместо кастомных неймспейсов надо использовать веб-компоненты. Они наследуются от HTMLElement, поэтому из коробки умеют все, что и стандартные элементы.
      А в качестве плюшек получите ShadowDOM с инкапсуляцией стилей и скриптов.


      1. den_lesnov
        05.01.2017 00:22
        +1

        Смотрели в сторону Custom Elements, но их нативно нет в IE/Edge, а полифиллы мы в этом проекте не хотим по религиозным причинам. Как только будет нативная поддержка, так обязательно заиспользуем. Спасибо за совет!


  1. EvilFox
    04.01.2017 23:29
    +1

    Я не удивлюсь, если через несколько лет Консорциум объявит язык XHTML deprecated и obsolete.
    Ну по факту так уже и есть, это уже стало понятно когда закопали XHTML2.
    Но я думаю не стоит сильно печалиться, сейчас чувствуется направление идёт в сторону браузер как виртуальная машина. То есть сейчас допиливается WebAssembly, дальше допилят WebGL до уровня когда он будет работать с системными контролами и поведением, и всё, не далека новая эра разметок которые рано или поздно будут куда лучше HTML и XHTML вместе взятых. Есть первые шаги в эту сторону в виде HTML GL и подобного.


    1. nightwolf_du
      04.01.2017 23:53

      Так не будет. Производители браузеров опять пойдут «кто в лес, кто по дрова» в плане поддержки стандартов.
      Уже не один раз пройдено.
      Мне кажется, что корень проблемы в том, что браузер — очень сложная программа, особенно в плане поддержки обратной совместимости, и цена внесения изменений — крайне высока. Поэтому мы опять получим две-три разные реализации, только теперь для WebAssembly и WebGL(или нового варианта разметки).
      Похоронить же сейчас все текущие технологии интернетов ради нового стандарта — нереал, как в старой картинке о 20+ стандартах.


      1. EvilFox
        05.01.2017 12:44
        +2

        Не знаю. То что было раньше и сейчас это земля и небо. Гораздо меньше требуется костылей чем раньше.
        Новый вариант разметки вряд ли будет сам по себе, если даже XHTML2 закопали и с трудом черепашими скоростями развивается HTML5.
        Однако WebGL даёт энтузиастам и заинтересованным компаниям попробовать себя в этом деле. WebAssembly просто позволит сделать это быстрее, компактнее и гибче (гибче в том смысле что можно от JS абстрагироваться). Потом уже что-то может стать стандартом и внедрено в обозреватели.
        Сначала нам правда скорее всего придётся пережить вот такие ужасы (в смысле бесполезные дизайны далёкие от удобства для вау-эффекта, перегруженные шейдерами/эффектами).


    1. Massacre
      05.01.2017 04:01
      +5

      И для браузера будет требоваться ещё и хорошая игровая видеокарта, ага?


      1. EvilFox
        05.01.2017 12:20
        +1

        Нет. Это дешевле отрисовки тормозного DOM. См. HTML GL.


        1. EvilFox
          05.01.2017 12:52
          +1

          http://engineering.flipboard.com/2015/02/mobile-web/


          1. daggert
            05.01.2017 22:59

            Не тыкнете меня в сравнения в виде графиков или т.п. цифр? Не увидел, пробежав по тексту взглядом. Особенно интересно по потреблению ресурсов глянуть.


    1. Large
      12.01.2017 02:54

      WebGL это низкоуровневый интерфейс для отрисовки точек, линий и треугольников, ни про какие контролы он не знает и знать не может, а библиотек которые рисуют контролы на WebGL достаточно, в том числе упомянутый рендер html gl. Есть еще интересный проект от мозиллы https://aframe.io/, но не думаю, что его стандартизируют.


  1. sshikov
    05.01.2017 10:57
    +3

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

    Т.е. теоретически да, в XHTML можно namespaces, но практически — лишь два или три заранее известных, а именно SVG, MathML, ну и собственно сам XHTML. Ну может где-то можно еще какой-то, который реализовали разработчики Mozilla, например.

    Отсюда мне кажется и все эти непонятки с атрибутом style (что воообще логично — потому что его наличия никто не обещал для кастомных элементов), и с javascript API — потому что никто практически не делал, и никто не знает, что реально нужно для реализации чего-то полезного и завершенного.


  1. ihost
    05.01.2017 13:25
    +4

    Безусловно, описанная в статье ситуация с XHTML довольно проблематичная, но скорее в идеологическом плане, поскольку технически те или иные задачи все-таки можно решить. Дело в том, что исторически направления XHTML и CSS selectors развивались параллельно, о чем как минимум свидетельствует наличие двух конкурентных языков для выбора элементов документа — XPath и CSS selector, при этом оба успешно применяются в сфере автоматизации web-приложений и функциональном тестировании, в том же Nightwatch-е к примеру.
    Далее по техническим вопросам. Во-первых, навигацию по XHTML лучше реализовывать посредством XPath, который изначально как раз предназначен для XML-документов, и поддерживается посредством функции document.evaluate(), по крайней мере в браузерах, приведенных автором статьи в последней таблице. Для IE старых версий тоже нет проблем, можно использовать объект new ActiveXObject("Msxml2.DOMDocument"), за-map-енный на текущее DOM-дерево, пример решения можно взять здесь https://sourceforge.net/projects/html-xpath/
    Это имеет свой смысл, поскольку в общем случае CSS и уж тем более CSS selectors являются чужеродными для произвольных XML-элементов, которые не обязаны ни поддерживать стили, ни иметь какой-то внешний вид для отображения — который должен по-хорошему определяться через XSLT-преобразование (Или, если интересует поддержка IE старых версий, через Element Behaviors, во времена которых как раз задумывался XHTML с определением пользовательского поведения элементов — https://msdn.microsoft.com/en-us/library/ms531426(v=vs.85).aspx).
    Во-вторых, с учетом современных тенденций в web-приложениях, включающую предварительное преобразование исходного кода в связке webpack + babel, можно сделать свою реализацию для resolve-а и transform-а исходного XHTML-кода, по аналогии с тем же JSX, который в итоге преобразования будет предоставлять набор элементов с некоторыми идентификаторами и классами, а исходный код будет выглядеть так, как задумал автор — с пространствами имен XHTML.
    Резюмируя по исходной задаче, решения можно добиться без polyfills, правда потребуется две версии dist-клиентского сценария — для современных браузеров через document.evaluate(), и если захотите поддержку IE старых версий — то через Element Behaviors API, которое кстати поддерживает о-о-о-очень широкую функциональность.


    1. LastDragon
      05.01.2017 15:06

      Xhtml это была попытка превратить html в xml, но оно не взлетело ибо xml гораздо строже к правильной структуре документа, и там где html просто продолжал работать, xhtml умирал. CSS всегда развивался как средство для задания визуального вида документа будь то html, xhml или xml (https://www.w3.org/Style/styling-XML), поэтому для стилей нужно использовать именно его (и оно, в отличии от xpath-а, заточено именно для этого, поэтому там и нет сложных селекторов), а не xslt.


      Xslt и вместе с ним xpath пришли из мира энтерпрайза где была нужда в сложных преобразованиях одного документа в другой (совсем не обязательно xml/html), но по сути только там и остались как из-за высокой сложности (через пару лет редактировать xslt шаблон это боль...), хреновой отладки, ресурсоёмкости (на сервере), так и из-за убогой поддержки основными браузерами (была нужна бажная http://dev.abiss.gr/sarissa и https://bugs.webkit.org/show_bug.cgi?id=10313 в рассвет эры ajax-а это был вообще фатальный недостаток, который помог окончательно похоронить xslt в вебе).


      1. LastDragon
        05.01.2017 15:15
        +1

        Или даже так "в рассвет эры ajax-а это был вообще фатальный недостаток, который вместе с развитием js и json помог окончательно похоронить и xml и xslt/xpath в вебе" :) (собственно, со времен этого бага степень поддержки xslt в браузерах вообще никак не поменялась, а тот же десятилетний xslt 2.0 нигде и никем не поддерживается до сих пор и с очень большой вероятностью поддерживаться уже и не будет).


        1. sshikov
          08.01.2017 14:06

          Согласен, кроме одного пункта — в итоге похорон родился например JSONPath в разных вариациях, который по сути есть тот же XPath, только чуть более убогий. Т.е. технология типа XPath все равно много кому и где нужна, и так или иначе существует, хотя и в другой форме.


  1. ilinsky
    07.01.2017 19:35
    +1

    Увы, так и есть: выпилив пространства имен из веба, авторы новых технологий (HTML5+ Web Components) никакой альтернативы не предложили. И если раньше в XML словари элементов идентифицировались по URI, то теперь это делается по простому условному префиксу в имени: ни версионности, ни ссылочности, ни уникальности…