ugrás a tartalomhoz

Cross-domain JavaScript kommunikáció egyszerűen

szjmn · 2010. Május. 21. (P), 08.24
Cross-domain JavaScript kommunikáció egyszerűen

Cross-domain JavaScript kommunikációra több lehetőség is adott – Flash, XML-RPC, postMessage() –, azonban ha egyszerű célokra keresünk egyszerű megoldást, akkor ezek közül egyik sem a legjobb. Vagy nem teljes körűen alkalmazható technikák, vagy a probléma látszólagos egyszerűségéhez képest túl bonyolultak és nem áll rendelkezésre mindkét oldalon a megvalósításukhoz szükséges programozói munkaóra. Sőt, az is előfordulhat, hogy a másik oldal nem kellően felkészült egy ilyen bonyolultabb csatorna kialakítására.

Az alábbi cikk Suhajda Gábor a májusi budapest.js találkozón tartott előadásának alapján készült. Az előadás videofelvétele megtekinthető.

Ilyen látszólag egyszerű probléma lehet egy iframe-be ágyazott microsite-ból átméretezni az iframe-et, vagy egy iframe-ben futtatott bannerből JavaScript hívásokat küldeni a szülő oldal felé (pl. lenyíló bannereknél). Ezek gyakorlati példák, ezeken keresztül szeretnék bemutatni egy egyszerű, megbízható megoldást.

A HTML5 által már támogatott postMessage() metódus ugyan pontosan erre szolgál, de mivel ez egy még nem teljes körűen implementált böngészőfunkció, ezért ha használjuk is, mindenképpen szükségünk van egy olyan fallback megoldásra, ami a postMessage()-et nem támogató böngészőkben is működik. Ha abban a szerencsés helyzetben vagyunk, hogy a cross-domain kommunikáció mindkét domainje a mi hatáskörünk, akkor könnyebb a dolgunk. Viszont ha az egyik domain nem a mi hatáskörünk, akkor szeretnénk egy olyan gyors és üzembiztos megoldást, amit a lehető legkevesebb levelezéssel és/vagy telefonálgatással össze tudunk lőni a másik oldallal.

Cross-domain kommunikáció iframe-ek használatával
Cross-domain kommunikáció iframe-ek használatával

Vegyük a microsite-os példát! Ha a microsite-nak vannak aloldalai és ezek különböző méretűek, akkor szeretnénk ezeket teljes valójukban, görgetősávok nélkül láttatni. Ehhez kell átméretezzük az iframe-et, amit a szülő oldal könnyedén meg tud tenni, azonban a másik domainen lévő microsite már kevésbé, mert a cross-domain JavaScript hívások nem engedélyezettek. Két azonos domainen lévő frame viszont tud egymással kommunikálni, ezért nincs más dolgunk, mint beiktatni a microsite-unkon belül még egy iframe-et, rejtve, amely a szülő oldal domainjéről hív meg valamit.

<iframe src="..." width="0" height="0" name="inner_iframe" id="inner_iframe"></iframe>

Ha maradunk az egyszerű, gyors és üzembiztos megoldásnál, akkor egy statikus HTML fájlt fogunk előkészíteni – ezzel lesz a legkevesebb gond a másik oldalon. Ez a fájl lehet végtelenül egyszerű is, de akár többféle feladatra is felkészíthetjük. A kommunikációt magát viszonylag könnyen elintézhetjük:

top.document.getElementById("iframe").setAttribute("height", 500);
top.fuggveny();

Tehát adott egy iframe-ünk a microsite-ban, ami tud kommunikálni a szülő site-tal. Most már csak az iframe-ben lévő fájlt kell valahogy manipuláljuk. Mivel a microsite és a rejtett iframe ismét más domainen vannak, ezért ebben az irányban sem indíthatunk JavaScript hívásokat, viszont az iframe-ben bármit megnyithatunk. Hogy ne kelljen az összes felmerülő lehetőséghez (pl. a microsite összes aloldalához) külön HTML fájlt készítenünk, ezért paraméterezni szeretnénk a HTML-t. Ha tartjuk magunkat ahhoz, hogy egy egyszerű HTML fájlt küldünk át a másik oldalnak, akkor a legkönnyebben a location hashben küldhetünk adatokat az iframe-be. Ezt natív JavaScript megoldásokkal ki tudjuk olvasni, majd fel tudjuk dolgozni, és a fentihez hasonlóan továbbíthatjuk a megfelelő kérést a szülő frame felé. Az iframe_connect.html#500 feldolgozása egy sorban:

