20 марта 2012 в 13:33

Автоматическая кросс-доменная установка высоты Iframe tutorial

Думаю, многие, кто сталкивался в своей работе с iframe, сталкивались и с задачей установки высоты этого самого айфрейма.

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

В яндексе можно найти множество решений этой проблемы, но большинство из них обладают одной проблемой: они не поддерживают возможность менять размеры окна когда содержимое iframe и родительский элемент находятся на разных доменах.

Есть одно неплохое кросс-доменное решение, но оно было написано в 2007 году, а с тех пор многое изменилось. Поэтому пришлось разрабатывать решение этой проблемы самостоятельно, основываясь на приведенном решении.



Собственно, основное, что поменялось — это появился инструмент window.postMessage, который позволяет окнам браузера обмениваться сообщениями. Благодаря этому мы сможем сделать аналог приведенного решения, обладающий следующими преимуществами:
  • В новых браузерах не будет спама в hash (и заодно будет корректно работать хеш-навигация на клиентской странице)
  • корректная работа в webkit
  • более простой и понятный код


Наш код не должен зависеть от каких-то внешних факторов, и для его использования должно быть достаточно на стороне содержимого фрейма подключить 2 js файла (один для postMessage, а ворой — наш), а на стороне клиента — подключить также 2 js файла и добавить айфрейму два аттрибута — id и name (должны быть идентичны), а также на onload повесить регистрацию iframe.

Для работы с postmessage мы будем использовать следующую библиотеку:
postmessage.freebaseapps.com
Эта библиотека является очень удобной оберткой для window.postMessage, которая, кроме прочего, также позволяет пользоваться передачей данных через hash в браузерах, которые не поддерживают window.postMessage.

Логика работы скрипта предельно проста — на клиентской странице мы «регистрируем» iframe — отправляем дочерней странице (которая в iframe) идентификатор этого iframe и подписываемся на сообщения от дочерней страницы. При получении сообщения — изменяем размер.

На стороне дочерней страницы (которая в iframe) мы в свою очередь подписываемся на сообщения о регистрации, когда оно приходит — запоминаем идентификатор и включаем таймер, который при изменении размера содержимого фрейма отправляет родительской странице информацию о том, что нужно поменять размер iframe.

Собственно вот и все. Сам код скриптов:

FrameManager.js («клиентский» скрипт)
var FrameManager =
{
    registerFrame : function(frame)
    {
        pm({
          target: window.frames[frame.id],
          type:   "register", 
          data:   {id:frame.id},
          url: frame.contentWindow.location
        });
        
        pm.bind(frame.id, function(data) {
            var iframe = document.getElementById(data.id);
            if (iframe == null) return;
            iframe.style.height = (data.height+12).toString() + "px";            
        });        
    }
};


Frame.js (скрипт для содержимого фрейма)
var FrameHeightManager =
{
    FrameId: '',
    getCurrentHeight : function()
    {
          myHeight = 0;
          
          if( typeof( window.innerWidth ) == 'number' ) {
            myHeight = window.innerHeight;
          } else if( document.documentElement && document.documentElement.clientHeight ) {
            myHeight = document.documentElement.clientHeight;
          } else if( document.body && document.body.clientHeight ) {
            myHeight = document.body.clientHeight;
          }
          
          return myHeight;      
    },    
    publishHeight : function()
    {
        if (this.FrameId == '') return;
        // если нет jQuery - воспользуемся решениями для  определения размеров из яндекса
        if(typeof jQuery === "undefined") {
            var actualHeight = (document.body.scrollHeight > document.body.offsetHeight)?document.body.scrollHeight:document.body.offsetHeight;
            var currentHeight = this.getCurrentHeight();            
        } else {
            var actualHeight = $("body").height();
            var currentHeight = $(window).height();            
        }

        if(Math.abs(actualHeight - currentHeight) > 20)
        {
            pm({
              target: window.parent,
              type: this.FrameId, 
              data: {height:actualHeight, id:this.FrameId}
            });
        }       
    }   

};

