Skip to content

7. Wdrożenie technologii Ajax w aplikacji Spring MVC

7.1. Rola AJAX w aplikacji internetowej

Jak dotąd analizowane przykłady szkoleniowe miały następującą architekturę:

Aby przejść z widoku [Vue1] do widoku [Vue2], przeglądarka:

  • wysyła żądanie do aplikacji internetowej;
  • odbiera widok [Vue2] i wyświetla go zamiast widoku [Vue1].

Jest to klasyczny schemat:

  • żądanie z przeglądarki;
  • wygenerowanie widoku przez serwer WWW w odpowiedzi na żądanie klienta;
  • wyświetlenie tego nowego widoku przez przeglądarkę.

Od kilku lat istnieje inny sposób interakcji między przeglądarką a serwerem internetowym: AJAX (Asynchronous JavaScript and XML). W rzeczywistości chodzi o interakcje między widokiem wyświetlanym przez przeglądarkę a serwerem internetowym. Przeglądarka nadal wykonuje to, co potrafi, czyli wyświetla widok HTML, ale jest teraz sterowana przez kod JavaScript wbudowany w wyświetlany widok HTML. Schemat wygląda następująco:

  • w [1] na stronie wyświetlanej w przeglądarce ma miejsce zdarzenie (kliknięcie przycisku, zmiana tekstu itp.). Zdarzenie to jest przechwytywane przez kod JavaScript (jS) osadzony na stronie;
  • w [2] kod JavaScript wysyła żądanie HTTP, tak jak zrobiłaby to przeglądarka. Żądanie ma charakter asynchroniczny: użytkownik może nadal korzystać ze strony bez blokowania jej działania w oczekiwaniu na odpowiedź na żądanie HTTP. Żądanie przechodzi przez standardowy proces przetwarzania. Nic (lub prawie nic) nie odróżnia go od zwykłego żądania;
  • w [3] wysyłana jest odpowiedź do klienta jS. Zamiast pełnego widoku HTML wysyłany jest raczej widok częściowy HTML, strumień XML lub jSON (notacja obiektowa JavaScript);
  • w [4] skrypt JavaScript pobiera tę odpowiedź i wykorzystuje ją do aktualizacji określonego obszaru wyświetlanej strony HTML.

Z punktu widzenia użytkownika następuje zmiana widoku, ponieważ to, co widzi, uległo zmianie. Nie dochodzi jednak do całkowitego przeładowania strony, a jedynie do częściowej modyfikacji wyświetlanej strony. Przyczynia się to do płynności działania i interaktywności strony: ponieważ nie następuje całkowite odświeżenie strony, można sobie pozwolić na obsługę zdarzeń, których wcześniej nie obsługiwano. Na przykład można zaproponować użytkownikowi listę opcji w miarę wpisywania znaków w polu wprowadzania danych. Przy każdym nowym wpisanym znaku wysyłane jest żądanie AJAX do serwera, który następnie zwraca kolejne propozycje. Bez Ajaxa tego rodzaju pomoc przy wpisywaniu była wcześniej niemożliwa. Nie można było przeładowywać nowej strony przy każdym wpisanym znaku.

7.2. Aktualizacja strony za pomocą strumienia HTML

7.2.1. Widoki

Zamierzamy przeanalizować następującą aplikację:

  • w [1] – czas załadowania strony;
  • w [2] wykonuje się cztery operacje arytmetyczne na dwóch liczbach rzeczywistych A i B;
  • w [3] odpowiedź serwera zostaje zapisana w określonym obszarze strony;
  • w [4] – czas obliczeń. Różni się on od czasu załadowania strony [5]. Ten ostatni jest równy [1], co wskazuje, że obszar [6] nie został ponownie załadowany. Ponadto wartości URL i [7] na stronie nie uległy zmianie.

7.2.2. Akcja [/ajax-01]

  

Kontroler [Ajax.java] definiuje następującą akcję [/ajax-01]:


    @RequestMapping(value = "/ajax-01", method = RequestMethod.GET, produces = "text/html; charset=UTF-8")
    public String ajax01(Locale locale, Model modèle, HttpSession session, String tempo) {
        // czy tempo jest prawidłowe?
        if (tempo != null) {
            boolean valide = false;
            int valueTempo = 0;
            try {
                valueTempo = Integer.parseInt(tempo);
                valide = valueTempo >= 0;
            } catch (NumberFormatException e) {

            }
            if (valide) {
                session.setAttribute("tempo", new Integer(valueTempo));
            }
        }
        // przygotowuje się szablon widoku [vue-01]
        ...
}
  • wiersz 2: akcja [/ajax-01] akceptuje tylko jeden parametr [tempo]. Jest to czas w milisekundach, przez który serwer będzie musiał czekać przed wysłaniem wyników operacji arytmetycznych;
  • wiersz 4: parametr [tempo] jest opcjonalny;
  • wiersze 5–12: sprawdzane jest, czy wartość parametru [tempo] jest prawidłowa;
  • wiersze 13–15: jeśli tak jest, wartość opóźnienia jest zapisywana w sesji. Oznacza to, że będzie obowiązywać tak długo, dopóki nie zostanie zmieniona;

Kod akcji [/ajax-01] wygląda następująco:


    @RequestMapping(value = "/ajax-01", method = RequestMethod.GET, produces = "text/html; charset=UTF-8")
    public String ajax01(Locale locale, Model modèle, HttpSession session, String tempo) {
        // czy tempo jest prawidłowe?
...
        // przygotowywany jest szablon widoku [vue-01]
        modèle.addAttribute("actionModel01", new ActionModel01());
...
        // widok
        return "vue-01";
}

Klasa [ActionModel01] służy głównie do enkapsulacji wartości wysyłanych przez akcję [/ajax-01]. W tym przypadku nic nie jest wysyłane. Tworzymy pustą klasę, którą umieszczamy w modelu, ponieważ korzysta z niej widok [vue-01.xml]. Klasa [ActionModel01] wygląda następująco:


package istia.st.springmvc.models;

import javax.validation.constraints.DecimalMin;
import javax.validation.constraints.NotNull;

public class ActionModel01 {

    // dane przesłane
    @NotNull
    @DecimalMin(value = "0.0")
    private Double a;

    @NotNull
    @DecimalMin(value = "0.0")
    private Double b;

    // metody pobierające i ustawiające
    ...
}
  • wiersze 11 i 15: dwie wartości typu realnego [a,b], które zostaną przesłane przez formularz;

Wróćmy do kodu akcji:


    @RequestMapping(value = "/ajax-01", method = RequestMethod.GET, produces = "text/html; charset=UTF-8")
    public String ajax01(Locale locale, Model modèle, HttpSession session, String tempo) {
...
        // przygotowujemy szablon widoku [vue-01]
        modèle.addAttribute("actionModel01", new ActionModel01());
        Resultats résultats = new Resultats();
        modèle.addAttribute("resultats", résultats);
...
        // widok
        return "vue-01";
}
  • wiersze 6–7: umieszczamy instancję typu [Resultats] w szablonie;

Typ [Resultats] umieszczony w modelu wygląda następująco:

  

package istia.st.springmvc.models;

public class Resultats {

    // dane
    private String aplusb;
    private String amoinsb;
    private String amultiplieparb;
    private String adiviseparb;
    private String heureGet;
    private String heurePost;
    private String erreur;
    private String vue;
    private String culture;

    // metody pobierające i ustawiające
    ...
}
  • wiersze 6–9: wynik czterech operacji arytmetycznych na liczbach [a,b];
  • wiersz 10: czas pierwszego załadowania strony;
  • wiersz 11: czas wykonania czterech operacji arytmetycznych;
  • wiersz 12: ewentualny komunikat o błędzie;
  • wiersz 13: ewentualny widok, który ma zostać wyświetlony;
  • wiersz 14: typ widoku, [fr-FR] lub [en-US];

Kod akcji [/ajax-01] wygląda następująco:


    @RequestMapping(value = "/ajax-01", method = RequestMethod.GET, produces = "text/html; charset=UTF-8")
    public String ajax01(ActionModel01 formulaire, Locale locale, Model modèle, HttpSession session) {
        ...
        // ustawienia regionalne
        setLocale(locale, modèle, résultats);
...
}
  • wiersz 5: metoda [setLocale] służy do umieszczenia w szablonie widoku kultury, która ma być używana, [fr-FR] lub [en-US]. Kultura ta jest przeznaczona dla kodu JavaScript osadzonego w widoku;

Metoda [setLocale] wygląda następująco:


    private void setLocale(Locale locale, Model modèle, Resultats résultats) {
        // obsługujemy tylko ustawienia regionalne fr-FR i en-US
        String language = locale.getLanguage();
        String country = null;
        switch (language) {
        case "fr":
            country = "FR";
            break;
        default:
            language = "en";
            country = "US";
            break;
        }
        // kultura
        résultats.setCulture(String.format("%s-%s", language, country));
}

W szablonie pojawi się ciąg znaków [${resultats.culture}] równy „fr-FR” lub „en-US”.

Wróćmy do akcji [/ajax-01]:


@RequestMapping(value = "/ajax-01", method = RequestMethod.GET, produces = "text/html; charset=UTF-8")
    public String ajax01(ActionModel01 formulaire, Locale locale, Model modèle, HttpSession session) {
...
        // ustawienia regionalne
        setLocale(locale, modèle, résultats);
        // czas
        résultats.setHeureGet(new SimpleDateFormat("hh:mm:ss").format(new Date()));
        // widok
        return "vue-01";
    }
  • wiersz 7: umieszczamy godzinę z akcji GET w szablonie;
  • wiersz 9: wyświetla się widok [vue-01.xml]:

7.2.3. Widok [vue-01.xml]

Widok [vue-01.xml] wygląda następująco:


<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
    <head>
        <meta name="viewport" content="width=device-width" />
        <title>Ajax-01</title>
        <link rel="stylesheet" href="/css/ajax01.css" />
        <script type="text/javascript" src="/js/jquery/jquery-2.1.1.min.js"></script>
        <script type="text/javascript" src="/js/jquery/jquery.validate.min.js"></script>
        <script type="text/javascript" src="/js/jquery/jquery.validate.unobtrusive.min.js"></script>
        <script type="text/javascript" src="/js/jquery/globalize/globalize.js"></script>
        <script type="text/javascript" src="/js/jquery/globalize/cultures/globalize.culture.fr-FR.js"></script>
        <script type="text/javascript" src="/js/jquery/globalize/cultures/globalize.culture.en-US.js"></script>
        <script type="text/javascript" src="/js/jquery/jquery.unobtrusive-ajax.js"></script>
        <script type="text/javascript" src="/js/json3.js"></script>
        <script type="text/javascript" src="/js/client-validation.js"></script>
        <script type="text/javascript" src="/js/local1.js"></script>
        <script th:inline="javascript">
            /*<![CDATA[*/
                    var culture = [[${resultats.culture}]];
                    Globalize.culture(culture);
                    /*]]>*/
        </script>
    </head>
    <body>
        <h2>Ajax - 01</h2>
        <p>
            <strong th:text="#{labelHeureGetCulture(${resultats.heureGet},${resultats.culture})}">
                Heure de chargement :
            </strong>
        </p>
        <h4>
            <p th:text="#{titre.part1}">
                Opérations arithmétiques sur deux nombres réels A et B positifs ou nuls
            </p>
        </h4>
        <form id="formulaire" name="formulaire" ... ">
...
        </form>
        <hr />
        <div id="resultats" />
    </body>
</html>
  • wiersze 7–12: biblioteki jQuery do walidacji i internacjonalizacji (kultury);
  • wiersz 15: biblioteka [client-validation] utworzona w punkcie 6.3;
  • wiersz 14: biblioteka jSON wykorzystywana przez bibliotekę [client-validation]. Jest ona opcjonalna, jeśli logi walidacji zostały wyłączone;
  • wiersz 13: biblioteka [Unobtrusive Ajax] firmy Microsoft. Biblioteka ta pozwala czasami uniknąć pisania kodu JavaScript;
  • wiersz 16: plik jS na nasze własne potrzeby;
  • wiersze 17–22: do obsługi formatów dat po stronie klienta – [fr-FR] i [en-US]. Kod ten już wcześniej omawialiśmy;
  • wiersz 27: skonfigurowany komunikat. Omówiliśmy je w paragrafie 5.18;
  • wiersze 36–38: formularz, do którego jeszcze wrócimy;
  • wiersz 40: obszar dokumentu, w którym JavaScript umieści odpowiedź serwera;

7.2.4. Formularz

 

W widoku [vue-01.xml] formularz wygląda następująco:


<form id="formulaire" name="formulaire" th:action="@{/ajax-02.html}" method="post" th:object="${actionModel01}" th:attr="data-ajax='true',data-ajax-loading='#loading',data-ajax-loading-duration='0',data-ajax-method='post',data-ajax-mode='replace',data-ajax-update='#resultats', data-ajax-begin='beforeSend',data-ajax-complete='afterComplete' ">
    <table>
        <thead>
            <tr>
                <th>
                    <span th:text="#{valeur.a}"></span>
                </th>
                <th>
                    <span th:text="#{valeur.b}"></span>
                </th>
            </tr>
        </thead>
        <tbody>
            <tr>
                <td>
                    <input type="text" th:field="*{a}" th:value="*{a}" data-val="true"
                        th:attr="data-val-required=#{NotNull},data-val-number=#{typeMismatch},data-val-min=#{actionModel01.a.min},data-val-min-value=#{actionModel01.a.min.value}" />
                </td>
                <td>
                    <input type="text" th:field="*{b}" th:value="*{b}" data-val="true"
                        th:attr="data-val-required=#{NotNull},data-val-number=#{typeMismatch},data-val-min=#{actionModel01.b.min},data-val-min-value=#{actionModel01.b.min.value}" />
                </td>
            </tr>
            <tr>
                <td>
                    <span class="field-validation-valid" data-valmsg-for="a" data-valmsg-replace="true"></span>
                    <span th:if="${#fields.hasErrors('a')}" th:errors="*{a}" class="error">Donnée
                        erronée
                    </span>
                </td>
                <td>
                    <span class="field-validation-valid" data-valmsg-for="b" data-valmsg-replace="true"></span>
                    <span th:if="${#fields.hasErrors('b')}" th:errors="*{b}" class="error">Donnée
                        erronée
                    </span>
                </td>
            </tr>
        </tbody>
    </table>
    <p>
        <input type="submit" th:value="#{action.calculer}" value="Calculer"></input>
        <img id="loading" style="display: none" src="/images/loading.gif" />
        <a href="javascript:postForm()" th:text="#{action.calculer}">Calculer</a>
    </p>
</form>

który generuje następujący HTML:


<form id="formulaire" name="formulaire" method="post" data-ajax-update="#resultats" data-ajax-complete="afterComplete"     data-ajax-begin="beforeSend" data-ajax-loading-duration="0" data-ajax-mode="replace" data-ajax="true" data-ajax-method="post" data-ajax-loading="#loading" action="/ajax-02.html">
    <table>
        <thead>
            <tr>
                <th>
                    <span>valeur de A</span>
                </th>
                <th>
                    <span>valeur de B</span>
                </th>
            </tr>
        </thead>
        <tbody>
            <tr>
                <td>
                    <input type="text" data-val="true" data-val-min="Le nombre doit être supérieur ou égal à 0" data-val-number="Format invalide" data-val-min-value="0" data-val-required="Le champ est obligatoire" value="" id="a" name="a" />
                </td>
                <td>
                    <input type="text" data-val="true" data-val-min="Le nombre doit être supérieur ou égal à 0" data-val-number="Format invalide" data-val-min-value="0" data-val-required="Le champ est obligatoire" value="" id="b" name="b" />
                </td>
            </tr>
            <tr>
                <td>
                    <span class="field-validation-valid" data-valmsg-for="a" data-valmsg-replace="true"></span>

                </td>
                <td>
                    <span class="field-validation-valid" data-valmsg-for="b" data-valmsg-replace="true"></span>

                </td>
            </tr>
        </tbody>
    </table>
    <p>
        <input type="submit" value="Calculer" />
        <img id="loading" style="display: none" src="/images/loading.gif" />
        <a href="javascript:postForm()">Calculer</a>
    </p>
</form>
  • wiersz 16: do pola [a] przypisane są walidatory [required], [number] i [min];
  • wiersz 19: to samo dotyczy pola [b];

Różne komunikaty znajdują się w plikach [messages.properties] projektu:

  

[messages_fr.properties]


NotNull=Le champ est obligatoire
typeMismatch=Format invalide
actionModel01.a.min=Le nombre doit être supérieur ou égal à 0
DecimalMin.actionModel01.a=Le nombre doit être supérieur ou égal à 0
DecimalMax.actionModel01.b=Le nombre doit être supérieur ou égal à 0
actionModel01.b.min=Le nombre doit être supérieur ou égal à 0
valeur.a=valeur de A
valeur.b=valeur de B
actionModel01.a.min.value=0
actionModel01.b.min.value=0
labelHeureCalcul=Heure de calcul : 
LabelErreur=Une erreur s''est produite : [{0}]
labelAplusB=A+B=
labelAmoinsB=A-B=
labelAfoisB=A*B=
labelAdivB=A/B=
titre.part1=Opérations arithmétiques sur deux nombres réels A et B positifs ou nuls
labelHeureGetCulture=Heure de chargement : [{0}], culture : [{1}]
action.calculer=Calculer
erreur.aleatoire=erreur aléatoire
resultats=Résultats
resultats.erreur=Une erreur s''est produite : [{0}]
resultats.titre=Résultats
message.zone=Nombre d'accès : 

[messages_en.properties]


NotNull=Required field
typeMismatch=Invalid format
actionModel01.a.min=The number must be greater or equal to 0
DecimalMin.actionModel01.a=The number must be greater or equal to 0
DecimalMax.actionModel01.b=The number must be greater or equal to 0
actionModel01.b.min=The number must be greater or equal to 0
valeur.a=A value
valeur.b=B value
actionModel01.a.min.value=0
actionModel01.b.min.value=0
labelHeureCalcul=Computing hour: 
LabelErreur=There was an error: [{0}]
labelAplusB=A+B=
labelAmoinsB=A-B=
labelAfoisB=A*B=
labelAdivB=A/B=
titre.part1=Arithmetic operations on two positive or equal to zero real numbers
labelHeureGetCulture=Loading hour: [{0}], culture: [{1}]
action.calculer=Calculate
erreur.aleatoire=randomly generated error
resultats=Results
resultats.erreur=Some error occurred : [{0}]
resultats.titre=Results
message.zone=Number of hits:

Przeanalizujmy teraz atrybuty tagu [form]:


<form id="formulaire" name="formulaire" method="post" data-ajax-update="#resultats" data-ajax-complete="afterComplete" data-ajax-begin="beforeSend" data-ajax-loading-duration="0" data-ajax-mode="replace" data-ajax="true" data-ajax-method="post" data-ajax-loading="#loading" action="/ajax-02.html">

Rozpoznajemy klasyczne atrybuty tagu [form]:


<form id="formulaire" name="formulaire" method="post" action="/ajax-02.html">

Od razu można zauważyć, że jeśli w przeglądarce wyświetlającej stronę JavaScript jest wyłączony, formularz zostanie wysłany do URL [/ajax-02.html]. Teraz przeanalizujmy pozostałe atrybuty:


<form ... data-ajax-update="#resultats" data-ajax-complete="afterComplete" data-ajax-begin="beforeSend" data-ajax-loading-duration="0" data-ajax-mode="replace" data-ajax="true" data-ajax-method="post" data-ajax-loading="#loading">

Atrybuty [data-ajax-xxx] są obsługiwane przez bibliotekę jS [unobtrusive-ajax], która została zaimportowana przez widok [vue-01.xml]:


<script type="text/javascript" src="/js/jquery/jquery.unobtrusive-ajax.js"></script>

Gdy występują atrybuty [data-ajax-xxx], element [submit] formularza zostanie uruchomiony poprzez wywołanie Ajax biblioteki [unobtrusive-ajax]. Znaczenie parametrów jest następujące:

  • [data-ajax="true"]: to właśnie obecność tego atrybutu powoduje, że element [submit] w formularzu zostanie przetworzony za pomocą AJAX;
  • [data-ajax-method="post"]: metoda [submit]. URL w żądaniu POST będzie odpowiadać wartości atrybutu [action="/ajax-02.html"];
  • [data-ajax-loading="#loading"]: identyfikator pola, które ma zostać wyświetlone w oczekiwaniu na odpowiedź serwera. Pole oznaczone identyfikatorem [loading] w widoku [vue-01.xml] jest następujące:

<img id="loading" style="display: none" src="/images/loading.gif" />

Jest to animowany obrazek oczekiwania, który będzie wyświetlany do momentu otrzymania odpowiedzi z serwera;

  • [data-ajax-loading-duration="0"]: czas oczekiwania w milisekundach, po upływie którego zostanie wyświetlony obszar [data-ajax-loading="#loading"]. W tym przypadku zostanie on wyświetlony natychmiast po rozpoczęciu oczekiwania;
  • [data-ajax-begin="beforeSend"]: funkcja jS, która ma zostać wykonana przed uruchomieniem funkcji [submit];
  • [data-ajax-complete="afterComplete"]: funkcja jS, która ma zostać wykonana po otrzymaniu odpowiedzi;
  • [data-ajax-update="#resultats"]: identyfikator pola, w którym zostanie umieszczony wynik przesłany przez serwer. Widok [vue-01.xml] zawiera następujące pole:

<div id="resultats" />
  • [data-ajax-mode="replace"]: tryb wstawiania wyniku do poprzedniego pola. Tryb [replace] spowoduje, że wynik „nadpisze” to, co znajdowało się wcześniej w polu o identyfikatorze [resultats];

Należy zauważyć, że kod JavaScript [submit] zostanie wykonany tylko wtedy, gdy walidatory uznają testowane wartości za prawidłowe.

Biblioteka jS [unobtrusive-ajax] ma dwa cele:

  • zapewnienie, że formularz prawidłowo dostosowuje się do obu możliwości: włączenia lub wyłączenia JavaScript w przeglądarce;
  • uniknięcie pisania kodu JavaScript. Zobaczymy, że w tym przypadku nie dało się tego uniknąć.

7.2.5. Akcja [/ajax-02]

Widzieliśmy, że przesłane wartości były kierowane do akcji [/ajax-02]. Wygląda ona następująco:


@RequestMapping(value = "/ajax-02", method = RequestMethod.POST, produces = "text/html; charset=UTF-8")
    public String ajax02(ActionModel01 formulaire, Locale locale, Model modèle, HttpSession session) throws InterruptedException {
        // tempo?
        Integer tempo = (Integer) session.getAttribute("tempo");
        if (tempo != null && tempo > 0) {
            Thread.sleep(tempo);
        }
        // przygotowujemy szablon kolejnego widoku
        Resultats résultats = new Resultats();
        modèle.addAttribute("resultats", résultats);
        // ustalamy ustawienia regionalne
        setLocale(locale, modèle, résultats);
        // godzina
        résultats.setHeurePost(new SimpleDateFormat("hh:mm:ss").format(new Date()));
        ...
}
  • na początek uprościmy sprawę: zakładamy, że akcja POST została rzeczywiście wywołana przez kod JavaScript w widoku [vue-01.xml]. Do tego założenia wrócimy nieco później;
  • wiersz 2: wartości [a,b] przesłane do serwera są umieszczane w szablonie [ActionModel01];
  • wiersze 4–7: jeśli użytkownik ustawił opóźnienie podczas poprzedniego GET, jest ono pobierane z sesji i następuje opóźnienie (wiersz 6). Ma to na celu umożliwienie użytkownikowi zobaczenia efektu działania atrybutu [data-ajax-loading="#loading"] w formularzu;
  • wiersze 9–10: do szablonu dodawany jest atrybut [resultats];
  • wiersz 12: do szablonu dodaje się ustawienie regionalne [fr-FR] lub [en-US];
  • wiersz 14: należy wprowadzić godzinę POST do szablonu;

Przypomnijmy typ [Resultats] umieszczony w szablonie:


public class Resultats {

    // dane
    private String aplusb;
    private String amoinsb;
    private String amultiplieparb;
    private String adiviseparb;
    private String heureGet;
    private String heurePost;
    private String erreur;
    private String vue;
    private String culture;

    // metody pobierające i ustawiające
...
}

Kod akcji [/ajax-02] wygląda następująco:


@RequestMapping(value = "/ajax-02", method = RequestMethod.POST, produces = "text/html; charset=UTF-8")
    public String ajax02(ActionModel01 formulaire, BindingResult result, Locale locale, Model modèle,    HttpSession session) throws InterruptedException {
...
        résultats.setHeurePost(new SimpleDateFormat("hh:mm:ss").format(new Date()));
        // generuje się błąd co drugi raz
        int val = new Random().nextInt(2);
        if (val == 0) {
            // zwracamy komunikat o błędzie
            résultats.setErreur("erreur.aleatoire");
            return "vue-03";
        }
...
    }
  • wiersze 6–11: w ramach przykładu pokazano, jak zwrócić klientowi stronę błędu jS. Co drugie wywołanie zwracany jest następujący widok [vue-03.xml]:

Warto zwrócić uwagę na wiersz 9 – w szablonie umieszczamy nie sam komunikat, lecz klucz komunikatu:

[messages_fr.properties]


erreur.aleatoire=erreur aléatoire

[messages_fr.properties]


erreur.aleatoire=randomly generated error

Kod widoku [vue-03.xml] wygląda następująco:


<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
    <body>
        <h4>Résultats</h4>
        <p>
            <strong>
                <span th:text="#{labelHeureCalcul}">Heure de calcul :</span>
                <span id="heureCalcul" th:text="${resultats.heurePost}"></span>
            </strong>
        </p>
        <p style="color: red;">
            <span th:text="#{LabelErreur(#{${resultats.erreur}})}">Une erreur s'est produite :</span>
            <!-- <span id="błąd" th:text="${resultats.erreur}"></span> -->
        </p>
    </body>
</html>

  • w wierszu 12 można zauważyć komunikat skonfigurowany za pomocą klucza komunikatu, który sam w sobie jest obliczany. Pojęcie to wprowadziliśmy w paragrafie 5.18 na stronie 170.

Kod akcji [/ajax-02] wygląda następująco:


@RequestMapping(value = "/ajax-02", method = RequestMethod.POST, produces = "text/html; charset=UTF-8")
    public String ajax02(ActionModel01 formulaire, BindingResult result, Locale locale, Model modèle,    HttpSession session) throws InterruptedException {
...
        // pobierane są wartości przesłane
        double a = formulaire.getA();
        double b = formulaire.getB();
        // tworzy się szablon
        résultats.setAplusb(String.valueOf(a + b));
        résultats.setAmoinsb(String.valueOf(a - b));
        résultats.setAmultiplieparb(String.valueOf(a * b));
        try {
            résultats.setAdiviseparb(String.valueOf(a / b));
        } catch (RuntimeException e) {
            résultats.setAdiviseparb("NaN");
        }
        // wyświetla się widok
        return "vue-02";
    }
  • wiersze 5–15: cztery operacje arytmetyczne są wykonywane na liczbach [a,b] i zawarte w instancji [Resultats] modelu;
  • wiersz 17: zwracany jest następujący widok [vue-02.xml]:

Widok [vue-02.xml] wygląda następująco:


<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
    <body>
        <h4>Résultats</h4>
        <p>
            <strong>
                <span th:text="#{labelHeureCalcul}">Heure de calcul :</span>
                <span id="heureCalcul" th:text="${resultats.heurePost}"></span>
            </strong>
        </p>
        <p>
            <span th:text="#{labelAplusB}">A+B=</span>
            <span id="aplusb" th:text="${resultats.aplusb}"></span>
        </p>
        <p>
            <span th:text="#{labelAmoinsB}">A-B=</span>
            <span id="amoinsb" th:text="${resultats.amoinsb}"></span>
        </p>
        <p>
            <span th:text="#{labelAfoisB}">A*B=</span>
            <span id="amultiplieparb" th:text="${resultats.amultiplieparb}"></span>
        </p>
        <p>
            <span th:text="#{labelAdivB}">A/B=</span>
            <span id="adiviseparb" th:text="${resultats.adiviseparb}"></span>
        </p>
    </body>
</html>

Niezależnie od tego, czy wynikiem jest widok [vue-02.xml], czy widok [vue-03.xml], wynik ten, HTML, jest umieszczany w polu oznaczonym jako [resultats] w widoku [vue-01.xml], co wynika z atrybutu [data-ajax-update="#resultats"] formularza.

7.2.6. POST zawierający wprowadzone wartości

Mamy tu problem z wprowadzonymi wartościami. Pracujemy z dwoma formatami [fr-FR] i [en-US], które w różny sposób zapisują liczby rzeczywiste. Zajmowaliśmy się już tą trudnością, gdy w paragrafie 6.3 na stronie 190 trzeba było wprowadzić liczby rzeczywiste w dwóch różnych formatach. W tym miejscu ponownie wykorzystamy narzędzia zastosowane wówczas. Mamy jednak dodatkową trudność: nie mamy dostępu do metody, która przetwarza wprowadzone wartości w formacie POST. Z tego powodu dodaliśmy następujące atrybuty do tagu formularza:

  • [data-ajax-begin="beforeSend"]: funkcja jS, która ma zostać wykonana przed przeprowadzeniem operacji [submit];
  • [data-ajax-complete="afterComplete"]: funkcja jS, która ma zostać wykonana po otrzymaniu odpowiedzi;

Nie mamy dostępu do funkcji jS, która ma wysłać wprowadzone wartości, ale możemy napisać dwie funkcje jS:

  • [beforeSend]: funkcja jS wykonywana przed funkcją POST;
  • [afterComplete]: funkcja jS wykonywana po otrzymaniu odpowiedzi na POST;

Te dwie funkcje znajdują się w pliku [local1.js]:

  

Plik [local1.js] inicjuje środowisko jS widoku [vue-01.xml] w następujący sposób:


// dane globalne
var loading;
var formulaire;
var résultats;
var a, b;

// podczas ładowania dokumentu
$(document).ready(function() {
    // pobieramy odniesienia do poszczególnych komponentów strony
    loading = $("#loading");
    formulaire = $("#formulaire");
    resultats = $('#wyniki');
    a = $("#a");
    b = $("#b");
    // ukrywa się niektóre elementy
    loading.hide();
    // analizowane są walidatory formularza
    $.validator.unobtrusive.parse(formulaire);
    // obsługujemy dwa ustawienia regionalne [fr_FR, en_US]
    // rzeczywiste dane [a,b] są wysyłane przez serwer w formacie anglosaskim
    // w razie potrzeby przekształca się je do formatu francuskiego
    checkCulture(2);
});
  • wiersz 22: funkcja [checkCulture] została przedstawiona nieco dalej;

Funkcja jS [beforeSend] będzie wyglądać następująco:


function beforeSend(jqXHR, settings) {
    // przed POST
    // liczby muszą być zapisane w formacie anglosaskim
    var culture = Globalize.culture().name;
    if (culture === 'fr-FR') {
        checkCulture(1);
        settings.data = formulaire.serialize();
    }
}

function afterComplete(jqXHR, settings) {
    ...
}

function checkCulture(mode) {
    if (mode == 1) {
        // liczby [a,b] należy zapisać w formacie anglosaskim
        var value1 = a.val().replace(",", ".");
        a.val(value1);
        var value2 = b.val().replace(",", ".");
        b.val(value2);
    }
    if (mode == 2) {
...
    }
}
  • wiersze 4–6: sprawdzamy, czy kultura widoku to [fr-FR]. W takim przypadku należy zmienić wartości przesyłane. Jeśli bowiem użytkownik wprowadził [1,6], należy przesłać wartość [1.6]; w przeciwnym razie wartość [1,6] zostanie odrzucona po stronie serwera. W tym celu wystarczy zamienić przecinek w wysyłanych wartościach na kropkę (wiersze 18–21);
  • nie można jednak na tym poprzestać. W rzeczywistości, gdy wywoływana jest funkcja [beforeSend], ciąg wartości wysyłanych [a=val1&b=valB] został już utworzony. Musimy więc ją zmodyfikować. Odbywa się to za pomocą drugiego parametru funkcji [settings];
  • wiersz 7: [settings.data] (settings jest parametrem funkcji) reprezentuje wysyłany ciąg znaków. Ciąg ten odtwarzamy za pomocą wyrażenia [formulaire.serialize()]. Wyrażenie to przeszukuje formularz w poszukiwaniu wartości do przesłania i tworzy ciąg POST. Następnie pobiera nowe wartości z [a,b] wraz z kropkami dziesiętnymi;

Jeśli nie podejmie się żadnych dalszych działań, serwer wyśle swoją odpowiedź, która zostanie poprawnie wyświetlona. Tylko że teraz wartości w [a,b] zawierają kropkę dziesiętną, podczas gdy nadal obowiązuje format [fr-FR]. Jeśli więc użytkownik tego nie zauważy i ponownie kliknie na [Calculer], walidatory poinformują go, że wartości [a,b] są nieprawidłowe. Co jest słuszne. W tym momencie uruchamia się funkcja [afterComplete], wykonywana po otrzymaniu wyniku:


function beforeSend(jqXHR, settings) {
    // przed POST
...
}

function afterComplete(jqXHR, settings) {
    // po POST
    // w razie potrzeby liczby należy ponownie przekształcić do formatu francuskiego
    var culture = Globalize.culture().name;
    if (culture === 'fr-FR') {
        checkCulture(2);
    }
}

function checkCulture(mode) {
    if (mode == 1) {
...
    }
    if (mode == 2) {
        // liczby są przekształcane na format francuski
        var value1 = a.val().replace(".", ",");
        a.val(value1);
        var value2 = b.val().replace(".", ",");
        b.val(value2);
    }
}
  • wiersze 9–12: jeśli format widoku to [fr-FR], liczby [a,b] są przekształcane do formatu francuskiego.

7.2.7. Testy

Oto kilka zrzutów ekranu z testów:

  • w [1] – odpowiedź serwera;
  • w [2] – odpowiedź serwera z komunikatem o błędzie;
  • w [3] ustalamy opóźnienie wynoszące 5 sekund. Oznacza to, że serwer odczeka 5 sekund przed wysłaniem odpowiedzi. W tagu [form] użyliśmy atrybutu [data-ajax-loading='#loading']. Parametr [loading] jest identyfikatorem obszaru, który jest:
    • wyświetlana przez cały czas oczekiwania;
    • ukrywana po otrzymaniu odpowiedzi z serwera;

W tym przypadku [loading] jest identyfikatorem animowanego obrazu, który jest widoczny w [4].

7.2.8. Wyłączenie obsługi JavaScript w kontekście [en-US]

Co się stanie, jeśli wyłączymy JavaScript w przeglądarce?

Przetwarzanie wprowadzonych wartości POST odbędzie się zgodnie z tagiem [form], którego atrybuty [data-ajax-attr] nie będą wykorzystywane. Wszystko przebiega tak, jakbyśmy mieli następujący tag [form]:


<form id="formulaire" name="formulaire" method="post" action="/ajax-02.html">

Wprowadzone wartości zostaną zatem przesłane do akcji [/ajax-02]. Nie zostaną one zweryfikowane po stronie klienta. Zatem zadziałają walidatory po stronie serwera. Działały one już wcześniej, ale w odniesieniu do wartości, które zostały wcześniej zweryfikowane po stronie klienta, a więc były poprawne. Teraz tak nie jest.

Modyfikujemy akcję [/ajax-02] w następujący sposób:


@RequestMapping(value = "/ajax-02", method = RequestMethod.POST, produces = "text/html; charset=UTF-8")
    public String ajax02(@Valid ActionModel01 formulaire, BindingResult result, Locale locale, Model modèle,    HttpSession session, HttpServletRequest request) throws InterruptedException {
        // żądanie Ajax?
        boolean isAjax = "XMLHttpRequest".equals(request.getHeader("X-Requested-With"));
        ...
    }
  • wiersz 4: akcja [/ajax-02] może być teraz wywoływana za pomocą POST Ajax lub za pomocą klasycznego POST. Musimy umieć rozróżnić te dwa przypadki. Robimy to na podstawie nagłówków HTTP wysyłanych przez przeglądarkę klienta;

Gdy przyjrzymy się komunikacji sieciowej w konsoli deweloperskiej przeglądarki Chrome (Ctrl-Shift-I) przy włączonej obsłudze JavaScript, zauważymy, że klient wysyła następujące nagłówki podczas wywołania POST:

Z powyższego wynika, że:

  • został wysłany nagłówek [X-Requested-With] [1];
  • parametr [X-Requested-With] został dodany do wartości przesłanych w [2];

Nie ma to miejsca w przypadku klasycznego nagłówka POST. Mamy zatem dwie możliwości odzyskania informacji: z nagłówków HTTP lub z przesłanych wartości. W wierszu 4 akcji [/ajax-02] wybrano pierwsze rozwiązanie.

Przejdźmy dalej do kodu tej akcji:


@RequestMapping(value = "/ajax-02", method = RequestMethod.POST, produces = "text/html; charset=UTF-8")
    public String ajax02(@Valid ActionModel01 formulaire, BindingResult result, Locale locale, Model modèle, HttpSession session, HttpServletRequest request) throws InterruptedException {
        // żądanie Ajax?
        boolean isAjax = "XMLHttpRequest".equals(request.getHeader("X-Requested-With"));
        // tempo?
        Integer tempo = (Integer) session.getAttribute("tempo");
        if (tempo != null && tempo > 0) {
            Thread.sleep(tempo);
        }
        // przygotowuje się szablon następnego widoku
        Resultats résultats = new Resultats();
        modèle.addAttribute("resultats", résultats);
        // ustawiamy ustawienia regionalne
        setLocale(locale, modèle, résultats);
        // godzina
        String heure = new SimpleDateFormat("hh:mm:ss").format(new Date());
        résultats.setHeurePost(heure);
        résultats.setHeureGet(heure);
        // prawidłowe żądanie?
        if (!isAjax && result.hasErrors()) {
            return "vue-01";
        }
...
  • wiersz 2: parametr [@Valid ActionModel01 formulaire] uruchamia walidatory po stronie serwera;
  • wiersze 20–22: jeśli wywołanie nie jest wywołaniem Ajax, a walidacja zakończyła się niepowodzeniem, zwracany jest widok [vue-01.xml] wraz z komunikatami o błędach.

Oto przykład:

Przejdźmy do analizy akcji [/ajax-02]:


@RequestMapping(value = "/ajax-02", method = RequestMethod.POST, produces = "text/html; charset=UTF-8")
    public String ajax02(@Valid ActionModel01 formulaire, BindingResult result, Locale locale, Model modèle,    HttpSession session, HttpServletRequest request) throws InterruptedException {
        // żądanie Ajax?
        boolean isAjax = "XMLHttpRequest".equals(request.getHeader("X-Requested-With"));
...
        // czy żądanie jest prawidłowe?
        if (!isAjax && result.hasErrors()) {
            return "vue-01";
        }
        // generowany jest błąd co drugie wywołanie
        int val = new Random().nextInt(2);
        if (val == 0) {
            // zwracany jest komunikat o błędzie
            résultats.setErreur("erreur.aleatoire");
            if (isAjax) {
                return "vue-03";
            } else {
                résultats.setVue("vue-03");
                return "vue-01";
            }
        }
...
  • wiersz 14: generowany jest losowy błąd;
  • wiersz 16: w przypadku wywołania Ajax zwracany jest widok [vue-03.xml], który zostanie umieszczony w obszarze oznaczonym przez [resultats];
  • wiersz 18: w przypadku wywołania innego niż Ajax umieszczamy widok do wyświetlenia w szablonie typu [Resultats];
  • wiersz 19: ponownie zwracamy widok [vue-01.xml];

Widok [vue-01.xml] zostaje zmodyfikowany w następujący sposób:


<div id="resultats" />
<div th:if="${resultats.vue}=='vue-02'" th:include="vue-02" />
<div th:if="${resultats.vue}=='vue-03'" th:include="vue-03" />
  • wiersz 3: widok [vue-03.xml] zostanie wstawiony poniżej obszaru [resultats];

Oto przykład:

Należy zauważyć, że od tej pory widoki [1] i [2] są identyczne.

Przejdźmy do analizy akcji [/ajax-02]:


    @RequestMapping(value = "/ajax-02", method = RequestMethod.POST, produces = "text/html; charset=UTF-8")
    public String ajax02(@Valid ActionModel01 formulaire, BindingResult result, Locale locale, Model modèle, HttpSession session, HttpServletRequest request) throws InterruptedException {
        // żądanie Ajax?
        boolean isAjax = "XMLHttpRequest".equals(request.getHeader("X-Requested-With"));
...
        // pobieramy wartości przesłane metodą POST
        double a = formulaire.getA();
        double b = formulaire.getB();
        // tworzy się model
        résultats.setAplusb(String.valueOf(a + b));
        résultats.setAmoinsb(String.valueOf(a - b));
        résultats.setAmultiplieparb(String.valueOf(a * b));
        try {
            résultats.setAdiviseparb(String.valueOf(a / b));
        } catch (RuntimeException e) {
            résultats.setAdiviseparb("NaN");
        }
        // wyświetlanie widoku
        if (isAjax) {
            return "vue-02";
        } else {
            résultats.setVue("vue-02");
            return "vue-01";
        }
}
  • wiersze 7–17: wyniki czterech operacji arytmetycznych są umieszczane w modelu;
  • wiersze 22–23: generowany jest widok [vue-01.xml] (wiersz 22) poprzez wstawienie do niego widoku [vue-02.xml] (wiersz 22);

Wstawienie to odbywa się w następujący sposób w widoku [vue-01.xml]:


<div id="resultats" />
<div th:if="${resultats.vue}=='vue-02'" th:include="vue-02" />
<div th:if="${resultats.vue}=='vue-03'" th:include="vue-03" />
  • wiersz 2: widok [vue-02.xml] zostanie wstawiony poniżej obszaru [resultats];

Oto przykład wykonania:

 

7.2.9. Wyłączenie JavaScriptu przy kulturze [fr-FR]

W przypadku ustawienia kulturowego [fr-FR] występuje następujący problem:

Wartości wprowadzone w formacie francuskim zostały uznane za nieprawidłowe. Serwer oczekuje bowiem liczb rzeczywistych w formacie anglosaskim. Rozwiązanie jest dość złożone. Utworzymy filtr, który będzie:

  • przechwyci żądanie;
  • zmieni przecinki w wysyłanych wartościach [a] i [b] na kropki dziesiętne;
  • a następnie przekaże nowe żądanie do akcji, która ma je przetworzyć;

Na początek dodajemy ukryte pole w widoku [vue-01.xml]:


<form ...>
...
</p>
    <!-- pola ukryte -->
    <input type="hidden" id="culture" name="culture" th:value="${resultats.culture}"></input>
</form>
  • wiersz 5: wartość pola [fr-FR] lub [en-US] jest umieszczana w polu atrybutu [name=culture]. Ponieważ tag [input] znajduje się w formularzu, jego wartość zostanie przesłana wraz z wartościami [a] i [b]. Otrzymamy wówczas przesłany ciąg znaków o postaci:
culture=fr-FR&a=12,7&b=20,78

Ważne jest, aby zrozumieć tę kwestię.

Następnie dodajemy filtr do konfiguracji aplikacji:

  

Plik [Config] zostaje zmodyfikowany w następujący sposób:


@Configuration
@ComponentScan({ "istia.st.springmvc.controllers", "istia.st.springmvc.models" })
@EnableAutoConfiguration
public class Config extends WebMvcConfigurerAdapter {
...
    @Bean
    public Filter cultureFilter() {
        return new CultureFilter();
    }
}
  • wiersz 7: fakt, że bean [cultureFilter] zwraca typ [Filter], sprawia, że jest on filtrem. Sam bean może nosić dowolną nazwę;

Kolejnym krokiem jest utworzenie samego filtra:

  

package istia.st.springmvc.config;

import java.io.IOException;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.web.filter.OncePerRequestFilter;

public class CultureFilter extends OncePerRequestFilter {

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {
        // kolejny handler
        filterChain.doFilter(new CultureRequestWrapper(request), response);
    }
}
  • wiersz 12: rozszerzamy klasę [OncePerRequestFilter], która jest klasą Spring, a naszym zadaniem jest przedefiniowanie metody [doFilterInternal] tej klasy;
  • wiersz 15: metoda [doFilterInternal] otrzymuje trzy parametry:
    • [HttpServletRequest request]: żądanie, które ma zostać przefiltrowane. Nie można go modyfikować,
    • [HttpServletResponse response]: odpowiedź, która zostanie wysłana do serwera. Filtr może zdecydować o samodzielnym jej wygenerowaniu,
    • [FilterChain filterChain]: łańcuch filtrów. Gdy metoda [doFilterInternal] zakończy swoją pracę, musi przekazać żądanie do następnego filtra w łańcuchu filtrów;
  • wiersz 18: tworzymy nowe żądanie na podstawie otrzymanego żądania [new CultureRequestWrapper(request)] i przekazujemy je do następnego filtra. Ponieważ nie można modyfikować pierwotnego żądania [HttpServletRequest request], tworzymy nowe;

Klasa [CultureRequestWrapper] ma następującą postać:

  

package istia.st.springmvc.config;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;

public class CultureRequestWrapper extends HttpServletRequestWrapper {

    public CultureRequestWrapper(HttpServletRequest request) {
        super(request);
    }

    @Override
    public String[] getParameterValues(String name) {
        // wartości przesłane a i b
        if (name != null && (name.equals("a") || name.equals("b"))) {
            String[] values = super.getParameterValues(name);
            String[] newValues = values.clone();
            newValues[0] = newValues[0].replace(",", ".");
            return newValues;
        }
        // inne przypadki
        return super.getParameterValues(name);
    }

}
  • wiersz 6: klasa [CultureRequestWrapper] dziedziczy po klasie [HttpServletRequestWrapper] i będzie redefiniować niektóre z jej metod;
  • wiersze 8–10: konstruktor, który odbiera żądanie do przefiltrowania i przekazuje je do klasy nadrzędnej;
  • należy tu pamiętać, że przefiltrowane żądanie ostatecznie trafi jako parametr wejściowy do klasy zwanej serwletem. W ramach Springa MVC ten serwlet jest typu [DispatcherServlet]. Klasa ta dysponuje różnymi metodami służącymi do pobierania parametrów żądania: [getParameter, getParameterMap, getParameterNames, getParameterValues, ...]. Należy przedefiniować metodę używaną przez serwlet. W tym celu należałoby zapoznać się z kodem klasy [DispatcherServlet]. Nie zrobiłem tego i przedefiniowałem różne metody. Ostatecznie przedefiniowano metodę [getParameterValues];
  • wiersz 13: metoda [getParameterValues] przyjmuje jako parametr nazwę jednego z parametrów zwracanych przez metodę [getParameterNames] i musi zwrócić tablicę jego wartości. Wiadomo bowiem, że jeden parametr może występować w zapytaniu w wielu egzemplarzach;
  • wiersz 18: zastępuje się przecinek kropką;

Oto przykład wykonania:

  • w [1] wartości [a,b] są wprowadzane w formacie francuskim;
  • w [2] – wyniki;
  • w [3] serwer zwrócił stronę z liczbami w formacie anglosaskim.

Ten ostatni problem można rozwiązać za pomocą Thymeleaf w następujący sposób w widoku [vue-01.xml]


<tr>
    <td>
        <input type="text" id="a" name="a"    th:value="${resultats.culture}=='fr-FR' and ${actionModel01.a}!=null? ${#strings.replace(actionModel01.a,'.',',')} : ${actionModel01.a}" data-val="true" th:attr="data-val-required=#{NotNull},data-val-number=#{typeMismatch},data-val-min=#{actionModel01.a.min},data-val-min-value=#{actionModel01.a.min.value}" />
    </td>
    <td>
        <input type="text" id="b" name="b" th:value="${resultats.culture}=='fr-FR' and ${actionModel01.b}!=null? ${#strings.replace(actionModel01.b,'.',',')} : ${actionModel01.b}" data-val="true" th:attr="data-val-required=#{NotNull},data-val-number=#{typeMismatch},data-val-min=#{actionModel01.b.min},data-val-min-value=#{actionModel01.b.min.value}" />
    </td>
</tr>

W wierszach 3 i 6 należy wprowadzić kilka zmian. Przyjrzyjmy się wierszowi 3:

  • zapisaliśmy [th:field="*{a}"]. Parametr [th:field] ustawia atrybuty [id, name, value] wygenerowanego tagu HTML [input]. W tym przypadku chcemy samodzielnie zarządzać atrybutem [value]. Dlatego też samodzielnie ustalamy atrybuty [id, name];
  • atrybut [th:value] ocenia wyrażenie wykorzystujące operator trójargumentowy ?. Sprawdzamy wyrażenie [${resultats.culture}=='fr-FR' and ${actionModel01.b}!=null]. Jeśli jest prawdziwe, nadajemy atrybutowi [value] wartość a [actionModel01.a], gdzie kropka dziesiętna jest zastąpiona przecinkiem. Jeśli jest fałszywe, przypisuje się atrybutowi [value] wartość z [actionModel01.a] bez zmian;
  • wiersz 6: powtarzamy tę samą czynność dla pola [b];

Oto przykładowe wykonanie:

  • w [1] liczby w polu [a,b] zachowały francuski zapis. Nie ma to miejsca w przypadku [2];

Ten nowy problem rozwiązuje się w taki sam sposób jak poprzedni. Modyfikujemy widok [vue-03.xml] w następujący sposób:


<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
    <body>
        <h4 th:text="#{resultats}">Résultats</h4>
        <p>
            <strong>
                <span th:text="#{labelHeureCalcul}">Heure de calcul :</span>
                <span id="heureCalcul" th:text="${resultats.heurePost}"></span>
            </strong>
        </p>
        <p>
            <span th:text="#{labelAplusB}">A+B=</span>
            <span id="aplusb" th:text="${resultats.culture}=='fr-FR' and ${resultats.aplusb}!=null? ${#strings.replace(resultats.aplusb,'.',',')} : ${resultats.aplusb}"></span>
        </p>
        <p>
            <span th:text="#{labelAmoinsB}">A-B=</span>
            <span id="amoinsb"     th:text="${resultats.culture}=='fr-FR' and ${resultats.amoinsb}!=null? ${#strings.replace(resultats.amoinsb,'.',',')} : ${resultats.amoinsb}"></span>
        </p>
        <p>
            <span th:text="#{labelAfoisB}">A*B=</span>
            <span id="amultiplieparb" th:text="${resultats.culture}=='fr-FR' and ${resultats.amultiplieparb}!=null? ${#strings.replace(resultats.amultiplieparb,'.',',')} : ${resultats.amultiplieparb}"></span>
        </p>
        <p>
            <span th:text="#{labelAdivB}">A/B=</span>
            <span id="adiviseparb" th:text="${resultats.culture}=='fr-FR' and ${resultats.adiviseparb}!=null? ${#strings.replace(resultats.adiviseparb,'.',',')} : ${resultats.adiviseparb}"></span>
        </p>
    </body>
</html>

Oto przykład:

Teraz mamy aplikację, która poprawnie obsługuje dwa układy kulturowych w środowisku, niezależnie od tego, czy korzysta z JavaScriptu, czy nie. Wymagało to znacznego skomplikowania kodu po stronie serwera. W dalszej części będziemy zawsze zakładać, że JavaScript w przeglądarce jest włączony. Pozwala to na realizację funkcji niemożliwych do wykonania wyłącznie w trybie serwerowym.

7.2.10. Obsługa linku [Calculer]

Przyjrzyjmy się linkowi [Calculer] na stronie głównej [vue-01.xml]:

Kod linku [Calculer] w widoku [vue-01.xml] wygląda następująco:


<a href="javascript:postForm()" th:text="#{action.calculer}">Calculer</a>

Funkcja jS [postForm] jest zdefiniowana w pliku [local1.js] w następujący sposób:


// dane globalne
var loading;
var formulaire;
var résultats;
var a, b;

function postForm() {
    // czy formularz jest poprawny?
    if (!formulaire.validate().form()) {
        // formularz nieprawidłowy – zakończono
        return;
    }
    // obsługujemy dwa ustawienia regionalne [fr_FR, en_US]
    // rzeczywiste dane [a,b] muszą być w każdym przypadku wysyłane w formacie anglosaskim
    // zostaną one przetworzone przez filtr [CultureFilter]

    // ręcznie wykonujemy wywołanie Ajax
    $.ajax({
        url : '/ajax-02',
        headers : {
            '„X-Requested-With”: „XMLHttpRequest”
        },
        type : 'POST',
        data : formulaire.serialize(),
        dataType : 'html',
        beforeSend : function() {
            loading.show();
        },
        success : function(data) {
            resultats.html(data);
        },
        complete : function() {
            loading.hide();
        },
        error : function(jqXHR) {
            résultats.html(jqXHR.responseText);
        }
    })
}
  • wiersze 2–5: przypomnijmy, że elementy te zostały zainicjowane przez funkcję [$(document).ready];
  • wiersze 9–12: uruchamiane są walidatory jS formularza. Jeśli któraś z wartości jest nieprawidłowa, wyrażenie [formulaire.validate().form()] zwraca wartość false. W takim przypadku walidator [submit] formularza zostaje anulowany;
  • wiersze 18–38: ręcznie wysyłane jest wywołanie Ajax;
  • wiersz 19: wartość URL jako cel wywołania Ajax;
  • wiersze 20–22: tablica nagłówków HTTP, które należy dodać do tych domyślnie obecnych w żądaniu HTTP. W tym miejscu dodajemy nagłówek HTTP, który poinformuje serwer, że wykonujemy wywołanie Ajax;
  • wiersz 23: zastosowana metoda HTTP;
  • wiersz 24: wysyłane dane. [formulaire.serialize] tworzy ciąg do wysłania [culture=fr-FR&a=12,7&b=20,89] z formularza o identyfikatorze [formulaire]. W tym miejscu ponownie pojawia się problem omówiony wcześniej: wartości [a,b] muszą zostać przesłane w formacie anglosaskim. Wiemy, że problem ten został już rozwiązany dzięki utworzeniu filtra [cultureFilter];
  • wiersz 25: oczekiwany typ danych zwracanych przez serwer. Wiemy, że serwer zwróci strumień o identyfikatorze HTML;
  • wiersz 26: metoda do wykonania w momencie rozpoczęcia żądania. W tym miejscu wskazano, że należy wyświetlić komponent o identyfikatorze [loading]. Jest to animowany obrazek oczekiwania;
  • wiersz 29: metoda do wykonania w przypadku pomyślnego zakończenia żądania Ajax. Parametr [data] stanowi pełną odpowiedź serwera. Wiemy, że jest to strumień HTML;
  • wiersz 30: aktualizujemy komponent o identyfikatorze [résultats], zastępując go wartością HTML z parametru [data].
  • wiersz 33: ukrywamy sygnał oczekiwania;
  • wiersz 35: funkcja wykonywana po otrzymaniu odpowiedzi z serwera, niezależnie od tego, czy jest to odpowiedź pomyślna, czy błąd;
  • wiersze 35–37: w przypadku błędu (serwer zwrócił odpowiedź HTTP ze statusem wskazującym na błąd po stronie serwera) wyświetlana jest odpowiedź serwera HTML w polu [resultats];

Oto przykład wykonania:

7.3. Aktualizacja strony HTML za pomocą strumienia jSON

W poprzednim przykładzie serwer WWW odpowiadał na żądanie Ajax HTTP strumieniem HTML. W strumieniu tym znajdowały się dane wraz z formatowaniem HTML. Proponujemy powrócić do poprzedniego przykładu, tym razem wykorzystując odpowiedzi jSON (JavaScript w notacji obiektowej), zawierające wyłącznie dane. Zaletą tego rozwiązania jest mniejsza ilość przesyłanych bajtów. Zakładamy, że w przeglądarce włączona jest obsługa JavaScript.

7.3.1. Akcja [/ajax-04]

Akcja [/ajax-04] jest identyczna z akcją [/ajax-01], z tą różnicą, że wyświetlany jest widok [vue-04.xml] zamiast widoku [vue-01.xml]:


@RequestMapping(value = "/ajax-04", method = RequestMethod.GET, produces = "text/html; charset=UTF-8")
    public String ajax04(Locale locale, Model modèle, HttpSession session, String tempo) {
        ...
        // widok
        return "vue-04";
    }

7.3.2. Widok [vue-04.xml]

 

Widok [vue-04.xml] zawiera treść widoku [vue-01.xml] z następującymi różnicami:


<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
    <head>
        ...
        <script type="text/javascript" src="/js/local4.js"></script>
        <script th:inline="javascript">
            /*<![CDATA[*/
                    var culture = [[${resultats.culture}]];
                    Globalize.culture(culture);
                    /*]]>*/
        </script>
    </head>
    <body>
        <h2>Ajax - 04</h2>
    ...
        <form id="formulaire" name="formulaire" th:object="${actionModel01}">
...
            <p>
                <img id="loading" style="display: none" src="/images/loading.gif" />
                <a href="javascript:postForm()" th:text="#{action.calculer}">Calculer</a>
            </p>
            <!-- ukryte pola -->
            <input type="hidden" id="culture" name="culture" th:value="${resultats.culture}"></input>
</form>
        <hr />
        <div id="entete">
            <h4 id="titre">Résultats</h4>
            <p>
                <strong>
                    <span id="labelHeureCalcul">Heure de calcul :</span>
                    <span id="heureCalcul">12:10:87</span>
                </strong>
            </p>
        </div>
        <div id="résultats">
            <p>
                A+B=
                <span id="aplusb">16,7</span>
            </p>
            <p>
                A-B=
                <span id="amoinsb">16,7</span>
            </p>
            <p>
                A*B=
                <span id="afoisb">16,7</span>
            </p>
            <p>
                A/B=
                <span id="adivb">16,7</span>
            </p>
        </div>
        <div id="erreur">
            <p style="color: red;">
                <span id="msgErreur">xx</span>
            </p>
        </div>
    </body>
</html>
  • wiersz 5: kod JavaScript widoku znajduje się teraz w pliku [local4.js];
  • wiersz 16: tag [form] nie zawiera już parametrów [data-ajax-attr] z biblioteki [Unobtrusive Ajax]. Nie będziemy go tutaj używać. Tag [form] nie posiada również atrybutów [method] i [action], które określają, w jaki sposób i gdzie należy przesłać wartości wprowadzone w formularzu. Wynika to z faktu, że zostanie on przesłany przez funkcję jS (wiersz 20);
  • wiersze 26–57: pole identyfikatora [resultats], które wcześniej było puste, zawiera teraz kod HTML służący do wyświetlania wyników;
  • wiersze 26–34: nagłówek wyników, w którym wyświetlana jest godzina obliczeń;
  • wiersze 35–52: wyniki czterech operacji arytmetycznych;
  • wiersze 53–57: ewentualny komunikat o błędzie wysłany przez serwer;

Kod jS, wykonywany podczas ładowania widoku [vue-04.xm], znajduje się w pliku [local4.js]. Ma on następującą postać:


// dane globalne
    var loading;
    var formulaire;
    var résultats;
    var titre;
    var labelHeureCalcul;
    var heureCalcul;
    var aplusb;
    var amoinsb;
    var afoisb;
    var adivb;
    var msgErreur;

// podczas ładowania dokumentu
$(document).ready(function() {
    // pobierane są odniesienia do różnych elementów strony
    loading = $("#loading");
    formulaire = $("#formulaire");
    résultats = $('#wyniki');
    titre=$("#titre");
    labelHeureCalcul=$("#labelHeureCalcul");
    heureCalcul=$("#heureCalcul");
    aplusb=$("#aplusb");
    amoinsb=$("#amoinsb");
    afoisb=$("#afoisb");
    adivb=$("#adivb");
    msgErreur=$("#msgErreur");
    // ukrywanie niektórych elementów
    résultats.hide();
    erreur.hide();
    loading.hide();
});
  • wiersze 17–27: pobierane są odniesienia jQuery wszystkich elementów strony;
  • wiersz 29: obszar wyników jest ukryty;
  • wiersz 30: podobnie jak obszar błędu;
  • wiersz 31: podobnie jak animowany obrazek oczekiwania;
  • wiersze 2–12: pobrane identyfikatory są definiowane jako zmienne globalne, aby inne funkcje mogły z nich korzystać;

7.3.3. Funkcja jS [postForm]

Link [Calculer] wygląda następująco:


<p>
    <img id="loading" style="display: none" src="/images/loading.gif" />
    <a href="javascript:postForm()" th:text="#{action.calculer}">Calculer</a>
</p>

Funkcja jS [postForm] jest zdefiniowana w pliku [local.js] w następujący sposób:


function postForm() {
    // formularz poprawny?
    if (!formulaire.validate().form()) {
        // formularz nieprawidłowy – zakończono
        return;
    }
    // ręcznie wysyłamy żądanie Ajax
    $.ajax({
        url : '/ajax-05',
        headers : {
            'Accept: 'application/json'
        },
        type : 'POST',
        data : formulaire.serialize(),
        dataType : 'json',
        beforeSend : onBegin,
        success : onSuccess,
        error : onError,
        complete : onComplete
    })
}

// przed wywołaniem Ajax
function onBegin() {
...
}

// po otrzymaniu odpowiedzi z serwera
// w przypadku powodzenia
function onSuccess(data) {
...
}

// po otrzymaniu odpowiedzi z serwera
// w przypadku niepowodzenia
function onError(jqXHR) {
...
}

// po [onSuccess, onError]
function onComplete() {
...
}
  • wiersze 3–6: przed wysłaniem wprowadzonych wartości są one sprawdzane. Jeśli są nieprawidłowe, nie wykonuje się operacji POST na formularzu;
  • wiersz 9: wprowadzone wartości są wysyłane do akcji [/ajax-05], którą omówimy nieco później;
  • wiersze 10–12: nagłówek HTTP informujący serwer, że oczekujemy odpowiedzi w formacie jSON;
  • wiersz 13: wprowadzone wartości zostaną wysłane;
  • wiersz 14: serializacja wprowadzonych wartości do postaci ciągu gotowego do wysłania w formacie [a=1,6&b=2,4&culture=fr-FR];
  • wiersz 15: typ odpowiedzi wysyłanej przez serwer. Będzie to jSON;
  • wiersz 16: funkcja do wykonania przed POST;
  • wiersz 17: funkcja do wykonania po otrzymaniu odpowiedzi z serwera, jeśli operacja zakończyła się powodzeniem. „Powodzenie” żądania HTTP mierzy się na podstawie statusu odpowiedzi serwera HTTP. Odpowiedź [HTTP/1.1 200 OK ] oznacza sukces. Odpowiedź [HTTP/1.1 500 Internal Server Error] oznacza niepowodzenie. To, co nazywamy statusem odpowiedzi HTTP, to kod [200] lub [500]. Niektóre z tych kodów są powiązane z „powodzeniem”, podczas gdy inne z „niepowodzeniem”;
  • wiersz 18: funkcja, która ma zostać wykonana po otrzymaniu odpowiedzi z serwera, gdy status HTTP tej odpowiedzi oznacza niepowodzenie;
  • wiersz 18: funkcja, która ma zostać wykonana jako ostatnia, po poprzednich funkcjach [onSuccess, onError];

Funkcja [onBegin] ma następującą postać:


// przed wywołaniem Ajax
function onBegin() {
    console.log("onBegin");
    // wyświetla się animowany obraz
    loading.show();
    // ukrywa się niektóre elementy widoku
    entete.hide();
    résultats.hide();
    erreur.hide();
}

Zanim przeanalizujemy pozostałe funkcje jS wywołania Ajax, musimy poznać odpowiedź wysłaną przez akcję [/ajax-05].

7.3.4. Akcja [/ajax-05]

Akcja [/ajax-05] wygląda następująco:


    @RequestMapping(value = "/ajax-05", method = RequestMethod.POST)
    @ResponseBody()
    // przetwarza POST z widoku [vue-04]
    public JsonResults ajax05(@Valid ActionModel01 formulaire, BindingResult result, Locale locale,    HttpServletRequest request, HttpSession session) throws InterruptedException {
        if(result.hasErrors()){
            // sytuacja nietypowa – nie wyświetla się nic
            return null;
        }
        ...
}
  • wiersz 2: atrybut [ResponseBody] wskazuje, że akcja [/ajax-05] sama zwraca odpowiedź do klienta. Ponieważ biblioteka jSON znajduje się wśród zależności projektu, Spring Boot automatycznie konfiguruje tego typu akcje tak, aby zwracały jSON. Zatem to łańcuch jSON typu [JsonResults] (wiersz 4) zostanie wysłany do klienta;
  • wiersz 2: wartości przesłane jako [a, b, culture] zostaną zamknięte w typie [ActionModel01], dla którego żądamy walidacji [@Valid ActionModel01]. Jest to tylko formalność. Wychodzimy z założenia, że w przeglądarce klienta włączona jest obsługa JavaScript, a zatem gdy wartości są przesyłane, zostały już zweryfikowane po stronie klienta. Niemniej jednak można przewidzieć przypadek niekontrolowanego POST, który nie korzystałby z naszego klienta jS. W takim przypadku walidacja może zakończyć się niepowodzeniem;
  • wiersze 5–7: w przypadku błędu zwracany jest pusty strumień jSON;

Kontynuujmy analizę akcji [/ajax-05]:


    @RequestMapping(value = "/ajax-05", method = RequestMethod.POST)
    @ResponseBody()
    // przetwarza POST z widoku [vue-04]
    public JsonResults ajax05(@Valid ActionModel01 formulaire, BindingResult result, Locale locale,
            HttpServletRequest request, HttpSession session) throws InterruptedException {
...
        // kontekst aplikacji Spring
        WebApplicationContext ctx = WebApplicationContextUtils.getWebApplicationContext(request.getServletContext());
        // tempo?
        Integer tempo = (Integer) session.getAttribute("tempo");
        if (tempo != null && tempo > 0) {
            Thread.sleep(tempo);
        }
    ...
        // zwracamy wynik
        return résultats;
}
  • wiersz 8: pobieramy kontekst [ctx] z aplikacji Spring. Jest on potrzebny do pobrania komunikatów z plików [messages.properties] na podstawie klucza komunikatu i ustawień regionalnych. Odbywa się to przy użyciu następującej składni:

ctx.getMessage(clé_message, tableau_de_paramètres, locale)
    • [clé_message]: klucz poszukiwanej wiadomości;
    • [locale]: używana lokalizacja. Jeśli więc lokalizacja ta to [en_US], wykorzystany zostanie plik [messages_en.properties];
    • [tableau_de_paramètres]: uzyskany komunikat można skonfigurować tak, jak w pliku [clé=message {0} {1}]. W tej wiadomości znajdują się dwa parametry [{0} {1}]. Jako drugi parametr [ctx.getMessage] należy podać tablicę zawierającą dwie wartości;
  • wiersze 10–13: jeśli w sesji występuje opóźnienie, zatrzymuje się bieżący wątek na czas trwania tego opóźnienia;

Akcja [/ajax-05] przebiega w następujący sposób:


        // przygotowujemy szablon następnego widoku
        JsonResults résultats = new JsonResults();
        ...
}
  • wiersz 2: utworzenie szablonu ciągu znaków jSON wysyłanego do klienta;

Szablon [JsonResults] wygląda następująco:

 

package istia.st.springmvc.models;

public class JsonResults {

    // dane
    private String titre;
    private String labelHeureCalcul;
    private String heureCalcul;
    private String aplusb;
    private String amoinsb;
    private String afoisb;
    private String adivb;
    private String msgErreur;

    // metody getter i setter
...

}
  • wiersze 6–13: każde z pól klasy [JsonResult] odpowiada polu o tej samej nazwie w klasie [id] w widoku [vue-04.xml]:

Akcja [/ajax-05] przebiega w następujący sposób:


        // przygotowujemy szablon następnego widoku
        JsonResults résultats = new JsonResults();
        // nagłówek
        résultats.setTitre(ctx.getMessage("resultats.titre", null, locale));
        résultats.setLabelHeureCalcul(ctx.getMessage("labelHeureCalcul", null, locale));
        résultats.setHeureCalcul(new SimpleDateFormat("hh:mm:ss").format(new Date()));
        // generowanie błędu co drugie wywołanie
        int val = new Random().nextInt(2);
        if (val == 0) {
            // zwracamy komunikat o błędzie
            résultats.setMsgErreur(ctx.getMessage("resultats.erreur",
                    new Object[] { ctx.getMessage("erreur.aleatoire", null, locale) }, locale));
            return résultats;
}
  • wiersz 2: utworzenie szablonu ciągu znaków jSON wysyłanego do klienta;
  • wiersze 4–6: tworzenie komunikatów nagłówka wyników;
  • wiersze 8–14: średnio co drugi raz generowany jest komunikat o błędzie. W takim przypadku proces nie jest kontynuowany, a ciąg znaków jSON jest zwracany do klienta (wiersz 13);
  • wiersz 11: oto przykład komunikatu z parametrami:

erreur.aleatoire=erreur aléatoire
resultats.erreur=Une erreur s''est produite : [{0}]

Akcja [/ajax-05] przebiega w następujący sposób:


        // pobieramy przesłane wartości
        double a = formulaire.getA();
        double b = formulaire.getB();
        // tworzy się model
        résultats.setAplusb(String.valueOf(a + b));
        résultats.setAmoinsb(String.valueOf(a - b));
        résultats.setAfoisb(String.valueOf(a * b));
        try {
            résultats.setAdivb(String.valueOf(a / b));
        } catch (RuntimeException e) {
            résultats.setAdivb("NaN");
        }
        // zwracamy wynik
return résultats;
  • wiersze 2–3: pobierane są wartości z [a] i [b];
  • wiersze 5–12: tworzy się cztery wyniki;
  • wiersz 14: ciąg znaków jSON [JsonResults] jest wysyłany do klienta;

Zobaczmy, jak to wygląda w przypadku klienta [Advanced Rest Client]:

  • w [1-2] wysyłamy żądanie POST do akcji [/ajax-05];
  • w [3] wysyłane są nieprawidłowe wartości;
  • w [4] serwer zwrócił pusty strumień;
  • w [1] wysyłane są poprawne wartości;
  • w [2] obiekt jSON zwrócony przez serwer, tym razem z komunikatem o błędzie;
  • w [1] wysyłane są prawidłowe wartości;
  • w [2] obiekt jSON zwrócony przez serwer, zawierający w tym przypadku cztery wyniki;
  • w [1] wysyłamy poprawne wartości;
  • w [2] udało się wywołać wyjątek po stronie serwera. Widać, że serwer ponownie wysyła obiekt jSON. W tej wiadomości widać, że status odpowiedzi HTTP wynosi [500], co wskazuje na wystąpienie błędu po stronie serwera;

7.3.5. Funkcja jS [postForm] – 2

Teraz, gdy znamy obiekt jSON zwrócony przez serwer, możemy go wykorzystać w kodzie JavaScript. Metoda [onSuccess], wykonywana, gdy serwer wysyła odpowiedź o statusie HTTP [200], wygląda następująco:


// po otrzymaniu odpowiedzi z serwera
// w przypadku powodzenia
function onSuccess(data) {
    console.log("onSuccess");
    // wypełnia się obszar wyników
    titre.text(data.titre);
    labelHeureCalcul.text(data.labelHeureCalcul);
    heureCalcul.text(data.heureCalcul);
    entete.show();
    // wyniki bez błędów
    if (!data.msgErreur) {
        aplusb.text(data.aplusb);
        amoinsb.text(data.amoinsb);
        afoisb.text(data.afoisb);
        adivb.text(data.adivb);
        résultats.show();
        return;
    }
    // wyniki z błędem
    msgErreur.text(data.msgErreur);
    erreur.show();
}
  • wiersz 3: parametr [data] to obiekt jSON zwrócony przez serwer:
 

Metoda [onError], wykonywana gdy status odpowiedzi HTTP wynosi [500], wygląda następująco:


// po otrzymaniu odpowiedzi z serwera
// w przypadku niepowodzenia
function onError(jqXHR) {
    console.log("onError");
    // błąd systemowy
    msgErreur.text(jqXHR.responseText);
    erreur.show();
}
  • wiersz 3: obiekt JQuery [jqXHR] posiada następujące właściwości:
    • responseText: tekst odpowiedzi serwera,
    • status: kod błędu zwrócony przez serwer,
    • statusText: tekst powiązany z tym kodem błędu;
  • wiersz 6: obiekt [jqXHR.responseText] jest następującym obiektem jSON:
 

7.3.6. Testy

Przyjrzyjmy się kilku zrzutom ekranu przedstawiającym działanie aplikacji internetowej:

 
 
 

7.4. Aplikacja internetowa z jedną stroną

7.4.1. Wprowadzenie

Technologia Ajax umożliwia tworzenie aplikacji jednostronicowych:

  • pierwsza strona jest wynikiem standardowego żądania przeglądarki;
  • kolejne strony są pobierane za pomocą wywołań Ajax. W rezultacie przeglądarka nigdy nie zmienia adresu URL i nigdy nie ładuje nowej strony. Ten typ aplikacji nazywamy aplikacją jednostronicową (APU) lub, w języku angielskim, Single Page Application (SPA).

Oto prosty przykład takiej aplikacji. Nowa aplikacja będzie miała dwa widoki:

  • w [1] akcja [/ajax-06] pozwala nam wyświetlić pierwszą stronę, stronę 1;
  • w [2] link umożliwia przejście do strony 2 za pomocą wywołania Ajax;
  • w [3] akcja URL nie uległa zmianie. Wyświetlana jest strona 2;
  • w [4] link pozwala nam powrócić do strony 1 za pomocą wywołania Ajax;
  • w [5] strona URL nie uległa zmianie. Wyświetlana jest strona 1.

7.4.2. Akcja [/ajax-06]

Kod akcji [/ajax-06] jest następujący:


    @RequestMapping(value = "/ajax-06", method = RequestMethod.GET, produces = "text/html; charset=UTF-8")
    public String ajax06() {
        return "vue-06";
}
  • wiersze 1–4: akcja [/ajax-06] ogranicza się do wyświetlenia widoku [vue-06.xml];

7.4.3. Widok [vue-06.xml]

Widok [vue-06.xml] wygląda następująco:


<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
    <head>
        <meta name="viewport" content="width=device-width" />
        <title>Ajax-06</title>
        <link rel="stylesheet" href="/css/ajax01.css" />
        <script type="text/javascript" src="/js/jquery/jquery-2.1.1.min.js"></script>
        <script type="text/javascript" src="/js/local6.js"></script>
    </head>
    <body>
        <h3>Ajax - 06 - Navigation dans une Application à Page Unique</h3>
        <div id="content" th:include="vue-07" />
    </body>
</html>
  • wiersz 8: widok wykorzystuje skrypt [local6.js];
  • wiersz 12: widok [vue-07.xml] jest dołączany do obszaru o identyfikatorze [content] w widoku [vue-06.xml];

7.4.4. Widok [vue-07.xml]

Widok [vue-07.xml] ma następującą postać:


<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
    <body>
        <h4>Page 1</h4>
        <p>
            <a href="javascript:gotoPage(2)">Page 2</a>
        </p>
    </body>
</html>

7.4.5. Funkcja jS [gotoPage]

Łącze [Page 2] widoku [vue-07.xml] wykorzystuje funkcję jS [gotoPage] zdefiniowaną w następującym pliku [local6.js]:


// dane ogólne
var content;

function gotoPage(num) {
    // ręczne wywołanie Ajax
    $.ajax({
        url : '/ajax-07',
        type : 'POST',
        data : 'num=' + num,
        dataType : 'html',
        beforeSend : function() {
        },
        success : function(data) {
            content.html(data)
        },
        complete : function() {
        },
        error : function(jqXHR) {
            // błąd systemowy
            content.html(jqXHR.responseText);
        }
    })
}

// podczas ładowania dokumentu
$(document).ready(function() {
    // pobieranie odwołań do różnych elementów strony
    content = $("#content");
});
  • wiersz 28: podczas ładowania strony zapisuje się obszar o identyfikatorze [content] i tworzy się z niego zmienną globalną (wiersz 2);
  • wiersz 4: funkcja [gotoPage] otrzymuje jako parametr numer strony (1 lub 2), która ma zostać wyświetlona w bieżącym widoku;
  • wiersz 7: funkcja URL jest wywoływana przez funkcję POST;
  • wiersz 8: funkcja URL z wiersza 7 jest wywoływana za pośrednictwem funkcji POST;
  • wiersz 9: wysłany ciąg znaków. Jest to parametr o nazwie [num]. Jego wartością jest numer strony (wiersz 4), która ma zostać wyświetlona w bieżącym widoku;
  • wiersz 10: serwer zwróci HTML, czyli identyfikator strony do wyświetlenia;
  • wiersze 13–15: w przypadku powodzenia (status HTTP równy 200) wartość HTML wysłana przez serwer jest umieszczana w polu identyfikatora [content];
  • wiersze 18–20: w przypadku niepowodzenia (status HTTP równy 500) wartość HTML wysłana przez serwer jest umieszczana w polu identyfikatora [content];

7.4.6. Akcja [/ajax-07]

Kod akcji [/ajax-07] jest następujący:


@RequestMapping(value = "/ajax-07", method = RequestMethod.POST, produces = "text/html; charset=UTF-8")
    public String ajax07(int num) {
        // num: numer strony
        switch (num) {
        case 1:
            return "vue-07";
        case 2:
            return "vue-08";
        default:
            return "vue-07";
        }
    }
  • wiersz 2: pobieramy parametr przesłany metodą POST o nazwie [num]. Przypominamy, że parametr w wierszu 2 musi nosić nazwę parametru przesłanego metodą POST, w tym przypadku [num]. [num] to numer strony lub widoku;
  • wiersze 5–6: w przypadku, gdy [num==1], zwracany jest widok [vue-07.xml];
  • wiersze 7–8: w przypadku, gdy [num==2], zwracany jest widok [vue-08.xml];
  • wiersze 9–10: w pozostałych przypadkach (co zazwyczaj jest niemożliwe) zwracany jest widok [vue-07.xml];

7.4.7. Widok [vue-08.xml]

Widok [vue-08.xml] tworzy stronę nr 2 aplikacji:


<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
    <body>
        <h4>Page 2</h4>
        <p>
            <a href="javascript:gotoPage(1)">Page 1</a>
        </p>
    </body>
</html>

7.5. Dołączenie kilku strumieni HTML do jednej odpowiedzi jSON

7.5.1. Wprowadzenie

Rozważamy następującą aplikację:

Strona [1] zawiera cztery obszary:

  • [Zone 1, Zone 3] to obszary, które pojawiają się / znikają po kliknięciu przycisku [Rafraîchir]. Zliczamy liczbę pojawień się każdego z tych dwóch obszarów [2]. Obszar [Zone 1] wykorzystuje język francuski, podczas gdy obszar [Zone 3] wykorzystuje język angielski;
  • obszar [Zone 2] występuje stale;
  • obszar [Saisies] jest zawsze dostępny;

Link [Valider] wyświetla następującą stronę [3]:

  • link [Retour à la page 1] przywraca stronę nr 1 do stanu, w jakim znajdowała się wcześniej ([4]);

Aplikacja składa się z jednej strony. Przeglądarka wysyła do serwera żądanie o wyświetlenie pierwszej strony. Kolejne strony są pobierane z serwera za pomocą wywołań Ajax.

7.5.2. Akcja [/ajax-09]

  

Akcja [/ajax-09] wygląda następująco:


    @RequestMapping(value = "/ajax-09", method = RequestMethod.GET, produces = "text/html; charset=UTF-8")
    public String ajax09() {
        return "vue-09";
}

Służy ona jedynie do wyświetlenia widoku [vue-09.xml].

7.5.3. Widoki XML

  

Widok [vue-09.xml] jest stroną główną aplikacji:


<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
    <head>
        <meta name="viewport" content="width=device-width" />
        <title>Ajax-09</title>
        <link rel="stylesheet" href="/css/ajax01.css" />
        <script type="text/javascript" src="/js/jquery/jquery-2.1.1.min.js"></script>
        <script type="text/javascript" src="/js/json3.js"></script>
        <script type="text/javascript" src="/js/local9.js"></script>
    </head>
    <body>
        <h3>Ajax - 09 - Navigation dans une Application à Page Unique</h3>
        <h3>avec des flux HTML embarqués dans des chaînes jSON</h3>
        <hr />
        <div id="content" th:include="vue-09-page1" />
        <img id="loading" src="/images/loading.gif" />
        <div id="erreur" style="background-color:lightgrey"></div>
    </body>
</html>
  • wiersz 9: plik JS używany w aplikacji;
  • wiersz 15: zawartość strony głównej;
  • wiersz 16: animowany obrazek oczekiwania:
  • wiersz 17: obszar do wyświetlania ewentualnego błędu;

Widok [vue-09-page1.xml] stanowi stronę 1 aplikacji:


<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
    <body>
        <h2>Page 1</h2>
        <!-- obszar 1 -->
        <fieldset id="zone1" style="background-color:pink">
            <legend>Zone 1</legend>
            <span id="zone1-content" th:text="xx">xx</span>
        </fieldset>
        <!-- strefa 2 -->
        <fieldset id="zone2" style="background-color:lightgreen">
            <legend>Zone 2</legend>
            <span>Ce texte reste toujours présent</span>
        </fieldset>
        <!-- strefa 3 -->
        <fieldset id="zone3" style="background-color:yellow">
            <legend>Zone 3</legend>
            <span id="zone3-content" th:text="zz">zz</span>
        </fieldset>
        <br />
        <p>
            <button onclick="javascript:postForm()">Rafraîchir</button>
        </p>
        <hr />
        <div id="saisies" th:include="vue-09-saisies">
        </div>
    </body>
</html>
  • wiersze 6–9: obszar [Zone 1]. Jego zawartość jest umieszczana w komponencie [id="zone1-content"];
  • wiersze 11–14: obszar [Zone 2], który pozostaje niezmieniony;
  • wiersze 16–19: pole [Zone 3]. Jego zawartość jest umieszczana w komponencie [id="zone3-content"];
  • wiersz 22: funkcja JS, która wysyła formularz;
  • wiersz 25: włączenie pola wprowadzania danych;

Należy zauważyć, że strona 1 nie zawiera tagu [form]. Wszystko będzie przetwarzane w JavaScript.

Widok [vue-09-saisies.xml] wygląda następująco:


<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
    <div id="saisies">
        <h4>Saisies :</h4>
        <p>
            Chaîne de caractères :
            <input type="text" id="text1" size="30" th:value="${value1}" />
        </p>
        <p>
            Nombre entier :
            <input type="text" id="text2" size="10" th:value="${value2}" />
        </p>
        <p>
            <a href="javascript:valider()">Valider</a>
        </p>
    </div>
</html>
  • wiersze 5–8: wprowadzenie ciągu znaków;
  • wiersze 13–16: wprowadzenie liczby całkowitej;
  • wiersz 14: funkcja JS, która wysyła wprowadzone wartości;

Ponownie należy zauważyć, że pole wprowadzania danych nie posiada znacznika [form].

W sumie strona nr 1 oferuje dwie funkcje:

  • [Rafraîchir]: odświeżająca pola 1 i 3. Działanie to jest przetwarzane przez serwer, który losowo zwraca:
    • pole 1 wraz z licznikiem odwiedzin i nic dla pola 3,
    • obszar 3 wraz z licznikiem odwiedzin i nic dla obszaru 1,
    • obie strefy wraz z licznikami odwiedzin;
  • [Valider]: wyświetla stronę 2 z wprowadzonymi wartościami lub komunikat o błędzie, jeśli wprowadzone dane są nieprawidłowe;

Najpierw zajmiemy się przyciskiem [Rafraîchir].

7.5.4. Kod JS do obsługi przycisku [Rafraîchir]

  

Kod pliku [local9.js] jest następujący:


// zmienne globalne
var content;
var loading;
var erreur;

// podczas ładowania dokumentu
$(document).ready(function() {
    // pobieramy odwołania do różnych elementów strony
    loading = $("#loading");
    loading.hide();
    erreur = $("#erreur");
    erreur.hide();
    content = $("#content");
});
  • wiersze 9–13: po załadowaniu strony głównej zapisywane są odniesienia do trzech elementów oznaczonych kodem [loading, erreur, content];
  • wiersze 2–4: odwołania do tych trzech komponentów są zapisywane w zmiennych globalnych. Pozostają one stałe, ponieważ trzy omawiane obszary są zawsze obecne na wyświetlanej stronie, niezależnie od momentu. Ponieważ pozostają one stałe, można je obliczyć w funkcji [$(document).ready] i udostępnić innym funkcjom w pliku JS;

Funkcja [postForm] obsługuje kliknięcie przycisku [Rafraîchir]:


function postForm() {
    console.log("postForm");
    // ręcznie wysyłamy żądanie Ajax
    $.ajax({
        url : '/ajax-10',
        headers : {
            'Accept: 'application/json'
        },
        type : 'POST',
        dataType : 'json',
        beforeSend : onBegin,
        success : onSuccess,
        error : onError,
        complete : onComplete
    })
}
  • wiersze 4–15: wywołanie Ajax do serwera;
  • wiersz 5: to akcja [ajax-10] będzie przetwarzać POST;
  • wiersze 6–8: odpowiedzią będzie jSON. Klient JS wskazuje, że akceptuje dokumenty jSON;
  • wiersz 9: akcja [ajax-10] jest wywoływana wraz z operacją POST;
  • wiersz 10: otrzymamy dane z jSON;
  • wiersz 11: funkcja wykonywana przed wywołaniem Ajax;
  • wiersz 12: funkcja wykonywana po otrzymaniu odpowiedzi z serwera, gdy operacja zakończyła się powodzeniem: [200 OK];
  • wiersz 13: funkcja wykonywana po otrzymaniu odpowiedzi z serwera, gdy operacja zakończyła się niepowodzeniem [500 Internal server error, ...];
  • wiersz 14: funkcja wykonywana po otrzymaniu odpowiedzi;

Funkcja [onBegin] ma następującą postać:


// przed wywołaniem Ajax
function onBegin() {
    console.log("onBegin");
    // obrazek oczekiwania
    loading.show();
}

Jej zadaniem jest jedynie uruchomienie animacji przedstawiającej oczekiwanie na wynik z serwera.

7.5.5. Akcja [/ajax-10]

  

Akcja [/ajax-10] działa w następujący sposób:


// sesja
    @Autowired
    private SessionModel1 session;
    // silnik Thymeleaf / Spring
    @Autowired
    private SpringTemplateEngine engine;

    @RequestMapping(value = "/ajax-10", method = RequestMethod.POST)
    @ResponseBody()
    public JsonResult10 ajax10(HttpServletRequest request, HttpServletResponse response) {
    ...
    }
  • wiersz 3: wstawia się sesję. Ma ona następujący typ [SessionModel1]:
  

package istia.st.springmvc.models;

import java.io.Serializable;

import org.springframework.context.annotation.Scope;
import org.springframework.context.annotation.ScopedProxyMode;
import org.springframework.stereotype.Component;

@Component
@Scope(value = "session", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class SessionModel1 implements Serializable {

    private static final long serialVersionUID = 1L;
    // dwa liczniki
    private int cpt1 = 0;
    private int cpt3 = 0;
    // trzy obszary
    private String zone1 = "xx";
    private String zone3 = "zz";
    private String saisies;
    private boolean zone1Active = true;
    private boolean zone3Active = true;

    // metody getter i setter
    ...
}

Sesja [SessionModel1] przechowuje następujące elementy:

  • wiersz 15: liczba wystąpień [cpt1], w których wyświetlane jest pole [Zone 1];
  • wiersz 16: liczba wyświetleń pola [Zone 3];
  • wiersze 18–20: przepływy HTML z obszarów [Zone 1], [Zone 3] i [Saisies]. Jest to konieczne w sekwencji [Page 1] --> [Page 2] --> [Page 1]. Podczas przechodzenia z [Page 2] do [Page 1] należy przywrócić [Page 1], a tym samym jego trzy pola;
  • wiersze 21–22: dwie wartości logiczne wskazujące, czy pola [Zone 1] i [Zone 3] są wyświetlane (widoczne);

Kolejny element wstawiony do kontrolera [AjaxController] jest następujący:


    // silnik Thymeleaf / Spring
    @Autowired
private SpringTemplateEngine engine;

Bean typu [SpringTemplateEngine] jest zdefiniowany w pliku konfiguracyjnym [Config]:

  

Jest on zdefiniowany w następujący sposób:


    @Bean
    public SpringResourceTemplateResolver templateResolver() {
        SpringResourceTemplateResolver templateResolver = new SpringResourceTemplateResolver();
        templateResolver.setPrefix("classpath:/templates/");
        templateResolver.setSuffix(".xml");
        templateResolver.setTemplateMode("HTML5");
        templateResolver.setCacheable(true);
        templateResolver.setCharacterEncoding("UTF-8");
        return templateResolver;
    }

    @Bean
    SpringTemplateEngine templateEngine(SpringResourceTemplateResolver templateResolver) {
        SpringTemplateEngine templateEngine = new SpringTemplateEngine();
        templateEngine.setTemplateResolver(templateResolver);
        return templateEngine;
}
  • wiersze 2–10: znamy bean typu [SpringResourceTemplateResolver], który pozwala nam zdefiniować pewne cechy widoków;
  • wiersze 13–17: bean typu [SpringTemplateEngine] pozwala nam zdefiniować „silnik” widoków, czyli klasę odpowiedzialną za generowanie odpowiedzi [Thymeleaf] dla klientów. [Thymeleaf] posiada „silnik” domyślny oraz inny, gdy jest używany w środowisku [Spring]. To właśnie ten drugi wykorzystujemy w tym przypadku;

Sygnatura akcji [/ajax-10] jest następująca:


@RequestMapping(value = "/ajax-10", method = RequestMethod.POST)
    @ResponseBody()
    public JsonResult10 ajax10(HttpServletRequest request, HttpServletResponse response) {
    ...
}
  • wiersz 1: akcja [/ajax-10] akceptuje wyłącznie POST;
  • wiersz 2: akcja [/ajax-10] sama zwraca odpowiedź klientowi. Zostanie ona automatycznie przekształcona w jSON;
  • wiersz 3: odpowiedź ma typ [JsonResult10], a jej treść jest następująca:
  

package istia.st.springmvc.models;

public class JsonResult10 {

    // dane
    private String content;
    private String zone1;
    private String zone3;
    private String erreur;
    private String saisies;
    private boolean zone1Active;
    private boolean zone3Active;

    public JsonResult10() {
    }

    // metody getter i setter
...
}
  • wiersz 6: treść HTML z pola oznaczonego jako [content];
  • wiersz 7: zawartość HTML z pola [Zone 1];
  • wiersz 8: zawartość HTML z pola [Zone 3];
  • wiersz 9: zawartość HTML z pola [Erreur];
  • wiersz 10: zawartość HTML z pola [Saisies];
  • wiersz 11: wartość logiczna wskazująca, czy pole [Zone 1] ma być wyświetlane;
  • wiersz 12: wartość logiczna wskazująca, czy pole [Zone 3] ma być wyświetlane;

Kod akcji [/ajax-10] jest następujący:


@RequestMapping(value = "/ajax-10", method = RequestMethod.POST)
    @ResponseBody()
    public JsonResult10 ajax10(HttpServletRequest request, HttpServletResponse response) {
        // kontekst Thymeleaf
        WebContext thymeleafContext = new WebContext(request, response, request.getServletContext());
        // odpowiedź
        JsonResult10 result = new JsonResult10();
        // sesja
        session.setZone1(null);
        session.setZone3(null);
        session.setZone1Active(false);
        session.setZone3Active(false);
        // zwracamy losową odpowiedź
        int cas = new Random().nextInt(3);
        switch (cas) {
        case 0:
            // strefa 1 aktywna
            setZone1(thymeleafContext, result);
            return result;
        case 1:
            // strefa 3 aktywna
            setZone3(thymeleafContext, result);
            return result;
        case 2:
            // strefy 1 i 3 aktywne
            setZone1(thymeleafContext, result);
            setZone3(thymeleafContext, result);
            return result;
        }
        return null;
    }
  • wiersz 5: pobieramy kontekst [Thymeleaf]. Później zobaczymy, do czego nam się przyda;
  • wiersz 7: na razie tworzymy pustą odpowiedź;
  • wiersze 9–12: umieszczamy w [null] dwa pola zawarte w sesji i wskazujemy, że nie powinny być wyświetlane. Te dwa pola zostaną wkrótce wygenerowane, ale możliwe jest, że wygenerowane zostanie tylko jedno z nich;
  • wiersze 14–29: generowane są oba pola;
  • wiersze 17–19: wygenerowano tylko pole [Zone 1];
  • wiersze 21–23: generowana jest tylko strefa [Zone 3];
  • wiersze 25–28: generowane są oba pola [Zone 1] i [Zone 3];

Strumień HTML z pola [Zone 1] jest generowany przy użyciu następującej metody:


    private void setZone1(WebContext thymeleafContext, JsonResult10 result) {
        // strefa 1 aktywna
        // strumień HTML
        int cpt1 = session.getCpt1() + 1;
        thymeleafContext.setVariable("cpt1", cpt1);
        thymeleafContext.setLocale(new Locale("fr", "FR"));
        String zone1 = engine.process("vue-09-zone1", thymeleafContext);
        result.setZone1(zone1);
        result.setZone1Active(true);
        // sesja
        session.setCpt1(cpt1);
        session.setZone1(zone1);
        session.setZone1Active(true);
}
  • wiersz 1: parametry to:
    • kontekst [Thymeleaf] typu [WebContext],
    • odpowiedź dla klienta w trakcie tworzenia typu [JsonResult10];
  • wiersz 3: zwiększa się licznik [cpt1] sesji, który zlicza, ile razy wyświetlany jest obszar [Zone 1];
  • wiersz 4: kontekst [Thymeleaf] typu [WebContext] zachowuje się nieco podobnie do szablonu [Model] w Spring MVC. Aby dodać element do szablonu, używa się [WebContext.setVariable]. W tym przypadku umieszczamy więc licznik [cpt1] w szablonie [Thymeleaf]. Umożliwi to obliczenie wyrażenia Thymeleaf [${cpt1}]
  • wiersz 5: kontekst [Thymeleaf] ma ustawioną lokalizację. Dzięki temu może on obliczać wyrażenia typu [#{clé_msg}]. W tym przypadku kontekstowi Thymeleaf przypisano francuską lokalizację;
  • wiersz 6: to najciekawsza instrukcja. Silnik Thymeleaf przetworzy widok [vue-09-zone1.xml] przy użyciu modelu i ustawień regionalnych, które właśnie obliczono, a zamiast wysłać wynikowy strumień HTML do klienta, zwróci go jako ciąg znaków;
  • wiersze 7–9: strumień HTML z obszaru [Zone 1], który został właśnie obliczony, jest zapisywany w sesji oraz w wyniku, który zostanie wysłany do klienta. Ponadto wskazano, że obszar [Zone 1] ma zostać wyświetlony;
  • wiersze 11–13: w sesji zapisywane są informacje dotyczące obszaru [Zone 1], aby umożliwić jego ponowne wygenerowanie;

Wiersz 7 dotyczy następującego widoku [vue-09-zone1.xml]:


<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
    <span th:text="#{message.zone}"></span>
    <span th:text="${cpt1}"></span>
</html>
  • wiersz 3: wyrażenie [#{message.zone}] zostanie obliczone przy użyciu szablonu lokalnego;
  • wiersz 4: wyrażenie [${cpt1}] zostanie obliczone przy użyciu szablonu Thymeleaf;

Komunikat o kluczu [message.zone] jest zdefiniowany w plikach komunikatów [messages_fr.properties] i [messages_en.properties]:

  

[messages_fr.properties]


message.zone=Nombre d'accès : 

[messages_en.properties]


message.zone=Number of hits: 

Strumień HTML z obszaru [Zone 3] jest generowany w podobny sposób:


    private void setZone3(WebContext thymeleafContext, JsonResult10 result) {
        // strefa 3 aktywna
        // strumień HTML
        int cpt3 = session.getCpt3() + 1;
        thymeleafContext.setVariable("cpt3", cpt3);
        thymeleafContext.setLocale(new Locale("en", "US"));
        String zone3 = engine.process("vue-09-zone3", thymeleafContext);
        result.setZone3(zone3);
        result.setZone3Active(true);
        // sesja
        session.setCpt3(cpt3);
        session.setZone3(zone3);
        session.setZone3Active(true);
}
  • wiersz 6: ustawieniem regionalnym strefy [Zone 3] jest angielskie;

7.5.6. Przetwarzanie odpowiedzi akcji [/ajax-10]

Wróćmy do kodu JS z [local9.js], który będzie przetwarzał odpowiedź serwera:


// po otrzymaniu odpowiedzi z serwera
// w przypadku powodzenia
function onSuccess(data) {
    console.log("onSuccess");
    // treść
    if (data.content) {
        content.html(data.content);
    }
    // strefa 1
    if (data.zone1Active) {
        $("#zone1").show();
        if (data.zone1) {
            $("#zone1-content").html(data.zone1);
        }
    } else {
        $("#zone1").hide();
    }
    // czy strefa 3 jest aktywna?
    if (data.zone3Active) {
        $("#zone3").show();
        if (data.zone3) {
            $("#zone3-content").html(data.zone3);
        }
    } else {
        $("#zone3").hide();
    }
    // wpisy?
    if (data.saisies) {
        $("#saisies").html(data.saisies);
    }
    // błąd?
    if (data.erreur) {
        erreur.text(data.erreur);
        erreur.show();
    } else {
        erreur.hide();
    }
}

Przypomnijmy strukturę Java odpowiedzi otrzymanej w wierszu 3 w zmiennej [data]:


public class JsonResult10 {

    // dane
    private String content;
    private String zone1;
    private String zone3;
    private String erreur;
    private String saisies;
    private boolean zone1Active;
    private boolean zone3Active;

}
  • wiersze 6–8: jeśli [data.content!=null], to inicjuje się pole [id=content] tą wartością. Pole to reprezentuje w całości [Page 1] lub [Page 2]. W niniejszym przykładzie mamy [data.content==null], a zatem pole [id=content] nie zostanie zmienione i nadal będzie wyświetlać [Page 1];
  • wiersze 10–17: wyświetla się [Zone 1], jeśli [data.zone1Active==true]. Jeśli ponadto [data.zone1!=null], wówczas zawartość [Zone 1] ulega zmianie, w przeciwnym razie pozostaje bez zmian;
  • wiersze 19–26: to samo dotyczy [Zone 3];
  • wiersze 28–30: jeśli występuje [data.saisies!=null], wówczas obszar [Saisies] jest odświeżany. W niniejszym przykładzie mamy [data.saisies==null], a zatem obszar [Saisies] pozostaje bez zmian;
  • wiersze 32–37: analogiczne rozumowanie dotyczy pola [Erreur] z następującymi niuansami:
    • wiersz 33: [data.erreur] będzie komunikatem o błędzie w formacie tekstowym;
    • wiersz 36: jeśli [data.erreur==null], to pole [Erreur] jest ukryte. Mogło ono bowiem zostać wyświetlone podczas poprzedniego zapytania;

W przypadku błędu po stronie serwera (HTTP, status typu 500 Internal server error) wykonywana jest następująca funkcja:


// po otrzymaniu odpowiedzi z serwera
// w przypadku niepowodzenia
function onError(jqXHR) {
    console.log("onError");
    // błąd systemowy
    erreur.text(jqXHR.responseText);
    erreur.show();
}

Aby zobaczyć taki błąd, zmodyfikujmy funkcję [postForm] w następujący sposób:


function postForm() {
    console.log("postForm");
    // pobieramy referencje z bieżącej strony
    ...
    // ręczne wywołanie Ajax
    $.ajax({
        url : '/ajax-10x',
        ...
    })
}
  • wiersz 7: wprowadzamy nieistniejącą funkcję URL;

Oto wyniki po kliknięciu przycisku [Rafraîchir]:

Ciekawe jest to, że błąd również został wysłany w postaci ciągu znaków jSON.

Metoda wykonywana po otrzymaniu odpowiedzi z serwera jest następująca:


// po [onSuccess, onError]
function onComplete() {
    console.log("onComplete");
    // obrazek ładowania
    loading.hide();
}

Po prostu ukrywa się animowany obrazek wskazujący na oczekiwanie.

7.5.7. Wyświetlanie strony [Page 2]

Kod HTML z linku [Valider] wygląda następująco:


<a href="javascript:valider()">Valider</a>

Funkcja JS [valider] jest następująca:


// sprawdzanie poprawności wprowadzonych wartości
function valider() {
    // wartość przesłana
    var post = JSON3.stringify({
        "value1" : $("#text1").val().trim(),
        "value2" : $("#text2").val().trim()
    });
    // ręczne wywołanie Ajax
    $.ajax({
        url : '/ajax-11A',
        headers : {
            'Accept: 'application/json',
            'Content-Type: 'application/json'
        },
        type : 'POST',
        data : post,
        dataType : 'json',
        beforeSend : onBegin,
        success : onSuccess,
        error : onError,
        complete : onComplete
    })
}
  • wiersze 4–7: mamy dwie wartości v1 i v2 do wysłania: te z komponentów wprowadzania danych oznaczonych jako [#text1] i [#text2]. Zrobimy teraz coś nowego. Prześlemy te dwie wartości w postaci ciągu znaków jSON {"value1":v1,"value2":v2};
  • wiersz 10: przesłane wartości zostaną skierowane do akcji [ajax-11A];
  • wiersz 12: ponieważ wiemy, że otrzymamy odpowiedź jSON, wskazujemy, że możemy ją odebrać w formacie jSON;
  • wiersz 13: informujemy serwer, że wyślemy mu wartość w postaci ciągu znaków o formacie jSON;
  • wiersze 15–16: tworzymy ciąg POST z wartości, którą chcemy wysłać;
  • wiersz 17: otrzymamy ciąg jSON;

7.5.8. Akcja [ajax-11A]

Akcja [ajax-11A], która przetwarza wysłany ciąg jSON, wygląda następująco:


@RequestMapping(value = "/ajax-11A", method = RequestMethod.POST, consumes = "application/json")
    @ResponseBody
    public JsonResult10 ajax11A(@RequestBody @Valid PostAjax11A post, BindingResult bindingResult, Locale locale,     HttpServletRequest request, HttpServletResponse response) {
        ...
    }
  • wiersz 1: za pomocą ["application/json"] wskazujemy, że akcja oczekuje dokumentu w formacie jSON. Dokument ten stanowi wartość przesłaną przez klienta;
  • wiersz 3: wartość przesłana zostanie pobrana z następującego obiektu [PostAjax11A post]:
  

package istia.st.springmvc.models;

import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;

import org.hibernate.validator.constraints.Range;

public class PostAjax11A {

    // dane
    @Size(min = 4, max = 6)
    @NotNull
    private String value1;
    @Range(min = 10, max = 14)
    @NotNull
    private Integer value2;

    // metody pobierające i ustawiające
    ...
}
  • struktura obiektu [PostAjax11A] musi odzwierciedlać strukturę przesłanego obiektu {"value1":v1,"value2":v2}. Konieczne jest zatem dodanie pól [value1] (wiersz 13) i [value2] (wiersz 16);
  • na oba pola nałożono ograniczenia integralności;

Wróćmy do kodu akcji [ajax-11A]:


@RequestMapping(value = "/ajax-11A", method = RequestMethod.POST, consumes = "application/json")
    @ResponseBody
    public JsonResult10 ajax11A(@RequestBody @Valid PostAjax11A post, BindingResult bindingResult, Locale locale,    HttpServletRequest request, HttpServletResponse response) {
        // kontekst Thymeleaf
        WebContext thymeleafContext = new WebContext(request, response, request.getServletContext());
        // odpowiedź
        JsonResult10 result = new JsonResult10();
        // czy post jest poprawny?
        if (bindingResult.hasErrors()) {
            // zwracamy stronę 1 z błędem
            result.setZone1Active(session.isZone1Active());
            result.setZone3Active(session.isZone3Active());
            result.setErreur(getErreursForModel(bindingResult));
            return result;
        }
        ...
}
  • wiersz 3: adnotacja [@RequestBody] określa dokument wysłany przez klienta. Jest to wartość przesłana przez niego w polu jSON. Zostanie ona zatem wykorzystana do utworzenia obiektu [PostAjax11A];
  • wiersz 3: adnotacja [@Valid] wymusza walidację przesłanej wartości;
  • wiersz 9: jeśli walidacja zakończy się niepowodzeniem:
    • wiersz 13: zwracany jest komunikat o błędzie,
    • wiersze 11–12: pola 1 i 3 są przywracane do stanu, w jakim się znajdowały (wyświetlone lub nie);

Obliczanie komunikatu o błędzie odbywa się w następujący sposób:


    private String getErreursForModel(BindingResult result) {
        StringBuffer buffer = new StringBuffer();
        for (FieldError error : result.getFieldErrors()) {
            StringBuffer bufferCodes = new StringBuffer("(");
            for (String code : error.getCodes()) {
                bufferCodes.append(String.format("%s ", code));
            }
            bufferCodes.append(")");
            buffer.append(String.format("[%s:%s:%s:%s]", error.getField(), error.getRejectedValue(), bufferCodes,
                    error.getDefaultMessage()));
        }
        return buffer.toString();
}

Jest to funkcja, z którą już się spotkaliśmy.

Akcja [ajax-11A] przebiega w następujący sposób:


@RequestMapping(value = "/ajax-11A", method = RequestMethod.POST, consumes = "application/json")
    @ResponseBody
    public JsonResult10 ajax11A(@RequestBody @Valid PostAjax11A post, BindingResult bindingResult, Locale locale,    HttpServletRequest request, HttpServletResponse response) {
        // kontekst Thymeleaf
        WebContext thymeleafContext = new WebContext(request, response, request.getServletContext());
        // odpowiedź
        JsonResult10 result = new JsonResult10();
        // czy wysłanie jest prawidłowe?
        if (bindingResult.hasErrors()) {
    ...
        }
        // zapisywanie pola wprowadzania danych
        thymeleafContext.setVariable("value1", post.getValue1());
        thymeleafContext.setVariable("value2", post.getValue2());
        session.setSaisies(engine.process("vue-09-saisies", thymeleafContext));
        // wysyłamy stronę 2
        result.setContent(engine.process("vue-09-page2", thymeleafContext));
        return result;
}
  • wiersze 13–14: przesłane wartości są umieszczane w kontekście Thymeleaf;
  • wiersz 15: przy użyciu tego kontekstu obliczany jest widok [vue-09-saisies] i umieszczany w sesji, aby można go było później odtworzyć;
  • wiersz 17: strona 2 jest umieszczana w wyniku, który zostanie wysłany do klienta;

Widok [vue-09-page2.xml] wygląda następująco:

  

<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
    <body>
        <h2>Page 2</h2>
        <p>
            <h4>Valeurs saisies :</h4>
            <p>
                Chaîne de caractères :
                <span th:text="${value1}"></span>
            </p>
            <p>
                Nombre entier :
                <span th:text="${value2}"></span>
            </p>
            <a href="javascript:retourPage1()">Retour à la page 1</a>
        </p>
    </body>
</html>
  • w wierszach 9 i 13 wyświetlane są wartości [value1, value2], które akcja [/ajax-11A] umieściła w kontekście Thymeleaf;

7.5.9. Przetwarzanie odpowiedzi akcji [/ajax-11A]

Po stronie klienta odpowiedź akcji [/ajax-10] jest przetwarzana przez funkcję [onSuccess]:


function onSuccess(data) {
    console.log("onSuccess");
    // treść
    if (data.content) {
        content.html(data.content);
    }
    // pole 1
    if (data.zone1Active) {
        $("#zone1").show();
        if (data.zone1) {
            $("#zone1-content").html(data.zone1);
        }
    } else {
        $("#zone1").hide();
    }
    // pole 3 aktywne?
    if (data.zone3Active) {
        $("#zone3").show();
        if (data.zone3) {
            $("#zone3-content").html(data.zone3);
        }
    } else {
        $("#zone3").hide();
    }
    // dane wprowadzone?
    if (data.saisies) {
        $("#saisies").html(data.saisies);
    }
    // błąd?
    if (data.erreur) {
        erreur.text(data.erreur);
        erreur.show();
    } else {
        erreur.hide();
    }
}

Omówiliśmy już ten kod. Rozważmy dwa przypadki: odpowiedź z błędem lub bez:

Z błędem

W tym przypadku akcja [/ajax-11A] wysłała odpowiedź jSON w postaci {"zone1":null, "zone3":null,"saisies":null,"erreur":erreur,"zone1Active":zone1Active,"zone3Active":zone3Active,"content":null}. Jeśli prześledzimy powyższy kod, zauważymy, że:

  • pole [content] nie ulega zmianie. Zawierało stronę nr 1;
  • wyświetlany jest obszar [Erreur];
  • obszary [Zone 1], [Zone 3], [Saisies] pozostają w niezmienionym stanie;

Bez błędów

W tym przypadku akcja [/ajax-11A] wysłała odpowiedź jSON o postaci {"strefa1":null, "strefa3":null,"wpisy":null,"błąd":null,"zone1Active":false,"zone3Active":false,"content":content}. Jeśli prześledzimy powyższy kod, zauważymy, że:

  • wyświetlana jest strefa [content]. Zawiera ona stronę nr 2;

Oto trzy przykłady działania:

Przypadek z błędem walidacji:

Przykład z błędem POST:

Ten typ błędu jest inny. Ponieważ Spring nie był w stanie przekonwertować ciągu znaków jSON na typ [PostAjax11A], zwrócił odpowiedź HTTP z wartością [status=400]. Akcja [ajax-11A] nie została wykonana;

Przypadek bez błędu:

7.5.10. Powrót do strony nr 1

Link [Retour vers la page 1] na stronie nr 2 wygląda następująco:


<a href="javascript:retourPage1()">Retour à la page 1</a>

Metoda JS [retourPage1] wygląda następująco:


// powrót do strony 1
function retourPage1() {
    // ręczne wywołanie Ajax
    $.ajax({
        url : '/ajax-11B',
        headers : {
            'Accept: 'application/json',
        },
        type : 'POST',
        dataType : 'json',
        beforeSend : onBegin,
        success : onSuccess,
        error : onError,
        complete : onComplete
    })
}

Tworzy ona akcję POST, bez wartości wpisanej, skierowaną do akcji [/ajax-11B].

7.5.11. Akcja [/ajax-11B]

Akcja [/ajax-11B] wygląda następująco:


    @RequestMapping(value = "/ajax-11B", method = RequestMethod.POST)
    @ResponseBody
    public JsonResult10 ajax11B(HttpServletRequest request, HttpServletResponse response) {
        // kontekst Thymeleaf
        WebContext thymeleafContext = new WebContext(request, response, request.getServletContext());
        // odpowiedź
        JsonResult10 result = new JsonResult10();
        // przywracamy stronę 1 do pierwotnego stanu
        result.setContent(engine.process("vue-09-page1", thymeleafContext));
        result.setSaisies(session.getSaisies());
        result.setZone1(session.getZone1());
        result.setZone3(session.getZone3());
        result.setZone1Active(session.isZone1Active());
        result.setZone3Active(session.isZone3Active());
        return result;
}

Akcja ma na celu ponowne wygenerowanie strony nr 1 wraz z trzema polami [Zone1, Zone3, Erreur]:

  • wiersz 9: strona nr 1 jest umieszczana w wyniku;
  • wiersz 10: obszar wprowadzania danych jest umieszczany w wyniku;
  • wiersz 11: obszar [Zone 1] jest umieszczany w wyniku;
  • wiersz 12: obszar [Zone 3] jest umieszczany w wyniku;
  • wiersze 13–14: do wyniku dodaje się stan pól [Zone 1] i [Zone 3];

7.5.12. Przetwarzanie odpowiedzi akcji [/ajax-11B]

Odpowiedź akcji [/ajax-11B] jest przetwarzana przez funkcję [onSuccess]:


function onSuccess(data) {
    console.log("onSuccess");
    // treść
    if (data.content) {
        content.html(data.content);
    }
    // strefa 1
    if (data.zone1Active) {
        $("#zone1").show();
        if (data.zone1) {
            $("#zone1-content").html(data.zone1);
        }
    } else {
        $("#zone1").hide();
    }
    // czy strefa 3 jest aktywna?
    if (data.zone3Active) {
        $("#zone3").show();
        if (data.zone3) {
            $("#zone3-content").html(data.zone3);
        }
    } else {
        $("#zone3").hide();
    }
    // wpisy?
    if (data.saisies) {
        $("#saisies").html(data.saisies);
    }
    // błąd?
    if (data.erreur) {
        erreur.text(data.erreur);
        erreur.show();
    } else {
        erreur.hide();
    }
}

Akcja [/ajax-11B] wysłała odpowiedź jSON w postaci {"zone1":zone1, "zone3":zone3,"saisies":saisies,"erreur":null,"zone1Active":zone1Active,"zone3Active":zone3Active,"content":content}. Analizując powyższy kod, można zauważyć, że:

  • pole [content] zostało zmodyfikowane. Zawierało stronę nr 2. Teraz będzie zawierało stronę nr 1;
  • obszar [Erreur] jest ukryty;
  • pola [Zone 1], [Zone 3], [Saisies] są wyświetlane w stanie, w jakim się znajdowały;

7.6. Zarządzanie sesją po stronie klienta

7.6.1. Wprowadzenie

W poprzednim akapicie zarządzaliśmy sesją o następującej strukturze:


public class SessionModel1 implements Serializable {

    // dwa liczniki
    private int cpt1 = 0;
    private int cpt3 = 0;
    // trzy strefy
    private String zone1 = "xx";
    private String zone3 = "zz";
    private String saisies;
    private boolean zone1Active = true;
    private boolean zone3Active = true;
...
}

Gdy liczba użytkowników jest bardzo duża, pamięć zajmowana przez sesje wszystkich tych użytkowników może stanowić problem. Zasadą jest zatem minimalizowanie jej rozmiaru. Model APU (aplikacja jednostronicowa) pozwala na zarządzanie sesją po stronie klienta i korzystanie z serwera WWW bez sesji. Strona jednostronicowa jest bowiem początkowo ładowana przez przeglądarkę. Wraz z nią pobierany jest towarzyszący jej plik JavaScript. Ponieważ strona nie jest odświeżana, plik JS pozostaje na stałe w przeglądarce w takiej postaci, w jakiej został pierwotnie załadowany. Można zatem wykorzystać jego zmienne globalne do przechowywania informacji o różnych działaniach użytkownika. Właśnie temu przyjrzymy się teraz. Będziemy nie tylko zarządzać sesją po stronie klienta, ale także przeprojektować aplikację JS tak, aby jak najmniej obciążać serwer.

7.6.2. Akcja [/ajax-12]

  

Akcja [/ajax-12] wygląda następująco:


    @RequestMapping(value = "/ajax-12", method = RequestMethod.GET, produces = "text/html; charset=UTF-8")
    public String ajax12() {
        return "vue-12";
}

Widok [vue-12.xml] wygląda następująco:

  

<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
    <head>
        <meta name="viewport" content="width=device-width" />
        <title>Ajax-12</title>
        <link rel="stylesheet" href="/css/ajax01.css" />
        <script type="text/javascript" src="/js/jquery/jquery-2.1.1.min.js"></script>
        <script type="text/javascript" src="/js/json3.js"></script>        
        <script type="text/javascript" src="/js/local12.js"></script>
    </head>
    <body>
        <h3>Ajax - 12 - Navigation dans une Application à Page Unique</h3>
        <h3>avec des flux HTML embarqués dans une chaîne jSON</h3>
        <h3>et une session gérée par le client JS</h3>
        <hr />
        <div id="content" th:include="vue-09-page1" />
        <img id="loading" src="/images/loading.gif" />
        <div id="erreur" style="background-color:lightgrey"></div>
    </body>
</html>
  • Ten widok jest identyczny z widokiem [vue-09], z tą różnicą, że w wierszu 9 zastosowano skrypt JS;

Wyświetlany widok wygląda następująco:

 

7.6.3. Kod JS do obsługi przycisku [Rafraîchir]

  

Kod pliku [local12.js] jest następujący:


// zmienne globalne
var content;
var loading;
var erreur;
var page1;
var page2;
var value1;
var value2;
var session = {
        "cpt1" : 0,
        "cpt3" : 0
    };

// podczas ładowania dokumentu
$(document).ready(function() {
    // pobieramy odniesienia do różnych elementów strony
    loading = $("#loading");
    loading.hide();
    erreur = $("#erreur");
    erreur.hide();
    content = $("#content");
});
  • wiersze 17–21: po załadowaniu strony głównej zapisuje się odniesienia do trzech komponentów oznaczonych kodem [loading, erreur, content] w zmiennych globalnych w wierszach 2–4;
  • wiersze 5–6: w celu zapisania obu stron;
  • wiersze 7–8: w celu zapisania dwóch wartości przesłanych przez link [Valider];
  • wiersz 9: sesja. Zapisuje po stronie klienta wartości liczników [cpt1, cpt3];

Funkcja [postForm] obsługuje kliknięcie przycisku [Rafraîchir]:


function postForm() {
    console.log("postForm");
    // zapisujemy sesję
    var post = JSON3.stringify(session);
    // ręcznie wykonuje się wywołanie Ajax
    $.ajax({
        url : '/ajax-13',
        headers : {
            'Accept: 'application/json',
            'Content-Type: 'application/json'
        },
        type : 'POST',
        data : post,
        dataType : 'json',
        beforeSend : onBegin,
        success : function(data) {
            ...
        },
        error : onError,
        complete : onComplete
    })
}

Różnice w stosunku do poprzedniej wersji są następujące:

  • funkcja URL w wierszu 7 jest inna;
  • wiersz 4: wysyłana jest wartość, podczas gdy wcześniej nie była wysyłana. Wartością tą jest ciąg znaków jSON z sesji. Zasada działania jest następująca:
    • klient wysyła sesję do serwera,
    • serwer ją modyfikuje i odsyła z powrotem,
    • klient zapisuje nową sesję;
  • wiersz 10: wysyłamy dokument w formacie jSON (wartość wysłana);
  • wiersz 13: mamy coś do wysłania;
  • wiersze 15–20: funkcje [beforeSend, error, complete] są takie same jak w poprzedniej wersji. Zmienia się jedynie funkcja [success] (wiersze 16–18);

7.6.4. Akcja [/ajax-13]

  

Akcja [/ajax-13] wygląda następująco:


    @RequestMapping(value = "/ajax-13", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
    @ResponseBody()
    public JsonResult13 ajax13(@RequestBody SessionModel2 session2, HttpServletRequest request,    HttpServletResponse response) {
    ...
}
  • wiersz 3: parametr [@RequestBody SessionModel2 session2] pobiera sesję przesłaną przez klienta. Ma ona następujący typ [SessionModel2]:
  

package istia.st.springmvc.models;

import java.io.Serializable;

public class SessionModel2 implements Serializable {

    private static final long serialVersionUID = 1L;
    // dwa liczniki
    private int cpt1 = 0;
    private int cpt3 = 0;

    // metody pobierające i ustawiające
    ...
}

Sesja [SessionModel2] przechowuje następujące elementy:

  • wiersz 9: liczba wystąpień [cpt1], w których wyświetlane jest pole [Zone 1];
  • wiersz 10: liczba wystąpień [cpt3], w których wyświetlane jest pole [Zone 3];

Przejdźmy do analizy kodu akcji [/ajax-13]:


    @RequestMapping(value = "/ajax-13", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
    @ResponseBody()
    public JsonResult13 ajax13(@RequestBody SessionModel2 session2, HttpServletRequest request,    HttpServletResponse response) {
    ...
}
  • w wierszu 3 typ odpowiedzi [JsonResult13] jest następujący:
  

package istia.st.springmvc.models;

public class JsonResult13 {

    // dane
    private String page2;
    private String zone1;
    private String zone3;
    private String erreur;
    private String value1;
    private Integer value2;

    // sesja
    private SessionModel2 session;

    // metody pobierające i ustawiające
    ...
}
  • wiersz 14: sesja. Serwer odsyła ją do klienta w celu zapamiętania;
  • wiersz 6: treść HTML strony nr 2;
  • wiersz 7: zawartość HTML obszaru [Zone 1];
  • wiersz 8: zawartość HTML obszaru [Zone 3];
  • wiersz 9: ewentualny komunikat o błędzie;
  • wiersze 10–11: dwie informacje obliczone przez serwer i wyświetlane na stronie nr 2;

Przejdźmy do analizy kodu akcji [/ajax-13]:


@RequestMapping(value = "/ajax-13", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
    @ResponseBody()
    public JsonResult13 ajax13(@RequestBody SessionModel2 session2, HttpServletRequest request,
            HttpServletResponse response) {
        // kontekst Thymeleaf
        WebContext thymeleafContext = new WebContext(request, response, request.getServletContext());
        // odpowiedź
        JsonResult13 result = new JsonResult13();
        result.setSession(session2);
        // zwracamy losową odpowiedź
        int cas = new Random().nextInt(3);
        switch (cas) {
        case 0:
            // strefa 1 aktywna
            setZone1B(thymeleafContext, result);
            return result;
        case 1:
            // strefa 3 aktywna
            setZone3B(thymeleafContext, result);
            return result;
        case 2:
            // strefy 1 i 3 aktywne
            setZone1B(thymeleafContext, result);
            setZone3B(thymeleafContext, result);
            return result;
        }
        return null;
    }
  • wiersz 9: sesja jest umieszczana w wyniku akcji;

Metoda [setZone1B], która aktywuje obszar [Zone 1], wygląda następująco:


    private void setZone1B(WebContext thymeleafContext, JsonResult13 result) {
        // odzyskuje się sesję
        SessionModel2 session = result.getSession();
        // strefa 1 aktywna
        // strumień HTML
        int cpt1 = session.getCpt1() + 1;
        thymeleafContext.setVariable("cpt1", cpt1);
        thymeleafContext.setLocale(new Locale("fr", "FR"));
        String zone1 = engine.process("vue-09-zone1", thymeleafContext);
        result.setZone1(zone1);
        // sesja
        session.setCpt1(cpt1);
}
  • wiersz 3: pobierana jest sesja. Zostanie ona zmodyfikowana w wierszu 12 przy użyciu nowego licznika [cpt1]. Należy pamiętać, że sesja ta zostanie odesłana do klienta;
  • wiersz 10: nowe pole [Zone 1];

Metoda [setZone3B], która aktywuje pole [Zone 3], działa analogicznie:


private void setZone3B(WebContext thymeleafContext, JsonResult13 result) {
        // odzyskiwanie sesji
        SessionModel2 session = result.getSession();
        // strefa 3 aktywna
        // strumień HTML
        int cpt3 = session.getCpt3() + 1;
        thymeleafContext.setVariable("cpt3", cpt3);
        thymeleafContext.setLocale(new Locale("en", "US"));
        String zone3 = engine.process("vue-09-zone3", thymeleafContext);
        result.setZone3(zone3);
        // sesja
        session.setCpt3(cpt3);
    }

7.6.5. Przetwarzanie odpowiedzi akcji [/ajax-13]

Po stronie klienta odpowiedź jSON z akcji [/ajax-13] jest przetwarzana przez następującą funkcję [onSuccess]:


function postForm() {
    console.log("postForm");
    // wysyłamy sesję
    var post = JSON3.stringify(session);
    // ręcznie wysyłamy żądanie Ajax
    $.ajax({
    ...
        success : function(data) {
            // zapisujemy sesję
            session = data.session;
            // aktualizujemy oba pola
            if (data.zone1) {
                $("#zone1-content").html(data.zone1);
                $("#zone1").show();
            } else {
                $("#zone1").hide();
            }
            if (data.zone3) {
                $("#zone3").show();
                $("#zone3-content").html(data.zone3);
            } else {
                $("#zone3").hide();
            }
        },
...
    })
}
  • wiersze 12–17: jeśli serwer umieścił coś w polu [zone1] odpowiedzi, należy ponownie wygenerować obszar [Zone 1] i wyświetlić go; w przeciwnym razie należy go ukryć;
  • wiersze 18–23: ta sama zasada dotyczy obszaru [Zone 3];

7.6.6. Wyświetlanie strony [Page 2]

Kod HTML linku [Valider] jest następujący:


<a href="javascript:valider()">Valider</a>

Funkcja JS [valider] jest następująca:


// sprawdzamy poprawność wprowadzonych wartości
function valider() {
    // zapisujemy stronę 1
    page1 = content.html();
    // zapisywanie wprowadzonych wartości
    value1 = $("#text1").val().trim();
    value2 = $("#text2").val().trim();
    // wartość przesłana
    var post = JSON3.stringify({
        "value1" : value1,
        "value2" : value2,
        "pageRequired" : page2 ? false : true
    });
    // ręczne wywołanie Ajax
    $.ajax({
        url : '/ajax-14',
        headers : {
            'Accept: 'application/json',
            'Content-Type: 'application/json'
        },
        type : 'POST',
        data : post,
        dataType : 'json',
        beforeSend : onBegin,
        success : function(data) {
        ...
        },
        error : onError,
        complete : onComplete
    })
}
  • utworzymy funkcję POST, która normalnie przeniesie nas na stronę nr 2;
  • wiersz 4: zapisujemy stronę nr 1, aby móc do niej powrócić w późniejszym czasie;
  • wiersze 6–7: poprzednia operacja nie zapisuje wprowadzonych wartości, a jedynie kod strony HTML. Dlatego teraz zapisujemy obie wartości wprowadzone w formularzu;
  • wiersze 9–13: obie wprowadzone wartości są umieszczane w ciągu znaków jSON. To właśnie ten ciąg zostanie wysłany;
  • wiersz 12: parametr wskazujący serwerowi, czy potrzebna jest strona nr 2. Postąpimy w następujący sposób. Najpierw zażądamy strony nr 2, a następnie zapiszemy ją w zmiennej JS [page2]. Następnie nie będziemy już jej ponownie żądać. Będziemy korzystać ze strony z pamięci podręcznej. W wierszu 2 wartość [pageRequired] wynosi [true], jeśli zmienna [page2] jest pusta, a w przeciwnym razie – [false];
  • należy zauważyć, że sesja nie jest wysyłana. Wynika to z faktu, że przechowuje ona liczniki, których akcja [/ajax-14] z wiersza 20 nie modyfikuje;

7.6.7. Akcja [/ajax-14]

Akcja [/ajax-14] wygląda następująco:


@RequestMapping(value = "/ajax-14", method = RequestMethod.POST)
    @ResponseBody
    public JsonResult13 ajax14(@RequestBody @Valid PostAjax14 post, BindingResult bindingResult, Locale locale,    HttpServletRequest request, HttpServletResponse response) {
        ...
    }
  • wiersz 3: odpowiedź ma zawsze typ [JsonResult13];
  • wiersz 3: wysyłana wartość jest zawarta w następującym typie [PostAjax14]:

package istia.st.springmvc.models;

public class PostAjax14 extends PostAjax11A {

    // strona 2
    private boolean pageRequired;

    // metody getter i setter
    ...
}
  • wiersz 3: klasa [PostAjax14] rozszerza klasę [PostAjax11A] z poprzedniej wersji. Ma zatem strukturę [value1, value2, pageRequired];

Akcja [/ajax-14] przebiega w następujący sposób:


    @RequestMapping(value = "/ajax-14", method = RequestMethod.POST)
    @ResponseBody
public JsonResult13 ajax14(@RequestBody @Valid PostAjax14 post, BindingResult bindingResult, Locale locale,    HttpServletRequest request, HttpServletResponse response) {
        // kontekst Thymeleaf
        WebContext thymeleafContext = new WebContext(request, response, request.getServletContext());
        // odpowiedź
        JsonResult13 result = new JsonResult13();
        // czy post jest poprawny?
        if (bindingResult.hasErrors()) {
            // zwracamy błąd
            result.setErreur(getErreursForModel(bindingResult));
            return result;
        }
        // wysyłamy stronę 2
        result.setValue1(post.getValue1());
        result.setValue2(post.getValue2());
        // czy strona jest wymagana?
        if (post.isPageRequired()) {
            result.setPage2(engine.process("vue-12-page2", thymeleafContext));
        }
        return result;
}
  • wiersze 9–13: jeśli przesłane wartości [value1, value2] są nieprawidłowe, zwracany jest komunikat o błędzie;
  • wiersze 15–16: zazwyczaj serwer powinien wykonać obliczenia na podstawie przesłanych wartości. W tym przypadku ogranicza się do ich odesłania, aby potwierdzić, że zostały prawidłowo odebrane;
  • wiersze 18–20: strona nr 2 jest zwracana tylko wtedy, gdy została zażądana przez klienta. W wierszu 19 widok [vue-12-page2] jest nowy:
 

<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
    <body>
        <h2>Page 2</h2>
        <p>
            <h4>Valeurs saisies :</h4>
            <p>
                Chaîne de caractères :
                <span id="value1"></span>
            </p>
            <p>
                Nombre entier :
                <span id="value2"></span>
            </p>
            <a href="javascript:retourPage1()">Retour à la page 1</a>
        </p>
    </body>
</html>
  • kod XML nie zawiera już wartości obliczanych przez Thymeleaf, jak to miało miejsce wcześniej;
  • zidentyfikowano miejsca, w których należy umieścić wartości zwracane przez serwer jako [value1, value2]. Wiersz 9, [id='value1'] wskazuje miejsce, w którym należy umieścić [value1]. W wierszu 13 to samo dotyczy wartości [value2];

7.6.8. Przetwarzanie odpowiedzi akcji [/ajax-14]

Odpowiedź akcji [/ajax-14] jest przetwarzana przez następującą funkcję [success]:


// sprawdzanie poprawności wprowadzonych wartości
function valider() {
    ...
    // ręczne wywołanie Ajax
    $.ajax({
        ...
        success : function(data) {
            // błąd?
            if (data.erreur) {
                // wyświetlanie błędu
                erreur.html(data.erreur);
                erreur.show();
            } else {
                // brak błędu
                erreur.hide();
                // strona 2
                if (page2) {
                    // korzystamy ze strony z pamięci podręcznej
                    content.html(page2);
                } else {
                    // zapisywanie strony 2
                    page2 = data.page2;
                    // wyświetla się
                    content.html(data.page2);
                }
                // aktualizowana jest na podstawie informacji z serwera
                $("#value1").text(data.value1);
                $("#value2").text(data.value2);
            }
        },
...
    })
}
  • wiersze 9–13: jeśli serwer zwrócił błąd, wyświetla się go;
  • wiersze 14–29: sytuacja, w której nie wystąpił błąd. Należy wówczas wyświetlić stronę nr 2;
  • wiersz 17: sprawdzamy, czy strona nr 2 jest już zapisana w zmiennej [page2];
  • wiersz 19: w takim przypadku używamy zmiennej [page2] do wyświetlenia strony nr 2;
  • wiersz 24: w przeciwnym razie wykorzystujemy pole [data.page2] dostarczone przez serwer;
  • wiersz 22: należy zapamiętać stronę nr 2, aby nie trzeba było jej ponownie żądać w przyszłości;
  • wiersze 27–28: na stronie nr 2 wyświetla się dwie informacje [value1, value2] przesłane przez serwer;

7.6.9. Powrót do strony nr 1

Link [Retour vers la page 1] na stronie nr 2 ma następującą postać:


<a href="javascript:retourPage1()">Retour à la page 1</a>

Metoda JS [retourPage1] wygląda następująco:


// powrót do strony 1
function retourPage1() {
    // odświeżamy stronę 1
    content.html(page1);
    // odświeżamy wprowadzone dane
    $("#text1").val(value1);
    $("#text2").val(value2);
}
  • jest to operacja JS bez interakcji z serwerem, ponieważ strona nr 1 została zapisana lokalnie w zmiennej [page1];
  • wiersz 4: odtwarzamy stronę nr 1;
  • wiersze 6–7: zapamiętano jedynie część strony nr 1 o nazwie HTML. Dane wprowadzone przez użytkownika nie zostały zapisane. Należy je zatem odtworzyć;

7.6.10. Wniosek

Wykorzystując możliwości modelu APU, udało nam się uprościć serwer WWW, który jest teraz bezstanowy (brak sesji) i mniej obciążony:

  • usunęliśmy interakcję z serwerem w funkcji JS ([retourPage1]);
  • serwer generuje stronę nr 2 tylko raz;

7.7. Strukturyzacja kodu JavaScript w warstwach

7.7.1. Wprowadzenie

Kod JavaScript poprzedniej aplikacji zaczyna stawać się zbyt złożony. Nadszedł czas, aby uporządkować go warstwowo. Aplikacja pozostanie taka sama jak poprzednio. Nie będziemy wprowadzać żadnych zmian na serwerze, z wyjątkiem zdefiniowania nowej strony startowej. Przeprojektujemy kod JS.

Nowa architektura będzie wyglądać następująco:

7.7.2. Strona startowa

Akcją uruchamiającą aplikację jest następująca akcja [/ajax-16]:


    @RequestMapping(value = "/ajax-16", method = RequestMethod.GET, produces = "text/html; charset=UTF-8")
    public String ajax16() {
        return "vue-16";
}

Wyświetla ona następujący widok: [vue-16.xml]:


<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
    <head>
        <meta name="viewport" content="width=device-width" />
        <title>Ajax-12</title>
        <link rel="stylesheet" href="/css/ajax01.css" />
        <script type="text/javascript" src="/js/jquery/jquery-2.1.1.min.js"></script>
        <script type="text/javascript" src="/js/json3.js"></script>
        <script type="text/javascript" src="/js/local16-dao.js"></script>
        <script type="text/javascript" src="/js/local16-ui.js"></script>
    </head>
    <body>
        <h3>Ajax - 16 - Navigation dans une Application à Page Unique</h3>
        <h3>Structuration du code JS</h3>
        <hr />
        <div id="content" th:include="vue-09-page1" />
        <img id="loading" src="/images/loading.gif" />
        <div id="erreur" style="background-color:lightgrey"></div>
    </body>
</html>
  • wiersze 9–10: kod JS został umieszczony w dwóch różnych plikach:
    • [local-ui] implementuje warstwę [présentation],
    • [local-dao] implementuje warstwę [DAO];
  

7.7.3. Implementacja warstwy [DAO]

7.7.4. Interfejs

Warstwa [DAO] w [local-dao.js] będzie udostępniać następujący interfejs warstwie [présentation]:


function updatePage1(deferred, sendMeBack)
w celu zaktualizowania strony 1 za pomocą przycisku [Rafraîchir]

function getPage2(deferred, sendMeBack, value1, value2, pageRequired)
w celu wyświetlenia strony 2 za pomocą przycisku [Valider]

W języku JavaScript nie ma pojęcia interfejsu. Użyłem tego terminu po prostu po to, aby wskazać, że warstwa [présentation] komunikuje się z warstwą [DAO] wyłącznie za pośrednictwem dwóch powyższych funkcji.

7.7.5. Implementacja interfejsu

Szkielet implementacji wygląda następująco:


var session = {
    "cpt1" : 0,
    "cpt3" : 0
};

// aktualizacja strony 1
function updatePage1(deferred, sendMeBack) {
...
}

// strona 2
function getPage2(deferred, sendMeBack, value1, value2, pageRequired) {
...
}

Celem warstwy [DAO] jest ukrycie przed warstwą [présentation] szczegółów żądań HTTP kierowanych do serwera WWW. Sesja stanowi część tych szczegółów. Jest ona zatem odtąd zarządzana przez warstwę [DAO].

7.7.5.1. Funkcja [updatePage1]

Funkcja [updatePage1] jest wywoływana przez warstwę [présentation] w celu odświeżenia strony 1. Jej kod wygląda następująco:


// aktualizacja strony 1
function updatePage1(deferred, sendMeBack) {
    // zapytanie HTTP
    executePost(deferred, sendMeBack, '/ajax-13', session);
}
  • wiersz 1: funkcja [updatePage1] otrzymuje dwa parametry:
    1. obiekt typu [jQuery.Deferred]. Ten typ obiektu przechowuje stan, który może przyjmować trzy wartości: ['pending', 'resolved', 'rejected']. Gdy trafia do funkcji [updatePage1], znajduje się w stanie [pending];
    2. obiekt JS, który ma zostać zwrócony w warstwie [présentation];

Wszystkie zapytania HTTP są realizowane przez następującą funkcję [executePost]:


// żądanie HTTP
function executePost(deferred, sendMeBack, url, post) {
    // ręcznie wysyłamy żądanie Ajax
    $.ajax({
        headers : {
            'Accept: 'application/json',
            'Content-Type: 'application/json'
        },
        url : url,
        type : 'POST',
        data : JSON3.stringify(post),
        dataType : 'json',
        success : function(data) {
            // zapisujemy sesję
            if (data.session) {
                session = data.session;
            }
            // zwracamy wynik
            deferred.resolve({
                "status" : 1,
                "data" : data,
                "sendMeBack" : sendMeBack
            });
        },
        error : function(jqXHR) {
            // zwracamy komunikat o błędzie
            deferred.resolve({
                "status" : 2,
                "data" : jqXHR.responseText,
                "sendMeBack" : sendMeBack
            });
        }
    });
}
  • wiersz 1: funkcja [executePost] wykonuje wywołanie Ajax typu POST. Oczekuje ona czterech parametrów:
    1. obiekt typu [jQuery.Deferred] w stanie [pending];
    2. obiekt typu JS, który ma zostać zwrócony w warstwie [présentation];
    3. obiekt URL z obiektu POST;
    4. wartość, którą należy wysłać jako obiekt JS;
  • wiersze 5–8: funkcja wysyłająca z jSON (wiersz 7) i odbierająca z jSON (wiersz 6);
  • wiersz 11: wartość do wysłania jest przekształcana na jSON;
  • wiersze 13–24: funkcja wykonywana w przypadku pomyślnego zakończenia wywołania Ajax;
  • wiersze 19–23: jeśli serwer zwrócił sesję, jest ona zapisywana;
  • wiersze 13–18: przekazują obiekt [deferred] do stanu [resolved], przekazując dodatkowo wynik zawierający następujące pola:
    • [status]: 1 oznacza sukces, 2 oznacza niepowodzenie,
    • [data]: odpowiedź serwera jSON,
    • [sendMeBack]: drugi parametr funkcji, który jest obiektem, który wywołujący chce pobrać;
  • wiersze 17–31: funkcja wykonywana w przypadku niepowodzenia wywołania Ajax. Postępujemy tak samo jak poprzednio, z dwiema różnicami:
    • [status] przyjmuje wartość 2, aby zasygnalizować błąd;
    • [data] to ponownie odpowiedź jSON z serwera, ale uzyskana w inny sposób;

7.7.5.2. Funkcja [getPage2]

Funkcja [getPage2] wygląda następująco:


// strona 2
function getPage2(deferred, sendMeBack, value1, value2, pageRequired) {
    // żądanie HTTP
    executePost(deferred, sendMeBack, '/ajax-14', {
        "value1" : value1,
        "value2" : value2,
        "pageRequired" : pageRequired,
    });
}
  • Funkcja przyjmuje następujące parametry:
    1. [deferred]: obiekt typu [jQuery.Deferred] w stanie [pending],
    2. [sendMeBack]: obiekt typu JS, który ma zostać zwrócony w warstwie [présentation],
    3. [value1]: pierwszy wpis na stronie 1,
    4. [value2]: drugi wpis na stronie 2,
    5. [pageRequired]: wartość logiczna wskazująca serwerowi, czy ma wysłać strumień HTML ze strony nr 2;
  • wywoływana jest funkcja [executePost] w celu wykonania niezbędnego zapytania HTTP;

7.7.6. Warstwa [présentation]

Warstwa [présentation] jest zaimplementowana przez plik [local-ui.js]. Ten ostatni zawiera kod z pliku [local12.js], przerobiony tak, aby korzystał z poprzedniej warstwy [DAO]. Zmianie uległy tylko dwie funkcje: [postForm] i [valider].

7.7.6.1. Funkcja [postForm]

Funkcja [postForm] ma następującą postać:


// aktualizacja strony 1
function postForm() {
    // aktualizujemy stronę 1
    var deferred = $.Deferred();
    loading.show();
    updatePage1(deferred, {
        'nadawca: „postForm”,
        'informacja: 10
    });
    // wyświetlanie wyników
    deferred.done(postFormDone);
}
  • wiersz 4: tworzy się obiekt [jQuery.Deferred]. Domyślnie znajduje się on w stanie [pending];
  • wiersz 5: wyświetlany jest obrazek oczekiwania
  • wiersze 6–9: wykonywana jest funkcja [updatePage1]. Przekazywany jest fikcyjny obiekt [sendMeBack], wyłącznie w celu pokazania, do czego może służyć;
  • wiersz 11: parametr funkcji [deferred.done] jest sam w sobie funkcją. Jest to funkcja, która ma zostać wykonana, gdy stan obiektu [deferred] przejdzie do stanu [resolved]. Właśnie widzieliśmy, że funkcja DAO [executePost] przekazywała stan tego obiektu do [resolved] po otrzymaniu odpowiedzi z serwera. Oznacza to, że w momencie uruchomienia funkcji [postFormDone] odpowiedź z serwera została już odebrana;

Funkcja [postFormDone] wygląda następująco:


function postFormDone(result) {
    // koniec oczekiwania
    loading.hide();
    // pobieranie danych
    var data = result.data
    // na potrzeby demonstracji
    console.log(JSON3.stringify(result.sendMeBack));
    // analiza statusu
    switch (result.status) {
    case 1:
        // aktualizujemy oba pola
        if (data.zone1) {
            $("#zone1-content").html(data.zone1);
            $("#zone1").show();
        } else {
            $("#zone1").hide();
        }
        if (data.zone3) {
            $("#zone3").show();
            $("#zone3-content").html(data.zone3);
        } else {
            $("#zone3").hide();
        }
        break;
    case 2:
        // wyświetlanie błędu
        erreur.html(data);
        break;
    }
}
  • wiersz 1: otrzymany parametr [result] jest parametrem przekazanym do metody [deferred.resolve] w funkcji [executePost], na przykład:

            // zwracamy wynik
            deferred.resolve({
                "status" : 1,
                "data" : data,
                "sendMeBack" : sendMeBack
});
  • wiersz 5: pobierana jest odpowiedź z serwera;
  • wiersze 10–24: znajduje się tu kod, który w poprzedniej wersji znajdował się w funkcji [onSuccess] należącej do funkcji [postForm];
  • wiersze 25–28: znajduje się tu kod, który w poprzedniej wersji znajdował się w funkcji [onError] należącej do funkcji [postForm];

7.7.6.2. Rola parametru [sendMeBack]

Do czego służy parametr [sendMeBack]? Przyjrzyjmy się kodowi wywołania funkcji [updatePage1]:


// aktualizacja strony 1
function postForm() {
    // aktualizujemy stronę 1
    var deferred = $.Deferred();
    loading.show();
    updatePage1(deferred, {
        'nadawca: „postForm”,
        'info: 10
    });
    // wyświetlanie wyników
    deferred.done(postFormDone);
}

oraz sygnaturę funkcji [validerDone]:


function postFormDone(result) {
}

W jaki sposób funkcja [postForm] może przekazać informacje do funkcji [postFormDone]? Ta ostatnia ma tylko jeden parametr [result]. Parametr ten jest tworzony przez funkcję [executePost] z warstwy [DAO]. Aby przekazać informacje do funkcji [postFormDone], funkcja [postForm] musi najpierw przekazać je do funkcji [updatePage1]. Taką rolę pełni parametr [sendMeBack]. Stosuje się go w następujący sposób:


function postFormDone(result) {
    // koniec oczekiwania
    loading.hide();
    // pobieranie danych
    var data = result.data
    // na potrzeby demonstracji
    console.log(JSON3.stringify(result.sendMeBack));
    // analiza statusu
    switch (result.status) {
...
  • wiersz 7, funkcja [postFormDone] odzyskała parametr [sendMeBack], który został pierwotnie przekazany do funkcji DAO [updatePage1] przez funkcję [postForm];

7.7.7. Funkcja [valider]

Funkcja [valider] ma następujący wygląd:


// weryfikacja wprowadzonych wartości
function valider() {
    // zapisywanie strony 1
    page1 = content.html();
    // zapisywanie wprowadzonych wartości
    value1 = $("#text1").val().trim();
    value2 = $("#text2").val().trim();
    // brak błędów
    erreur.hide();
    // wyświetla się strona 2
    var deferred = $.Deferred();
    loading.show();
    getPage2(deferred, {
        'nadawca: „potwierdź”,
        '„info”: 20
    }, value1, value2, page2 ? false : true);
    // wyświetlanie wyników
    deferred.done(validerDone);
}

a funkcja [validerDone] (wiersz 18) ma następującą postać:


function validerDone(result) {
    // koniec oczekiwania
    loading.hide();
    // pobieranie danych
    var data = result.data
    // na potrzeby demonstracji
    console.log(JSON3.stringify(result.sendMeBack));
    // analiza statusu
    switch (result.status) {
    case 1:
        // błąd?
        if (data.erreur) {
            // wyświetlanie błędu
            erreur.html(data.erreur);
            erreur.show();
        } else {
            // brak błędu
            erreur.hide();
            // strona 2
            if (page2) {
                // wykorzystuje się stronę z pamięci podręcznej
                content.html(page2);
            } else {
                // zapisywanie strony 2
                page2 = data.page2;
                // wyświetla się
                content.html(data.page2);
            }
            // aktualizowanie strony na podstawie informacji z serwera
            $("#value1").text(data.value1);
            $("#value2").text(data.value2);
        }
        break;
    case 2:
        // wyświetlanie błędu
        erreur.html(data);
        erreur.show();
        break;
    }
}
  • wiersz 5: pobierana jest odpowiedź z serwera;
  • wiersze 10–32: znajduje się tu kod, który w poprzedniej wersji znajdował się w funkcji [onSuccess] należącej do funkcji [valider];
  • wiersze 34–38: znajduje się tu kod, który w poprzedniej wersji znajdował się w funkcji [onError] należącej do funkcji [valider];

7.7.8. Testy

Aplikacja nadal działa tak jak dotychczas, a w konsoli przeglądarki Chrome widoczne są parametry [sendMeBack] funkcji [postForm] i [valider]:

 

7.8. Conclusion

Wróćmy do ogólnego schematu aplikacji Spring MVC:

Dzięki kodowi JavaScript osadzonemu na stronach HTML i wykonywanemu w przeglądarce oraz dzięki modelowi APU można przenieść część kodu do przeglądarki i uzyskać następującą architekturę:

  • mamy architekturę klient-serwer [2] / [1], w której klient i serwer komunikują się za pomocą jSON;
  • w [1] warstwa internetowa Spring MVC dostarcza widoki, fragmenty widoków oraz dane w formacie jSON;
  • w [2]: kod JavaScript osadzony w widoku ładowanym podczas uruchamiania aplikacji może być podzielony na warstwy:
    • warstwa [présentation] odpowiada za interakcje z użytkownikiem,
    • warstwa [DAO] odpowiada za dostęp do danych za pośrednictwem serwera WWW [1],
    • warstwa [métier] może nie istnieć lub przejąć niektóre z niepoufnych funkcji warstwy [métier] serwera w celu odciążenia go;
  • klient [2] może buforować niektóre widoki, aby ponownie odciążyć serwer. Zarządza on sesją;