24 November 2015

Прогулка с динозаврами: как я адаптировал веб-приложение под IE 7

CSSJavaScriptHTML
Sandbox
image

Недавно я решил отправить свой проект, над которым работал в свободное время последние несколько лет, на конкурс в одну компанию. Я сел и стал думать, нет ли каких-то нюансов, которые я не учёл, и которые могут испортить впечатление и уменьшить шансы на успех. И первое, что мне пришло в голову — проект не работает под IE ниже 9 версии. То есть совсем, там стояла блокировка. После логина появлялось окошко с красивым предупреждением, что браузер не поддерживается, и пользователя опять перебрасывало на форму входа. Довольно изящно — но неприятно. Что, если у человека стоит Windows XP, а сторонних браузеров нет? Незадача.

И вот я решил потратить столько времени, сколько будет нужно, но при этом добиться стабильной безошибочной работы системы как минимум в IE 8. Я готов был убрать часть функций, если будет необходимо — но всё должно было выглядеть аккуратно и работать без сбоев. Предвкушая долгие и тяжёлые мучения (уже был несколько раз подобный опыт), я взялся за дело. Если вам интересно узнать, с какими сложностями я столкнулся, и какие изменения пришлось внести в разные компоненты — добро пожаловать под кат.

Я стал вспоминать, почему я так сделал — и вспомнил, что даже не стал пытаться исправить огрехи вёрстки после того, как увидел, что данные не приходят через XHR. Рассудив, что мессенджер без автообновления данных, где надо всё время жать F5 — это полный цирк, я и ввёл ту блокировку.

Однако после этого уже было 2-3 случая, когда я сталкивался с похожими симптомами, и успешно их устранял. А про этот проект — как-то забыл, что здесь проблема была та же. Это первое исправление и позволило устранить самую главную проблему, что, в свою очередь, открыло дорогу к дальнейшим улучшениям.

Ниже я приведу основные несовместимости, которые возможно придётся учесть, если вы решите обеспечить поддержку старых версий Internet Explorer. Итак, поехали!

IE не знает, что такое кодировка CP1251 (все версии)


Данная проблема — парадокс — присутствует до сих пор. Ей подвержены все версии IE, вплоть до 11-ой. Возможно, Microsoft не считает это багом, но при разработке это приходится учитывать — иначе браузер будет игнорировать все пришедшие через AJAX данные. Решений здесь два: либо добавить кодировку через .htaccess (если у вас Apache), либо добавлять её через header() прямо в PHP коде. Второй вариант универсальнее (должен работать с любым веб-сервером), но более трудоёмкий, и явно засоряет код, поэтому я предпочитаю следующую строку в конфиге:

AddDefaultCharset windows-1251

После этого заголовок ответа Content-Type: text/html; charset=CP1251 изменится на Content-Type: text/html; charset=windows-1251, и всё заработает.

Отсутствие поддержки некоторых псевдоклассов (IE8 и ниже)


Например, не работает селектор nth-child(). Что создаёт большие проблемы, когда вёрстка и стили уже написаны. Адекватных решений, опять же, всего два — либо прописать нужным блокам (например, вторым в контейнере) особый класс во всех местах в HTML, либо написать одну JavaScript функцию, и в ней назначить нужные стили через style. Мне второй вариант показался более выигрышным, поскольку разметка не засоряется ненужными классами, меньше работы, проще удалить в случае чего — и главное, в этом случае можно этот код запускать только для IE нужных версий, обернув его в if(). Обратите внимание: в случае выбора первого варианта одними условными комментариями не обойтись, менять придётся саму разметку, добавляя лишний атрибут class там, где он в общем-то не нужен.

Отсутствие поддержки background-size (IE8 и ниже)


Да, свойство background-size является частью CSS3, поэтому его поддержки нет. Предсказуемо, но очень печально. Представим к примеру, что у нас есть маленькая иконка, которая почти нужного размера, но не совсем (например, она больше процентов на 20-50). При разработке мы могли сделать обычный div, назначить её фоном и задать background-size. В IE8 же мы получим её в полном, более крупном размере. Тут можно поменять вёрстку, сделав через img, и задав ему размеры. Но обычно дешевле сделать одну (или столько, сколько необходимо) копий картинки нужных размеров, и оставить разметку как есть.