pm.bind("register", function(data) {
    FrameHeightManager.FrameId = data.id;
    // не забываем передать правильный this
    window.setInterval(function() {FrameHeightManager.publishHeight.call(FrameHeightManager)}, 300);
});


И напишем 2 тестовых html-страницы:

test.html (родительская)
<!DOCTYPE html>
<html>
<body>
    <script src="postmessage.js"></script>
    <script src="FrameManager.js"></script>                
    <iframe height="10" id="frame1" name="frame1" src="test2.html" onload="FrameManager.registerFrame(this)" scrolling="no" frameborder="0" marginheight="0" marginwidth="0" ></iframe>   
</body>
</html>


test2.html (дочерняя)
<!DOCTYPE html>
<html>
<body>
    <!--<script src="http://yandex.st/jquery/1.7.1/jquery.min.js"></script>-->
    <script src="postmessage.js"></script>
    <script src="Frame.js"></script>
<div style="border:1px solid red;margin:0; height:200px;">
  test
  </div>
</body>
</html>


В дочерней странице также можно расскомментировать участок кода с подключением jquery — тогда определение высоты будет более точным. Буду благодарен за нормальные кросс-браузерные функции для определения размеров страницы и окна, я использовал первые, которые нашел — они работают, но пикселей на 10 в вебките допускают погрешность.

Для меня это некритично, потому что у меня на стороне клиента (т.е. в дочерней странице) есть jquery, но у кого-то, возможно, его не будет — тогда код все равно сохранит работоспособность.
Искандер Гиниятуллин @rednaxi
карма
127,0
рейтинг 0,0
WEB-разработчик
Самое читаемое Разработка

