Опыт использования пространств имён в клиентском 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, а именно пространства имён. Мне хотелось, чтобы компоненты аудиопроигрывателя были самодельными элементами, находящимися в собственном неймспейсе:
Как положено, пространство имён и его префикс объявлены в открывающем тэге 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('https://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. Надеюсь, что до этого времени нам с Денисом удастся закончить очередную версию аудиопроигрывателя, чтобы она сохранилась в Интернете хотя бы как музейный экспонат.
Автор: den_lesnov