Вот тебе, бабушка, и неймспейсы


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


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


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

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


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




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


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


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


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


  • О компании
  • Контакты

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



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


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


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



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



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


        // не работает

    Соответствующий атрибут появляется в дереве 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. Надеюсь, что до этого времени нам с Денисом удастся закончить очередную версию аудиопроигрывателя, чтобы она сохранилась в Интернете хотя бы как музейный экспонат.

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

    • 4 января 2017 в 21:40

      0

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

    © Habrahabr.ru