среда, 29 февраля 2012 г.

Встраивание поддержки стандартов в DOM-дерево

В предыдущих статьях я приводил код, позволяющий добиться частичной поддержки стандартных методов в IE6-8, используя минимум ресурсов. Но встраивание его в DOM-модель производилось пользователем вручную. В данной статье я намерен показать, как этого можно было бы избежать, сделав стандартизацию IE прозрачной.

В одном из предыдущих постов я описал функцию, которая добавляет метод addEventListener к любому DOM-объекту при помощи нестандартного IE`шного метода attachEvent (в последующих постах я привёл для неё ряд улучшений - 1, 2). И всё бы хорошо, но сама по себе эта функция, очевидно, проблемы не решает, ведь теперь её нужно вызывать для всех объектов DOM-дерева, с которыми работает сценарий.

Можно было бы, конечно, вставлять эти вызовы в основной код, корректно отрабатывающий во всех браузерах, кроме IE6-8, но это его замусорит. Я уже не раз писал, что в современных проектах код часто итак достаточно сложен, так что отвлекаться при его написании ещё и на нестандартные браузеры часто слишком накладно как с позиции трудоёмкости написания, так и с позиции трудоёмкости его последующего изменения при отладке и расширении.

Так что я решил пойти несколько другим путём.

Я написал основной код на стандартном JavaScript`е строго в рамках стандартов, отладил всё в стандартных браузерах - IE9, а так же в последних версиях Chrome, Safari, Opera и FireFox, и лишь потом взялся за IE6-8, создав для них отдельный файл сценария в проекте. Добавление этого сценария можно произвести очень удобно - IE умеет понимать комментарии специального вида, которые диктуют ему различное поведение в отличие от его различных версий. Я написал так:

<!--[if lt IE 9]><script type="text/javascript" src="/_scripts/_lib/patch.js"></script><![endif]-->
Очень удобно, что все остальные браузеры считают этот код простым комментарием и не вставляют его, а IE 8`й и более ранних версий этот код вставляет и выполняет. Этот "комментарий" я вставил до объявления всех остальных сценариев, добившись, таким образом, что бы у меня была возможность в этом сценарии именно нужным образом подготовить браузер к выполнению стандартного кода, следующего дальше.

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

Легко понять, что попытка решить эту задачу в общем слишком глобальна, ведь модель DOM по сути представляет собой в большей степени коллекцию интерфейсов, а не конструкторов, как многие ошибочно полагают. В действительности у нас нет, например, конструктора Node и конструктора HTMLElement в браузерной среде - у нас есть только фабрика document, имеющая методы, которые возвращают объекты, соответствующие этим интерфейсам. Так что бесполезно было бы пытаться добавлять функционал к прототипам этих интерфейсов - они ни на что не влияют, а зачастую и вообще не существуют в браузерном окружении. Если мы попытаемся добавить стандартный, но отсутствующий в IE6-8 метод к Node.prototype или HTMLElement.prototype, надеясь, что данный метод появится у всех HTMLElement`ов, мы неминуемо будем разочарованы их отсутствием, а то и вообще получим ошибку при обращении к несуществующему объекту. Так что добавлением стандартного функционала надо заниматься на уровне фабрики - т.е. объекта document.

Однако сказать это проще, чем сделать. Ведь способов получить DOM-объекты в JavaScript масса и учесть все возможные их комбинации не представляется возможным! Вот, допустим, мы переопределим стандартный и наиболее часто встречающийся метод document.getElementById примерно так:

document.getElementById = function(){
    var src = document.getElementById;
    return function() {
        var /** @type HTMLElement */ result = src.apply(this, arguments);
        if (result !== null)
            setAddEventListener(result);
        return result;
    }
}();
Вроде бы, всё замечательно, но это, ведь, не единственный способ получения элемента. Вот вернули мы этим нашим методом в стандартный сценарий element, а в этом стандартном сценарии производится вызов свойства parentNode или nextSibling, и на него уже производится попытка повесить обработчик события стандартным образом - с этим как? Ведь, это же свойства, а не функции, и в IE6-8 у нас нет методов определения их в качестве properties - ни ECMAScript5`овских Object.defineProperties c Object.defineProperty, ни даже FireFox`овских нестандартных (и кстати уже deprecated) __defineGetter__ с __defineSetter__`ом. Так что мы не можем обработать их вызовы. Можем, конечно, написать так:
setAddEventListener(result)(result.parentNode)(result.nextSibling);
, но если в стандартном коде свойство parentNode будет вызвано несколько раз - и этот объект прописывать, а остальные?.. В общем, реальных комбинаций может быть очень много - это, конечно, конечное множество, но полноценный качественный алгоритм, который переберёт все возможные варианты, неминуемо серьёзно затормозит работу браузера, так что это решение не приемлемо.

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

document.getElementById = function(){
    var src = document.getElementById;
    return function() {
        var /** @type HTMLElement */ result = src.apply(this, arguments);
        if (result !== null)
            setAddEventListener(result)(result.parentNode);
        return result;
    }
}();
Кроме того, мне был нужен слушатель для элемента, возвращаемого конструктором Image, который по аналогии был реализован так же сходным образом, лишь с незначительными изменениями:
Image = function (){
    var src = Image;
    return function(){
        var /** @type HTMLImageElement */ result = new src;
        setAddEventListener(result);
        return result;
    }
}();

Комментариев нет: