Zobrazení HTML staženého pomocí AJAX

Když AJAXem stáhnete nějaké HTML, můžete ho zobrazit na stránce několika způsoby. Ty se liší tím, co se staženým kódem můžete dělat a jaký bude výsledek.

jQuery.load()

Nejjednodušší způsob je nechat celý proces na nějakém frameworku. Například jQuery má metody load(), ale většina frameworků má nějakou podobnou metodu nebo komponentu (např. ExtJS/Sencha má komponentu Panel, do kterého můžete načíst obsah pomocí zadané URL).

//GET pro HTML soubor
$('#container')
    .load('/static/license.html');
//GET
$('#container')
    .load('/api/html?profile=12345');
//POST
$('#container').load('/api/html', {
    profile: 12345
});

Do metody load() jednoduše předáte URL, kterou chcete stáhnout a případně ještě parametry, které jsou potřeba, aby server správně HTML vygeneroval (v případě, že nestahujete statické HTML).

Metoda má ještě dvě alternativy. V první můžete omezit, jaký prvek se má do kontejneru zobrazit v případě, že server vrátí více než kolik potřebujete:

$('#user-picture').load(
    '/api/html?profile=12345 img.profile');

V tomto případě server vrátí celý profil, ale jQuery z něj vybere pouze obrázek se třídou profile a ten zobrazí v kontejneru. Nevýhodou tohoto přístupu je, že nemůžete dynamicky vybrat prvek podle toho, co server vrátil (tzn. že identifikátor požadovaného prvku musíte znát před odesláním requestu).

Do funkce ještě můžete předat callback, pomocí kterého můžete ošetřit např. chybu (pokud server nevrátí platné HTML, metoda load() jen vymaže obsah kontejneru) nebo upravit přijatý HTML kód. Nemůžete ale ovlivnit, co funkce nastaví do prvku, protože callback se zavolá až poté, co jQuery zpracuje odpověď a vloží ji do kontejneru. Sice ho můžete znovu přepsat uvnitř callbacku, ale v tom případě už můžete použít některou z ostatních metod.

$('#user-picture').load(
    '/api/html?profile=12345 img.profile',
    function(response, status) {
        if ('success' !== status) {
            alert('Chyba při stahování!');
            return;
        }
        if (!$(response)
            .find('img.profile').length) {
            $('#user-picture').html(
                '<img src="none.jpg" />');
        }
    }
);

