Асинхронная загрузка произвольного html

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

    Гугл в этом плане не сильно помогает, т.к. основной описываемый метод — подмена document.write своим собственным методом, который тупо добавляет аргумент в конец документа, но если вызов идёт не в конце и писать надо куда-то в середину — возникает проблема. На хабре описывался фрейморк Fullajax, который вроде справляется с этим, но как именно — я пока не смотрел.

    У меня возникла другая идея, возможно — велосипед, но желание попробовать было слишком сильным. А именно, подгружать отложенный код в скрытый iframe, а после загрузки переносить его содержимое туда, где оно должно быть. Более того, чтобы не делать лишних запросов к серверу, используется data URI. Такой подход работает в FF и Opera но не работает в IE и Chrome.

    Проблема с IE в том, что тот вообще не поддерживает data URI до 8-й версии, а в 8-й позволяет таким образом кодировать только картинки и стили но не html. В Chrome проблема в безопасности, он не даёт читать содержимое фрейма с data URI, считая что он загружен с другого домена. Наверняка загрузка iframe с сервера а не встраивание будет работать во всех броузерах, но я пока не проверял.

    Само собой, куски DOM нельзя копировать между фреймами. Часть броузеров имеют метод document.importNode, но, как пишет Anthony Holdener, он не копирует обработчики событий, поэтому лучше всегда копировать вручную. Кроме этого, куски script с document.write при этом тоже копируются, что точно также очищает страницу, как и тупое копирование innerHTML, так что копировать вручную приходится в любом случае.

    Но если вырезать скрипты — а зачем они нужны, если они уже отработали и можно скопировать полученный в результате их работы DOM? — то могут потеряться методы и глобальные переменные, используемые в обработчиках событий. В качестве решения я копирую все свойства окна (window) из iframe, которых нет у главного окна. Возможно при этом будут проблемы с замыканиями (closure), не проверял. При попытке прочитать часть свойств возникают исключения, поэтому блок копирования нужно обернуть в try/catch.

    Остаются стили, ибо подгружаемый html-код вполне может содержать CSS, который надо как-то скопировать. Как это сделать правильно я, честно говоря, не нашёл, и воспользовался методом window.getComputedStyle (он не кросс-броузерный, поэтому в IE надо будет делать по-другому), который содержит уже конечные стили элементов после вычисления всех классов и явно указанных свойств. При копировании DOM-иерархии я смотрю их в скрытом фрейме, куда подгружен html-код, и явно прописываю создаваемым элементам. Но копировать всё что есть — тоже не выход, поэтому пришлось составить «белый список» свойств, и аналогично обернуть в try/catch.

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

    <iframe style='display:none' onLoad='l("...",this)' src='data:text/html;base64,...'></iframe>

    где первое многоточие — ID элемента, в который надо загрузить код, второе — сам код, закодированный в base64. Естественно, такой iframe можно создавать динамически в любой момент, например при событиях ready или load. Загрузчик выглядит так:

    1. var allowedStyles = {
    2.   color: true,
    3.   cursor: true,
    4.   backgroundColor: true,
    5.   backgroundImage: true,
    6.   borderTopWidth: true,
    7.   borderRightWidth: true,
    8.   borderBottomWidth: true,
    9.   borderLeftWidth: true,
    10.   display: true,
    11.   fontFamily: true,
    12.   fontSize: true,
    13.   fontSizeAdjust: true,
    14.   fontStretch: true,
    15.   fontStyle: true,
    16.   fontVariant: true,
    17.   fontWeight: true,
    18.   paddingTop: true,
    19.   paddingRight: true,
    20.   paddingBottom: true,
    21.   paddingLeft: true,
    22.   textAlign: true,
    23.   textDecoration: true,
    24. };
    25.  
    26. function im(node, rec, w2) {
    27.   switch (node.nodeType) {
    28.     case document.ELEMENT_NODE:
    29.       if (node.nodeName == 'SCRIPT') return false;
    30.       if (node.nodeName == 'IFRAME') return document.importNode(node,true);
    31.       var newNode = document.createElement(node.nodeName);
    32.       // does the node have any attributes to add?
    33.       if (node.attributes && node.attributes.length > 0)
    34.         for (var i = 0, il = node.attributes.length; i < il; i++) {
    35.           var attrName = node.attributes[i].nodeName;
    36.           newNode.setAttribute(attrName, node.getAttribute(attrName));
    37.         }
    38.       // are we going after children too, and does the node have any?
    39.       if (rec && node.childNodes && node.childNodes.length > 0)
    40.         for (var i = 0, il = node.childNodes.length; i < il; i++) {
    41.           var newChild = im(node.childNodes[i], rec, w2);
    42.           if (newChild) newNode.appendChild(newChild);
    43.         }
    44.       //
    45.       var styles = w2.getComputedStyle(node, null);
    46.       for (var s in styles) try {
    47.         if (allowedStyles[s]) newNode.style[s] = styles[s];
    48.       } catch (e) {}
    49.       return newNode;
    50.     case document.TEXT_NODE:
    51.     case document.CDATA_SECTION_NODE:
    52.       return document.createTextNode(node.nodeValue);
    53.   }
    54. };
    55.  
    56. function l(name, iframe) {
    57.   for (var i in iframe.contentWindow)
    58.     try {
    59.       if (window[i] === undefined)
    60.         window[i] = iframe.contentWindow[i];
    61.     } catch (e) {}
    62.  
    63.   var d = document.getElementById(name);
    64.   var children = iframe.contentDocument.body.childNodes;
    65.   for (var i=0, l=children.length; i<l; i++) {
    66.     var clone = im(children[i], true, iframe.contentWindow);
    67.     if (clone) d.appendChild(clone);
    68.   }
    69. }
    * This source code was highlighted with Source Code Highlighter.


    Подход вполне рабочий, в данный момент, как я уже писал, работает только в FF и Opera, и довольно сырой, но мне хотелось поскорее поделиться идеей и почитать комментарии умных людей, прежде чем доделывать дальше. Одна из проблем, которая не решена — что делать если в подгружаемом коде в свою очередь тоже содержится iframe. Для рекламы это не редкость. Сейчас копируется элемент iframe вместе с src, но при этом содержимое фрейма загружается заново, и получается что фрейм грузится дважды. Копировать его содержимое через DOM или брать innerHTML, кодировать base64 и прописывать в src=«data:...» не выход, т.к. iframe может грузиться с другого домена, и доступа к его содержимому из соображений безопасности броузер не даст. Поэтому код, содержащий iframe, лучше так не подгружать.
    Поделиться публикацией
    Похожие публикации
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама
    Комментарии 17
    • +2
      Интересный подход, но что-то подсказывает, что на клиенте в итоге он приведет к замедлению загрузки и тормозам. На каком количестве одновременно установленных баннеров вам приходилось его тестировать?
      • +2
        4-5 рекламных блоков, визуально грузится быстрее.
        можно на nnm-club.ru посмотреть, сорри если реклама
        • +6
          это не рекламные блоки у вас, а помои какието.
          уж простите конечно, но лично для меня что это — что порнотизеры — одно и тоже…

          в тоже время нормальную рекламу описанным выше образом какраз нельзя вставлять себе на сайт, причин две — либо дохнет таргетирование и контексная привязка, либо ( если реклама не на столько умна ) реферер.
          Не возможность отследить рекламодателю откуда был переход и почему — оч хренова.
      • –8
        >В связи с последними инициативами гугла учитывать время загрузки страницы становится всё более актуально асинхронно подгружать части веб-страниц уже после загрузки основного минимума.

        Другой топик:
        >Ряд крупных компаний, среди которых Google, KDDI, Globe Telecom и пр. планируют в ближайшее время приступить к строительству международной кабельной системы под названием «Юго-восточный азиатско-японский кабель» (Southeast Asia Japan Cable, SJC).
        >пропускная способность канала составит 17 Тбит/с; с возможностью увеличения до 23 Тбит/с.

        фейспалм.жпг
        • 0
          Почему бы просто не всосать нужно содержимое через XHR и не добавить его через innerHTML?
          • 0
            если в уже готовый документ добавляется document.write — он заменит весь документ а не допишет в него, т.к. старый уже закрыт — он откроет новый для записи
            • 0
              Так он не выполнится при вставке через innerHTML.
              Но я вообще не вижу смысла использовать document.write(). Или так принято вставлять рекламу?
              • +1
                Увы, многие баннерокрутилки работают именно так. Хотя иногда код удается переписать без document.write()
              • 0
                а почему не подменить document.write своим кодом, который не будет себя так вести?
            • 0
              Идея подгрузки вспомогательной информации после основного контента, достаточно старая. Кроме скорости есть еще соображения повышения доли уникального контента на странице.
              Но как это будет оценивать Гугл не очень понятно. Он однозначно парсит динамический контент. По ссылке которую вы привели утверждается что он учитывает суммарное время подгрузки и HTML и JS. В этом случае непонятно за счет чего можно достигнуть сокращения суммарной скорости загрузки.
              • 0
                вот тут говорится что время загрузки считается до события DocumentComplete, т.е. если что-то подгружать в onLoad это уже на время, учитываемое гуглом, влиять не будет.

                и для конечных пользователей, если рекламный блок висит вверху страницы с подгрузкой внешнего js, то пока он не загрузится — остальная страница видна не будет. при отложенной подгрузке содержимое страницы видно сразу.
                • 0
                  Вы сослались на «последние инициативы гугла». Насколько я понял, там высказывается мнение, что будет учитываться все — и время подгрузки основного документа и время подгрузки JS.
              • +1
                интересное решение, но кажется довольно сложным, наверно будет куча подводных камней, да и что делать с Хромом и ИЕ, проще переопределить document.write, будет кроссбраузерно
                • 0
                  а iframe-то зачем? Web Optimizer юзает перемещение блоков в конец в собственных div'ах. Уже полгода безотказно работает везде.

                  document.write переопределять — зло. Быстро ломает любую обратную совместимость с другими решениями.
                  • 0
                    А если потом переопределять его обратно?
                    • 0
                      можно, а смысл? если можно проще
                      • 0
                        Я помню решал таким образом отложенную вставку Яндекс.Директа — при падениях Яндекса, не загружались страницы с контекстом. Уже потом они сделали опцию, позволяющую вставлять код рекламы в указанный элемент.

                  Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.