How-To: Подставляем асинхронный HTML/JS код посредством JS

imageОсновной задачей системы управления рекламными сетями, является вставка кода этих сетей в код сайтов конечных пользователей. Вообще такие системы могут использоваться для решения широкого круга задач — от A/B-тестирования эффективности объявлений разных форматов, до размещения нескольких видов рекламных материалов на нескольких площадках параллельно или добавления к ним дополнительных эффектов (преимущественно анимации). Все нужно для упрощения управления рекламой на сайтах и отладки процесса аналитики, что в конечном итоге выливается в увеличение дохода от интернет-рекламы при продаже трафика.

При этом, в общем случае описать, какой код относится именно к рекламной системе, а какой нет, практически невозможно, поэтому в сегодняшнем топике мы рассмотрим более общую задачу — внедрение в код сайта конечного пользователя произвольного кода HTML/JS.

Задача Допустим, что у нас есть контейнер вида

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

Поясним на примере:

Вставляемый скрипт:

somescript1.js: document.write (»<" + "div>someresult1.1<" + "/div>»); document.write (»<" + "script class='loaded' src='somescript2.js'>» + «document.write (\»<\" + \"div>someresult1.2<\" + \"/div>\»)» + »<" + "/script>»);

somescript2.js: document.write (»<" + "div>someresult2.1<" + "/div>»); В данном случае скрипты somescript1.js и somescript2.js являются примерами сценариев первого и второго уровня вложенности соответственно. Кроме того somescript1.js моделирует поведение системы в случае, если в теле загружаемого скрипта также есть какой-либо код.Система, которую следует разработать, должна загружать в контейнер следующий код:

someresult1
someresult2
someresult1.1
someresult2.1
someresult1.1
someresult2.1
someresult3
someresult4
Решение Для упрощения дальнейших объяснений введем дополнительную терминологию: Анонимный скрипт — JS-скрипт, с пустым атрибутом «src», или без него. Загружаемый скрипт — JS-скрипт, загружающийся со стороннего сервера и, потому, имеющий непустой атрибут «src». Указатель вывода — указывает на элемент, после которого должна осуществляться вставка содержимого, генерируемого при помощи document.write. Вставка кода HTML не вызывает никаких проблем, однако при вставке JS-скриптов обнаруживается ряд подводных камней: Синхронные скрипты могут содержать document.write, который не работает в асинхронном режиме. Скрипты могут быть загружаемыми, что лишает нас возможности анализа их текста. Загружаемые скрипты обращаются только к глобальным объектам, соответственно любые настройки окружающего пространства могут быть только глобальными. Скрипты могут порождать другие скрипты. При этом синхронные скрипты могут порождать другие синхронные скрипты, которые сохраняют возможность использования document.write. Довольно очевидным выходом из положения является подмена глобального document.write на собственную функцию, которая могла бы работать похожим образом. document.write = function (html){ … }; Здесь, в целом, понятно все, кроме одного момента: куда именно наша функция должна вставлять код, являющийся результатом своей работы? JavaScript однопоточен, а значит, для анонимных синхронных скриптов просто напрашивается следующее решение: при получении ответа с сервера устанавливать для контейнера глобальный признак (указатель ввода), который будет использоваться подменяющей функцией. Если все скрипты синхронны и анонимны, то они должны подставляться по очереди, что при правильном смещении указателя вывода приводит к получению корректного результата.

В итоге весь цикл обработки подставленного кода реализуется без разрывов в потоке исполнения и не вызывает вопросов.

image

Хьюстон, у нас проблемы Все совсем не так легко и просто в том случае, когда нам встречается загружаемый скрипт. В подобной ситуации кажется логичным остановить исполнение всех прочих сценариев до момента загрузки и окончательно выполнения текущего загружаемго скрипта. Эта схема гарантированно работает благодаря событию onload, но скорость работы такого решения довольно мала, так что нужно найти способ получше.И такое решение есть, правда работает оно лишь для браузеров семейства Interner Explorer — это событие onreadystatechange, позволяющее создать для загружающего скрипта оболочку в виде обработчика, который переместит указатель вывода у нашего подменённого document.write к месту расположения скрипта перед тем, как тот запустится, и — при необходимости — восстановит исходный указатель вывода после завершения работы скрипта. К сожалению, пойти таким путем не удастся, если мы имеем дело с любым браузером, отличным от IE, поскольку нигде, кроме детища Microsoft нет поддержки событий, происходящих после загрузки скрипта, но до его выполнения.

Остается только один путь — сделать так, чтобы наша функция, подменяющая document.write, могла сама определять, из какого скрипта её вызывают. И в большинстве современных браузеров (IE11, Firefox, Chrome, последних версий Opera) для загружаемых скриптов это возможно, хотя и с некоторыми оговорками. Из-за того, что такие сценарии выполняются в глобальном пространстве имен, невозможно создать копию функции для каждого загружаемого скрипта. Казалось бы, это значит, что определить место вставки результата работы document.write можно только на основании входных параметров — строки.

Это так только на первый взгляд. На самом же деле во всех упомянутых выше браузерах есть возможность добраться до адреса, с которого загружался скрипт, вызвавший наш подмененный document.write. Это делается через стек, из которого можно получить искомый адрес, а уже по этому адресу установить искомый скрипт.

image

Очередные сложности Вроде бы все отлично — мы нашли отличное решение задачи, но снова возникают трудности. Прежде всего, в том случае, если у нас есть несколько одинаковых скриптов, то необходимо как-то обеспечить их поочередное выполнение в заранее известном порядке. Второй момент — если скрипт содержит несколько вызовов document.write, то нужно еще как-то гарантировать правильность результатов выполнения каждого из них, ведь в станартном случае, каждая функция будет записывать данные сразу после «своего» скрипта, а не после последнего элемента, созданного предыдущим document.write из скрипта.Получается, что помимо прочего, после срабатывания функции необходимо добавить еще и ссылку на последний элемент, созданный из этого скрипта.

Завершающий этап Остается еще один возможный вариант развития событий — наличие в одном куске кода и анонимных и загружаемых скриптов. Поскольку в обычных условиях без всяких подмен document.write может использоваться только в синхронном потоке, то для того, чтобы исполняемый код давал такой же результат, что и при обычном последовательном выполнении, нам необходимо обеспечить поочередную загрузку и срабатывания всех скриптов.Для анонимных сценариев это, очевидным образом, получается само собой, а для загружаемых скриптов придется прерывать поток выполнения нашей подмены document.write на моменте ожидания загрузки и восстанавливать его посредством события onload.

Рассмотрим пример из начала топика для получения понимания последовательности действий.

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

Вставленный скрипт вызывает document.write для создания скрипта 2, который создаст два первых тестовых div. Скрипт 2 вызывает document.write для создания someresult1 и someresult2. Выполнение скрипта 2 заканчивается, управление возвращается исходному document.write. При этом, благодаря тому, что подмена глобальная, указатель вывода смотрит на созданный someresult2. Таким образом скрипт 1 продолжает создавать элементы. Теперь создается скрипт 3 и, поскольку он загружаемый, выполнение document.write прерывается до срабатывания onload скрипта 3. Предварительно document.write проверяет все остальные скрипты на предмет наличия у них того же пути загрузки и помечает их. Загружается скрипт 3, он вызывает document.write, из которого одним из описанных нами способов (в зависимости от браузера) происходит обнаружение указателя вывода document.write. В IE указатель вывода подставляется в момент загрузки кода перед его выполнением; в современных браузерах — с помощью стека непосредственно в момент вызова document.write; для остальных знание о точке вывода обеспечивается предсказуемостью порядка выполнения скриптов (блокировкой). Document.write вставляет someresult1.1 и помечает скрипт 3 на предмет указателя вывода. Скрипт 3 вызывает document.write, который определяет вызвавший его скрипт и, следуя пометке, сделанной предыдущим вызовом, смещает указатель вывода, после чего создает скрипт loaded и someresult1.2. Выполнение прерывается до загрузки и срабатывания скрипта loaded. Грузится скрипт loaded и вызывает document.write, которая определяет указатель вывода и создает someresult2.1. Срабатывает onload скрипта loaded, возвращая управление коду обработки document.write скрипта 3, который, в свою очередь, завершается и провоцирует событие onload скрипта 3, возвращающее управление в скрипт 1. Скрипт 1 создает скрипт 4, благодаря глобальности document.write в момент возврата управления указатель вывода поправляется с учетом операций, выполненных функцией document.write. Таким образом, скрипт 4 появляется в конце уже созданного куска кода. Выполнение document.write прерывается с предварительной пометкой о том, что выполняется еще не созданный скрипт 3. Для скрипта 4 повторяется вся процедура, уже описанная для скрипта 3 (пп. 4–8). Управление возвращается скрипту 1, который создает скрипт 5. Скрипт 5 вызывает document.write для создания someresult3 и someresult4. Управление возвращается скрипту 1. Скрипт 1 завершается. При беглом взгляде со стороны кажется, что в описанной последовательности нет ничего сложного, однако следует помнить, что поток выполнения прерывался 6 раз: На загрузку скриптов 3 и 4 (очевидно, моментальную, но формально это тоже разрыв, и в него может что-нибудь вклиниться). Внутри скриптов 3 и 4 (хотя в примере разрыва между ними нет, но он вполне может быть, ведь это загружаемый скрипт, строение которого в общем случае неизвестно). Две загрузки скрипта loaded, причем вторая, хоть и формальная, но разрыв в исполнении оставляет. И главная хитрость заключается именно в том, чтобы в каждый момент при вызове document.write использовался правильный указатель вывода.Заключение Теперь рассмотрим финальную надстройку, предназначенную для одновременной вставки n кодов. В принципе, рассмотренный алгоритм не имеет явных противопоказаний к многопоточности — следует лишь оговориться, что структуры, хранящие цепи скриптов и текущие указатели вывода для различных контейнеров, должны быть своими. А значит, мы подменяем document.write уже не просто функцией, а диспетчером, который подготовит контекст и только после этого вызовет наш аналог document.write.Соответственно, на выбор можно предложить две схемы реализации: либо наш аналог document.write должен быть объектом, и мы используем диспетчер, управляющий n экземплярами таких объектов, либо мы храним массив из n контекстов, и наш диспетчер просто устанавливает указатель на текущий контекст для данного аналога document.write.

Таким образом, если предположить, что имеется два контейнера, в которые мы пытаемся установить код примера, то порядок выполнения будет почти тем же — за исключением того, что в точках разрыва потока выполнения будет вклиниваться второй контейнер, вызывая смену контекста или рабочего объекта. Например, после шага 3 для первого контейнера будут следовать шаги 1 и 2 второго контейнера. На шаге 3 алгоритм должен обнаружить, что скрипт с точно таким же src уже загружается, и прерваться, ожидая его исполнения. Первый контейнер выполняется до шага 5 включительно, после чего отдает управление ждущему второму контейнеру, который продолжает выполняться с шага 3.

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

На сегодня все! Всем спасибо за внимание, будем рады ответить на вопросы в комментариях.

© Habrahabr.ru