Статьи

Когда не удается обнаружить функцию JavaScript

Когда-то, обнаружение браузера было запасом для программистов JavaScript. Если бы мы знали, что что-то работает в IE5, но не в Netscape 4, мы бы проверили этот браузер и соответственно обработали код. Что-то вроде этого:

if(navigator.userAgent.indexOf('MSIE 5') != -1) { //we think this browser is IE5 } 

Но гонка вооружений уже началась, когда я впервые присоединился к этой отрасли! Продавцы добавляли дополнительные значения в строку user-agent, поэтому они выглядели бы как браузером своего конкурента, так и своим собственным. Например, это Safari 5 для Mac:

 Mozilla / 5.0 (Macintosh; Intel Mac OS X 10_6_8) AppleWebKit / 534.59.10 (KHTML, как Gecko) Версия / 5.1.9 Safari / 534.59.10

Это будет соответствовать тестам для Safari и Webkit, а также KHTML (кодовая база Konqueror, на которой основан Webkit); но он также соответствует Gecko (который является движком рендеринга Firefox) и, конечно, Mozilla (потому что почти каждый браузер претендует на звание Mozilla по историческим причинам ).

Цель добавления всех этих значений — обойти обнаружение браузера . Если сценарий предполагает, что только Firefox может обрабатывать определенную функцию, в противном случае он может исключить Safari, даже если он, вероятно, будет работать. И не забывайте, что пользователи сами могут менять своего пользовательского агента — я знаю, что мой браузер настроен для идентификации как Googlebot / 1.0 , поэтому я могу получить доступ к контенту, который владелец сайта считает доступным только для сканирования!

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

Функция обнаружения просто проверяет те функции, которые мы хотим использовать. Например, если нам нужен getBoundingClientRect (чтобы получить позицию элемента относительно области просмотра), тогда важно то , поддерживает ли его браузер , а не какой это браузер; поэтому вместо тестирования поддерживаемых браузеров мы проверяем саму функцию:

 if(typeof document.documentElement.getBoundingClientRect != "undefined") { //the browser supports this function } 

Браузеры, которые не поддерживают эту функцию, будут возвращать тип "undefined" и, следовательно, не будут проходить условие. Без необходимости проверять скрипт в каком-либо конкретном браузере, мы знаем, что он либо будет работать правильно, либо молча завершится сбоем.

Или мы …?

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

Объект ActiveX

Пожалуй, самый известный пример, когда обнаружение функций не удается, — это тестирование ActiveXObject для выполнения Ajax-запроса в Internet Explorer.

ActiveX является примером объекта позднего связывания , практический смысл которого заключается в том, что вы не можете знать , будет ли он поддерживаться, пока не попробуете его использовать . Таким образом, такой код выдаст ошибку, если у пользователя отключен ActiveX:

 if(typeof window.ActiveXObject != "undefined") { var request = new ActiveXObject("Microsoft.XMLHTTP"); } 

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

 if(typeof window.ActiveXObject != "undefined") { try { var request = new ActiveXObject("Microsoft.XMLHTTP"); } catch(ex) { request = null; } if(request !== null) { //... we have a request object } } 

HTML-атрибуты, сопоставленные со свойствами DOM

Отображения свойств часто используются для проверки поддержки API, который идет с атрибутом HTML5 . Например, проверяя, что элемент с [draggable="true"] поддерживает API draggable , ищем свойство draggable :

 if("draggable" in element) { //the browser supports drag and drop } 

Проблема здесь в том, что IE8 или более ранняя версия автоматически сопоставляет все атрибуты HTML со свойствами DOM . Вот почему getAttribute такой старый беспорядок в старых версиях, потому что он вообще не возвращает атрибут, он возвращает свойство DOM .

Это означает, что если мы используем элемент, который уже имеет атрибут:

 <div draggable="true"> ... </div> 

Затем перетаскиваемый тест вернет true в IE8 или более ранней версии, даже если они не поддерживают его.

Атрибут может быть любым:

 <div nonsense="true"> ... </div> 

Но результат будет таким же — IE8 или более ранний вернет true для ("nonsense" in element) .

Решением в этом случае является тестирование с элементом, который не имеет атрибута , и самый безопасный способ сделать это — использовать созданный элемент:

 if("draggable" in document.createElement("div")) { //the browser really supports drag and drop } 

Предположения о поведении пользователя

Возможно, вы видели такой код, используемый для обнаружения сенсорных устройств:

 if("ontouchstart" in window) { //this is a touch device } 

Большинство сенсорных устройств реализуют искусственную задержку перед срабатыванием событий click (обычно около 300 мс ), так что элементы могут быть удвоены без нажатия также. Но это может заставить приложение чувствовать себя вялым и не отвечающим, поэтому разработчики иногда разветвляют события, используя этот функциональный тест:

 if("ontouchstart" in window) { element.addEventListener("touchstart", doSomething); } else { element.addEventListener("click", doSomething); } 

Однако это условие исходит из ложного предположения о том, что, поскольку устройство поддерживает касание, будет использоваться касание. Но как насчет ноутбуков с сенсорным экраном? Пользователь может касаться экрана или использовать мышь или трекпад; код выше не может с этим справиться, поэтому щелчок мышью ничего не изменит.

Решение в этом случае вовсе не состоит в том, чтобы проверять поддержку событий — вместо этого, связывайте оба события одновременно, а затем используйте preventDefault чтобы не preventDefault сгенерировать щелчок:

 element.addEventListener("touchstart", function(e) { doSomething(); e.preventDefault(); }, false); element.addEventListener("click", function() { doSomething(); }, false); 

Вещи, которые просто не работают

Признавать это болезненно, но иногда это не та функция, которую нам нужно проверять — это браузер, потому что конкретный браузер заявляет о поддержке чего-то, что не работает. Недавним примером этого является setDragImage() в Opera 12 (который является методом объекта перетаскивания dataTransfer ).

Тестирование функций здесь не проходит, потому что Opera 12 утверждает, что поддерживает его; обработка исключений также не поможет, потому что она не выдает никаких ошибок. Это просто не работает:

 //Opera 12 passes this condition, but the function does nothing if("setDragImage" in e.dataTransfer) { e.dataTransfer.setDragImage("ghost.png", -10, -10); } 

Теперь это может быть хорошо, если все, что вам нужно, это попытаться добавить пользовательское изображение перетаскивания, и с удовольствием оставим значение по умолчанию, если это не поддерживается (что и будет). Но что, если вашему приложению действительно нужно собственное изображение, если браузеры, которые его не поддерживают, должны иметь совершенно иную реализацию (т.е. использовать собственный JavaScript для реализации всех режимов перетаскивания)?

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

Таким образом, возникает вопрос — какой самый безопасный способ реализовать обнаружение браузера?

У меня есть две рекомендации:

  1. Используйте собственные объектные тесты в предпочтении информации navigator .
  2. Используйте его для исключения браузеров, а не их включения.

Например, Opera 12 или более ранняя версия может быть обнаружена с помощью объекта window.opera , поэтому мы можем проверить поддержку перетаскивания с этим исключением:

 if(!window.opera && ("draggable" in document.createElement("div"))) { //the browser supports drag and drop but is not Opera 12 } 

Лучше использовать проприетарные объекты, а не стандартные, потому что результат теста менее похож на изменение при выпуске нового браузера. Вот некоторые из моих любимых примеров:

 if(window.opera) { //Opera 12 or earlier, but not Opera 15 or later } if(document.uniqueID) { //any version of Internet Explorer } if(window.InstallTrigger) { //any version of Firefox } 

Объектные тесты также можно комбинировать с тестированием функций, чтобы установить поддержку конкретной функции в конкретном браузере или, в крайнем случае, определить более точные условия браузера:

 if(document.uniqueID && window.JSON) { //IE with JSON (which is IE8 or later) } if(document.uniqueID && !window.Intl) { //IE without the Internationalization API (which is IE10 or earlier) } 

Мы уже отмечали, что строка userAgent является ненадежным беспорядком, но строка vendor самом деле вполне предсказуема и может использоваться для надежного тестирования Chrome или Safari:

 if(navigator.vendor == 'Google Inc.') { //any version of Chrome } if(navigator.vendor == 'Apple Computer, Inc.') { //any version of Safari (including iOS builds) } 

Золотое правило ко всему этому — быть предельно осторожным . Убедитесь, что вы тестируете условия в максимально возможном количестве браузеров, и тщательно обдумайте их с точки зрения прямой совместимости — стремитесь использовать условия браузера для исключения браузеров из-за известной ошибки, а не включать их из-за известной функции (которая это то, что функция тестирования)

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

Выбор синтаксиса теста

Прежде чем мы начнем, я хотел бы изучить различные виды синтаксиса, которые мы можем использовать для объектных и функциональных тестов. Например, следующий синтаксис стал распространенным в последние годы:

 if("foo" in bar) { } 

Мы не могли использовать это в прошлом, потому что IE5 и его современники выдавали ошибку по синтаксису; но теперь это больше не проблема, поскольку нам не нужно поддерживать эти браузеры.

В сущности, это точно так же, как это, но короче, чтобы написать:

 if(typeof bar.foo != "undefined") { } 

Однако условия тестирования часто пишутся с опорой на автоматическое преобразование типов:

 if(foo.bar) { } 

Мы использовали этот синтаксис ранее в некоторых тестах объектов браузера (таких как тест для window.opera ), и это было безопасно из-за того, как объекты оценивают — любой определенный объект или функция всегда будет иметь значение true , тогда как если бы он был неопределенным, оценил бы к false .

Но мы можем тестировать что-то, что действительно возвращает null или пустую строку, обе из которых оцениваются как false . Например, свойство style.maxWidth иногда используется для исключения IE6 :

 if(typeof document.documentElement.style.maxWidth != "undefined") { } 

Свойство maxWidth значение true только в true случае, если оно поддерживается и имеет значение, определенное автором, поэтому, если мы напишем тест таким образом, он может завершиться ошибкой:

 if(document.documentElement.style.maxWidth) { } 

Общее правило таково: использование автоматического преобразования типов безопасно для объектов и функций , но не обязательно безопасно для строк и чисел или значений, которые могут быть нулевыми .

Сказав это — если вы можете безопасно использовать его, то сделайте это, потому что это обычно намного быстрее в современных браузерах (предположительно, потому что они оптимизированы именно для такого рода условий).

Подробнее об этом см .: Автоматическое преобразование типов в реальном мире .