Pull to refresh

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

Reading time 5 min
Views 8.9K
В связи с последними инициативами гугла учитывать время загрузки страницы становится всё более актуально асинхронно подгружать части веб-страниц уже после загрузки основного минимума. Реклама — один из претендентов на отложенную загрузку, но простой 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, лучше так не подгружать.
Tags:
Hubs:
+12
Comments 17
Comments Comments 17

Articles