Автоматическая кросс-доменная установка высоты 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, но у кого-то, возможно, его не будет — тогда код все равно сохранит работоспособность.
Поделиться публикацией
AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама
Комментарии 45
  • –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-ми. Спасибо за статью и пример. Очень помогло.
                                      • 0
                                        postmessage.freebaseapps.com

                                        Выдаёт 404. Обновите ссылочку в статье?.. :)

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