top.document.getElementById("iframe").setAttribute("height", (document.location.hash + '').split('#')[1]);

Ezzel gyakorlatilag meg is oldottuk az eredeti problémát, az alapja tehát tényleg egyszerű. De nem hagyjuk ennyiben, gondoljuk kicsit tovább a lehetőségeket!

A fenti példában id alapján tudtunk hivatkozni az iframe-re, azonban ha a szülő oldalon nincs ilyen attribútuma az iframe-nek és nem kérhetünk ilyen változtatásokat, akkor elérhetjük a neve alapján is. (Ha esetleg neve sincs az iframe-nek, akkor a document.getElementsByTagName("iframe") tömb a következő lehetőségünk.)

A következő példában abból a feltevésből indulunk ki, hogy az iframe-nek biztosan lesz neve, viszont még nem tudjuk biztosan, hogy mi – pl. azért, mert még nem készült el a túloldalon a site. Ezért aztán az iframe nevét is paraméterben adjuk át a HTML-nek, amit így a mi oldalunkról tudunk véglegesíteni. Ehhez tehát további adatokat kell átadnunk a hashben, valamilyen módon tagolva ezeket: iframe_connect.html#frame=content_frame;height=500. Ennek a feldolgozásánál már lehetünk kicsit alaposabbak:

var d = {frame: "content_frame"},
    x = (document.location.hash + '').split('#')[1].split(';');

if (x.length != 0) {
    for (var p in x) {
        d[x[p].split('=')[0]] = x[p].split('=')[1];
    }
    
    if (d['height']) {
        top.document.getElementsByName(d['frame'])[0].setAttribute("height", d['height']);
    }
}

Ha szükséges, JavaScript hívásokat is tudunk küldeni a szülő oldalra. Egy lenyíló bannernél sokszor kapunk információkat arra vonatkozóan, hogy milyen scriptekkel lehet működtetni a lenyílást és visszazáródást, tehát meghívunk pl. egy resizeLayer(500) függvényt: iframe_connect.html#height=500;jsCallback=resizeLayer;jsParams=500.

var x  = (document.location.hash + '').split('#')[1].split(';'),
    d  = {frame: "content_frame"},
    js = {f: false, p: []};

if (x.length == 0) {
    return;
}

for (var p in x) {
    var n = x[p].split('=')[0],
        v = x[p].split('=')[1];

    switch (n) {
        case "jsCallback":
            js.f = v;
            break;
        case "jsParams":
            js.p = v.split(',');
            break;
        default:
            d[n] = v;
    }
}

if (js.f != false) {
    top[js.f].apply(this, js.p);
}

if (d['height']) {
    top.document.getElementsByName(d['frame'])[0].setAttribute("height", d['height']);
}

Még egy funkcióval kell kiegészítsük a HTML fájlunkat: ha szükséges, élő kommunikációt is tudnia kell. Egy microsite aloldalainál erre ritkán lesz szükség, hiszen amikor az aloldal betöltődik, meghívjuk a rejtett iframe-ben a másik oldalon lévő fájlt a megfelelő hash paraméterekkel, az pedig beállítja az iframe magasságát. Ha azonban más jellegű hívásokra van szükség, vagy olyan események indítják a folyamatot, amelyek nem generálnak oldalbetöltést, akkor csak az iframe-ben lévő fájl hash paraméterét változtatjuk meg.

Az iframe-ben tehát figyelnünk kell a hash változásait. Számtalan JavaScript-keretrendszer (pl. jQuery plugin) kínál ilyen szolgáltatásokat, de nekünk semmi extra nem kell, elég egy egyszerű listener, ami cselekszik, amikor megváltozik a hash. A legelegánsabb megoldás a window objektum onhashchange eseményének figyelése, ami azonban – az elején már említett postMessage()-hez hasonlóan – egy nem teljes körűen támogatott funkció, ezért egy fallbackkel egészítettem ki a használatát. Így csak ott terheljük setTimeout()-tal a böngészőt, ahol mindenképpen szükséges:


