Современный DOM: полифиллы

В старых IE, особенно в IE8 и ниже, ряд стандартных DOM-свойств не поддерживаются или поддерживаются плохо.

Если говорить о современных браузерах, то они тоже не все идут «в ногу», всегда какие-то современные возможности реализуются сначала в одном, потом в другом.

Но это не значит, что нужно ориентироваться на самый старый браузер из поддерживаемых!

Для того, чтобы не думать об устаревших браузерах, а писать современный код, который при этом работает везде, используют полифиллы.

Полифиллы

«Полифилл» (англ. polyfill) – это библиотека, которая добавляет в старые браузеры поддержку возможностей, которые в современных браузерах являются встроенными.

Один полифилл мы уже видели, когда изучали собственно JavaScript – это библиотека ES5 shim. Если её подключить, то в IE8- начинают работать многие возможности ES5. Работает она через модификацию стандартных объектов и их прототипов. Это типично для полифиллов.

В работе с DOM несовместимостей гораздо больше, как и способов их обхода.

Что делает полифилл?

Для примера добавим в DOM поддержку свойства firstElementChild, если её нет. Здесь речь, конечно, об IE8, в других браузерах оно и так поддерживается, но пример типовой.

Вот код для такого полифилла:

if (document.documentElement.firstElementChild === undefined) { // (1)

  Object.defineProperty(Element.prototype, 'firstElementChild', { // (2)
    get: function() {
      var el = this.firstChild;
      do {
        if (el.nodeType === 1) {
          return el;
        }
        el = el.nextSibling;
      } while (el);

      return null;
    }
  });
}

Если этот код запустить, то firstElementChild появится у всех элементов в IE8.

Общий вид этого полифилла довольно типичен. Обычно полифилл состоит из двух частей:

  1. Проверка, есть ли встроенная возможность.
  2. Эмуляция, если её нет.

Проверка встроенного свойства

Для проверки встроенной поддержки firstElementChild мы можем просто обратиться к document.documentElement.firstElementChild.

Если DOM-свойство firstElementChild поддерживается, то его значение не может быть undefined. Если детей нет – свойство равно null, но не undefined.

Сравним:

alert( document.head.previousSibling ); // null, поддержка есть
alert( document.head.blabla ); // undefined, поддержки нет

За счёт этого работает проверка в первой строке полифилла.

Важная тонкость – элемент, который мы тестируем, должен по стандарту поддерживать такое свойство.

Попытаемся, к примеру, проверить «поддержку» свойства value. У input оно есть, у div такого свойства нет:

var div = document.createElement('div');
var input = document.createElement('input');

alert( input.value ); // пустая строка, поддержка есть
alert( div.value ); // undefined, поддержки нет
Поддержка значений свойств

Если мы хотим проверить поддержку не свойства целиком, а некоторых его значений, то ситуация сложнее.

Например, нам интересно, поддерживает ли браузер <input type="range">. То есть, понятно, что свойство type у input, в целом, поддерживается, а вот конкретный тип <input>?

Для этого можно создать <input> с таким type и посмотреть, подействовал ли он.

Например:

<input type="radio">
<input type="no-such-type">

<script>
  alert( document.body.children[0].type ); // radio, поддерживается
  alert( document.body.children[1].type ); // text, не поддерживается
</script>
  1. Первый input имеет type="radio". Этот тип точно поддерживается, поэтому input.type имеет значение "radio", как и указано.
  2. Второй input имеет type="no-such-type". В качестве типа, для примера, специально указано заведомо неподдерживаемое значение. При этом input.type равен "text", таково значение по умолчанию. Мы можем прочитать его и увидеть, что поддержки нет.

Эта проверка работает, так как хоть в HTML-атрибут type и можно присвоить любую строку, но DOM-свойство type по стандарту хранит реальный тип input'а.

Добавляем поддержку свойства

Если мы осуществили проверку и видим, что встроенной поддержки нет – полифилл должен её добавить.

Для этого вспомним, что DOM элементы описываются соответствующими JS-классами.

Например:

Они наследуют, как мы видели ранее, от HTMLElement, который является общим родительским классом для HTML-элементов.

А HTMLElement, в свою очередь, наследует от Element, который является общим родителем не только для HTML, но и для других DOM-структур, например для XML и SVG.

Для добавления нужной возможности берётся правильный класс и модифицируется его prototype.

Например, можно добавить всем элементам в прототип функцию:

Element.prototype.sayHi = function() {
  alert( "Привет от " + this );
}

document.body.sayHi(); // Привет от [object HTMLBodyElement]

Сложнее – добавить свойство, но это тоже возможно, через Object.defineProperty:

Object.defineProperty(Element.prototype, 'lowerTag', {
  get: function() {
    return this.tagName.toLowerCase();
  }
});

alert( document.body.lowerTag ); // body
Геттер-сеттер и IE8

В IE8 современные методы для работы со свойствами, такие как Object.defineProperty, Object.getOwnPropertyDescriptor и другие не поддерживаются для произвольных объектов, но отлично работают для DOM-элементов.

Чем полифиллы и пользуются, «добавляя» в IE8 многие из современных методов DOM.

Какова поддержка свойства?

А нужен ли вообще полифилл? Какие браузеры поддерживают интересное нам свойство или метод?

Зачастую такая информация есть в справочнике MDN, например для метода remove(): https://developer.mozilla.org/en-US/docs/Web/API/ChildNode.remove – табличка совместимости внизу.