Při uvádění callbacku dejte pozor na to, že v jQuery existují dvě metody load() – pokud má jako první parametr řetězec a druhý funkci, načítá obsah z URL; pokud má funkci jako první parametr, nastavuje naopak handler události onload (zkrácený zápis $().on('load', function() {}; ).

Výhody:

  • jednoduchost stažení
  • možnost přepínat mezi GET a POST jen podle parametrů

Nevýhody:

  • musíte mít dopředu připravený kontejner
  • musíte dopředu znát selektor prvku, pokud nechcete celé HTML

Metody ajax(), get(), post()

Pokud se vám nehodí metoda load(), můžete použít funkci ajax() (nebo její modifikace get() a post()) a následně ručně zpracovat odpověď a nastavit ji tam, kam potřebujete.

$.ajax({
    url: '/api/html',
    data: { profile: 12345 },
    dataType: 'html'
}).always(function(response, status) {
    if ('success' !== status) {
        alert('Chyba při stahování!');
        return;
    }
    var
        html = $(response),
        el = html.find('img.profile');

    if (!el.length) {
        el = html.find('.error');
    }
    $('container').empty().append(el);
});

Výhody:

  • můžete přesně určit, co z odpovědi vás zajímá
  • můžete stažené HTML vložit kam chcete

Nevýhody:

  • pro každý request musíte psát callback a ošetřovat chyb
  • musíte si pamatovat jména parametrů metody $.ajax()

Vložení do iFramu

Stažený HTML samozřejmě nemusíte vkládat do stránky samotné, ale můžete pro staženou stránku vytvořit iFrame:

$.ajax({
    url: '/api/html',
    data: { profile: 12345 },
    dataType: 'html'
}).always(function(response, status) {
    if ('success' !== status) {
        alert('Chyba při stahování!');
        return;
    }
    var
        html = $(response),
        ifrm = $('<iframe />')
            .attr('src', 'about:blank')
            .hide()
            .appendTo($('body'));
        iDoc = $(ifrm[0]
                  .contentWindow.document);

    iDoc.find('body').append(html);
    ifrm
        .width('100%')
        .height(ifrm[0]
            .contentWindow.innerHeight)
    ;
    $('container').append(ifrm.show());
});

Při vytváření iFrame musíte pamatovat na několik věcí. iFramu musíte nastavit nějaký src, protože jinak by byl prázdný a nedalo by se s ním pracovat. Nastavením na about:blank vytvoříte v iFrame základní strukturu HTML-HEAD-BODY, do které následně můžete vkládat váš kód.

Aby se ale src zpracoval, musíte iFrame vložit do stránky, před čímž je vhodné ho skrýt, aby nerozbil stránku před tím, než ho budete schopni upravit a zobrazit na správné pozici.

Poté, co iFrame zpracuje načtenou (prázdnou) stránku, můžete pomocí jeho vlastnosti contentWindow získat přístup k document elementu, pomocí kterého pak můžete pracovat v jeho DOMem.

Poznámka 1: contentWindow je vlastnost původního HTMLElementu IFRAME, takže pokud máte iFrame jako jQuery wrapper, musíte nejprve získat HTMLElement pomocí metody get(0) nebo jako pole [0].

Poznámka 2: některé prohlížeče podporují místo contentWindow.document přímo vlastnost contentDocument, ale vzhledem k tomu, že není univerzální a delší zápis není zase o tolik delší, je lepší používat univerzální contentWindow.document.

Když získáte přístup do vnitřního dokumentu, můžete pak najít tagy HEAD nebo BODY a vložit do nich, co potřebujete. Následně již můžete iFrame zobrazit do stránky tam, kam potřebujete.

Pokud potřebujete zjistit rozměry vnitřního okna, můžete používat contentWindow stejně jako používáte window u hlavní stránky.

Výhody:

  • styly a skripty v iFramu neovlivní aktuální stránku

Nevýhody:

  • složitější vytváření iFramu
  • nutnost nastavení CSS souborů do hlavičky iFramu pro správné naformátování obsahu

Přepsání dokumentu

Poslední možností, jak zpracovat stažený HTML kód, je přepsat celý aktuálně zobrazený obsah. To se hodí v případě, že stáhnete stránku a zjistíte, že nic z ní nejste schopni zobrazit v současné stránce a musíte danou stránku zobrazit jako novou. Pokud byste ale jednoduše přepsali document.location (nebo znovu zavolali form.submit(); ), došlo by k nové stažení stránky, což by trvalo déle a navíc by nemuselo vrátit stejný výsledek (např. u formuláře by došlo k dvojitému uložení dat).

Základem přepsání dokumentu je použití metody document.write(), která umožňuje zapisovat HTML kód přímo do dokumentu (bez nutnosti pracovat s DOMeme). V případě, že již došlo k zavolání document.onload(), smaže jakýkoliv zápis do dokumentu současný obsah a začne vytvářet nový:

url = '/api/html?profile=12345';
$.ajax({
    url: url,
    dataType: 'html'
}).always(function(response, status) {
    if ('success' !== status) {
        alert('Chyba při stahování!');
        return;
    }
    var
        html = $(response),
        required = ('img.profile, .error');

    if (!html.find(required).length) {
        //stránka neobsahuje žádný
        //z požadovaných elementů
        //může jít třeba o login stránku

        if (window.history) {
            history.replaceState({
                reloadUrl: location.href
            }, '', location.href);
            history.pushState({
                reloadHtml: response
            }, '', url);
        } //if(window.history)

        document.open('text/html','replace');
        document.write(response);
        document.close();

        return;
    }

    //... pokračuje zpracování HTML
});

Před tím, než zapíšete nový obsah pomocí metody document.write(), musíte nejprve zavolat metodu document.open(), které předáte jako parametry typ obsahu a zda jím má přepsat stávající obsah.

Typ obsahu by mohl teoreticky zahrnovat jakýkoliv MIME typ, ale v současnosti prohlížeče podporují pouze html, takže nic jiného než „text/html“ nemá smysl. Parametr se uvádí hlavně proto, že potřebujete uvést i druhý parametr.

Podle W3C specifikace by neuvedení druhého parametru mělo automaticky vytvořit nový záznam v History a zachovat se stejně, jako kdyby uživatel klikl na odkaz nebo tlačítko. Nicméně ve WebKitu (kvůli chybě) tohle nefunguje a obsah se vždy přepíše. Proto je nutné i pro ostatní prohlížeče uvést parametr jako „replace“ (což je stejné jako True), aby se všechny chovali stejně.

Poté, co zapíšete do dokumentu nové HTML, musíte ještě zavolat metodu document.close(), čím způsobíte nové vyvolání document.onload() (které bude nyní to z nové stránky).

Oprava History objektu

Jelikož výše uvedeným zavoláním document.open() nahradíte současnou stránku, přestanou fungovat správně tlačítka Zpět a Vpřed v prohlížeči. Aby k tomu nedošlo, musíte, před zápisem nového obsahu, pomocí metody history.pushState() vložit do historie novou položku, která bude reprezentovat nový obsah – jako třetí parametr uvedeme URL tohoto obsahu, aby uživatel věděl, která stránka je zobrazena.

Použití history.pushState() ale vytvoří pouze fiktivní záznam, který po kliknutí na Zpět nebo Vpřed nezpůsobí změnu obsahu stránky, je potřeba ještě dopsat metodu, která tohle zajistí. Proto jsme ve výše uvedeném kódu ukládali do statutu stránky hodnoty reloadUrl a reloadHtml:

var allowPopState = false;
$(function() {
    setTimeout('allowPopState = true;', 1);
});


$(window).on('popstate', function(event) {
    if (!history.state) { return; }
    if (!allowPopState &&
        document.readyState === 'complete')
        {
        event.preventDefault();
        event.stopImmediatePropagation();
        return;
    }
    if (history.state.reloadHtml) {
        //stránka byla vytvořena
        // z načteného HTML kódu
        document.open('text/html','replace');
        document.write(
                   history.state.reloadHtml);
        document.close();
    } else if (history.state.reloadUrl) {
        //normální stránka z URL
        if (history.state.reloadUrl
                 === location.href) {
             location.reload();
        } else {
            location.replace(
                 history.state.reloadUrl);
        }
    }
});

Metoda hlídá událost popstate, která se vyvolá při kliku na tlačítka Zpět nebo Vpřed. Pokud v statutu stránky najde HTML kód (který byl dříve stažen pomocí AJAX), zapíše ho do obsahu stránky (tak jako poprvé). Pokud naopak najde ve statutu URL, musí stránku znovu stáhnout stejně, jako kdyby uživatel klikl na tlačítko Aktualizovat (Refresh). Je potřeba správně použít buď metodu location.reload() nebo location.replace()! V opačném případě by se buď zobrazil špatný obsah stránky nebo by došlo k nekonečnému vyvolávání popstate události a prohlížeč by se zasekl.

Pozor na to, že prohlížeč v okamžiku zavolání document.write() smaže DOM a všechny skripty současného dokumentu. Jediná metoda, která je schopna pokračovat je ta, která document.write() zavolala. Po jejím skončení již nezůstane z původní stránky nic!

Safari a starší Webkit verze obsahují chybu, kdy se popstate událost vyvolávají i při prvním načtení. Z toho důvodu je potřeba ošetření popstate povolit teprve poté, co se celý dokument načte. Pokud k popstate události dojde během provádění onload události, je potřeba takový popstate ignorovat (a nejlépe zastavit metodami preventDefault() a stopImmediatePropagation() ). K tomu slouží globální proměnná allowPopState. Podmínka testující readyState zajišťuje, aby mohl uživatel kliknout na tlačítka Zpět a Vpřed ještě během načítání stránky.

Write is evil… ale ne tady

Některé editory a validátory mohou hlásit chybu na řádku s document.write(). To je proto, že obecně je write považováno za nebezpečnou funkci, protože může vyvolávat útočné skripty. V tomto případě ale naopak požadujeme, aby se všechny skripty ze staženého HTML vyvolaly a tudíž je toto použití metody správné (a chybu můžete ignorovat). Pokud chcete chybu odstranit, použijte následující trik:

function replaceDocument(html) {
    //sestav jméno metody skriptem, aby
    //validátor nevěděl, o co se jedná
    var method = 'wr'+'ite';
    document.open('text/html','replace');
    //zavolání write jménem
    document[method](html);
    document.close();
}

Výhody:

  • HTML se zobrazí jako samostatná stránka
  • Není potřeba stránku stahovat dvakrát (AJAX a normálně)

Nevýhody:

  • dojde ke smazání původní stránky
  • musíte sami ošetřit správu historie a obnovení stránky
  • nová stránka musí stáhnout všechny skripty a styly a vykreslit se, což z pohledu uživatele vypadá stejně jako normální stažení stránky

Přepsání iFrame

Můžete samozřejmě zkombinovat dvě předchozí metody a stažený HTML kód zapsat do iFrame. Stačí místo document.write() použít iframe.contentWindow.document.write():

function(html, iframe) {
    var iDoc = $(iframe).get(0)
            .contentWindow.document;
    iDoc.write(html);
    iDoc.close();
}

Zde již není potřeba volat metodu document.open(), protože iFrame nemá vlastní historii a nezáleží tak, zda obsah přepíšete nebo přidáte nový. Metoda document.open() se zavolá automaticky s výchozími parametry před zavoláním document.write().

Můžete ale požadovat, aby hlavní stránka obsahovala nový záznam v historii, který iFrame zobrazí nebo skryje.

Pozor na XmlHttpRequest

Některé serverové frameworky jsou schopny rozlišit normální a AJAXové requesty a pak se k nim chovají různě – např. negenerují hlavičku stránky nebo neprovádí některé funkce.

V případě, že potřebujete přes AJAX stáhnout celou stránku, může být potřeba buď prohlížeči zakázat odeslání hlavičky X-Requested-With nebo naopak odeslat ještě další hlavičku a na serveru upravit podmínky, které rozlišují AJAX requesty.

$.ajax({
    //...
    //změní XHR hlavičku,
    // aby server nepoznal AJAX
    headers: {
      'X-Requested-With': 'HtmlLoader'
    },
    //NEBO - XHR hlavička se neposílá
    //u requestů na jiný server
    crossDomain: true,
    //NEBO - přepíše hlavičku
    // před odesláním requestu
    beforeSend: function(xhr){
        xhr.setRequestHeader(
                  'X-Requested-With',
                  'HtmlLoader');
    }
});

nebo:

$.ajax({
    //...
    //hlavička s požadavkem na plné HTML
    headers: {
      'X-Requested-Content': 'FullHtml'
    },
    //NEBO - změna před odesláním
    beforeSend: function(xhr){
        xhr.setRequestHeader(
                  'X-Requested-Content',
                  'FullHtml'
        );
    }
});

Pro oba případy by serverová část mělo kontrolovat request takto:

<?php $request = /* získání requestu */; 
$headers = $request->getHeaders();
$isAjax = ('XMLHttpRequest' ===
          $headers['X-Requested-With']);
$isFullContent = ('FullHtml' ===
          $headers['X-Requested-Content']);

function generateHeader() {
    if ($isAjax && !$isFullContent) {
        return; //no header for AJAX
    }
    //...
}

1 komentář u „Zobrazení HTML staženého pomocí AJAX“

Napsat komentář

Vaše emailová adresa nebude zveřejněna. Vyžadované informace jsou označeny *

Tato stránka používá Akismet k omezení spamu. Podívejte se, jak vaše data z komentářů zpracováváme..