Отсутствие display: inline-block (IE7 и ниже)


Здесь следует заменить на display: inline, он работает абсолютно так же (внешние отступы поддерживаются, и этого обычно достаточно). В особенно трудных случаях можно попробовать float: left (или float: right, если выравнивание нужно по правому краю, правда в этом случае порядок элементов изменится на противоположный, и вам придётся дописывать ещё и JS код).

Баг со списками (IE7 и ниже)


Я часто использую список не по прямому назначению: например, для создания ряда горизонтальных ссылок, или пунктов выпадающего меню. Но на этот раз пришлось поменять разметку, ибо решения для данной проблемы я так и не нашёл. IE7 (и IE8 в режиме эмуляции) добавляет слева от всех пунктов отступ. При этом padding у контейнера равен нулю, margin у элементов <li> — тоже, а фиксированное расстояние в 26 пикселей значится в инспекторе как offset (левее margin). При этом через CSS его убрать никак не получается (или я не нашёл как это сделать).

UPD: andy128k подсказал решение проблемы: необходимо задать для элементов списка list-style-position: outside, и отступ пропадёт.

Сложности с подмешиванием собственных функций в прототип (IE8 и ниже)


В браузерах, вроде Chrome, Opera и Firefox можно добавлять нужные свойства в прототип элементов через объект HTMLElement. В IE 8 при попытке это сделать получим исключение, причём даже если просто написать if с этим именем — поэтому обязательно нужно использовать блок try {}. Но аналогичную функцию там выполняет объект Element. А вот в IE7 нельзя сделать и этого. Можно конечно добавлять свойства в Object.prototype — но это не очень-то хорошая практика, на мой взгляд. Поэтому если обязательно нужна поддержка IE7, а подмешиваемых функций не очень много — проще отказаться от синтаксиса с точкой вовсе. И писать что-то вида addClass(obj, className) вместо obj.addClass(className).

Отсутствие nextElementSibling и previousElementSibling (IE7 и ниже)


Да-да, этих свойств тоже нету. Совсем. Есть конечно коллекция children, по которой можно путешествовать, но вот в ряде мест, где надо получить следующий элемент, приходится использовать дурацкие циклы while() в одну строку, перебирая nextSibling / previousSibling, пока не найдём элемент нужного класса, либо с nodeType равным единице. Тут следует заметить, что в IE соответствующих версий nextSibling и previousSibling как раз пропускают текстовые узлы. Только вот в других браузерах это не так, и кроссбраузерность сразу исчезнет (код перестанет работать везде кроме IE), если сделать простую замену.

Отсутствие объекта JSON (IE7 и ниже)


Тут всё просто — пишем свой парсер/кодировщик (благо, это не так уж сложно). Можно использовать и eval, особенно если источник данных — доверенный, например наш сервер. Но знающие люди не рекомендуют всё равно этого делать :)

Неожиданные грабли с HTTPS (IE7 и ниже)


В моём проекте используется получение данных со стороннего ресурса через API по протоколу HTTPS. Можете представить моё удивление, когда на реальном IE7 на виртуальной машине я не получил тех данных через JSONP, которые прекрасно получал в IE8 в режиме эмуляции IE7. Уж не знаю, в чём там было дело, но похоже, что-то с набором поддерживаемых шифров — сервер просто не даёт установить соединение. Проверить это было легко — просто открыл в новой вкладке требуемый адрес и увидел ошибку SSL. И такая же ошибка была при открытии любой страницы того сайта. Что интересно — протоколы шифрования в настройках у IE7 и IE8 одни и те же: SSL 2.0 (не отмечен), SSL 3.0 и TLS 1.0. И длина ключа в окне «О программе» и там и там равна 128 бит. Но почему-то IE7 сервер отшивает. Если кто знает причину такого явления — пишите в комментарии.

Другое устройство selection


