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

в 18:31, , рубрики: css, html, javascript, xhtml, xml namespaces

Опыт использования пространств имён в клиентском 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('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

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js