Также бывает полезен сервис http://caniuse.com, например для elem.matches(css): http://caniuse.com/#feat=matchesselector.

Итого

Если вы поддерживаете устаревшие браузеры – и здесь речь идёт не только про старые IE, другие браузеры тоже обновляются не у всех мгновенно – не обязательно ограничивать себя в использовании современных возможностей.

Многие из них легко полифиллятся добавлением на страницу соответствующих библиотек.

Для поиска полифилла обычно достаточно ввести в поисковике "polyfill", и нужное свойство либо метод. Как правило, полифиллы идут в виде коллекций скриптов.

Полифиллы хороши тем, что мы просто подключаем их и используем везде современный DOM/JS, а когда старые браузеры окончательно отомрут – просто выкинем полифилл, без изменения кода.

Типичная схема работы полифилла DOM-свойства или метода:

  • Создаётся элемент, который его, в теории, должен поддерживать.
  • Соответствующее свойство сравнивается с undefined.
  • Если его нет – модифицируется прототип, обычно это Element.prototype – в него дописываются новые геттеры и функции.

Другие полифиллы сделать сложнее. Например, полифилл, который хочет добавить в браузер поддержку элементов вида <input type="range">, может найти все такие элементы на странице и обработать их, меняя внешний вид и работу через JavaScript. Это возможно. Но если уже существующему <input> поменять type на range – полифилл не «подхватит» его автоматически.

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

Один из лучших сервисов для полифиллов: polyfill.io. Он даёт возможность вставлять на свою страницу скрипт с запросом к сервису, например:

<script src="//cdn.polyfill.io/v1/polyfill.js?features=es6"></script>

При запросе сервис анализирует заголовки, понимает, какая версия какого браузера к нему обратилась и возвращает скрипт-полифилл, добавляющий в браузер возможности, которых там нет. В параметре features можно указать, какие именно возможности нужны, в примере выше это функции стандарта ES6. Подробнее – см. примеры и список возможностей.

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

Примеры полифиллов:

Более мелкие библиотеки, а также коллекции ссылок на них:

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

Задачи

важность: 5

Метод elem.matches(css) в некоторых старых браузерах поддерживается под старым именем matchesSelector или с префиксами, то есть: webkitMatchesSelector (старый Chrome, Safari), mozMatchesSelector (старый Firefox) или Element.prototype.msMatchesSelector (старый IE).

Создайте полифилл, который гарантирует стандартный синтаксис elem.matches(css) для всех браузеров.

Код для полифилла здесь особенно прост.

Реализовывать ничего не надо, просто записать нужный метод в Element.prototype.matches, если его там нет:

(function() {

  // проверяем поддержку
  if (!Element.prototype.matches) {

    // определяем свойство
    Element.prototype.matches = Element.prototype.matchesSelector ||
      Element.prototype.webkitMatchesSelector ||
      Element.prototype.mozMatchesSelector ||
      Element.prototype.msMatchesSelector;

  }

})();
важность: 5

Метод elem.closest(css) для поиска ближайшего родителя, удовлетворяющего селектору css, не поддерживается некоторыми браузерами, например IE11-.

Создайте для него полифилл.

Код для этого полифилла имеет стандартный вид:

(function() {

  // проверяем поддержку
  if (!Element.prototype.closest) {

    // реализуем
    Element.prototype.closest = function(css) {
      var node = this;

      while (node) {
        if (node.matches(css)) return node;
        else node = node.parentElement;
      }
      return null;
    };
  }

})();

Обратим внимание, что код этого полифилла использует node.matches, то есть может в свою очередь потребовать полифилла для него. Это типичная ситуация – один полифилл тянет за собой другой. Именно поэтому сервисы и библиотеки полифиллов очень полезны.

важность: 3

Свойство textContent не поддерживается IE8. Однако, там есть свойство innerText.

Создаёте полифилл, который проверяет поддержку свойства textContent, и если её нет – создаёт его, используя innerText. Получится, что в IE8 «новое» свойство textContent будет «псевдонимом» для innerText.

Хотя свойство innerText и работает по-иному, нежели textContent, но в некоторых ситуациях они могут быть взаимозаменимы. Именно на них направлен полифилл.

Код для полифилла здесь имеет стандартный вид:

(function() {

  // проверяем поддержку
  if (document.documentElement.textContent === undefined) {

    // определяем свойство
    Object.defineProperty(HTMLElement.prototype, "textContent", {
      get: function() {
        return this.innerText;
      },
      set: function(value) {
        this.innerText = value;
      }
    });
  }

})();

Единственный тонкий момент – в проверке поддержки.

Мы часто можем использовать уже существующий элемент. В частности, при проверке firstElementChild мы можем проверить его наличие в document.documentElement.

Однако, в данном случае попытка получить document.documentElement.textContent при поддержке этого свойства приведёт к совершенно лишним затратам времени и памяти, так как браузер будет динамически генерировать строку из содержимого документа.

Поэтому лучше бы использовать пустой DOM-элемент. Но это лишь оптимизация, общий подход верен.

Карта учебника

Комментарии

перед тем как писать…
  • Приветствуются комментарии, содержащие дополнения и вопросы по статье, и ответы на них.
  • Для одной строки кода используйте тег <code>, для нескольких строк кода — тег <pre>, если больше 10 строк — ссылку на песочницу (plnkr, JSBin, codepen…)
  • Если что-то непонятно в статье — пишите, что именно и с какого места.