var hashchange = false,
    h          = false;

if ("onhashchange" in window) {
    hashchange          = true;
    window.onhashchange = hashListener;
}

window.onload = hashListener;

function hashListener() {
    if (document.location.hash != h) { /* ... */ }
    
    if (!hashchange) {
        var t = setTimeout(hashListener, 100);
    }
}

Az egészet összeillesztve kapunk egy mindentudó HTML fájlt, amivel a legtöbb egyszerű JavaScript hívást gond nélkül át tudjuk vezetni a szülő oldalra és a megfelelő paraméterezéssel akár többször is használhatjuk változtatás nélkül:

var hashchange = false,
    h          = false,
    d          = {frame: "content_frame"},
    js         = {f: false, p: []};

if ("onhashchange" in window) {
    hashchange          = true;
    window.onhashchange = hashListener;
}

window.onload = hashListener;

function hashListener() {
    if (document.location.hash != h) {
        var x = (document.location.hash + '').split('#')[1].split(';');
        
        h = document.location.hash;
        
        if (x.length == 0) {
            return;
        }
    
        for (var p in x) {
            var n = x[p].split('=')[0],
                v = x[p].split('=')[1];

            switch (n) {
                case "jsCallback":
                    js.f = v;
                    break;
                case "jsParams":
                    js.p = v.split(',');
                    break;
                default:
                    d[n] = v;
            }
        }
        
        if (js.f != false) {
            top[js.f].apply(this, js.p);
           }
           
        if (d['height']) {
            top.document.getElementsByName(d['frame'])[0].setAttribute("height", d['height']);
        }
    }
    
    if (!hashchange) {
        var t = setTimeout(hashListener, 100);
    }
}

Ugyan az átadott HTML-nek a paraméterezése a mi feladatunk, hiszen mi illesztjük be a microsite-unkba vagy bannerünkbe, tehát nem kéne elrontanunk, de azért érdemes kivédeni a hibalehetőségeket, hogy egy apró hiba esetén se dobjon errort a böngésző. Ha ezzel megvagyunk, más dolgunk már nincs is, mint átküldeni a HTML fájlt a másik domain kezelőjének, és megkérdezni tőle, hogy hova fogja kitenni, hogy mi aztán azon a címen tudjuk meghívni a rejtett iframe-ünkben. Problémák még így is előfordulhatnak, de ha mindent jól csináltunk, a megoldás minden böngészőben megteszi amit szeretnénk és bonyolult megoldások helyett akár néhány perc és egy-két emailváltás elég a megvalósításához.

A fenti kódokat használó működő demót találtok a http://zawar.hu/dev/iframe címen. Egy oda-vissza kommunikáló megoldás a http://zawar.hu/dev/iframe2 alatt érhető el. A lényege, hogy a microsite kódjában is van egy hash listener, ami a neki hash-ben átküldött adatokat feldolgozza. Ez egy kevésbé kidolgozott, inkább csak elméleti példa.

Kapcsolódó linkek:

 
szjmn arcképe
szjmn
2000 óta foglalkozik webfejlesztéssel és webdesignnal. Sokáig szabadúszóként, később egy hírportál fejlesztőjeként dolgozott, 2008 óta a Carnation front-end fejlesztője. Fő érdeklődési köre a JavaScript, a social media fejlesztés és marketing, valamint a usability. Hobbija a street art és a random absztrakció.
1

CORS

szjmn · 2010. Május. 25. (K), 16.48
kibontakozóban egy újabb lehetséges megoldás az eredeti problémára:
Cross-Origin Resource Sharing (CORS) is a W3C Working Draft that defines how the browser and server must communicate when accessing sources across origins. The basic idea behind CORS is to use custom HTTP headers to allow both the browser and the server to know enough about each other to determine if the request or response should succeed or fail.

bővebben: