jQuery - Ajax

Obsługa AJAX w czystym JavaScript nie jest najprostszą sprawą, a na pewno nie zawsze najwygodniejszą. Możliwe, że właśnie dlatego bardzo często do działania z AJAX wybierana była biblioteka jQuery, która bardzo ułatwia zabawę z asynchronicznymi połączeniami.
Poniżej przyjrzymy się najczęściej używanym funkcjom.

$.ajax()

Pierwsza funkcja i od razu z grubej rury. Funkcja $.ajax(), bo o niej mowa to główna a zarazem najbardziej rozbudowana funkcja z zestawu. Nasza bohaterka ma postać:


$.ajax({
    url         : "example.php", //wymagane, gdzie się łączymy
    method      : "post", //typ połączenia, domyślnie get
    contentType : 'application/json', //gdy wysyłamy dane czasami chcemy ustawić ich typ
    dataType    : 'json', //typ danych jakich oczekujemy w odpowiedzi
    data        : { //dane do wysyłki
        name : 'Marcin',
        country : 'Polska'
    })
});

Jedynym wymaganym atrybutem jest tutaj url. Poza nim możemy określić dodatkowe atrybuty, wśród których znajdują się między innymi:

method lub type typ połączenia: post, get, delete, put, patch. Domyślnym jest get i wtedy nie musimy podawać tego parametru
data obiekt zawierający przekazywane dane
dataType typ danych jaki oczekujemy w odpowiedzi. Omówimy go za moment. Jeżeli nie zostanie ustawiony, jQuery spróbuje określić typ danych na podstawie tego co dostanie w odpowiedzi. Najczęściej będzie to 'json'
contentType typ danych jakie wysyłamy na serwer. Czasami musimy poinformować serwer jakiego typu dane mu wysyłamy

W praktyce parametrów możliwych do ustawienia jest o wiele więcej, ale większości z nich na co dzień zwyczajnie się nie używa. Warto jednak przyjrzeć się stronie http://jqapi.com/#p=jQuery.ajax i zapoznać się z kilkoma z nich.

Proste przykłady użycia tej funkcji mogą wyglądać tak jak poniżej:


//pobieranie danych w formacie JSON
$.ajax({
    url : "example.php",
    dataType : "json"
})
.done(res => console.log(res));

//wysyłanie danych i otrzymanie danych w formacie JSON
$.ajax({
    url : "example.php",
    method : "post",
    dataType : "json",
    data : {
        name : "Karol",
        age : 13
    }
})
.done(res => console.log(res));

Funkcje zwrotne

Jak widzisz w powyższych przykładach, po zakończeniu połączenia odpalana jest funkcja zwrotna done(), do której przekazywane są dane zwrócone z serwera (w naszym przypadku trafią pod zmienną res). Można powiedzieć, że jest to taki event load dla XMLHttpRequest.

Funkcje takie możemy podawać jako dodatkowe właściwości funkcji $.ajax(), ale też - tak jak powyżej - jako dodatkowe podawane po kropce funkcje. Są to kolejno:

done() wywoływane po pomyślnym wysłaniu danych do skryptu (kiedy serwer nie zwrócił błędu typu 400, 404, 500 itp)
fail() wywoływane gdy nastąpi błąd w połączeniu - np. 404 - nie ma takiego adresu
always() wywoływane po zakończeniu połączenia - bez względu czy zakończyło się błędem czy sukcesem

Użycie takich funkcji może mieć postać:


$.ajax({
    url : '...'
})
.done(function(res) {
    console.log(res); //dostaliśmy odpowiedź z serwera, wypisujemy ją
})
.fail(function() {
    alert("Wystąpił błąd w połączeniu");
})
.always(function() {
    $('.loading').hide();
});

Przykładowe zastosowania funkcji zwrotnych mogą mieć postać:


//wczytywanie danych z serwera
$.ajax({
    url : '...',
    dataType : "json"
})
.done(function(response) {
    console.log(response);
});

//wysyłanie danych na serwer i wypisanie zwracanej odpowiedzi
$.ajax({
    url : '...',
        method : "post",
        data : { //na serwer trafią dwie dane - name i pet
            name : "Marcin",
            pet : "Super Szamson"
        },
        dataType : "json" //oczekujemy odpowiedzi w formacie json
})
.done(function(response) {
    console.log(response);
})
.fail(function() {
    $('.form-message').html( "Wystąpił błąd" ); //pokazujemy użytkownikowi komunikat
})

//wysyłanie danych w formacie json i wypisanie zwracanej odpowiedzi
const ob = {
    name : "Marcin",
    pet : {
        name : "Szamson",
        color : "brown",
        speed : 1000000
    }
};

$.ajax({
    url : '...',
        method : "post",
        contentType : "application/json",
        data : JSON.stringify(ob),
        dataType : "json"
})
.done(response => { //funkcje srzałkowe jak najbardziej na propsie
    console.log(response);
})
.fail(() => {
    alert("Wystąpił błąd w połączeniu");
})
.always(() => {
    $('.loading').hide();
});

Prosty przykład pobierania danych

Dla utrwalenia poznanej wiedzy spróbujmy wykonać jakieś realne połączenie. Naszym celem będzie ściągnięcie danych z testowego API. Pod adresem https://jsonplaceholder.typicode.com/users mamy przykładową listę użytkowników. Po kliknięciu na przycisk pobierzmy ją i w liście wypiszmy imiona i emaile użytkowników:


<button class="button test-ajax" type="button">Kliknij i zobacz w konsoli debugera</button>
<ul class="user-list"></ul>

const apiUrl = "https://jsonplaceholder.typicode.com";
const $list = $('.user-list');

$('.test-ajax').on('click', function() {
    const $btn = $(this);

    //dodajemy klasę .loading do buttona, która doda mu ikonkę ładowania
    //i wyłączamy go by użytkownik nie był nadgorliwy
    $btn.addClass('loading');
    $btn.prop('disabled', true);

    //wykonujemy połączenie
    $.ajax({
        url : apiUrl + '/users',
        dataType : 'json'
    })
    .done((res) => {
        //czyścimy listę przed dodaniem użytkowników
        //inaczej ponowne kliknięcie duplikowało by użytkowników na liście
        $list.empty();

        //robimy pętlę po zwróconej tablicy
        //dołączając do listy kolejne LI z imieniem i emailem użytkownika
        res.forEach(el => {
            $list.append(`<li>${el.name} : ${el.email}</li>`);
        })
    })
    .always(() => {
        //po zakończeniu wczytywania wyłączamy loading
        //i włączamy przycisk
        $btn.removeClass('loading');
        $btn.prop('disabled', false);
    });
});

    Prosty przykład wysyłania danych

    Skoro udało nam się pobrać dane, spróbujmy je wysłać. Tutaj także skorzystamy z API https://jsonplaceholder.typicode.com/. Tym razem spróbujmy dodać nowy post wysyłając odpowiednie dane (ich strukturę możemy zobaczyć tutaj) na adres https://jsonplaceholder.typicode.com/posts. API pod tym adresem jest testowe, to znaczy, że dodany post nie zostanie zapisany realnie w bazie, ale API będzie symulowało takie działanie.

    
    <form class="form" id="formSubmit">
        <div class="form-row">
                <label for="inputTitle">Tytuł postu</label>
                <input type="text" id="inputTitle">
        </div>
        <div class="form-row">
                <label for="inpBody">Treść postu</label>
                <textarea id="inpBody"></textarea>
        </div>
        <div class="form-row">
                <button type="submit" class="button">Wyślij i sprawdź w konsoli odpowiedź</button>
        </div>
    </form>
    
    
    const apiUrl = "https://jsonplaceholder.typicode.com";
    
    //pobieramy wszystkie niezbędne elementy
    const $form = $('.form');
    const $inputTitle = $('#inpTitle');
    const $inputBody = $('#inpBody');
    const $submitBtn = $form.find(":submit");
    
    //podpinamy się pod wysłany formularz
    $form.on("submit", function(e) {
        e.preventDefault();
    
        //po kliknięciu wyłączam submit i dodaję mu loading
        $submitBtn.addClass('loading');
        $submitBtn.prop('disabled', true);
    
        //wysyłamy dane
        $.ajax({
            url: apiUrl + '/posts',
            method : "POST",
            dataType : "json",
            data : {
                userId : 1, //przykładowy user
                title : $inputTitle.val(), //wartości danych pobieram z pól
                body : $inputBody.val()
            }
        })
        .done(function(res) {
            console.log("Użytkownik został dodany do bazy", res);
        })
        .always(function() {
            //po zakończeniu połączenia włączam submit i wyłączam klasę loading
            $submitBtn.removeClass('loading');
            $submitBtn.prop('disabled', false);
        });
    });
    

    Skrócone funkcje

    jQuery udostępnia nam też kilka funkcji będących alternatywnym, skróconymi wariantami użycia powyżej funkcji $.ajax czyli:

    $.get()
    $.post()
    $.getJSON()
    $.getSCRIPT()
    $elemet.load()

    $.get()

    Funkcja $.get() jest skróconym zapisem wywołania funkcji $.ajax() w postaci:

    
    $.ajax({
        url: url,
        success: success, //funkcja wykonująca się po zakończeniu połączenia
        data: data, //opcjonalne - dane do wysłania
        dataType: dataType //opcjonalne - typ danych jakich oczekujemy
    });
    

    i służy do wykonywania połączeń typu get. Przykładowe zastosowania tej funkcji mogą mieć postać:

    
    //pobieranie danych
    $.get("example.php", function(res){
        console.log("Dane otrzymane: ", res);
    });
    
    
    //pobieranie danych z dodatkowym wysłaniem jakiś danych
    $.get("example.php", { page : 1 }, function(res){
        console.log("Dane otrzymane: ", res);
    });
    
    
    //pobieranie danych z wykorzystaniem funkcji done
    $.get("example.php", { page : 1})
    .done(resp => {
        console.log(resp);
    })
    .fail(() => {
        alert("Wystąpił błąd w połączeniu");
    })
    .always(() => {
        $('.loading').hide();
    });
    

    $.post()

    Funkcja $.post jest skróconym zapisem wywołania funkcji $.ajax w postaci:

    
    $.ajax({
        method: "POST",
        url: url,
        data: data, //wysyłane dane na serwer
        success: success,
        dataType: dataType
    });
    

    i służy do wykonywania połączeń typu post:

    
    //pobieramy html i wstawiamy go w element .result
    $.post( "ajax/test.html", function( data ) {
      $(".result").html( data );
    });
    
    
    //wysyłamy dane na serwer
    $.post("example.php", {
        name: "Johny",
        surname: "Bravo"
    });
    
    
    //wysyłamy dane i wypisujemy odpowiedź w funkcji done
    $.post("example.php", {
        name: "Johny",
        surname: "Bravo"
    })
    .done(resp => {
        console.log(resp);
    });
    

    $.getJSON()

    Funkcja $.getJSON() to skrócony zapis konstrukcji:

    
    $.ajax({
        dataType: "json",
        url: url,
        data: data,
        success: success
    });
    

    i służy do pobierania danych typu json. Przykładowe użycie tej funkcji może mieć postać:

    
    $.getJSON("test.json", function(res) {
      res.forEach(el => console.log(el) );
    });
    

    Podobnie do poprzednich funkcji możemy tutaj wysyłać dodatkowe dane i używać nowych funkcji zwrotnych:

    
    $.getJSON("test.json", { id : 100})
    .done(resp => {
        console.log(resp);
    })
    .fail(() => {
        alert("Wystąpił błąd w połączeniu");
    });
    

    $.getScript()

    Funkcja $.getSCRIPT() to skrócony zapis kodu:

    
    $.ajax({
        url: url,
        dataType: "script",
        success: success
    });
    

    i służy do wczytania i wykonania skryptu JavaScript. Jej przykładowe użycie może mieć postać:

    
    $.getScript("test.js"); //wczytanie i wykonanie skryptu
    

    I znowu - podobnie jak przy innych funkcjach możemy tutaj wysyłać dodatkowe dane i używać nowych funkcji zwrotnych:

    
    $.getScript("test.js")
    .done(function( script, textStatus ) {
        console.log( textStatus ); //treść skryptu
        console.log( textStatus ); //status wczytania - np. success
    })
    .fail(() => {
        alert("Wystąpił błąd w połączeniu");
    });
    

    $element.load()

    Ostatnią z omawianych funkcji jest $element.load(), która jest mniej więcej odpowiednikiem kodu $.get(url, data, success) i służy do wczytywania treści z serwera i umieszczenia jej w jakimś elemencie.

    Jej przykładowe użycie może mieć postać:

    
    //wczytuję plik article.html i umieszczam go w div #result
    $("#result").load( "article.html" );
    
    
    //wczytuję plik article.html ale tylko zawartość diva #content
    $("#result").load( "article.html #content" );
    
    
    //wczytuje treść artykułu przekazując dodatkowe dane na serwer - np. id wczytywanego artykułu
    $("#result").load( "article.php", { id: 100 } );
    
    
    //wczytuję dane przekazując dodatkowy parametr
    //dodatkowo po wczytaniu danych odpalam funkcję, która pokaże div#result
    $("#result").load("feeds.php", { limit: 25 }, function() {
        //funkcja zwrotna odpalana po wczytaniu
        $("#result").slideDown();
    });
    

    Przykład użycia metody load()

    Przykład użycia metody load możesz zobaczyć poniżej.

    Jest to prosta lista 3 linków prowadzących do stron z opisami bohaterów.
    Za pomocą jquery podpinamy im event kliknięcia. Przerywamy domyślą akcję (przenoszenie), a następnie za pomocą load wczytujemy treść z danego pliku html.
    W zakładce network debugera możesz sprawdzić jak wyglądają odpowiedzi:

    
    <ul class="tabs">
        <li class="tabs-el"><a href="data-batman.html" class="tabs-link">Batman</a></li>
        <li class="tabs-el"><a href="data-superman.html" class="tabs-link">Superman</a></li>
        <li class="tabs-el"><a href="data-flash.html" class="tabs-link">Flash</a></li>
    </ul>
    <div class="tabs-content"></div>
    
    
    const $links = $('.tabs a');
    $links.on("click", function(e) {
        e.preventDefault();
        //pobieram dokąd prowadzi link
        const href = $(this).attr('href');
    
        //wyłączam innym tabom klasę .tabs-el-active
        $(this).parent().siblings().removeClass('tabs-el-active');
        //i dodaję klikniętemu
        $(this).parent().addClass('tabs-el-active');
    
        //wczytuję treść
        $('.tabs-content').load(href + ' #content', function() {
            console.log("Wczytano treść");
        });
    });
    $links.eq(0).click(); //klikam na 1 link
    
    Powyższy skrypt ma tą zaletę, że użytkownik nawet bez CSS i JS będzie mógł dostać się do treści. Jeżeli JS nie zadziała, linki po prostu przeniosą użytkownika na daną podstronę. Użyteczność nie ta sama, ale dostanie się do treści możliwe. Mały detal a cieszy...

    Dynamiczne selekty z wykorzystaniem JSON

    Kolejny przykład wykorzystania powyższej wiedzy to mechanizm 2 zależnych od siebie selektów. Opcje w 2 selekcie będą zależne od tego, co wybierzemy w 1 selekcie.
    Poniższy przykład pokazuje, jak taki problem rozwiązać z wykorzystaniem json:

    
        <select id="select1" name="gender">
            <option value="-1">Wybierz płeć</option>
            <option value="k">Kobiety</option>
            <option value="m">Mężczyźni</option>
        </select>
    
        <select id="select1" disabled name="personNames">
            <option value="-1">---</option>
        </select>
    

    Mamy więc 2 selekty. Pierwszy z nich służy do wybrania płci, a drugi będzie służyć do wyboru imienia.
    Po wybraniu płci w 1 selekcie, sprawdzamy czy ma ona jakąś sensowną wartość (czyli jest różna od -1) i wysyłamy ją do serwera. Następnie aktywujemy drugi selekt i za pomocą pętli dynamicznie ustawiamy jego optiony.

    
    const $select1 = $('#select1');
    const $select2 = $('#select2');
    
    $select1.on('change', function() {
        const selectVal = $(this).find('option:selected').val();   //pobieramy wartość wybranego selekta
    
        if (selectVal !== -1) {  //jeżeli jest inna niż -1 (czyli jeżeli został wybrany model)
            $.ajax({
                type: "post",
                url: "skryptNaSerwerze.php",
                dataType : 'json',
                data: {
                    gender : selectVal
                }
            })
            .done(function(response) {
                $select2.prop('disabled', 0); //aktywujemy 2 selekt
                $select2.empty(); //czyścimy go
    
                response.forEach(function(el) {
                    $select2.append('<option value="'+el.value+'">'+el.name+'</option>');
                });
    
                //ustawiamy jako wybrany pierwszy option w selekcie 2
                $select2.find('option').eq(0).prop('selected', 1);
            })
            .fail(function() {
                console.warn('wystąpił błąd');
            })
        } else {
            $select2.empty();
            $select2.prop('disabled', 1);
        }
    });
    
    $select1.val(-1);
    $select2.prop('disabled', 1);
    

    Aby powyższy skrypt miał sens, musimy stworzyć skrypt skryptNaSerwerze.php, który na podstawie przesłanego POST będzie wysyłał do przeglądarki json z odpowiednimi wartościami, na podstawie których tworzymy nowe optiony. Poniżej przedstawiam wersję dla php

    
    <?php
    $gender = $_POST['gender'];
    
    $names = Array();
    
    $names['m'] = Array();
    $names['m'][0] = Array('name'=>'Arnold', 'value'=>'1');
    $names['m'][1] = Array('name'=>'Bartek', 'value'=>'2');
    $names['m'][2] = Array('name'=>'Darek', 'value'=>'3');
    $names['m'][3] = Array('name'=>'Kamil', 'value'=>'4');
    $names['m'][4] = Array('name'=>'Michał', 'value'=>'5');
    
    $names['k'] = Array();
    $names['k'][0] = Array('name'=>'Ania', 'value'=>'1');
    $names['k'][1] = Array('name'=>'Beata', 'value'=>'2');
    $names['k'][2] = Array('name'=>'Kamila', 'value'=>'3');
    $names['k'][3] = Array('name'=>'Patrycja', 'value'=>'4');
    $names['k'][4] = Array('name'=>'Zenona', 'value'=>'5');
    
    echo json_encode($names[$gender]);
    

    Sprawdź w konsoli (zakładka Network) jak doczytywane są dane.

    Dodatek: poprzednie wersje jQuery

    W poprzednich wersjach jQuery metody done(), fail() i always() istniały pod nazwami success, error i complete i były deklarowane jako parametry obiektu konfiguracyjnego funkcji $.ajax():

    
    $.ajax({
        url : '...',
        success : function(response) {...},
        error : function() {...},
        complete : function() {...}
    })
    

    Używanie funkcji zwrotnych w takiej postaci jest możliwe dalej, co zresztą robiliśmy kilka razy powyżej. Jeżeli wrócisz wzrokiem do powyższych opisów metod skróconych ($.get(), $.post() itp.), zobaczysz, że w tych skróconych metodach funkcje zwrotne podawaliśmy właśnie jako jeden z parametrów (przy czym mogliśmy też użyć nowszego podejścia).

    Od wersji jQuery 1.5 zostały wprowadzone omawiane powyżej alternatywne funkcje zwrotne done(), fail() i always(). Wiąże się to z wprowadzeniem do jQuery obietnic, dzięki którym możemy pisać przyjemniejszy kod przy pracy z asynchronicznością.

    W poprzednich wersjach gdy chcieliśmy wykonać jakąś funkcję po zakończeniu połączenia, musieliśmy ją odpalać z wnętrza ciała deklaracji danej funkcji asynchronicznej np.:

    
    $.ajax({
        url : "example.php",
        success : function(response) {
            printData(response);
        },
        error : function() {...},
        complete : function() {...}
    })
    
    
    $.get(url : "example.php", function(response) {
        printData(response);
    });
    

    Powodowało to, że nasz kod stawał się cięższy w utrzymaniu, ponieważ musieliśmy skakać wzrokiem po całym kodzie i przyglądać się wnętrzu danych funkcji:

    
    function printData(data) {
        console.log(data);
    }
    
    function loadData() {
        $.ajax({
            url : '...',
            success : function(resp) {
                printData(resp);
            }
        })
    }
    
    $('#button1').on('click', function() {
        loadData(); //stąd do loadData, a z loadData do printData1, a z printData1 do...
    });
    

    Ale i to nie koniec problemów.
    Wyobraź sobie, że powyższa funkcja loadData() powinna być funkcją uniwersalną, którą moglibyśmy wykorzystać do pobierania różnych danych.
    Po pobraniu danych powinniśmy móc wywoływać różne funkcje do obróbki zwróconych danych. Ale jak to zrobić - skoro w powyższym kodzie po wczytaniu danych zawsze odpalana jest ta sama funkcja printData()?. Problem ukazuje poniższy kod:

    
    function printData1(data) {
        console.log(data);
    }
    
    function printData2(data) {
        console.log(data);
    }
    
    function loadData() {
        $.ajax({
            url : '...',
            success : function(resp) {
                printData1(resp);
            }
        })
    }
    
    $('#button1').on('click', function() {
        //po wczytaniu danych chcę odpalić printData1 - tu się uda, bo powyżej właśnie ona jest odpalana
        //ale dalej musimy skakać po kodzie wzrokiem i analizować wnętrze loadData
        loadData();
    });
    
    $('#button2').on('click', function() {
        //po wczytaniu danych chcę odpalić printData2 - tylko jak????
        loadData();
    });
    

    Rozwiązaniem takiej sytuacji było zastosowanie funkcji zwrotnych (callback):

    
    function printData1(data) {
        console.log(data);
    }
    
    function printData2(data) {
        console.log(data);
    }
    
    function loadData(fn) {
        $.ajax({
            url : '...',
            success : function(resp) {
                fn(resp);
            }
        })
    }
    
    $('#button1').on('click', function() {
        //nie musimy skakać wzrokiem i po wczytaniu danych spokojnie odpalamy różne funkcje
        loadData(printData1);
        //lub
        loadData(function(data) {
            printData1(data);
        });
    });
    
    $('#button2').on('click', function() {
        loadData(printData2);
    });
    

    Dzięki nowemu podejściu powyższe zagadnienia możemy zapisać w o wiele prostszy sposób:

    
    function loadData() {
        return $.ajax({
            url : url
        });
    }
    
    $('#button1').on('click', function() {
        loadData().done(data => printData1(data));
    });
    
    $('#button2').on('click', function() {
        loadData().done(data => printData2(data));
    });
    

    Dodatkowo dzięki temu, że każda z omawianych funkcji zwraca obietnicę, bardzo łatwo możemy czekać na zakończenie kilku różnych połączeń, co nawet z wykorzystaniem funkcji zwrotnych nie było by najprzyjemniejsze.

    
    const getData1 = function() {
        return $.get({
            url : '...'
        })
    }
    
    const getData2 = function() {
        return $.post({
            url : '...'
        })
    }
    
    const getData3 = function() {
        return $.ajax({
            url : '...'
        })
    }
    
    //czekamy na skończenie działania naszych funkcji
    $.when(getData1, getData2, getData3).then((res1, res2, res3) => {
        console.log(res1[0], res2[0], res3[0]);
    }, () => {
        console.log('Fail: ', error, error.status, error.statusText);
    });