Tag Archives: postMessage

Невидимо вграждане на външно съдържание с iframe

от Гонзо
лиценз CC BY-NC-SA

Поради различни съображения понякога се налага да използваме външни услуги за част от съдържанието в сайта. В един точно такъв случай ми се наложи да вградя външното съдържание в сайта с iframe и решението трябваше да отговаря на следните изисквания:

  • Височината на iframe-а винаги да отговаря на височината на вградената страница, за да няма втори скролбар.
  • При навигация във вградената страница да се променя и адреса в основната, като промененият адрес да зарежда съответната страница от външната услуга.
  • Заглавието на документа да се променя заедно със навигацията във вграденото съдържание.

Особеното е, че при зареждане на съдържание от друг домейн в iframe JavaScript от едната страница няма достъп до другата. Поради тази причина оразмеряването на iframe според съдържанието изглежда на пръв поглед проблемно. След малко търсене на най-доброто решение се оказа, че то лесно ще ми помогне за изпълнението и на другите две изисквания. Добре де, за втората точка пипнах малко и бекенда на основния сайт, но няма да навлизам в подробности, защото там не съм в свои води.

И така, най-разумното решение за изравняване на височината на iframe с неговото съдържание, когато то се зарежда от друг домейн, е чрез postMessage. Всеки път, когато се зареди страница в рамката, тя праща съобщение към родителската страница с височината на съдържанието. Родителската страница съответно като получи такова съобщение оразмерява рамката.

Вътре в рамката имаме това:

window.addEventListener('load', function(e){
  if(window.parent && window.parent.postMessage){
    var height = document.body.scrollHeight;
    window.parent.postMessage(height, '*');
  }
}, false);

А в основния сайт имаме това:

window.addEventListener('message', function(e) {
  var data = e.originalEvent.data;
  if (!data) return;
  document.getElementById('theframe').style.height = data + 'px';
}, false);

И след като можем да си изпратим някакви данни от единия прозорец към другия, тогава защо не си изпратим всички данни, от които имаме нужда? Какво ни трябва още? Адреса на страницата в рамката и заглавието на страницата. Всъщност няма да ни трябва целия адрес на страницата, а само пътя. Решението ми за синхронизиране на адресите на двете страници беше да добавя пътя от рамката като параметър към адреса на основната страница, и да го използвам за формиране на атрибута src на рамката при зареждане на страницата. Естествено, включих параметър като част от пътя в адреса на основната страница чрез URL Rewrite.

Това, което пропуснах е как точно ще променим адреса на основната страница като получим съобщението от рамката? В първия момент опитах да използвам history.pushState() – имаме навигация и искаме да дадем възможност потребителя да се върне назад и после напред и… имаше обслужване на събитието popstate и се увъртех като пиле в кълчища. Стана така, защото не взех предвид факта, че навигацията вътре в рамката прави записи в историята на браузъра и чрез pushState аз ги дублирам. Много по-лесно се оказа да използвам history.replaceState() – бутоните на браузъра за навигиране в историята управляват рамката, а чрез събитието popstate и postMessage данните за това се пращат към основната страница. Крайният код придоби следния вид:

iframe.js

function sendParent(e){
  if(window.parent && window.parent.postMessage){
    var height = document.body.scrollHeight,
        path = document.location.pathname,
        title = document.title;
    window.parent.postMessage({height: height, path: path, title: title}, '*');
  }
}
window.addEventListener('load', function(e){
  sendParent();
}, false);
window.addEventListener('message', function(e){
  var data = e.data || e.originalEvent.data;
  if(data == 'get') {
    sendParent();
  }
}, false);
window.addEventListener('popstate', function(e){
  setTimeout(sendParent, 0);
}, false);
document.addEventListener("page:load", function() {
  sendParent();
}, false);

parent.js

var theframe = document.getElementById('theframe');
window.addEventListener('message', function(e) {
  var data = e.originalEvent.data;
  if (!data) return;
  theframe.style.height = data.height + 'px';
  window.history.replaceState(null, '', '/help' + data.path);
  window.scroll(0, 0);
  document.title = data.title;
}, false);
window.addEventListener('resize', function(e) {
  theframe.contentWindow.postMessage('get', '*');
}, false);

На края на кода на рамката сигурно сте забелязали събитието page:load – оказа се, че услугата, която вграждаме използва turbolinks и това е събитието, което се случва когато turbolinks зареди съдържанието на нова страница. А на края на кода на основната старница трябва да ви е направило впечатление обработката на resize на прозореца – ако се промени размерът му е много вероятно да се промени и височината на съдържанието в рамката и тогава питаме рамката за размера на съдържанието. Сигурно първото нещо, което си мислите е „А що не го закачим направо на resize на рамката?“ – ами щото resize на рамката се случва и когато ѝ зададем нов размер, което ще изпрати още веднъж съобщение към прозореца, той пак ще сложи нов размер на рамката… Е, на втория – третия път спира тоя пинг-понг, ама кому е нужно…

Та те така, получи се съвсем невидимо за потребителя – зарежда се съдържание, линковете в рамката сменят като че ли цялата страница, бутоните „напред“ и „назад“ на браузъра работят както се очаква… Идилия! Бих ви го показал, но още не сме готови с някои други неща, ще сложа линка когато му дойде времето. Вервайте ми!