![image](http://habrastorage.org/getpro/habr/post_images/a1c/71e/b4f/a1c71eb4f93d99d2cf1e6f5df50d86e6.jpg)
Основной задачей системы управления рекламными сетями, является вставка кода этих сетей в код сайтов конечных пользователей. Вообще такие системы могут использоваться для решения широкого круга задач — от A/B-тестирования эффективности объявлений разных форматов, до размещения нескольких видов рекламных материалов на нескольких площадках параллельно или добавления к ним дополнительных эффектов (преимущественно анимации). Все нужно для упрощения управления рекламой на сайтах и отладки процесса аналитики, что в конечном итоге выливается в увеличение дохода от интернет-рекламы при продаже трафика.
При этом, в общем случае описать, какой код относится именно к рекламной системе, а какой нет, практически невозможно, поэтому в сегодняшнем топике мы рассмотрим более общую задачу — внедрение в код сайта конечного пользователя произвольного кода HTML/JS.
Задача
Допустим, что у нас есть контейнер вида <div class=’container’></div>
и необходимо реализовать загрузку в него кода, который получен с сервера и содержит HTML-разметку и JS-скрипты (могут быть как асинхронными, так и синхронными).
Задача заключается в обеспечении работоспособности полученного решения для n подобных контейнеров, одновременно существующих на одной странице.
Поясним на примере:
Вставляемый скрипт: <script id=1> document.write("<" + "script id=2>" + "document.write(\"<\" + \"div>someresult1<\" + \"/div>\");" + "document.write(\"<\" + \"div>someresult2<\" + \"/div>\");" + "<" + "/script>"); document.write("<" + "script id=3 src='somescript1.js'>" + "<" + "/script>"); document.write("<" + "script id=4 src='somescript1.js'>" + "<" + "/script>"); document.write("<" + "script id=5>" + "document.write(\"<\" + \"div>someresult3<\" + \"/div>\");" + "document.write(\"<\" + \"div>someresult4<\" + \"/div>\");" + "<" + "/script>"); </script> 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
моделирует поведение системы в случае, если в теле загружаемого скрипта также есть какой-либо код.
Система, которую следует разработать, должна загружать в контейнер следующий код:
<script id="1"> document.write("<" + "script id=2>" + "document.write(\"<\" + \"div>someresult1<\" + \"/div>\");" + "document.write(\"<\" + \"div>someresult2<\" + \"/div>\");" + "<" + "/script>"); document.write("<" + "script id=3 src='somescript1.js'>" + "<" + "/script>"); document.write("<" + "script id=4 src='somescript1.js'>" + "<" + "/script>"); document.write("<" + "script id=5>" + "document.write(\"<\" + \"div>someresult3<\" + \"/div>\");" + "document.write(\"<\" + \"div>someresult4<\" + \"/div>\");" + "<" + "/script>"); </script> <script id="2"> document.write("<" + "div>someresult1<" + "/div>"); document.write("<" + "div>someresult2<" + "/div>"); </script> <div>someresult1</div> <div>someresult2</div> <script id="3" src="somescript1.js"></script> <div>someresult1.1</div> <script class="loaded" src="somescript2.js"> document.write("<" + "div>someresult1.2<" + "/div>") </script> <div>someresult2.1</div> <script id="4" src="somescript1.js"></script> <div>someresult1.1</div> <script class="loaded" src="somescript2.js"> document.write("<" + "div>someresult1.2<" + "/div>") </script> <div>someresult2.1</div> <script id="5"> document.write("<" + "div>someresult3<" + "/div>"); document.write("<" + "div>someresult4<" + "/div>"); </script> <div>someresult3</div> <div>someresult4</div>
Решение
Для упрощения дальнейших объяснений введем дополнительную терминологию:
- Анонимный скрипт — JS-скрипт, с пустым атрибутом “src”, или без него.
- Загружаемый скрипт — JS-скрипт, загружающийся со стороннего сервера и, потому, имеющий непустой атрибут “src”.
- Указатель вывода — указывает на элемент, после которого должна осуществляться вставка содержимого, генерируемого при помощи
document.write
.
Вставка кода HTML не вызывает никаких проблем, однако при вставке JS-скриптов обнаруживается ряд подводных камней:
- Синхронные скрипты могут содержать document.write, который не работает в асинхронном режиме.
- Скрипты могут быть загружаемыми, что лишает нас возможности анализа их текста.
- Загружаемые скрипты обращаются только к глобальным объектам, соответственно любые настройки окружающего пространства могут быть только глобальными.
- Скрипты могут порождать другие скрипты. При этом синхронные скрипты могут порождать другие синхронные скрипты, которые сохраняют возможность использования
document.write
.
Довольно очевидным выходом из положения является подмена глобального document.write
на собственную функцию, которая могла бы работать похожим образом.
document.write = function(html){ … };
Здесь, в целом, понятно все, кроме одного момента: куда именно наша функция должна вставлять код, являющийся результатом своей работы?
JavaScript однопоточен, а значит, для анонимных синхронных скриптов просто напрашивается следующее решение: при получении ответа с сервера устанавливать для контейнера глобальный признак (указатель ввода), который будет использоваться подменяющей функцией. Если все скрипты синхронны и анонимны, то они должны подставляться по очереди, что при правильном смещении указателя вывода приводит к получению корректного результата.
В итоге весь цикл обработки подставленного кода реализуется без разрывов в потоке исполнения и не вызывает вопросов.
Хьюстон, у нас проблемы
Все совсем не так легко и просто в том случае, когда нам встречается загружаемый скрипт. В подобной ситуации кажется логичным остановить исполнение всех прочих сценариев до момента загрузки и окончательно выполнения текущего загружаемго скрипта. Эта схема гарантированно работает благодаря событию onload, но скорость работы такого решения довольно мала, так что нужно найти способ получше.
И такое решение есть, правда работает оно лишь для браузеров семейства Interner Explorer — это событие onreadystatechange
, позволяющее создать для загружающего скрипта оболочку в виде обработчика, который переместит указатель вывода у нашего подменённого document.write к месту расположения скрипта перед тем, как тот запустится, и — при необходимости — восстановит исходный указатель вывода после завершения работы скрипта. К сожалению, пойти таким путем не удастся, если мы имеем дело с любым браузером, отличным от IE, поскольку нигде, кроме детища Microsoft нет поддержки событий, происходящих после загрузки скрипта, но до его выполнения.
Остается только один путь — сделать так, чтобы наша функция, подменяющая document.write
, могла сама определять, из какого скрипта её вызывают. И в большинстве современных браузеров (IE11, Firefox, Chrome, последних версий Opera) для загружаемых скриптов это возможно, хотя и с некоторыми оговорками. Из-за того, что такие сценарии выполняются в глобальном пространстве имен, невозможно создать копию функции для каждого загружаемого скрипта. Казалось бы, это значит, что определить место вставки результата работы document.write
можно только на основании входных параметров — строки.
Это так только на первый взгляд. На самом же деле во всех упомянутых выше браузерах есть возможность добраться до адреса, с которого загружался скрипт, вызвавший наш подмененный document.write
. Это делается через стек, из которого можно получить искомый адрес, а уже по этому адресу установить искомый скрипт.
Очередные сложности
Вроде бы все отлично — мы нашли отличное решение задачи, но снова возникают трудности. Прежде всего, в том случае, если у нас есть несколько одинаковых скриптов, то необходимо как-то обеспечить их поочередное выполнение в заранее известном порядке. Второй момент — если скрипт содержит несколько вызовов 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.
В дальнейшем либо первый, либо второй контейнер продолжает выполнение до следующей точки разрыва (в зависимости от того, какой из них закончит выполнение раньше). Дальнейшая последовательность уже рассмотрена и не несет ничего нового.
На сегодня все! Всем спасибо за внимание, будем рады ответить на вопросы в комментариях.
ссылка на оригинал статьи http://habrahabr.ru/company/advertone_ru/blog/219713/
Добавить комментарий