Комментарии (44)

  • –1
    Всё круто, но ифреймы… как-то я сомневаюсь, что они прям уж так необходимы для организации виджетов. Js-api и дивы милее сердцу.
    • +1
      Собственно, изначально разрабатывал для того, чтобы довести до ума виджеты в Piwik — они там были сначала флешовые — было проще, потом они сделали рендеринг всех графиков без флеша и соответственно виджеты стали в iframe.

      Нужно было с минимальными затратами сделать, чтобы вставленные на страницы сайта виджеты из статистики (у меня пивик используется в качестве инструмента статистики для поддоменов пользователей) выглядели нормально и автоматически ресайзились под размер.

      Тратить свое время на переписывание движка виджетов в пивике в данный момент я не готов, а написать 2 небольших js-файла, которые надо просто подключить и все становится как хочется — почему нет.

      Может еще кому пригодится (в piwik я уже отправил патч)
    • +1
      Если у вас виджет запускается через яваскрипт — то если тот сайт, с которого он запускается взломали — считайте и ваш также взломали (ну либо виджет со стрёмного ресурса взяли). Можно будет сделать любые изменения на вашей странице и, кроме того, вытянуть куки пользователя.

      Если сделать window.parent у ифрэйма, который с другого домена — бравзер ругнётся.
    • +3
      ифреймы… как-то я сомневаюсь, что они прям уж так необходимы для организации виджетов. Js-api и дивы милее сердц

      При необходимости размещать защищённые данные в виджетах на чужих доменах иного решения просто нет. Если вы разместите виджет в диве и примете с сайта виджета защищённые данные с каким угодно шифрованием, тело виджета должно содержать расшифровку, следовательно, данные будут уже не защищены от взлома и использования третьими лицами.

      При размещении в фрейме вы спокойно работаете с данными и выдаёте наружу только результат, через мембрану фрейма, по postMessage. Защита, разумеется не от пользователя, а от вирусных скриптов, к которым пользователь отношения не имеет. Если его страница заражена, через фрейм скрипты не проберутся, данные виджета будут в безопасности. С дивами этого добиться невозможно.
    • +1
      Iframe будет жить всегда. Также, как и два бессмертных элемента верстки &nbsp; и <br />.
      • 0
        В xhtml нет &nbsp; там &#160;
        • НЛО прилетело и опубликовало эту надпись здесь
          • 0
            я таких не встречал, Opera как натыкается начинает всю страницу парсить как html, a FF вовсе отказываются отображать страницу.
            • НЛО прилетело и опубликовало эту надпись здесь
              • +1
                Ну например вот мои &nbsp;
                home.poofeg.ru/test/test.xhtml
                • НЛО прилетело и опубликовало эту надпись здесь
                  • 0
                    А лучше отказаться от &nbsp; от греха подальше :)
                    • НЛО прилетело и опубликовало эту надпись здесь
        • 0
          Лично я вместо пробела пользую распорку (прозрачный gif 1х1px или растягиваю его до нужных мне размеров).
  • +1
    очень пригодилось. спасибо!
  • +1
    Спасибо огромное! Как раз была проблема смены высоты фрейма в зависимости от содержимого.
    • 0
      Переписал код на основной странице на jQuery и добавил animate(). Пока что не видел проблем с погрешностью все ресайзится точно.
      • 0
        если пользоваться jQuery на дочерней странице то погрешности и не должно быть
      • 0
        Хм, почему-то вот это вот:

        pm({
        	target: window.frames[frame.id],
        	type:   "register", 
        	data:   {id:frame.id},
        	url: frame.contentWindow.location
        });


        не заработало в ОгнеЛисе. Такое ощущение, что Firefox не умеет window.frames['frame_id']. Переписал так:

        var target = undefined;
        for (var i=0; i<window.frames.length;i++){
        	if (window.frames[i].frameElement == frame){
        		target = window.frames[i];
        		break;		
        	}
        }
        pm({
        	target: target,
        	type:   "register", 
        	data:   {id:frame.id},
        	url: frame.contentWindow.location
        });
        • +1
          У меня работает — попробуйте фрейму кроме id добавить еще аттрибут name, идентичный этому id
          • 0
            Да, спасибо, совсем забыл про имя. Похоже Firefox ведет себя правильно, а Chrome слегка упрощает жизнь разработчику, выбирая айфреймы также и по id.
  • +1
    В яндексе можно найти множество решений этой проблемы, но большинство из них обладают одной проблемой: они не поддерживают возможность менять размеры окна когда содержимое iframe и родительский элемент находятся на разных доменах.

    Видимо искать лучше в гугле, потому что когда мне надо было какое-то кроссдоменное решение, я нашёл это:
    benalman.com/code/projects/jquery-postmessage/examples/iframe/
    По сути то же самое, что и у вас (postMessage с фоллбеком на location.hash), только сделано 3 года назад :)
    • 0
      По сути тоже самое, но требует jQuery в дочернем и родительском документе.

      Мое решение обходится без него (хотя конечно присутствие jQuery в дочернем документе упрощает дело)
    • 0
      Ну и еще мое решение лучше тем, что позволяет без обработки напильником расположить на странице любое количество фреймов — достаточно кажому iframe прописать id, name и зарегистрировать (FrameManager.registerFrame(this)) его.

      Но все равно спасибо за ссылку.
  • +1
    Я делал реализацию на postMessage + хак с window.opener в старых ослах (как реализовано в Open Social). Соответственно, никаких таймаутов, хешей, 1 скрипт на все страницы.
    • +3
      К сожалению, не знаком с Open Social. Поделитесь информацией с хаком window.opener?

      Таймаут нужен для отслеживания изменения высоты документа внутри фрейма, насколько мне известно, по-другому это никак не отследишь — onresize срабатывает при изменении размеров окна, а не документа.
      • +1
        Хак заключается в том, что осел позволяет менять свойство opener объекта window у созданного фрейма:
        iframe.contentWindow.opener = {
          postMessage: function (data, domain){};
        };
        


        Соответственно, из фрейма делаем уже кроссбраузерный вызов:
        window.opener.postMessage('data', '*');
        


        Причем, как советует гугл, объект для opener лучше создавать через vbscript из-за соображений безопасности (каких конкретно не помню):
        function onMessage(evt, domain) {}; // Обработчик postMessage, в осле evt == evt.data
        
        window._onMessage = onMessage;
        window.execScript(
          'Private onMessage\n' +
        
          'Class PostMsg\n' +
            'Public Sub postMessage(data, domain)\n' +
              'Call onMessage(data, domain)\n' +
            'End Sub\n' +
          'End Class\n' +
        
          'Set onMessage = window._onMessage\n' +
          'Set window._onMessage = New PostMsg', 'vbscript');
        


        Ну а window._onMessage присваивается как opener в созданные фреймы.
  • +2
    Аналогичное решение на юзерскрипте делалось в HabrAjax для кнопки Google Plus для передачи количества «лайков» наружу. Кто заинтересуется, может скрипт там посмотреть, он без сторонних библиотек типа postMessage.js, всё на виду. А вообще, планирую на этой неделе написать статью (уже написана) по этой же теме с проекцией на юзерскрипты, потому что из-за них в Хроме есть баг передачи данных через фрейм, который там обойдён.
  • –2
    #site1
    document.domain = 'com'

    #site2
    document.domain = 'com'

    • 0
      Таким скриптом вы можете очень хорошо положить #site1, особенно если он вам не принадлежит. Вот сделали такую глупость, а потом вебмастер #site1 не может понять, почему в IE он создаёт frame, пытается к нему достучаться и получает Security Exception.
      • 0
        по какой причине не может достучаться? Все может.
        и можно делать domain='com' только при выполнении условия каких либо
        • 0
          Вот прекрасное обсуждение: stackoverflow.com/questions/1886547/access-is-denied-javascript-error-when-trying-to-access-the-document-object-of

          document.domain = document.domain;
          


          И ты уже не можешь достучаться к динамически созданному IFrame'у.
          • –1
            это только IE касается чтоли? меня ie вообще никоим образом не интересует.
            • 0
              IE пользуется значительная часть интернет обывателей. Так что когда речь идёт о массовых и кроссбраузерных решениях, то что конкретно вас ie не интересует, никого больше не интересует.
              • +1
                я не говорил о кроссбраузерных решениях, у меня сейчас есть своя четкая задача под один только хром поэтому я и сказал что мне не важен ие. хотя я и забыл что топик о решении в целом.
  • 0
    Не замеряли быстродействие такого метода? Сколько времени занимает один сеанс запрос-ответ между айфреймами? Например, у Facebook JS SDK есть проблема, что фенкция запроса параметров из iFrame занимает от 500мс и до бесконечности (FB.Canvas.getInfo), что очень замедляет работу приложения.
  • НЛО прилетело и опубликовало эту надпись здесь
  • 0
    Один из самых интересных моментов в этой задаче — общение между доменами, вы скинули на отдельную библиотеку, остальное достаточно просто, и задачу эту не раз уже решали в любом сервисе, который так или иначе держит у себя внешние приложения. См, например метод gadgets.window.adjustHeight в opensocial вообще и в apache shindig в частности. Интереснее было бы, если бы вы предложили архитектуру для предоставления API фреймам внутри сайта для общения с контейнером.
    • 0
      Так postMessage это фактически и есть простая обертка (API), позволяющая общаться фреймам внутри сайта с контейнером, а также окнам браузера между собой.

      Задачу решать-то решали, но вот найти решение в открытом доступе было достаточно сложно (например, если я никогда не пользовался opensocial — откуда мне знать, что я там смогу найти ответ на мой вопрос?)
      Теперь решение есть, и судя по количеству добавлений в избранное — оно достаточно востребованное.
  • 0
    Как вообще изменить высоту этого iframe на iPad?
    В нём он всегда растягивается по высоте содержимого и никак иначе =(
    • +1
      Как я понял, проблема в том, что мобильный webkit (на ios, android и т.д.) не имеет нормальной возможности скроллить что-то внутри элемента с фиксированной шириной/высотой, поэтому там все растягивается.

      Тем не менее, есть решение:
      iScroll
  • 0
    В яндекс виджетах это, кстати, тоже както решено, но без подключения js в дочерние файлы:
    api.yandex.ru/wdgt/doc/apiref/reference/widget-adjustiframeheight.xml
  • 0
    Классный метод взаимодействия между iframe-ми. Спасибо за статью и пример. Очень помогло.

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