Web API window.postMessage İle Pencereler Arası İletişim

Bir web sayfası veya uygulamasında pencereler (popup, tab, iframe) arasında iletişime ihtiyacımız olabilir. Google Tag Manager Scroll Derinliği İşlemleri başlıklı yazımda şöyle bir cümle kullanmıştım; "bir sayfa içerisinde iframe etiketi kullanıldığında internet tarayıcısı HTML belgesi için ayrı, iframe için ayrı window nesnesi yaratır.". HTML5 ve API Kullanımları yazısının da devamı olarak bu konuyu farklı bir şekilde ele alacağım.

window Nesnesi

window nesnesi tarayıcı penceresi olarak da ifade edebiliriz. Web tarayıcısı ile ilgili özellik ve metotları barındıran tarayıcı nesnesidir. Tarayıcı temelinde değerlendirmek gerekirse, nesneler hiyerarşisinde en tepede yer almaktadır. Diğer tüm nesneler bu nesnenin alt nesneleri olarak tanımlanırlar. window içeriği aynı zamanda global JavaScript nesnesi olarak erişilebilir. Nodejs tarafında da global olarak ifade edilir. window nesnesine uygulanan genel bir standart olmasa da tüm modern tarayıcılar tarafından desteklenir.

window nesnesi window, history, location, navigator, frames, screen ve document alt nesnelerine sahiptir, bu nesneler açılmış olan pencerenin tümünü hedefleyen yöntemleri içerirler1. Alt nesnelerin herbirine de kendilerine ait, özellik, metot ve alt nesnelere sahiptir.

Tarayıcınızın console alanına window yazarak nesne ile ilişkili alanları görüntüleyebilirsiniz. Her kod çalıştırma ortamında da (pencere/sekme), sadece bir tek JavaScript window nesnesi bulunabilir.

Gelelim iframe konusuna. Bir sayfa (document nesnesi) içerisinde iframe kullanılıyorsa, tarayıcı HTML belgesi için bir window nesnesi ve her iframe için de bir ek pencere nesnesi oluşturur2. frames ile özelliği ile iframe elemenlerine ulaşabilir3, frameElement ile tanımlı iframe elementine erişebiliriz4.

Özetlemek gerekirse, tarayıcımız içerisindeki pencereler ve iframe elementleri ayrı window nesnelerine sahipler.

Soru şu, bu nesneler arasında iletişimi nasıl sağlarız? Örneğin, bir sayfa içerisinden popup içerisine ya da bir iframe içerisinden kapsayıcıya (ya da tersi) nasıl bilgi gönderebiliriz?

window.postMessage API

window.postMessage, iki pencere/çerçeve arasında (cross-window communication) veri mesajları gönderilip alınabilmesine izin veren bir JavaScript metodudur. Bu metod sayesinde cross-domain (alan adları) arasında da güvenli bir şekilde veri mesajları taşıyabilmekteyiz. Diğer durumda, metodun çalışabilmesi için alan adı, protokol ve port tanımlarına uyulması gerekir5. Detaylarına birazdan örnek işlemler üzerinden değineceğim. Öncesinde bu metod için tanımlanan argümanlara bakalım6.

hedefWindow.postMessage(mesaj, alanadi, transfer);
hedefWindow
Mesajın gönderileceği window nesnesi kaynağını işaret eder. window.open, window.opener, HTMLIFrameElement.contentWindow, window.parent, window.frames ile tanımlanabilir. Aşağıda, window.open ve HTMLIFrameElement.contentWindow ile ilgili örnekleri görebilirsiniz.
mesaj
Pencereler/çerçeveler arasında gönderilecek mesajı içerir.
alanadi
targetWindow kaynağını belirtir. Bu şekilde verinin hangi kaynaktan geldiğini kontrol edebilir ve işlem gerçekleştirilmesini sağlayabiliriz. Geliştirme ortamında "*" tanımı yapılabilir. Ancak, kullanım aşamasında URL ya da URI olarak bir kaynak belirtilmesine dikkat edilmelidir7.
transfer
Opsiyonel bir argümandır. Mesajla birlikte aktarılan Transferable nesneler dizisidir.

Peki, bu mesajları nasıl alırız?

MessageEvent

MessageEvent interface, bir hedef nesne (target) tarafından alınan bir mesajı temsil eder8.

window.addEventListener("message", event => {
  if (event.origin !== "http://example.org:8080") return;
}, false);

Mesaj kaynağına event.origin, mesaj içeriğine event.data ve mesajı gönderen pencere nesnesine event.source ile erişebiliriz. event.source, farklı origin'lere sahip iki pencere arasında iki yönlü iletişim kurmak amacıyla kullanılabilir9.

Artık örnek işlemlere geçebiliriz. İlk örneği aynı alan adı altındaki sayfalar için popup aracılığı ile gerçekleştirelim.

İlk olarak ana sayfamızı, index.htm, oluşturalım.

<script>
let childwin = null;

let openChild = () =>  {
  childwin = window.open('./popup.htm', "popup", 'height=300px, width=500px');
}

