1 августа 2019 г.

Современный 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-элемент. Но это лишь оптимизация, общий подход верен.

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

Комментарии вернулись :)

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