Pull to refresh

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

CSS *JavaScript *HTML *

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


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


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

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


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

Tags:
Hubs:
Total votes 26: ↑25 and ↓1 +24
Views 20K
Comments Comments 17