let sendMessage = () => childwin
  .postMessage(document
    .getElementById('txt')
    .value, '*');
</script>

<button onclick="openChild()">Open Popup</button>

<input type="text" id="txt" value="take a deep breath!" />
<button onclick="sendMessage()">send it</button>

Yukarıdaki kod ile temelde yapılan işlem bir popup pencere açmak, bir text input içerisindeki değeri (value) bu popup'e postMessage ile mesaj (message) olarak göndermek. Şimdi de bu mesajı alacak olan popup sayfasının popup.htm koduna bakalım.

<div id="data">
  <strong>my todo list</strong>
</div>

<script>
  const list = document.createElement('ul');
  document.getElementById('data').appendChild(list).setAttribute('id', 'myList');

  window.addEventListener("message", event => {
    const item = document.createElement('li');
    const itemVal = document.createTextNode(event.data);
    document.getElementById('myList').appendChild(item);
    item.appendChild(itemVal);
  });
</script>

Bir div ve JavaScript kodumuz var. JavaScript tarafında div içerisinde her yeni gelen mesaj için bir li etiketi oluşturuyor ve değer olarak index.htm içerisindeki input değerini kullanıyor. Sayfalar arasındaki etkileşim aşağıdakine benzer bir görüntüye sahip olacaktır.

Bu örnekte aynı alan adı altındaki sayfaları kullanmıştık. İkinci örnekte süreci farklı alan adları altında bulunan sayfalar üzerinden çift yönlü olarak ele alalım. Bu defa popup yerine iframe elementini kullanacağım. İlk dosyamızın adı yine index.htm olsun ve example.com alan adı altında yayınlansın. Son olarak, mesajı gönderen kaynağı da kontrol edelim.

<iframe src="http://subdomain.webstore.com/frame.htm" id="ifrm" width="400" height="250"></iframe>

<input type="text" id="txt" value="take a deep breath!" />
<button onclick="sendMessage()">send it</button>

<script>
let elIfrm = document.getElementById('ifrm').contentWindow;

let sendMessage = () => elIfrm
  .postMessage(document
    .getElementById('txt')
    .value, 'http://subdomain.webstore.com');
</script>

iframe içeriği sayfa oluşturulduktan sonra yüklenmiş olacaktır. Yukarıdaki örnekte JavaScript ile text input içerisindeki değeri iframe içerisine mesaj olarak göndermekteyiz. frame.htm içeriğinde çok fazla bir şey değişmedi.

<div id="data">
  <strong>my todo list</strong>
</div>

<script>
  const list = document.createElement('ul');
  document.getElementById('data').appendChild(list).setAttribute('id', 'myList');

  window.addEventListener("message", event => {
    if (event.origin !== "http://example.com") return;
    const item = document.createElement('li');
    const itemVal = document.createTextNode(event.data);
    document.getElementById('myList').appendChild(item);
    item.appendChild(itemVal);
  });
</script>

Şimdi, ekleme işleminin ardından origin'e bir onay mesajı döndürelim. Bu onay mesajı da iframe içerisinde oluşturulan listedeki child element sayısı olsun. Karşılaştırma yapabilmeniz için ilgili sayfalara ait kodları yeniden paylaşıyorum. İlk kodumuz index.htm'e ait.

<iframe src="http://subdomain1.dnomia.com/frame.htm" id="ifrm" width="400" height="250"></iframe>

<input type="text" id="txt" value="take a deep breath!" />
<button onclick="sendMessage()">send it</button>

<div id="status"></div>

<script>
let elIfrm = document.getElementById('ifrm').contentWindow;

let sendMessage = () => elIfrm
  .postMessage(document
    .getElementById('txt')
    .value, 'http://subdomain1.dnomia.com');

window.addEventListener("message", event => {
  if (event.origin !== "http://subdomain1.dnomia.com") return;
  document.getElementById('status').textContent = event.data
});
</script>

Şimdi de frame.htm içeriğimize bakalım.

<div id="data">
  <strong>my todo list</strong>
</div>

<script>
  const list = document.createElement('ul');
  document.getElementById('data').appendChild(list).setAttribute('id', 'myList');

  window.addEventListener("message", event => {
    if (event.origin !== "http://differentsubdomain.ezza.work") return;
    const item = document.createElement('li');
    const itemVal = document.createTextNode(event.data);
    document.getElementById('myList').appendChild(item);
    item.appendChild(itemVal);

    event.source.postMessage(document.getElementById('myList').childElementCount + ' item(s)', 'http://differentsubdomain.ezza.work');
  });
</script>

Evet, origin kontrollerimizi yaptık, dönüş mesajını önceki mesaj kaynağı üzerinden gerçekleştirdik ve birbirleri arasında mesaj taşıyabilen sayfa ve çerçeveler elde ettik.

Artık pencereler ve çerçeveler arasında veri taşıyabileceğimizi biliyoruz. Bu işlemi farklı amaçlar doğrultusunda kullanmak da mümkün. İlerleyen zaman içerisinde bu konuya dair farklı kullanım örneklerinden ayrıca bahsedeceğim.