Если нам надо каким-то образом работать с выделением — не важно, выделение это просто на веб-странице, в input или textarea — для IE нужен отдельный код. Не то, чтобы он был сильно сложнее в общем случае — однако в простых ситуациях (вроде «вставить текст в позицию курсора в текстовое поле») — подход и правда менее очевидный и требует более глубоких знаний.

В интернете распространено куча вариантов кода для кроссбраузерного обрамления фрагмента текста тегом, или вставки текста в произвольную позицию (что по сути очень похоже). Мой вариант, который я в своё время откуда-то скопировал и толком не протестил (поскольку разработку я веду в Опере), не работал. Правильный вариант был найден на днях на одном известном форуме, авторство его коллективное. В общем я к нему отношения не имею, но приведу здесь его в сильно сокращённом и упрощённом виде (чтобы понятна была идея, и не было лишнего):

    if (detectIE() < 9) {
       window.sel = null
       var f = function() { window.sel = document.selection.createRange().duplicate() }
       document.getElementById('inputField').onselect = f
       document.getElementById('inputField').onkeyup = f
       document.getElementById('inputField').onclick = f
    }

   function tagSelectedText(obj, tag) {

      if (document.selection) {
         if (!window.sel) return
         document.getElementById('inputField').focus()
         var sel = window.sel
         var len = sel.text.length

         sel.text = '<' + tag + '>' + sel.text + '</' + tag + '>';

         if (len == 0) sel.moveEnd('character', -(tag.length+3));
         sel.select();
      }
      // ...код для других браузеров
}

Идея в том, что есть невидимый объект выделения (и часто не в единственном числе, это иногда кстати бывает видно в Опере, если что-то выделять мышью, перед этим в JS программно не сбросив как следует все Range). Но он пропадает, как только поле теряет фокус. Причём на onblur присваивать функцию его дублирования бесполезно: уже поздно. Поэтому onselect. onkeyup и onclick нужны для того, чтобы сохранять пустое выделение (нулевой длины) по мере набора текста или перемещения курсора по полю стрелками либо кликом мышки.

В момент нажатия на кнопку мы берём сохранённый объект, обновляем свойство text реального объекта выделения, используя свойство сохранённого объекта. Если надо, двигаем курсор. Не забываем вызвать select() — это важно сделать, чтобы результат отобразился в браузере.

Тормоза порой выявляют ошибки в логике


В одном месте в коде JavaScript (в месте, где создаётся интерфейс аудиоплеера) делался запрос на внешний сервер через JSONP. Функция-обработчик получала нужный плеер (тот, с которым работали в функции createPlayer() в момент создания запроса) по его id, который вычислялся исходя из ответа на запрос, и прописывала ему атрибут с URL трека, вычисляемый из того же ответа. Везде, включая IE8, всё работало хорошо и так, как задумано. А в IE7 функция, вызываемая через JSONP, не могла получить объект-контейнер плеера по id.

Оказалось, что проблема была в том, что в момент вызова getElementByID() контейнер сообщения, в котором находился искомый контейнер плеера, ещё не был добавлен в нужное место методом appendChild(), который находился после вызова createMessage(), создающего сообщение. Но уже существовал, и id имел правильный. На самом деле элементы, созданные через createElement(), не становятся доступны мгновенно. Это нормально. И конечно же следует добавлять элемент в DOM, а потом уже к нему обращаться. Но обычно я сначала присваиваю все обработчики и наполняю элемент дочерними узлами, а только потом добавляю его. Поэтому здесь злую шутку сыграло то, что код функции-обработчика срабатывал в неизвестный момент времени, и в IE7 к этому моменту исполнение основного потока оказывалось ещё не завершено (либо DOM не актуализировался).

Особенности работы с DOM тоже выявляют ошибки в логике


Здесь всё было в какой-то степени ещё интереснее. Библиотека xPlayer имеет возможность подключения и удаления «слушателей» (при её разработке в своё время специально было это сделано на случай если понадобится, вот оно и понадобилось). Эта возможность была использована, чтобы подключить плееры-контроллеры (те плееры, что отображаются в сообщениях) к мастер-плееру наверху страницы. Мастер-плеер позволяет скроллить переписку и даже переключаться между комнатами, при этом всё время имея доступ к управлению воспроизведением. При возвращении в комнату, где был запущен трек (или загрузке блока сообщений, где он был запущен) происходит «подхват» нужного контроллера при его инициализации. Код инициализации контроллера сравнивает специальный uid, формируемый из URL и названия трека, с uid у мастера, и по их совпадению и делает вывод, что этот контроллер «тот самый». При этом все контроллеры без исключения при создании имеют набор функций, обновляющих свой UI при разных событиях. И эти функции регистрируются как «слушатели» в мастере.

В IE8 вскрылась неожиданная ошибка — после перехода в другую комнату функция-слушатель продолжает выполняться, и падает при попытке определить clientWidth элемента, который удалён из документа после очистки innerHTML зоны просмотра!

С одной стороны — ссылка на элемент была передана в качестве контекста при регистрации функции, и сборщик мусора не должен уничтожать объект, даже несмотря на то, что он уже не является частью DOM. Но это в других браузерах — тут похоже объект как раз уничтожался, и свойство clientWidth бралось уже от нулевого указателя. Другие браузеры не выдавали никаких ошибок в консоль (скорее всего там clientWidth равнялся 0, а объект существовал со всеми своими потомками вне DOM), и при возвращении в старую комнату всё работало отлично.

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

Очевидно, что сама функция обновления работала, а вот функция-слушатель — нет. Ну и проблема вероятнее всего была в том, что новая функция-слушатель при возвращении в прежнюю комнату добавлялась в список (с привязкой к новому объекту), но до неё очередь не успевала дойти, поскольку при запуске старой, которая перед ней, код падал с ошибкой. А ошибка банальна донельзя — при смене комнаты список функций-слушателей очищать надо :)

К слову, это проблему и решило.

Безопасность превыше всего (IE8 и ниже)


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

input.click()

Но в IE8 и более младших версиях при попытке отправить форму с файлом, выбранным таким образом (например, через вызов form.submit() повешенный на onchange инпута) мы получим ошибку:

image

Microsoft считает, что это небезопасная операция, поэтому даёт нам по рукам. Поэтому стилизовать файл-инпут привычным образом в Internet Explorer не получится. Кстати, даже если сделать его видимым через JS перед отправкой формы — ничего не изменится. Нужно, чтобы файл был выбран именно прямым кликом по инпуту. Поэтому единственный способ — сделать инпут прозрачным с помощью filter: alpha(opacity=0), и подложить под него стилизованную кнопку. Тогда пользователь будет кликать по реальному (прозрачному) инпуту.

А что насчёт IE6?


С IE6 всё плохо.

image

Я никогда толком не работал с этим браузером как разработчик — не довелось в силу возраста. Но читал где-то мельком, что там вообще другая блочная модель, и вообще нужна куча костылей. Пока в IE6 всё выглядит так же, как в IE7 при неуказанном DOCTYPE (т.н. quirks mode), или как в IE8 в режиме эмуляции «IE7: режим совместимости».

Но если у кого-то есть идеи, что надо дописать в стилях, чтобы ситуация хоть чуть-чуть исправилась — буду признателен.



Кроме того, не следует забывать, чтобы работа с событиями везде была организована кроссбраузерно (иначе код будет падать, и ничего хорошего не получится), чтобы скруглённые углы элементов там, где это жизненно необходимо, были выполнены картинками (или хотя бы имели картинки как альтернативный fallback). Кроме того, объекты, которые имеют эффект плавного появления и затухания через прозрачность, не будут иметь сглаживания шрифта (правда, этот баг можно обойти, присваивая фильтр до того, как display установим в block, и убирая его вообще после завершения эффекта).

Ну и даже после всего этого, серьёзное веб-приложение будет работать довольно медленно (подтормаживает даже обычная кастомная прокрутка, причём при не очень больших объёмах контента). Но по крайней мере при выполнении всех этих условий получившийся результат можно будет назвать graceful degradation. Наверное.
Tags:htmlcssjavascriptie7ie8
Hubs: CSS JavaScript HTML
+11
12.2k 57
Comments 95
Popular right now