Skip to content

7. Ajaxifizierung einer Spring-MVC-Anwendung

7.1. Die Rolle von AJAX in einer Webanwendung

Bisher hatten die Lernbeispiele, die wir behandelt haben, die folgende Architektur:

Um von einer Ansicht [View1] zu einer Ansicht [View2] zu wechseln, führt der Browser folgende Schritte aus:

  • eine Anfrage an die Webanwendung;
  • empfängt die Ansicht [View2] und zeigt sie anstelle der Ansicht [View1] an.

Dies ist das klassische Muster:

  • Anfrage vom Browser;
  • der Webserver generiert als Antwort an den Client eine Ansicht;
  • Anzeige dieser neuen Ansicht durch den Browser.

Seit einigen Jahren gibt es eine weitere Art der Interaktion zwischen dem Browser und dem Webserver: AJAX (Asynchronous JavaScript and XML). Dabei handelt es sich um Interaktionen zwischen der vom Browser angezeigten Ansicht und dem Webserver. Der Browser tut weiterhin das, was er am besten kann – eine HTML-Ansicht anzeigen –, wird nun jedoch durch JavaScript gesteuert, das in die angezeigte HTML-Ansicht eingebettet ist. Das Diagramm sieht wie folgt aus:

  • In [1] tritt auf der im Browser angezeigten Seite ein Ereignis ein (ein Klick auf eine Schaltfläche, eine Textänderung usw.). Dieses Ereignis wird von in die Seite eingebettetem JavaScript (JS) abgefangen;
  • In [2] sendet der JavaScript-Code eine HTTP-Anfrage, genau wie es der Browser getan hätte. Die Anfrage ist asynchron: Der Benutzer kann weiterhin mit der Seite interagieren, ohne blockiert zu werden, während er auf die HTTP-Antwort wartet. Die Anfrage folgt dem Standard-Verarbeitungsablauf. Nichts (oder nur sehr wenig) unterscheidet sie von einer Standardanfrage;
  • In [3] wird eine Antwort an den JS-Client gesendet. Anstelle einer vollständigen HTML-Ansicht wird in der Regel eine teilweise HTML-Ansicht, ein XML-Feed oder JSON (JavaScript Object Notation) gesendet;
  • In [4] ruft JavaScript diese Antwort ab und verwendet sie, um einen Bereich der angezeigten HTML-Seite zu aktualisieren.

Für den Benutzer ergibt sich eine Änderung in der Ansicht, da sich das, was er sieht, geändert hat. Es findet jedoch kein vollständiges Neuladen der Seite statt; stattdessen erfolgt nur eine teilweise Änderung der angezeigten Seite. Dies trägt dazu bei, die Seite flüssiger und interaktiver zu gestalten: Da kein vollständiges Neuladen der Seite erfolgt, können wir Ereignisse verarbeiten, die zuvor nicht verwaltet werden konnten. Zum Beispiel, indem dem Benutzer eine Liste von Optionen angeboten wird, während er Zeichen in ein Eingabefeld tippt. Mit jedem neu eingegebenen Zeichen wird eine AJAX-Anfrage an den Server gesendet, der dann zusätzliche Vorschläge zurückgibt. Ohne AJAX war diese Art der Eingabeunterstützung zuvor unmöglich. Wir konnten nicht bei jedem eingegebenen Zeichen eine neue Seite neu laden.

7.2. Aktualisieren einer Seite mit einem HTML-Feed

7.2.1. Die Ansichten

Wir schlagen vor, die folgende Anwendung zu untersuchen:

  • in [1] die Ladezeit der Seite;
  • in [2] werden die vier arithmetischen Operationen an zwei reellen Zahlen A und B durchgeführt;
  • in [3] wird die Antwort des Servers in einem Bereich der Seite angezeigt;
  • in [4] die Zeit der Berechnung. Diese unterscheidet sich von der Ladezeit der Seite [5]. Letztere entspricht [1], was zeigt, dass der Bereich [6] nicht neu geladen wurde. Außerdem hat sich die URL der Seite [7] nicht geändert.

7.2.2. Die Aktion [/ajax-01]

  

Der Controller [Ajax.java] definiert die folgende Aktion [/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) {
        // valid tempo?
        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));
            }
        }
        // prepare the view model [view-01]
        ...
}
  • Zeile 2: Die Aktion [/ajax-01] akzeptiert nur einen Parameter [tempo]. Dies ist die Dauer in Millisekunden, die der Server warten muss, bevor er die Ergebnisse der arithmetischen Operationen sendet;
  • Zeile 4: Der Parameter [tempo] ist optional;
  • Zeilen 5–12: Wir überprüfen, ob der Wert des Parameters [tempo] gültig ist;
  • Zeilen 13–15: Ist dies der Fall, wird der Timeout-Wert in der Sitzung gespeichert. Das bedeutet, dass er so lange gültig bleibt, bis er geändert wird;

Der Code für die Aktion [/ajax-01] setzt sich wie folgt fort:


    @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) {
        // valid tempo?
...
        // prepare the view model [view-01]
        modèle.addAttribute("actionModel01", new ActionModel01());
...
        // view
        return "vue-01";
}

Die Klasse [ActionModel01] dient in erster Linie dazu, die von der Aktion [/ajax-01] gesendeten Werte zu kapseln. Hier wird nichts gesendet. Wir erstellen eine leere Klasse und fügen sie in das Modell ein, da die Ansicht [vue-01.xml] sie verwendet. Die Klasse [ActionModel01] sieht wie folgt aus:


package istia.st.springmvc.models;
 
import javax.validation.constraints.DecimalMin;
import javax.validation.constraints.NotNull;
 
public class ActionModel01 {
 
    // posted data
    @NotNull
    @DecimalMin(value = "0.0")
    private Double a;
 
    @NotNull
    @DecimalMin(value = "0.0")
    private Double b;
 
    // getters and setters
    ...
}
  • Zeilen 11 und 15: zwei reelle Zahlen [a,b], die über ein Formular übermittelt werden;

Kehren wir zum Aktionscode zurück:


    @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) {
...
        // prepare the view model [view-01]
        modèle.addAttribute("actionModel01", new ActionModel01());
        Resultats résultats = new Resultats();
        modèle.addAttribute("resultats", résultats);
...
        // view
        return "vue-01";
}
  • Zeilen 6–7: Wir fügen dem Modell eine Instanz vom Typ [Results] hinzu;

Der im Modell platzierte Typ [Results] sieht wie folgt aus:

  

package istia.st.springmvc.models;
 
public class Resultats {
 
    // data
    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;
 
    // getters and setters
    ...
}
  • Zeilen 6–9: die Ergebnisse der vier arithmetischen Operationen mit den Zahlen [a, b];
  • Zeile 10: die Zeit, zu der die Seite ursprünglich geladen wurde;
  • Zeile 11: der Zeitpunkt, zu dem die vier arithmetischen Operationen ausgeführt wurden;
  • Zeile 12: etwaige Fehlermeldungen;
  • Zeile 13: die anzuzeigende Ansicht, falls vorhanden;
  • Zeile 14: die Spracheinstellung der Ansicht, [fr-FR] oder [en-US];

Der Code für die Aktion [/ajax-01] lautet wie folgt:


    @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) {
        ...
        // local
        setLocale(locale, modèle, résultats);
...
}
  • Zeile 5: Die Methode [setLocale] wird verwendet, um die in der Ansichtsvorlage zu verwendende Locale festzulegen, [fr-FR] oder [en-US]. Diese Locale ist für das in die Ansicht eingebettete JavaScript vorgesehen;

Die Methode [setLocale] lautet wie folgt:


    private void setLocale(Locale locale, Model modèle, Resultats résultats) {
        // we only manage fr-FR, en-US locales
        String language = locale.getLanguage();
        String country = null;
        switch (language) {
        case "fr":
            country = "FR";
            break;
        default:
            language = "en";
            country = "US";
            break;
        }
        // culture
        résultats.setCulture(String.format("%s-%s", language, country));
}

In der Vorlage entspricht die Zeichenfolge [${results.culture}] entweder 'fr-FR' oder 'en-US'.

Kehren wir zur Aktion [/ajax-01] zurück:


@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) {
...
        // local
        setLocale(locale, modèle, résultats);
        // hour
        résultats.setHeureGet(new SimpleDateFormat("hh:mm:ss").format(new Date()));
        // view
        return "vue-01";
    }
  • Zeile 7: Die Zeit aus der GET-Anfrage in der Vorlage festlegen;
  • Zeile 9: Wir zeigen die Ansicht [vue-01.xml] an:

7.2.3. Die Ansicht [view-01.xml]

Die Ansicht [view-01.xml] sieht wie folgt aus:


<!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>
  • Zeilen 7–12: die jQuery-Bibliotheken für Validierung und Internationalisierung (Kulturen);
  • Zeile 15: die in Abschnitt 6.3 erstellte [client-validation]-Bibliothek;
  • Zeile 14: die von der [client-validation]-Bibliothek verwendete JSON-Bibliothek. Sie ist optional, wenn Validierungsprotokolle deaktiviert wurden;
  • Zeile 13: Microsofts [Unobtrusive Ajax]-Bibliothek. Diese Bibliothek ermöglicht es Ihnen manchmal, das Schreiben von JavaScript zu vermeiden;
  • Zeile 16: eine JavaScript-Datei für unsere eigenen Zwecke;
  • Zeilen 17–22: zur Verarbeitung der Locales [fr-FR] und [en-US] auf der Client-Seite. Wir sind diesem Code bereits begegnet;
  • Zeile 27: eine konfigurierte Meldung. Diese haben wir in Abschnitt 5.18 behandelt;
  • Zeilen 36–38: das Formular, auf das wir später zurückkommen werden;
  • Zeile 40: der Bereich des Dokuments, in dem JavaScript die Antwort des Servers platzieren wird;

7.2.4. Das Formular

 

In der Ansicht [vue-01.xml] sieht das Formular wie folgt aus:


<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>

was den folgenden HTML-Code erzeugt:


<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>
  • Zeile 16: Das Feld [a] ist mit den Validatoren [required], [number] und [min] verknüpft;
  • Zeile 19: dasselbe gilt für das Feld [b];

Die verschiedenen Meldungen befinden sich in den [messages.properties]-Dateien des Projekts:

  

[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:

Betrachten wir nun die Attribute des [form]-Tags:


<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">

Wir erkennen die Standardattribute des [form]-Tags:


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

Es ist sofort ersichtlich, dass das Formular an die URL [/ajax-02.html] gesendet wird, wenn JavaScript im Browser, der die Seite anzeigt, deaktiviert ist. Betrachten wir nun die anderen Attribute:


<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">

Die [data-ajax-xxx]-Attribute werden von der JavaScript-Bibliothek [unobtrusive-ajax] verarbeitet, die von der Ansicht [vue-01.xml] importiert wurde:


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

Wenn die [data-ajax-xxx]-Attribute vorhanden sind, wird die [submit]-Schaltfläche des Formulars über einen Ajax-Aufruf aus der [unobtrusive-ajax]-Bibliothek ausgeführt. Die Parameter haben folgende Bedeutungen:

  • [data-ajax="true"]: Das Vorhandensein dieses Attributs bewirkt, dass die [submit]-Aktion des Formulars über Ajax ausgeführt wird;
  • [data-ajax-method="post"]: Die Methode des [submit]. Die POST-URL ist die des Attributs [action="/ajax-02.html"];
  • [data-ajax-loading="#loading"]: Die ID eines Bereichs, der während des Wartens auf die Antwort des Servers angezeigt werden soll. Der durch [loading] in der Ansicht [vue-01.xml] identifizierte Bereich ist wie folgt:

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

Dies ist ein animiertes Lade-Bild, das angezeigt wird, bis die Antwort des Servers empfangen wurde;

  • [data-ajax-loading-duration="0"]: Die Wartezeit in Millisekunden, bevor der Bereich [data-ajax-loading="#loading"] angezeigt wird. Hier wird er angezeigt, sobald die Wartezeit beginnt;
  • [data-ajax-begin="beforeSend"]: die JavaScript-Funktion, die vor dem Absenden ausgeführt werden soll;
  • [data-ajax-complete="afterComplete"] : die JavaScript-Funktion, die ausgeführt werden soll, wenn die Antwort empfangen wurde;
  • [data-ajax-update="#resultats"]: die ID des Bereichs, in dem das vom Server gesendete Ergebnis platziert wird. Die Ansicht [vue-01.xml] enthält den folgenden Bereich:

<div id="resultats" />
  • [data-ajax-mode="replace"]: Der Modus zum Einfügen des Ergebnisses in den vorherigen Bereich. Der Modus [replace] bewirkt, dass das Ergebnis alles „überschreibt“, was zuvor im Bereich mit der ID [resultats] stand;

Beachten Sie, dass das JavaScript-Ereignis [submit] nur ausgelöst wird, wenn die Validatoren die geprüften Werte als gültig deklariert haben.

Die JavaScript-Bibliothek [unobtrusive-ajax] verfolgt zwei Ziele:

  • sicherzustellen, dass sich das Formular korrekt an beide Möglichkeiten anpasst: unabhängig davon, ob JavaScript im Browser aktiviert oder deaktiviert ist;
  • das Schreiben von JavaScript zu vermeiden. Wir werden sehen, dass dies in diesem Fall nicht vermieden werden konnte.

7.2.5. Die Aktion [/ajax-02]

Wir haben gesehen, dass die übermittelten Werte an die Aktion [/ajax-02] gesendet wurden. Sie sieht wie folgt aus:


@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);
        }
        // prepare the model for the next view
        Resultats résultats = new Resultats();
        modèle.addAttribute("resultats", résultats);
        // we set the locale
        setLocale(locale, modèle, résultats);
        // hour
        résultats.setHeurePost(new SimpleDateFormat("hh:mm:ss").format(new Date()));
        ...
}
  • Wir vereinfachen die Sache vorerst: Wir gehen davon aus, dass die POST-Anfrage tatsächlich vom JavaScript in der Ansicht [vue-01.xml] gesendet wurde. Wir werden diese Annahme etwas später noch einmal betrachten;
  • Zeile 2: Die gesendeten Werte [a,b] werden im Modell [ActionModel01] abgelegt;
  • Zeilen 4–7: Wenn der Benutzer bei einer vorherigen GET-Anfrage ein Timeout festgelegt hat, wird dieses aus der Sitzung abgerufen und das Timeout angewendet (Zeile 6). Dies dient dazu, dem Benutzer die Wirkung des Attributs [data-ajax-loading="#loading"] im Formular zu zeigen;
  • Zeilen 9–10: Dem Modell wird ein [results]-Attribut hinzugefügt;
  • Zeile 12: Die Locale [fr-FR] oder [en-US] wird dem Modell hinzugefügt;
  • Zeile 14: Wir legen die POST-Zeit im Modell fest;

Erinnern Sie sich an den Typ [Resultats], der dem Modell hinzugefügt wurde:


public class Resultats {
 
    // data
    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;
 
    // getters and setters
...
}

Der Code für die Aktion [/ajax-02] setzt sich wie folgt fort:


@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()));
        // we generate an error every other time
        int val = new Random().nextInt(2);
        if (val == 0) {
            // an error message is returned
            résultats.setErreur("erreur.aleatoire");
            return "vue-03";
        }
...
    }
  • Zeilen 6–11: In diesem Beispiel zeigen wir, wie eine Fehlerseite an den JavaScript-Client zurückgegeben wird. In der Hälfte der Fälle geben wir die folgende Ansicht [view-03.xml] zurück:

Beachten Sie Zeile 9: Was wir in die Vorlage einfügen, ist keine Meldung, sondern ein Meldungsschlüssel:

[messages_fr.properties]


erreur.aleatoire=erreur aléatoire

[messages_fr.properties]


erreur.aleatoire=randomly generated error

Der Code der Ansicht [vue-03.xml] lautet wie folgt:


<!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="error" th:text="${resultats.erreur}"></span> -->
        </p>
    </body>
</html>
 
  • Zeile 12: Beachten Sie eine Nachricht, die durch einen Nachrichtenschlüssel konfiguriert wird, der selbst berechnet wird. Wir haben dieses Konzept in Abschnitt 5.18, Seite 170, vorgestellt.

Der Code für die Aktion [/ajax-02] setzt sich wie folgt fort:


@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 {
...
        // retrieve posted values
        double a = formulaire.getA();
        double b = formulaire.getB();
        // we build the 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");
        }
        // the view is displayed
        return "vue-02";
    }
  • Zeilen 5–15: Die vier arithmetischen Operationen werden auf die Zahlen [a, b] angewendet und in der Instanz [Resultats] des Modells gekapselt;
  • Zeile 17: Die folgende Ansicht [view-02.xml] wird zurückgegeben:

Die Ansicht [view-02.xml] sieht wie folgt aus:


<!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>

Unabhängig davon, ob das Ergebnis die Ansicht [vue-02.xml] oder die Ansicht [vue-03.xml] ist, wird dieses HTML-Ergebnis aufgrund des Attributs [data-ajax-update="#resultats"] des Formulars in den durch [resultats] gekennzeichneten Bereich der Ansicht [vue-01.xml] eingefügt.

7.2.6. POST der eingegebenen Werte

Hier stehen wir vor einer Herausforderung mit den gesendeten Werten. Wir arbeiten mit zwei Sprachumgebungen [fr-FR] und [en-US], die reelle Zahlen unterschiedlich darstellen. Wir haben dieses Problem in Abschnitt 6.3, Seite 190, behandelt, als wir reelle Zahlen in zwei verschiedenen Sprachumgebungen per POST senden mussten. Wir werden die dort verwendeten Werkzeuge wiederverwenden. Allerdings stehen wir vor einer zusätzlichen Herausforderung: Wir haben keinen Zugriff auf die Methode, die das Senden der eingegebenen Werte per POST übernimmt. Aus diesem Grund haben wir dem Formular-Tag die folgenden Attribute hinzugefügt:

  • [data-ajax-begin="beforeSend"]: die JavaScript-Funktion, die vor dem Absenden des Formulars ausgeführt werden soll;
  • [data-ajax-complete="afterComplete"]: die JavaScript-Funktion, die ausgeführt werden soll, wenn die Antwort empfangen wurde;

Wir haben keinen Zugriff auf die JavaScript-Funktion, die die eingegebenen Werte per POST übermittelt, aber wir können zwei JavaScript-Funktionen schreiben:

  • [beforeSend]: eine JavaScript-Funktion, die vor dem POST-Aufruf ausgeführt wird;
  • [afterComplete]: eine JavaScript-Funktion, die nach Erhalt der POST-Antwort ausgeführt wird;

Diese beiden Funktionen werden in einer Datei namens [local1.js] abgelegt:

  

Die Datei [local1.js] initialisiert die JavaScript-Umgebung der Ansicht [vue-01.xml] wie folgt:


// global data
var loading;
var formulaire;
var résultats;
var a, b;
 
// document loading
$(document).ready(function() {
    // retrieve the references of the page's various components
    loading = $("#loading");
    formulaire = $("#formulaire");
    resultats = $('#resultats');
    a = $("#a");
    b = $("#b");
    // we hide certain elements
    loading.hide();
    // parse the form validators
    $.validator.unobtrusive.parse(formulaire);
    // we manage two locales [fr_FR, en_US]
    // the reals [a,b] are sent by the server in Anglo-Saxon format
    // we put them in French format if necessary
    checkCulture(2);
});
  • Zeile 22: Die Funktion [checkCulture] wird etwas weiter unten beschrieben;

Die JavaScript-Funktion [beforeSend] sieht wie folgt aus:


function beforeSend(jqXHR, settings) {
    // before POST
    // numbers must be posted in Anglo-Saxon format
    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) {
        // we put the numbers [a,b] in Anglo-Saxon format
        var value1 = a.val().replace(",", ".");
        a.val(value1);
        var value2 = b.val().replace(",", ".");
        b.val(value2);
    }
    if (mode == 2) {
...
    }
}
  • Zeilen 4–6: Wir prüfen, ob die Spracheinstellung der Ansicht [fr-FR] ist. In diesem Fall müssen die übermittelten Werte geändert werden. Wenn der Benutzer nämlich [1,6] eingegeben hat, muss der Wert [1.6] gesendet werden; andernfalls wird der Wert [1,6] auf der Serverseite abgelehnt. Dazu ändern Sie einfach das Komma in den gesendeten Werten in einen Dezimalpunkt (Zeilen 18–21);
  • aber damit ist es noch nicht getan. Wenn die Funktion [beforeSend] aufgerufen wird, ist die Zeichenkette der übermittelten Werte [a=val1&b=valB] bereits aufgebaut. Wir müssen sie daher ändern. Dies geschieht mithilfe des zweiten Parameters der Funktion [settings];
  • Zeile 7: [settings.data] (settings ist ein Funktionsparameter) steht für die gesendete Zeichenkette. Wir erstellen diese Zeichenkette neu mit dem Ausdruck [form.serialize()]. Dieser Ausdruck durchläuft das Formular, um die zu sendenden Werte zu finden, und erstellt die POST-Zeichenkette. Er übernimmt dann die neuen Werte von [a,b] mit Dezimalpunkten;

Wenn wir nichts weiter unternehmen, sendet der Server seine Antwort, die korrekt angezeigt wird. Die Werte von [a,b] enthalten nun jedoch Dezimalstellen, obwohl wir uns immer noch in der Locale [fr-FR] befinden. Wenn der Benutzer dies nicht bemerkt und erneut auf [Berechnen] klickt, melden die Validatoren, dass die Werte [a,b] ungültig sind. Was korrekt ist. Hier kommt die Funktion [afterComplete] ins Spiel, die beim Empfang des Ergebnisses ausgeführt wird:


function beforeSend(jqXHR, settings) {
    // before POST
...
}
 
function afterComplete(jqXHR, settings) {
    // after POST
    // numbers must be supplied in French format if necessary
    var culture = Globalize.culture().name;
    if (culture === 'fr-FR') {
        checkCulture(2);
    }
}
 
function checkCulture(mode) {
    if (mode == 1) {
...
    }
    if (mode == 2) {
        // put the numbers in French format
        var value1 = a.val().replace(".", ",");
        a.val(value1);
        var value2 = b.val().replace(".", ",");
        b.val(value2);
    }
}
  • Zeilen 9–12: Wenn die Spracheinstellung der Ansicht [fr-FR] ist, werden die Zahlen [a,b] in das französische Format konvertiert.

7.2.7. Tests

Hier sind einige Test-Screenshots:

  • in [1], die Antwort des Servers;
  • in [2] die Antwort des Servers mit einer Fehlermeldung;
  • in [3] wird ein Timeout von 5 Sekunden festgelegt. Das bedeutet, dass der Server 5 Sekunden wartet, bevor er seine Antwort sendet. Im [form]-Tag haben wir das Attribut [data-ajax-loading='#loading'] verwendet. Der Parameter [loading] ist die Kennung eines Bereichs, der:
    • während der gesamten Wartezeit angezeigt wird;
    • nach dem Empfang der Serverantwort ausgeblendet wird;

Hier ist [loading] die Kennung eines animierten Bildes, das in [4] zu sehen ist.

7.2.8. Deaktivieren von JavaScript mit der Locale [en-US]

Was passiert, wenn wir JavaScript im Browser deaktivieren?

Der POST der eingegebenen Werte erfolgt gemäß dem [form]-Tag, dessen [data-ajax-attr]-Attribute nicht verwendet werden. Alles läuft so ab, als hätten wir das folgende [form]-Tag:


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

Die eingegebenen Werte werden daher an die Aktion [/ajax-02] gesendet. Sie wurden auf der Client-Seite nicht validiert. Daher übernehmen die serverseitigen Validatoren. Diese waren bereits zuvor beteiligt, jedoch bei Werten, die bereits auf der Client-Seite validiert worden waren und daher korrekt waren. Dies ist nun nicht mehr der Fall.

Wir ändern die Aktion [/ajax-02] wie folgt:


@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 {
        // ajax request?
        boolean isAjax = "XMLHttpRequest".equals(request.getHeader("X-Requested-With"));
        ...
    }
  • Zeile 4: Die Aktion [/ajax-02] kann nun über einen Ajax-POST oder einen Standard-POST aufgerufen werden. Wir müssen in der Lage sein, zwischen diesen beiden Fällen zu unterscheiden. Dazu verwenden wir die vom Client-Browser gesendeten HTTP-Header;

Wenn wir uns den Netzwerkverkehr in den Chrome DevTools (Strg-Umschalt-I) bei aktiviertem JavaScript ansehen, stellen wir fest, dass der Client während der POST-Anfrage die folgenden Header sendet:

Wie oben gezeigt:

  • Es wurde ein [X-Requested-With]-Header gesendet [1];
  • ein [X-Requested-With]-Parameter wurde zu den gesendeten Werten hinzugefügt [2];

Dies ist bei einem Standard-POST nicht der Fall. Wir haben daher zwei Möglichkeiten, die Informationen abzurufen: entweder aus den HTTP-Headern oder aus den übermittelten Werten. In Zeile 4 der Aktion [/ajax-02] wurde die erste Lösung gewählt.

Fahren wir mit dem Code für diese Aktion fort:


@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 {
        // ajax request?
        boolean isAjax = "XMLHttpRequest".equals(request.getHeader("X-Requested-With"));
        // tempo?
        Integer tempo = (Integer) session.getAttribute("tempo");
        if (tempo != null && tempo > 0) {
            Thread.sleep(tempo);
        }
        // prepare the model for the next view
        Resultats résultats = new Resultats();
        modèle.addAttribute("resultats", résultats);
        // we set the locale
        setLocale(locale, modèle, résultats);
        // hour
        String heure = new SimpleDateFormat("hh:mm:ss").format(new Date());
        résultats.setHeurePost(heure);
        résultats.setHeureGet(heure);
        // valid request?
        if (!isAjax && result.hasErrors()) {
            return "vue-01";
        }
...
  • Zeile 2: Der Parameter [@Valid ActionModel01 form] löst die serverseitigen Validatoren aus;
  • Zeilen 20–22: Wenn es sich bei der Anfrage nicht um eine Ajax-Anfrage handelt und die Validierung fehlgeschlagen ist, wird die Ansicht [vue-01.xml] mit Fehlermeldungen zurückgegeben.

Hier ist ein Beispiel:

Setzen wir unsere Untersuchung der Aktion [/ajax-02] fort:


@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 {
        // ajax request?
        boolean isAjax = "XMLHttpRequest".equals(request.getHeader("X-Requested-With"));
...
        // valid request?
        if (!isAjax && result.hasErrors()) {
            return "vue-01";
        }
        // we generate an error every other time
        int val = new Random().nextInt(2);
        if (val == 0) {
            // an error message is returned
            résultats.setErreur("erreur.aleatoire");
            if (isAjax) {
                return "vue-03";
            } else {
                résultats.setVue("vue-03");
                return "vue-01";
            }
        }
...
  • Zeile 14: Es wird ein zufälliger Fehler generiert;
  • Zeile 16: Im Falle eines Ajax-Aufrufs wird die Ansicht [vue-03.xml] zurückgegeben und in den durch [resultats] identifizierten Bereich eingefügt;
  • Zeile 18: Bei einem Nicht-Ajax-Aufruf wird die anzuzeigende Ansicht in das Modell [Resultats] eingefügt;
  • Zeile 19: Die Ansicht [vue-01.xml] wird erneut gerendert;

Die Ansicht [vue-01.xml] wird wie folgt geändert:


<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" />
  • Zeile 3: Die Ansicht [view-03.xml] wird unterhalb des Bereichs [results] eingefügt;

Hier ein Beispiel:

Beachten Sie, dass die Zeiten [1] und [2] nun identisch sind.

Setzen wir unsere Untersuchung der Aktion [/ajax-02] fort:


    @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 {
        // ajax request?
        boolean isAjax = "XMLHttpRequest".equals(request.getHeader("X-Requested-With"));
...
        // retrieve posted values
        double a = formulaire.getA();
        double b = formulaire.getB();
        // we build the 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");
        }
        // the view is displayed
        if (isAjax) {
            return "vue-02";
        } else {
            résultats.setVue("vue-02");
            return "vue-01";
        }
}
  • Zeilen 7–17: Die Ergebnisse der vier arithmetischen Operationen werden in die Vorlage eingefügt;
  • Zeilen 22–23: Die Ansicht [vue-01.xml] (Zeile 22) wird durch Einfügen der Ansicht [vue-02.xml] (Zeile 22) gerendert;

Diese Einfügung erfolgt in [vue-01.xml] wie folgt:


<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" />
  • Zeile 2: Die Ansicht [vue-02.xml] wird unterhalb des Bereichs [resultats] eingefügt;

Hier ist ein Beispiel für die Ausgabe:

 

7.2.9. Deaktivieren von JavaScript mit der Locale [fr-FR]

Bei der Sprachumgebung [fr-FR] tritt folgendes Problem auf:

Werte, die im französischen Format eingegeben wurden, wurden als ungültig deklariert. Der Grund dafür ist, dass der Server reelle Zahlen im angelsächsischen Format erwartet. Die Lösung ist recht komplex. Wir werden einen Filter erstellen, der:

  • die Anfrage abfängt;
  • die Kommas in den übermittelten Werten [a] und [b] in Dezimalpunkte umwandelt;
  • die neue Anfrage dann an die Aktion weiterleitet, die sie verarbeiten muss;

Zunächst fügen wir der Ansicht [vue-01.xml] ein verstecktes Feld hinzu:


<form ...>
...
</p>
    <!-- hidden fields -->
    <input type="hidden" id="culture" name="culture" th:value="${resultats.culture}"></input>
</form>
  • Zeile 5: Die Kultur [fr-FR] oder [en-US] wird in das Attributfeld [name=culture] gesetzt. Da sich das [input]-Tag im Formular befindet, wird sein Wert zusammen mit den Werten von [a] und [b] übermittelt. Wir erhalten dann eine übermittelte Zeichenkette in der Form:
culture=fr-FR&a=12,7&b=20,78

Es ist wichtig, diesen Punkt zu verstehen.

Als Nächstes fügen wir einen Filter in die Anwendungskonfiguration ein:

  

Die [Config]-Datei wird wie folgt geändert:


@Configuration
@ComponentScan({ "istia.st.springmvc.controllers", "istia.st.springmvc.models" })
@EnableAutoConfiguration
public class Config extends WebMvcConfigurerAdapter {
...
    @Bean
    public Filter cultureFilter() {
        return new CultureFilter();
    }
}
  • Zeile 7: Die Tatsache, dass die [cultureFilter]-Bean einen Typ [Filter] zurückgibt, macht sie zu einem Filter. Die Bean selbst kann einen beliebigen Namen haben;

Der nächste Schritt besteht darin, den Filter selbst zu erstellen:

  

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 {
        // next handler
        filterChain.doFilter(new CultureRequestWrapper(request), response);
    }
}
  • Zeile 12: Wir erweitern die Klasse [OncePerRequestFilter], eine Spring-Klasse, und müssen die Methode [doFilterInternal] dieser Klasse überschreiben;
  • Zeile 15: Die Methode [doFilterInternal] erhält drei Informationen:
    • [HttpServletRequest request]: die zu filternde Anfrage. Diese kann nicht geändert werden,
    • [HttpServletResponse response]: die an den Server zu sendende Antwort. Der Filter kann diese selbst generieren,
    • [FilterChain filterChain]: die Filterkette. Sobald die Methode [doFilterInternal] ihre Arbeit beendet hat, muss sie die Anfrage an den nächsten Filter in der Filterkette weiterleiten;
  • Zeile 18: Wir erstellen aus der empfangenen Anfrage eine neue [new CultureRequestWrapper(request)] und übergeben sie an den nächsten Filter. Da wir die ursprüngliche Anfrage [HttpServletRequest request] nicht ändern können, erstellen wir eine neue;

Die Klasse [CultureRequestWrapper] sieht wie folgt aus:

  

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) {
        // posted values a and 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;
        }
        // other cases
        return super.getParameterValues(name);
    }
 
}
  • Zeile 6: Die Klasse [CultureRequestWrapper] erweitert die Klasse [HttpServletRequestWrapper] und überschreibt einige ihrer Methoden;
  • Zeilen 8–10: Der Konstruktor, der die zu filternde Anfrage entgegennimmt und an die übergeordnete Klasse weiterleitet;
  • Es ist wichtig zu verstehen, dass die gefilterte Anfrage letztendlich als Eingabeparameter für eine Klasse namens Servlet dient. Bei Spring MVC ist dieses Servlet vom Typ [DispatcherServlet]. Diese Klasse verfügt über verschiedene Methoden zum Abrufen von Anfrageparametern: [getParameter, getParameterMap, getParameterNames, getParameterValues, ...]. Die vom Servlet verwendete Methode muss neu definiert werden. Dazu müsste man den Code der Klasse [DispatcherServlet] lesen. Ich habe das nicht getan und verschiedene Methoden neu definiert. Letztendlich war es die Methode [getParameterValues], die neu definiert wurde;
  • Zeile 13: Die Methode [getParameterValues] nimmt als Parameter den Namen eines der von der Methode [getParameterNames] zurückgegebenen Parameter entgegen und muss ein Array seiner Werte zurückgeben. Tatsächlich wissen wir, dass ein Parameter in einer Anfrage mehrfach vorkommen kann;
  • Zeile 18: Das Komma wird durch einen Dezimalpunkt ersetzt;

Hier ist ein Ausführungsbeispiel:

  • in [1] werden die Werte [a,b] im französischen Format eingegeben;
  • in [2] die Ergebnisse;
  • in [3] hat der Server eine Seite mit Zahlen im angelsächsischen Format zurückgegeben.

Dieses Problem lässt sich mit Thymeleaf in der Ansicht [vue-01.xml] wie folgt beheben


<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>

In den Zeilen 3 und 6 sind mehrere Änderungen vorzunehmen. Konzentrieren wir uns auf Zeile 3:

  • Wir hatten [th:field="*{a}"] geschrieben. Der Parameter [th:field] legt die Attribute [id, name, value] des generierten HTML-Tags [input] fest. Hier möchten wir das Attribut [value] selbst verwalten. Daher legen wir auch die Attribute [id, name] selbst fest;
  • das Attribut [th:value] wertet einen Ausdruck unter Verwendung des ternären Operators ? aus. Wir prüfen den Ausdruck [${resultats.culture}=='fr-FR' and ${actionModel01.b}!=null]. Ist er wahr, setzen wir das Attribut [value] auf den Wert von [actionModel01.a], wobei der Dezimalpunkt durch ein Komma ersetzt wird. Ist er falsch, setzen wir das Attribut [value] unverändert auf den Wert von [actionModel01.a];
  • Zeile 6: Wir machen dasselbe für das Feld [b];

Hier ist ein Ausführungsbeispiel:

  • In [1] behalten die Zahlen [a,b] die französische Schreibweise bei. Dies ist in [2] nicht der Fall;

Dieses neue Problem wird auf die gleiche Weise behandelt wie das vorherige. Wir ändern die Ansicht [vue-03.xml] wie folgt:


<!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>

Hier ist ein Beispiel:

Wir haben nun eine Anwendung, die zwei Sprachumgebungen in einer Umgebung korrekt verarbeitet, in der JavaScript möglicherweise verwendet wird oder auch nicht. Um dies zu erreichen, mussten wir die Komplexität des serverseitigen Codes erheblich erhöhen. Künftig gehen wir immer davon aus, dass JavaScript im Browser aktiviert ist. Dies ermöglicht Funktionen, die im reinen Servermodus nicht möglich sind.

Betrachten wir den Link [Berechnen] auf der Hauptseite [vue-01.xml]:

Der Code für den Link [Berechnen] in der Ansicht [vue-01.xml] lautet wie folgt:


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

Die JavaScript-Funktion [postForm] ist in der Datei [local1.js] wie folgt definiert:


// global data
var loading;
var formulaire;
var résultats;
var a, b;
 
function postForm() {
    // valid form?
    if (!formulaire.validate().form()) {
        // invalid form - terminated
        return;
    }
    // we manage two locales [fr_FR, en_US]
    // the real [a,b] must be posted in Anglo-Saxon format in all cases
    // they will be filtered by [CultureFilter]
 
    // make a manual Ajax call
    $.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);
        }
    })
}
  • Zeilen 2–5: Zur Erinnerung: Diese Elemente wurden durch die Funktion [$(document).ready] initialisiert;
  • Zeilen 9–12: Wir führen die JavaScript-Validatoren des Formulars aus. Ist einer der Werte ungültig, gibt der Ausdruck [form.validate().form()] „false“ zurück. In diesem Fall wird der [submit]-Befehl des Formulars abgebrochen;
  • Zeilen 18–38: Wir führen einen manuellen Ajax-Aufruf durch;
  • Zeile 19: Die Ziel-URL des Ajax-Aufrufs;
  • Zeilen 20–22: Ein Array von HTTP-Headern, die zu den standardmäßig in der HTTP-Anfrage enthaltenen hinzugefügt werden sollen. Hier fügen wir den HTTP-Header hinzu, der dem Server signalisiert, dass wir einen Ajax-Aufruf durchführen;
  • Zeile 23: die verwendete HTTP-Methode;
  • Zeile 24: die zu übermittelnden Daten. [formulaire.serialize] erstellt die zu übermittelnde Zeichenkette [culture=fr-FR&a=12,7&b=20,89] aus dem Formular mit der ID [formulaire]. Hier stoßen wir auf das zuvor besprochene Problem: Die Werte [a,b] müssen im angelsächsischen Format übermittelt werden. Wir wissen, dass dieses Problem nun mit der Erstellung des Filters [cultureFilter] gelöst wurde;
  • Zeile 25: der erwartete Rückgabedatentyp. Wir wissen, dass der Server einen HTML-Stream zurückgeben wird;
  • Zeile 26: Die Methode, die beim Start der Anfrage ausgeführt werden soll. Hier legen wir fest, dass die Komponente mit der ID [loading] angezeigt werden muss. Dabei handelt es sich um das animierte Lade-Symbol;
  • Zeile 29: Die Methode, die ausgeführt werden soll, wenn die Ajax-Anfrage erfolgreich ist. Der Parameter [data] ist die vollständige Antwort vom Server. Wir wissen, dass es sich um einen HTML-Stream handelt;
  • Zeile 30: Wir aktualisieren die Komponente mit der ID [results] mit dem HTML-Code aus dem Parameter [data].
  • Zeile 33: Wir blenden die Ladeanzeige aus;
  • Zeile 35: Funktion, die ausgeführt wird, wenn die Serverantwort empfangen wird, unabhängig davon, ob es sich um einen Erfolg oder einen Fehler handelt;
  • Zeilen 35–37: Tritt ein Fehler auf (der Server hat eine HTTP-Antwort mit einem Statuscode zurückgegeben, der auf einen serverseitigen Fehler hinweist), wird die HTML-Antwort des Servers im Bereich [results] angezeigt;

Hier ist ein Beispiel für die Ausführung:

7.3. Aktualisierung einer HTML-Seite mit einem JSON-Feed

Im vorherigen Beispiel hat der Webserver auf die Ajax-HTTP-Anfrage mit einem HTML-Stream geantwortet. Dieser Stream enthielt Daten mit HTML-Formatierung. Wir greifen das vorherige Beispiel wieder auf, verwenden diesmal jedoch JSON-Antworten (JavaScript Object Notation), die nur die Daten enthalten. Der Vorteil besteht darin, dass weniger Bytes übertragen werden. Wir gehen davon aus, dass JavaScript im Browser aktiviert ist.

7.3.1. Die Aktion [/ajax-04]

Die Aktion [/ajax-04] ist identisch mit der Aktion [/ajax-01], mit dem Unterschied, dass sie die Ansicht [vue-04.xml] anstelle der Ansicht [vue-01.xml] anzeigt:


@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) {
        ...
        // view
        return "vue-04";
    }

7.3.2. Die Ansicht [view-04.xml]

 

Die Ansicht [view-04.xml] verwendet den Hauptteil der Ansicht [view-01.xml] mit folgenden Unterschieden:


<!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>
            <!-- hidden fields -->
            <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>
  • Zeile 5: Das JavaScript der Ansicht befindet sich nun in der Datei [local4.js];
  • Zeile 16: Das [form]-Tag enthält nicht mehr die [data-ajax-attr]-Parameter aus der [Unobtrusive Ajax]-Bibliothek. Wir werden diese hier nicht verwenden. Das [form]-Tag enthält auch nicht mehr die Attribute [method] und [action], die festlegen, wie und wohin die im Formular eingegebenen Werte übermittelt werden sollen. Das liegt daran, dass das Formular durch eine JavaScript-Funktion übermittelt wird (Zeile 20);
  • Zeilen 26–57: Der Bereich mit der ID [resultats], der zuvor leer war, enthält nun HTML-Code zur Anzeige der Ergebnisse;
  • Zeilen 26–34: die Ergebnisüberschrift, in der die Rechenzeit angezeigt wird;
  • Zeilen 35–52: die Ergebnisse der vier arithmetischen Operationen;
  • Zeilen 53–57: etwaige vom Server gesendete Fehlermeldungen;

Der JavaScript-Code, der beim Laden der Ansicht [vue-04.xm] ausgeführt wird, befindet sich in der Datei [local4.js]. Er lautet wie folgt:


// global data
    var loading;
    var formulaire;
    var résultats;
    var titre;
    var labelHeureCalcul;
    var heureCalcul;
    var aplusb;
    var amoinsb;
    var afoisb;
    var adivb;
    var msgErreur;
 
// document loading
$(document).ready(function() {
    // retrieve the references of the page's various components
    loading = $("#loading");
    formulaire = $("#formulaire");
    résultats = $('#résultats');
    titre=$("#titre");
    labelHeureCalcul=$("#labelHeureCalcul");
    heureCalcul=$("#heureCalcul");
    aplusb=$("#aplusb");
    amoinsb=$("#amoinsb");
    afoisb=$("#afoisb");
    adivb=$("#adivb");
    msgErreur=$("#msgErreur");
    // we hide certain elements
    résultats.hide();
    erreur.hide();
    loading.hide();
});
  • Zeilen 17–27: Abrufen der jQuery-Referenzen für alle Elemente auf der Seite;
  • Zeile 29: Der Ergebnisbereich wird ausgeblendet;
  • Zeile 30: ebenso wie der Fehlerbereich;
  • Zeile 31: ebenso wie das animierte Lade-Bild;
  • Zeilen 2–12: Die abgerufenen Referenzen werden global gemacht, damit andere Funktionen darauf zugreifen können;

7.3.3. Die jS-Funktion [postForm]

Der Link [Calculate] lautet wie folgt:


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

Die JavaScript-Funktion [postForm] ist in der Datei [local.js] wie folgt definiert:


function postForm() {
    // valid form?
    if (!formulaire.validate().form()) {
        // invalid form - terminated
        return;
    }
    // make a manual Ajax call
    $.ajax({
        url : '/ajax-05',
        headers : {
            'Accept' : 'application/json'
        },
        type : 'POST',
        data : formulaire.serialize(),
        dataType : 'json',
        beforeSend : onBegin,
        success : onSuccess,
        error : onError,
        complete : onComplete
    })
}
 
// before the Ajax call
function onBegin() {
...
}
 
// on receipt of the server response
// in case of success
function onSuccess(data) {
...
}
 
// on receipt of the server response
// in case of failure
function onError(jqXHR) {
...
}
 
// after [onSuccess, onError]
function onComplete() {
...
}
  • Zeilen 3–6: Bevor wir die eingegebenen Werte absenden, überprüfen wir sie. Sind sie falsch, senden wir das Formular nicht ab;
  • Zeile 9: Die eingegebenen Werte werden an die Aktion [/ajax-05] gesendet, die wir später noch genauer erläutern werden;
  • Zeilen 10–12: Ein HTTP-Header, der dem Server mitteilt, dass wir eine Antwort im JSON-Format erwarten;
  • Zeile 13: Die eingegebenen Werte werden gesendet;
  • Zeile 14: Serialisierung der eingegebenen Werte in eine Zeichenkette, die zum Senden bereit ist [a=1,6&b=2,4&culture=fr-FR];
  • Zeile 15: Der Typ der vom Server gesendeten Antwort. Es handelt sich um JSON;
  • Zeile 16: Die Funktion, die vor dem POST ausgeführt werden soll;
  • Zeile 17: Die Funktion, die bei erfolgreichem Empfang der Serverantwort ausgeführt werden soll. Der „Erfolg“ einer HTTP-Anfrage wird durch den Status der HTTP-Antwort des Servers bestimmt. Eine Antwort [HTTP/1.1 200 OK] ist eine erfolgreiche Antwort. Eine Antwort [HTTP/1.1 500 Internal Server Error] ist eine Fehlerantwort. Was als Status einer HTTP-Antwort bezeichnet wird, ist der Code [200] oder [500]. Einige dieser Codes stehen für „Erfolg“, während andere für „Fehler“ stehen;
  • Zeile 18: die Funktion, die beim Empfang der Serverantwort ausgeführt werden soll, wenn der HTTP-Status dieser Antwort ein Fehlerstatus ist;
  • Zeile 18: die Funktion, die zuletzt ausgeführt werden soll, nach den vorangehenden Funktionen [onSuccess, onError];

Die Funktion [onBegin] lautet wie folgt:


// before the Ajax call
function onBegin() {
    console.log("onBegin");
    // we show the moving image
    loading.show();
    // hide certain elements of the view
    entete.hide();
    résultats.hide();
    erreur.hide();
}

Bevor wir uns mit den anderen JavaScript-Funktionen des Ajax-Aufrufs befassen, müssen wir die von der Aktion [/ajax-05] gesendete Antwort kennen.

7.3.4. Die Aktion [/ajax-05]

Die Aktion [/ajax-05] sieht wie folgt aus:


    @RequestMapping(value = "/ajax-05", method = RequestMethod.POST)
    @ResponseBody()
    // processes the POST of view [view-04]
    public JsonResults ajax05(@Valid ActionModel01 formulaire, BindingResult result, Locale locale,    HttpServletRequest request, HttpSession session) throws InterruptedException {
        if(result.hasErrors()){
            // abnormal case - nothing returned
            return null;
        }
        ...
}
  • Zeile 2: Das Attribut [ResponseBody] gibt an, dass die Aktion [/ajax-05] selbst die Antwort an den Client zurückgibt. Da eine JSON-Bibliothek in den Projektabhängigkeiten enthalten ist, konfiguriert Spring Boot diese Art von Aktion automatisch so, dass sie JSON zurückgibt. Daher wird die JSON-Zeichenkette vom Typ [JsonResults] (Zeile 4) an den Client gesendet;
  • Zeile 2: Die übermittelten Werte [a, b, culture] werden in einen Typ [ActionModel01] gekapselt, den wir validieren [@Valid ActionModel01]. Dies dient lediglich der Form. Wir sind davon ausgegangen, dass JavaScript im Browser des Clients aktiviert ist, sodass die übermittelten Werte bei ihrem Eintreffen bereits auf der Client-Seite überprüft wurden. Wir können jedoch den Fall einer nicht autorisierten POST-Anfrage vorhersehen, die unseren JavaScript-Client nicht verwendet. In diesem Fall kann die Validierung fehlschlagen;
  • Zeilen 5–7: Im Falle eines Fehlers geben wir ein leeres JSON-Objekt zurück;

Setzen wir unsere Untersuchung der Aktion [/ajax-05] fort:


    @RequestMapping(value = "/ajax-05", method = RequestMethod.POST)
    @ResponseBody()
    // processes the POST of view [view-04]
    public JsonResults ajax05(@Valid ActionModel01 formulaire, BindingResult result, Locale locale,
            HttpServletRequest request, HttpSession session) throws InterruptedException {
...
        // spring application context
        WebApplicationContext ctx = WebApplicationContextUtils.getWebApplicationContext(request.getServletContext());
        // tempo?
        Integer tempo = (Integer) session.getAttribute("tempo");
        if (tempo != null && tempo > 0) {
            Thread.sleep(tempo);
        }
    ...
        // we return the result
        return résultats;
}
  • Zeile 8: Wir rufen den Kontext [ctx] aus der Spring-Anwendung ab. Diesen benötigen wir, um Nachrichten aus den [messages.properties]-Dateien anhand eines Nachrichten-Schlüssels und einer Locale abzurufen. Dies geschieht mit der folgenden Syntax:

ctx.getMessage(clé_message, tableau_de_paramètres, locale)
    • [message_key]: der Schlüssel der gesuchten Nachricht;
    • [locale]: die verwendete Sprachumgebung. Wenn diese Sprachumgebung also [en_US] ist, wird die Datei [messages_en.properties] verwendet;
    • [parameter_array]: Die abgerufene Nachricht kann wie in [key=message {0} {1}] parametrisiert werden. Diese Nachricht enthält zwei Parameter [{0} {1}]. Sie müssen ein Array mit zwei Werten als zweiten Parameter von [ctx.getMessage] angeben;
  • Zeilen 10–13: Wenn in der Sitzung ein Timeout auftritt, wird der aktuelle Thread für die Dauer des Timeouts angehalten;

Die Aktion [/ajax-05] wird wie folgt fortgesetzt:


        // on prépare le modèle de la prochaine vue
        JsonResults résultats = new JsonResults();
        ...
}
  • Zeile 2: Erstellung der JSON-String-Vorlage, die an den Client gesendet wird;

Das [JsonResults]-Modell sieht wie folgt aus:

 

package istia.st.springmvc.models;
 
public class JsonResults {
 
    // data
    private String titre;
    private String labelHeureCalcul;
    private String heureCalcul;
    private String aplusb;
    private String amoinsb;
    private String afoisb;
    private String adivb;
    private String msgErreur;
 
    // getters and setters
...
 
}
  • Zeilen 6–13: Jedes Feld in der Klasse [JsonResult] entspricht einem Feld mit derselben [id] in der Ansicht [vue-04.xml]:

Die Aktion [/ajax-05] wird wie folgt fortgesetzt:


        // on prépare le modèle de la prochaine vue
        JsonResults résultats = new JsonResults();
        // entête
        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()));
        // on génère une erreur une fois sur deux
        int val = new Random().nextInt(2);
        if (val == 0) {
            // on renvoie un message d'erreur
            résultats.setMsgErreur(ctx.getMessage("resultats.erreur",
                    new Object[] { ctx.getMessage("erreur.aleatoire", null, locale) }, locale));
            return résultats;
}
  • Zeile 2: Erstellen der JSON-String-Vorlage, die an den Client gesendet wird;
  • Zeilen 4–6: Erstellen der Nachrichten für den Ergebnisse-Header;
  • Zeilen 8–14: Im Durchschnitt wird bei jedem zweiten Versuch eine Fehlermeldung generiert. In diesem Fall wird der Prozess dort beendet und die JSON-Zeichenkette an den Client zurückgegeben (Zeile 13);
  • Zeile 11: Hier ist ein Beispiel für eine parametrisierte Meldung:

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

Die Aktion [/ajax-05] wird wie folgt fortgesetzt:


        // on récupère les valeurs postées
        double a = formulaire.getA();
        double b = formulaire.getB();
        // on construit le modèle
        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");
        }
        // on rend le résultat
return résultats;
  • Zeilen 2–3: Abrufen der Werte von [a] und [b];
  • Zeilen 5–12: Wir bilden die vier Ergebnisse;
  • Zeile 14: Die JSON-Zeichenkette [JsonResults] wird an den Client gesendet;

Schauen wir uns an, wie das mit dem [Advanced Rest Client] funktioniert:

  • In [1-2] senden wir eine POST-Anfrage an die Aktion [/ajax-05];
  • in [3] senden wir falsche Werte;
  • in [4] hat der Server eine leere Antwort zurückgegeben;
  • In [1] senden wir korrekte Werte;
  • in [2] das vom Server zurückgegebene JSON-Objekt, hier mit einer Fehlermeldung;
  • In [1] senden wir korrekte Werte;
  • in [2] das vom Server zurückgegebene JSON-Objekt, das die vier Ergebnisse anzeigt;
  • in [1] senden wir korrekte Werte;
  • in [2] haben wir eine serverseitige Ausnahme ausgelöst. Wir sehen, dass der Server weiterhin ein JSON-Objekt sendet. In dieser Meldung sehen wir, dass der HTTP-Status der Antwort [500] lautet, was darauf hinweist, dass ein serverseitiger Fehler aufgetreten ist;

7.3.5. Die jS-Funktion [postForm] – 2

Da wir nun das vom Server zurückgegebene JSON-Objekt kennen, können wir es in JavaScript verwenden. Die Methode [onSuccess], die ausgeführt wird, wenn der Server eine Antwort mit dem HTTP-Status [200] sendet, lautet wie folgt:


// on receipt of the server response
// in case of success
function onSuccess(data) {
    console.log("onSuccess");
    // fill in the results area
    titre.text(data.titre);
    labelHeureCalcul.text(data.labelHeureCalcul);
    heureCalcul.text(data.heureCalcul);
    entete.show();
    // error-free results
    if (!data.msgErreur) {
        aplusb.text(data.aplusb);
        amoinsb.text(data.amoinsb);
        afoisb.text(data.afoisb);
        adivb.text(data.adivb);
        résultats.show();
        return;
    }
    // results with error
    msgErreur.text(data.msgErreur);
    erreur.show();
}
  • Zeile 3: Der Parameter [data] ist das vom Server zurückgegebene JSON-Objekt:
 

Die Methode [onError], die ausgeführt wird, wenn der HTTP-Antwortstatus [500] lautet, lautet wie folgt:


// on receipt of the server response
// in case of failure
function onError(jqXHR) {
    console.log("onError");
    // system error
    msgErreur.text(jqXHR.responseText);
    erreur.show();
}
  • Zeile 3: Das jQuery-Objekt [jqXHR] verfügt über folgende Eigenschaften:
    • responseText: der Text der Serverantwort,
    • status: der vom Server zurückgegebene Fehlercode,
    • statusText: der diesem Fehlercode zugeordnete Text;
  • Zeile 6: Das Objekt [jqXHR.responseText] ist das folgende JSON-Objekt:
 

7.3.6. Tests

Sehen wir uns einige Screenshots der Webanwendung in Aktion an:

 
 
 

7.4. Einseitige Webanwendung

7.4.1. Einführung

Mit der Ajax-Technologie können Sie Single-Page-Anwendungen erstellen:

  • Die erste Seite wird über eine Standard-Browseranfrage geladen;
  • die nachfolgenden Seiten werden über Ajax-Aufrufe geladen. Dadurch ändert der Browser nie seine URL und lädt nie eine neue Seite. Diese Art von Anwendung wird als Single-Page-Anwendung (SPA) bezeichnet.

Hier ist ein einfaches Beispiel für eine solche Anwendung. Die neue Anwendung wird zwei Ansichten haben:

  • In [1] ruft die Aktion [/ajax-06] die erste Seite, Seite 1, auf;
  • in [2] ermöglicht ein Link die Navigation zu Seite 2 über einen Ajax-Aufruf;
  • in [3] hat sich die URL nicht geändert. Die angezeigte Seite ist Seite 2;
  • In [4] ermöglicht ein Link die Rückkehr zu Seite 1 über einen Ajax-Aufruf;
  • in [5] hat sich die URL nicht geändert. Die angezeigte Seite ist Seite 1.

7.4.2. Die Aktion [/ajax-06]

Der Code für die Aktion [/ajax-06] lautet wie folgt:


    @RequestMapping(value = "/ajax-06", method = RequestMethod.GET, produces = "text/html; charset=UTF-8")
    public String ajax06() {
        return "vue-06";
}
  • Zeilen 1–4: Die Aktion [/ajax-06] rendert einfach die Ansicht [vue-06.xml];

7.4.3. Die Ansicht [vue-06.xml]

Die Ansicht [vue-06.xml] sieht wie folgt aus:


<!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>
  • Zeile 8: Die Ansicht verwendet ein Skript [local6.js];
  • Zeile 12: Die Ansicht [vue-07.xml] ist im Bereich mit der ID [content] der Ansicht [vue-06.xml] enthalten;

7.4.4. Die Ansicht [vue-07.xml]

Die Ansicht [vue-07.xml] sieht wie folgt aus:


<!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. Die jS-Funktion [gotoPage]

Der Link [Seite 2] in der Ansicht [vue-07.xml] verwendet die jS-Funktion [gotoPage], die in der folgenden Datei [local6.js] definiert ist:


// global data
var content;
 
function gotoPage(num) {
    // make a manual Ajax call
    $.ajax({
        url : '/ajax-07',
        type : 'POST',
        data : 'num=' + num,
        dataType : 'html',
        beforeSend : function() {
        },
        success : function(data) {
            content.html(data)
        },
        complete : function() {
        },
        error : function(jqXHR) {
            // system error
            content.html(jqXHR.responseText);
        }
    })
}
 
// document loading
$(document).ready(function() {
    // retrieve the references of the page's various components
    content = $("#content");
});
  • Zeile 28: Beim Laden der Seite speichern wir das Element mit der ID [content] und machen es zu einer globalen Variablen (Zeile 2);
  • Zeile 4: Die Funktion [gotoPage] erhält als Parameter die Seitenzahl (1 oder 2), die in der aktuellen Ansicht angezeigt werden soll;
  • Zeile 7: Die Ziel-URL für die POST-Anfrage;
  • Zeile 8: Die URL aus Zeile 7 wird per POST angefordert;
  • Zeile 9: Die gesendete Zeichenkette. Ein Parameter namens [num] wird gesendet. Sein Wert ist die Seitenzahl (Zeile 4), die in der aktuellen Ansicht angezeigt werden soll;
  • Zeile 10: Der Server gibt HTML zurück, genauer gesagt den HTML-Code für die anzuzeigende Seite;
  • Zeilen 13–15: Bei Erfolg (HTTP-Status 200) wird der vom Server gesendete HTML-Code in das Element mit der ID [content] eingefügt;
  • Zeilen 18–20: Wenn die Anfrage fehlschlägt (HTTP-Status 500), wird der vom Server gesendete HTML-Code in das Feld mit der ID [content] eingefügt;

7.4.6. Die Aktion [/ajax-07]

Der Code für die Aktion [/ajax-07] lautet wie folgt:


@RequestMapping(value = "/ajax-07", method = RequestMethod.POST, produces = "text/html; charset=UTF-8")
    public String ajax07(int num) {
        // num : page number
        switch (num) {
        case 1:
            return "vue-07";
        case 2:
            return "vue-08";
        default:
            return "vue-07";
        }
    }
  • Zeile 2: Wir rufen den übermittelten Parameter namens [num] ab. Beachten Sie, dass der Parameter in Zeile 2 denselben Namen wie der übermittelte Parameter haben muss, in diesem Fall [num]. [num] ist eine Seiten- oder Ansichtsnummer;
  • Zeilen 5–6: Wenn [num==1] ist, geben wir die Ansicht [vue-07.xml] zurück;
  • Zeilen 7–8: Wenn [num==2] ist, geben wir die Ansicht [vue-08.xml] zurück;
  • Zeilen 9–10: In allen anderen Fällen (was normalerweise unmöglich ist) wird die Ansicht [vue-07.xml] zurückgegeben;

7.4.7. Die Ansicht [view-08.xml]

Die Ansicht [view-08.xml] bildet Seite 2 der Anwendung:


<!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. Einbetten mehrerer HTML-Streams in eine JSON-Antwort

7.5.1. Einführung

Betrachten Sie die folgende Anwendung:

Seite [1] hat vier Bereiche:

  • [Zone 1] und [Zone 3] sind Zonen, die erscheinen oder verschwinden, wenn auf die Schaltfläche [Aktualisieren] geklickt wird. Wir zählen, wie oft jede dieser beiden Zonen erscheint [2]. In der Zone [Zone 1] wird Französisch verwendet, während in der Zone [Zone 3] Englisch verwendet wird;
  • [Zone 2] ist immer vorhanden;
  • der Abschnitt [Einträge] ist immer sichtbar;

Der Link [Absenden] zeigt die nächste Seite an [3]:

  • Der Link [Zurück zu Seite 1] stellt den vorherigen Zustand von Seite 1 wieder her [4];

Die Anwendung ist eine Single-Page-Anwendung. Die erste Seite wird vom Browser vom Server angefordert. Nachfolgende Seiten werden über Ajax-Aufrufe vom Server abgerufen.

7.5.2. Die Aktion [/ajax-09]

  

Die [/ajax-09]-Aktion läuft wie folgt ab:


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

Es zeigt einfach die Ansicht [vue-09.xml] an.

7.5.3. XML-Ansichten

  

Die Ansicht [vue-09.xml] ist die Master-Seite der Anwendung:


<!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>
  • Zeile 9: die in der Anwendung verwendete JS-Datei;
  • Zeile 15: der Inhalt der Master-Seite;
  • Zeile 16: ein animiertes Lade-Bild:
  • Zeile 17: Bereich zur Anzeige etwaiger Fehler;

Die Ansicht [vue-09-page1.xml] ist Seite 1 der Anwendung:


<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
    <body>
        <h2>Page 1</h2>
        <!-- zone 1 -->
        <fieldset id="zone1" style="background-color:pink">
            <legend>Zone 1</legend>
            <span id="zone1-content" th:text="xx">xx</span>
        </fieldset>
        <!-- zone 2 -->
        <fieldset id="zone2" style="background-color:lightgreen">
            <legend>Zone 2</legend>
            <span>Ce texte reste toujours présent</span>
        </fieldset>
        <!-- zone 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>
  • Zeilen 6–9: der Bereich [Zone 1]. Sein Inhalt wird in die Komponente [id="zone1-content"] eingefügt;
  • Zeilen 11–14: der Bereich [Zone 2], der sich nicht ändert;
  • Zeilen 16–19: der Bereich [Zone 3]. Sein Inhalt wird in die Komponente [id="zone3-content"] eingefügt;
  • Zeile 22: die JS-Funktion, die das Formular absendet;
  • Zeile 25: Einbindung des Eingabebereichs;

Beachten Sie, dass Seite 1 kein [form]-Tag enthält. Alles wird in JavaScript abgewickelt.

Die Ansicht [vue-09-saisies.xml] sieht wie folgt aus:


<!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>
  • Zeilen 5–8: Geben Sie eine Zeichenkette ein;
  • Zeilen 13–16: Geben Sie eine ganze Zahl ein;
  • Zeile 14: die JS-Funktion, die die eingegebenen Werte übermittelt;

Beachten Sie erneut, dass das Eingabefeld kein [form]-Tag hat.

Insgesamt verfügt Seite 1 über zwei Funktionen:

  • [Aktualisieren]: aktualisiert die Zonen 1 und 3. Diese Aktion wird vom Server ausgeführt, der nach dem Zufallsprinzip Folgendes zurückgibt:
    • Feld 1 mit seinem Zugriffszähler und nichts für Feld 3,
    • Zone 3 mit ihrem Zugriffszähler und nichts für Zone 1,
    • beide Zonen mit ihren Zugriffszählern;
  • [Absenden]: Zeigt Seite 2 mit den eingegebenen Werten an oder eine Fehlermeldung, falls die eingegebenen Daten ungültig sind;

Wir konzentrieren uns zunächst auf die Schaltfläche [Aktualisieren].

7.5.4. Der JS-Code für die Schaltfläche [Aktualisieren]

  

Der Code in der Datei [local9.js] lautet wie folgt:


// global variables
var content;
var loading;
var erreur;
 
// document loading
$(document).ready(function() {
    // retrieve the references of the page's various components
    loading = $("#loading");
    loading.hide();
    erreur = $("#erreur");
    erreur.hide();
    content = $("#content");
});
  • Zeilen 9–13: Beim Laden der Master-Seite werden die Verweise auf die drei durch [loading, error, content] identifizierten Komponenten gespeichert;
  • Zeilen 2–4: Die Verweise auf diese drei Komponenten werden in globalen Variablen gespeichert. Sie bleiben konstant, da die drei betreffenden Bereiche unabhängig vom Zeitpunkt immer auf der angezeigten Seite vorhanden sind. Da sie konstant bleiben, können sie in [$(document).ready] berechnet und an die anderen Funktionen in der JS-Datei weitergegeben werden;

Die Funktion [postForm] verarbeitet den Klick auf die Schaltfläche [Refresh]:


function postForm() {
    console.log("postForm");
    // make a manual Ajax call
    $.ajax({
        url : '/ajax-10',
        headers : {
            'Accept' : 'application/json'
        },
        type : 'POST',
        dataType : 'json',
        beforeSend : onBegin,
        success : onSuccess,
        error : onError,
        complete : onComplete
    })
}
  • Zeilen 4–15: der Ajax-Aufruf an den Server;
  • Zeile 5: Die Aktion [ajax-10] verarbeitet den POST-Request;
  • Zeilen 6–8: Die Antwort erfolgt im JSON-Format. Der JS-Client gibt an, dass er JSON-Dokumente akzeptiert;
  • Zeile 9: Die Aktion [ajax-10] wird mit einer POST-Operation aufgerufen;
  • Zeile 10: Wir erhalten JSON;
  • Zeile 11: die vor dem Ajax-Aufruf ausgeführte Funktion;
  • Zeile 12: Die Funktion, die beim Empfang der Serverantwort ausgeführt wird, wenn diese erfolgreich ist [200 OK];
  • Zeile 13: Die Funktion, die beim Empfang der Serverantwort ausgeführt wird, wenn diese fehlschlägt [500 Internal Server Error, ...];
  • Zeile 14: Die Funktion, die nach dem Empfang der Antwort ausgeführt wird;

Die Funktion [onBegin] lautet wie folgt:


// before the Ajax call
function onBegin() {
    console.log("onBegin");
    // waiting image
    loading.show();
}

Es zeigt einfach das animierte Lade-Bild an, während auf die Antwort des Servers gewartet wird.

7.5.5. Die Aktion [/ajax-10]

  

Die Aktion [/ajax-10] läuft wie folgt ab:


// the session
    @Autowired
    private SessionModel1 session;
    // the Thymeleaf / Spring engine
    @Autowired
    private SpringTemplateEngine engine;
 
    @RequestMapping(value = "/ajax-10", method = RequestMethod.POST)
    @ResponseBody()
    public JsonResult10 ajax10(HttpServletRequest request, HttpServletResponse response) {
    ...
    }
  • Zeile 3: Die Sitzung wird injiziert. Sie hat den folgenden 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;
    // two meters
    private int cpt1 = 0;
    private int cpt3 = 0;
    // the three zones
    private String zone1 = "xx";
    private String zone3 = "zz";
    private String saisies;
    private boolean zone1Active = true;
    private boolean zone3Active = true;
 
    // getters and setters
    ...
}

Die Sitzung [SessionModel1] speichert Folgendes:

  • Zeile 15: die Anzahl [cpt1], wie oft der Bereich [Zone 1] angezeigt wird;
  • Zeile 16: die Anzahl [cpt3], wie oft der Bereich [Zone 3] angezeigt wird;
  • Zeilen 18–20: die HTML-Streams für die Zonen [Zone 1], [Zone 3] und [Inputs]. Dies ist in der Abfolge [Seite 1] --> [Seite 2] --> [Seite 1] erforderlich. Beim Wechsel von [Seite 2] zu [Seite 1] müssen [Seite 1] und ihre drei Zonen wiederhergestellt werden;
  • Zeilen 21–22: zwei Boolesche Werte, die angeben, ob die Zonen [Zone 1] und [Zone 3] angezeigt werden (sichtbar sind);

Das andere Element, das in den [AjaxController] eingefügt wird, lautet wie folgt:


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

Die [SpringTemplateEngine]-Bean ist in der [Config]-Konfigurationsdatei definiert:

  

Es ist wie folgt definiert:


    @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;
}
  • Zeilen 2–10: Wir kennen bereits die [SpringResourceTemplateResolver]-Bean, mit der wir bestimmte Eigenschaften der Ansichten definieren können;
  • Zeilen 13–17: Mit dem [SpringTemplateEngine]-Bean können wir die View-„Engine“ definieren, also die Klasse, die für die Generierung von [Thymeleaf]-Antworten an Clients zuständig ist. [Thymeleaf] verfügt über eine Standard-„Engine“ und eine weitere, die bei Verwendung in einer [Spring]-Umgebung zum Einsatz kommt. Letztere verwenden wir hier;

Die Signatur der Aktion [/ajax-10] lautet wie folgt:


@RequestMapping(value = "/ajax-10", method = RequestMethod.POST)
    @ResponseBody()
    public JsonResult10 ajax10(HttpServletRequest request, HttpServletResponse response) {
    ...
}
  • Zeile 1: Die Aktion [/ajax-10] akzeptiert nur eine POST-Anfrage;
  • Zeile 2: Die Aktion [/ajax-10] gibt die Antwort an den Client zurück. Diese wird automatisch in JSON konvertiert;
  • Zeile 3: Die Antwort ist vom Typ [JsonResult10] wie folgt:
  

package istia.st.springmvc.models;
 
public class JsonResult10 {
 
    // data
    private String content;
    private String zone1;
    private String zone3;
    private String erreur;
    private String saisies;
    private boolean zone1Active;
    private boolean zone3Active;
 
    public JsonResult10() {
    }
 
    // getters and setters
...
}
  • Zeile 6: der HTML-Inhalt des durch [content] gekennzeichneten Bereichs;
  • Zeile 7: der HTML-Inhalt des Bereichs [Zone 1];
  • Zeile 8: der HTML-Inhalt des Bereichs [Zone 3];
  • Zeile 9: der HTML-Inhalt des Bereichs [Error];
  • Zeile 10: der HTML-Inhalt des Bereichs [Inputs];
  • Zeile 11: ein Boolescher Wert, der angibt, ob der Bereich [Zone 1] angezeigt werden soll;
  • Zeile 12: ein Boolescher Wert, der angibt, ob der Bereich [Zone 3] angezeigt werden soll;

Der Code für die Aktion [/ajax-10] lautet wie folgt:


@RequestMapping(value = "/ajax-10", method = RequestMethod.POST)
    @ResponseBody()
    public JsonResult10 ajax10(HttpServletRequest request, HttpServletResponse response) {
        // thymeleaf context
        WebContext thymeleafContext = new WebContext(request, response, request.getServletContext());
        // answer
        JsonResult10 result = new JsonResult10();
        // session
        session.setZone1(null);
        session.setZone3(null);
        session.setZone1Active(false);
        session.setZone3Active(false);
        // randomize an answer
        int cas = new Random().nextInt(3);
        switch (cas) {
        case 0:
            // zone 1 active
            setZone1(thymeleafContext, result);
            return result;
        case 1:
            // zone 3 active
            setZone3(thymeleafContext, result);
            return result;
        case 2:
            // active zones 1 and 3
            setZone1(thymeleafContext, result);
            setZone3(thymeleafContext, result);
            return result;
        }
        return null;
    }
  • Zeile 5: Wir rufen den [Thymeleaf]-Kontext ab. Wozu er dient, sehen wir später;
  • Zeile 7: Wir erstellen vorerst eine leere Antwort;
  • Zeilen 9–12: Wir setzen die beiden Felder in der Sitzung auf [null] und legen fest, dass sie nicht angezeigt werden sollen. Diese beiden Felder werden in Kürze generiert, es ist jedoch möglich, dass nur eines davon generiert wird;
  • Zeilen 14–29: Beide Felder werden generiert;
  • Zeilen 17–19: Es wird nur die Zone [Zone 1] generiert;
  • Zeilen 21–23: Es wird nur die Zone [Zone 3] generiert;
  • Zeilen 25–28: Sowohl [Zone 1] als auch [Zone 3] werden generiert;

Der HTML-Fluss für die Zone [Zone 1] wird nach folgender Methode generiert:


    private void setZone1(WebContext thymeleafContext, JsonResult10 result) {
        // zone 1 active
        // flow 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);
        // session
        session.setCpt1(cpt1);
        session.setZone1(zone1);
        session.setZone1Active(true);
}
  • Zeile 1: Die Parameter sind:
    • der [Thymeleaf]-Kontext vom Typ [WebContext],
    • die derzeit erstellte Antwort an den Client vom Typ [JsonResult10];
  • Zeile 3: Wir erhöhen den Sitzungszähler [cpt1], der die Anzahl der Anzeigen der Zone [Zone 1] zählt;
  • Zeile 4: Der [Thymeleaf]-Kontext vom Typ [WebContext] verhält sich in gewisser Weise wie das [Model] in Spring MVC. Um ein Element zum Modell hinzuzufügen, verwenden wir [WebContext.setVariable]. Hier fügen wir den Zähler [cpt1] in das [Thymeleaf]-Modell ein. Dadurch kann der Thymeleaf-Ausdruck [${cpt1}] ausgewertet werden
  • Zeile 5: Der [Thymeleaf]-Kontext verfügt über eine Locale. Dadurch kann er Ausdrücke vom Typ [#{key_msg}] auswerten. Hier ordnen wir dem Thymeleaf-Kontext eine französische Locale zu;
  • Zeile 6: Dies ist die interessanteste Anweisung. Die Thymeleaf-Engine verarbeitet die Ansicht [vue-09-zone1.xml] unter Verwendung der soeben berechneten Vorlage und Locale und gibt die resultierende HTML-Ausgabe nicht an den Client weiter, sondern als String zurück;
  • Zeilen 7–9: Die soeben berechnete HTML-Ausgabe für den Bereich [Zone 1] wird in der Sitzung und im an den Client zu sendenden Ergebnis gespeichert. Zusätzlich legen wir fest, dass der Bereich [Zone 1] angezeigt werden muss;
  • Zeilen 11–13: Informationen zum Bereich [Zone 1] werden in der Sitzung gespeichert, damit er neu generiert werden kann;

Zeile 7 verarbeitet die folgende Ansicht [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>
  • Zeile 3: Der Ausdruck [#{message.zone}] wird unter Verwendung der Locale ausgewertet;
  • Zeile 4: Der Ausdruck [${cpt1}] wird anhand der Thymeleaf-Vorlage ausgewertet;

Die Schlüsselmeldung [message.zone] ist in den Meldungsdateien [messages_fr.properties] und [messages_en.properties] definiert:

  

[messages_fr.properties]


message.zone=Nombre d'accès : 

[messages_en.properties]


message.zone=Number of hits: 

Der HTML-Fluss für den Bereich [Zone 3] wird auf ähnliche Weise generiert:


    private void setZone3(WebContext thymeleafContext, JsonResult10 result) {
        // zone 3 active
        // flow 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);
        // session
        session.setCpt3(cpt3);
        session.setZone3(zone3);
        session.setZone3Active(true);
}
  • Zeile 6: Die Spracheinstellung für die Zone [Zone 3] ist Englisch;

7.5.6. Verarbeitung der Antwort der Aktion [/ajax-10]

Kehren wir zum JS-Code in [local9.js] zurück, der die Serverantwort verarbeitet:


// on receipt of the server response
// in case of success
function onSuccess(data) {
    console.log("onSuccess");
    // content
    if (data.content) {
        content.html(data.content);
    }
    // zone 1
    if (data.zone1Active) {
        $("#zone1").show();
        if (data.zone1) {
            $("#zone1-content").html(data.zone1);
        }
    } else {
        $("#zone1").hide();
    }
    // zone 3 active?
    if (data.zone3Active) {
        $("#zone3").show();
        if (data.zone3) {
            $("#zone3-content").html(data.zone3);
        }
    } else {
        $("#zone3").hide();
    }
    // seized?
    if (data.saisies) {
        $("#saisies").html(data.saisies);
    }
    // mistake?
    if (data.erreur) {
        erreur.text(data.erreur);
        erreur.show();
    } else {
        erreur.hide();
    }
}

Sehen wir uns die Java-Struktur der in Zeile 3 in der Variablen [data] empfangenen Antwort an:


public class JsonResult10 {
 
    // data
    private String content;
    private String zone1;
    private String zone3;
    private String erreur;
    private String saisies;
    private boolean zone1Active;
    private boolean zone3Active;
 
}
  • Zeilen 6–8: Wenn [data.content] nicht null ist, wird das Feld [id=content] damit initialisiert. Dieses Feld repräsentiert [Seite 1] oder [Seite 2] in ihrer Gesamtheit. In diesem Beispiel ist [data.content == null], sodass die Zone [id=content] nicht geändert wird und weiterhin [Seite 1] anzeigt;
  • Zeilen 10–17: Zeige [Zone 1] an, wenn [data.zone1Active == true] ist. Wenn darüber hinaus [data.zone1 != null] ist, wird der Inhalt von [Zone 1] geändert; andernfalls bleibt er unverändert;
  • Zeilen 19–26: Das Gleiche gilt für [Zone 3];
  • Zeilen 28–30: Wenn [data.saisies!=null], wird die Zone [Saisies] aktualisiert. In dieser Demonstration ist [data.saisies==null], sodass die Zone [Saisies] unverändert bleibt;
  • Zeilen 32–37: Ähnliches gilt für das Feld [Error], mit folgenden Nuancen:
    • Zeile 33: [data.error] ist eine Fehlermeldung im Textformat;
    • Zeile 36: Wenn [data.error] null ist, wird das Feld [Error] ausgeblendet. Dies liegt daran, dass es möglicherweise bei der vorherigen Anfrage angezeigt wurde;

Im Falle eines serverseitigen Fehlers (HTTP-Status wie 500 Internal Server Error) wird die folgende Funktion ausgeführt:


// on receipt of the server response
// in case of failure
function onError(jqXHR) {
    console.log("onError");
    // system error
    erreur.text(jqXHR.responseText);
    erreur.show();
}

Um einen solchen Fehler zu sehen, ändern wir die Funktion [postForm] wie folgt:


function postForm() {
    console.log("postForm");
    // retrieve references to the current page
    ...
    // make a manual Ajax call
    $.ajax({
        url : '/ajax-10x',
        ...
    })
}
  • Zeile 7: Wir geben eine URL ein, die nicht existiert;

Hier sind die Ergebnisse, wenn Sie auf die Schaltfläche [Aktualisieren] klicken:

Es ist interessant festzustellen, dass der Fehler auch in Form einer JSON-Zeichenkette gesendet wurde.

Die Methode, die nach dem Empfang der Serverantwort ausgeführt wird, lautet wie folgt:


// after [onSuccess, onError]
function onComplete() {
    console.log("onComplete");
    // waiting image
    loading.hide();
}

Wir blenden das animierte Lade-Bild einfach aus.

7.5.7. Anzeigen der Seite [Seite 2]

Der HTML-Code für den Link [Absenden] lautet wie folgt:


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

Die JS-Funktion [validate] lautet wie folgt:


// validation of entered values
function valider() {
    // posted value
    var post = JSON3.stringify({
        "value1" : $("#text1").val().trim(),
        "value2" : $("#text2").val().trim()
    });
    // make a manual Ajax call
    $.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
    })
}
  • Zeilen 4–7: Wir haben zwei Werte, v1 und v2, die gesendet werden sollen: diese stammen aus den Eingabekomponenten, die mit [#text1] und [#text2] gekennzeichnet sind. Wir werden etwas Neues ausprobieren. Wir werden diese beiden Werte als JSON-String {"value1":v1,"value2":v2} senden;
  • Zeile 10: Die gesendeten Werte werden an die Aktion [ajax-11A] weitergeleitet;
  • Zeile 12: Da wir wissen, dass wir eine JSON-Antwort erhalten werden, geben wir an, dass wir JSON empfangen können;
  • Zeile 13: Wir teilen dem Server mit, dass wir ihm den gesendeten Wert als JSON-String übermitteln werden;
  • Zeilen 15–16: Wir senden den zu übermittelnden Wert per POST;
  • Zeile 17: Wir erhalten JSON;

7.5.8. Die Aktion [ajax-11A]

Die [ajax-11A]-Aktion, die die gesendete JSON-Zeichenkette verarbeitet, sieht wie folgt aus:


@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) {
        ...
    }
  • Zeile 1: Mit ["application/json"] geben wir an, dass die Aktion ein Dokument im JSON-Format erwartet. Dieses Dokument ist der vom Client gesendete Wert;
  • Zeile 3: Der gesendete Wert wird im folgenden [PostAjax11A post]-Objekt abgerufen:
  

package istia.st.springmvc.models;
 
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
 
import org.hibernate.validator.constraints.Range;
 
public class PostAjax11A {
 
    // data
    @Size(min = 4, max = 6)
    @NotNull
    private String value1;
    @Range(min = 10, max = 14)
    @NotNull
    private Integer value2;
 
    // getters and setters
    ...
}
  • Die Struktur des Objekts [PostAjax11A] muss mit der Struktur des gesendeten Objekts {"value1":v1,"value2":v2} übereinstimmen. Daher sind die Felder [value1] (Zeile 13) und [value2] (Zeile 16) erforderlich;
  • Wir haben für beide Felder Integritätsbeschränkungen festgelegt;

Kehren wir zum Code für die Aktion [ajax-11A] zurück:


@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) {
        // thymeleaf context
        WebContext thymeleafContext = new WebContext(request, response, request.getServletContext());
        // answer
        JsonResult10 result = new JsonResult10();
        // valid post?
        if (bindingResult.hasErrors()) {
            // page 1 is returned with an error
            result.setZone1Active(session.isZone1Active());
            result.setZone3Active(session.isZone3Active());
            result.setErreur(getErreursForModel(bindingResult));
            return result;
        }
        ...
}
  • Zeile 3: Die Annotation [@RequestBody] bezieht sich auf das vom Client gesendete Dokument. Dies ist der vom Client im JSON-Format gesendete Wert. Er wird daher zum Erstellen des Objekts [PostAjax11A] verwendet;
  • Zeile 3: Die Annotation [@Valid] erzwingt die Validierung des gesendeten Werts;
  • Zeile 9: Wenn die Validierung fehlschlägt:
    • Zeile 13: Es wird eine Fehlermeldung zurückgegeben;
    • Zeilen 11–12: Die Felder 1 und 3 werden in ihren vorherigen Zustand zurückversetzt (angezeigt oder nicht);

Die Fehlermeldung wird wie folgt berechnet:


    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();
}

Das ist eine Funktion, die wir schon kennen.

Die Aktion [ajax-11A] wird wie folgt fortgesetzt:


@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) {
        // thymeleaf context
        WebContext thymeleafContext = new WebContext(request, response, request.getServletContext());
        // answer
        JsonResult10 result = new JsonResult10();
        // valid post?
        if (bindingResult.hasErrors()) {
    ...
        }
        // the input field is saved
        thymeleafContext.setVariable("value1", post.getValue1());
        thymeleafContext.setVariable("value2", post.getValue2());
        session.setSaisies(engine.process("vue-09-saisies", thymeleafContext));
        // send page 2
        result.setContent(engine.process("vue-09-page2", thymeleafContext));
        return result;
}
  • Zeilen 13–14: Die übermittelten Werte werden in den Thymeleaf-Kontext gesetzt;
  • Zeile 15: Anhand dieses Kontexts berechnen wir die Ansicht [vue-09-saisies] und speichern sie in der Sitzung, damit wir sie später neu generieren können;
  • Zeile 17: Seite 2 wird in das Ergebnis eingefügt, das an den Client gesendet wird;

Die Ansicht [view-09-page2.xml] sieht wie folgt aus:

  

<!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>
  • In den Zeilen 9 und 13 werden die Werte [value1, value2] angezeigt, die die Aktion [/ajax-11A] in den Thymeleaf-Kontext eingefügt hat;

7.5.9. Verarbeitung der Antwort der Aktion [/ajax-11A]

Auf der Client-Seite wird die Antwort der Aktion [/ajax-10] von der Funktion [onSuccess] verarbeitet:


function onSuccess(data) {
    console.log("onSuccess");
    // content
    if (data.content) {
        content.html(data.content);
    }
    // zone 1
    if (data.zone1Active) {
        $("#zone1").show();
        if (data.zone1) {
            $("#zone1-content").html(data.zone1);
        }
    } else {
        $("#zone1").hide();
    }
    // zone 3 active?
    if (data.zone3Active) {
        $("#zone3").show();
        if (data.zone3) {
            $("#zone3-content").html(data.zone3);
        }
    } else {
        $("#zone3").hide();
    }
    // seized?
    if (data.saisies) {
        $("#saisies").html(data.saisies);
    }
    // mistake?
    if (data.erreur) {
        erreur.text(data.erreur);
        erreur.show();
    } else {
        erreur.hide();
    }
}

Wir haben diesen Code bereits kommentiert. Betrachten wir die beiden Fälle: eine Antwort mit oder ohne Fehler:

Mit Fehler

In diesem Fall hat die Aktion [/ajax-11A] eine JSON-Antwort in der Form {"zone1":null, "zone3":null,"saisies":null,"erreur":erreur,"zone1Active":zone1Active,"zone3Active":zone3Active,"content":null} gesendet. Wenn wir dem obigen Code folgen, sehen wir, dass:

  • sich das Feld [content] nicht ändert. Es enthielt Seite #1;
  • das Feld [Error] wird angezeigt;
  • die Felder [Zone 1], [Zone 3] und [Entries] unverändert bleiben;

Kein Fehler

In diesem Fall hat die Aktion [/ajax-11A] eine JSON-Antwort in der Form {"zone1":null, "zone3":null,"saisies":null,"erreur":null,"zone1Active":false,"zone3Active":false,"content":content} gesendet. Wenn wir dem obigen Code folgen, sehen wir, dass:

  • das Feld [content] angezeigt wird. Es enthält Seite Nr. 2;

Hier sind drei Beispiele für die Ausführung:

Ein Fall mit einem Validierungsfehler:

Ein Fall mit einem POST-Fehler:

Diese Art von Fehler ist anders. Da Spring die JSON-Zeichenkette nicht in den Typ [PostAjax11A] konvertieren konnte, gab es eine HTTP-Antwort mit [status=400] zurück. Die Aktion [ajax-11A] wurde nicht ausgeführt;

Ein Fall ohne Fehler:

7.5.10. Zurück zu Seite 1

Der Link [Zurück zu Seite 1] auf Seite 2 lautet wie folgt:


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

Die JS-Methode [returnPage1] lautet wie folgt:


// back to page 1
function retourPage1() {
    // make a manual Ajax call
    $.ajax({
        url : '/ajax-11B',
        headers : {
            'Accept' : 'application/json',
        },
        type : 'POST',
        dataType : 'json',
        beforeSend : onBegin,
        success : onSuccess,
        error : onError,
        complete : onComplete
    })
}

Es sendet eine POST-Anfrage ohne übermittelte Daten an die Aktion [/ajax-11B].

7.5.11. Die Aktion [/ajax-11B]

Die Aktion [/ajax-11B] sieht wie folgt aus:


    @RequestMapping(value = "/ajax-11B", method = RequestMethod.POST)
    @ResponseBody
    public JsonResult10 ajax11B(HttpServletRequest request, HttpServletResponse response) {
        // thymeleaf context
        WebContext thymeleafContext = new WebContext(request, response, request.getServletContext());
        // answer
        JsonResult10 result = new JsonResult10();
        // we return page 1 in its original state
        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;
}

Die Aktion muss Seite Nr. 1 mit ihren drei Bereichen [Bereich1, Bereich3, Fehler] neu laden:

  • Zeile 9: Seite 1 wird dem Ergebnis hinzugefügt;
  • Zeile 10: Die Eingabezone wird dem Ergebnis hinzugefügt;
  • Zeile 11: Das Feld [Zone 1] wird in das Ergebnis aufgenommen;
  • Zeile 12: Die Zone [Zone 3] wird dem Ergebnis hinzugefügt;
  • Zeilen 13–14: Der Status der Zonen [Zone 1] und [Zone 3] wird dem Ergebnis hinzugefügt;

7.5.12. Verarbeitung der Antwort der Aktion [/ajax-11B]

Die Antwort der Aktion [/ajax-11B] wird von der Funktion [onSuccess] verarbeitet:


function onSuccess(data) {
    console.log("onSuccess");
    // content
    if (data.content) {
        content.html(data.content);
    }
    // zone 1
    if (data.zone1Active) {
        $("#zone1").show();
        if (data.zone1) {
            $("#zone1-content").html(data.zone1);
        }
    } else {
        $("#zone1").hide();
    }
    // zone 3 active?
    if (data.zone3Active) {
        $("#zone3").show();
        if (data.zone3) {
            $("#zone3-content").html(data.zone3);
        }
    } else {
        $("#zone3").hide();
    }
    // seized?
    if (data.saisies) {
        $("#saisies").html(data.saisies);
    }
    // mistake?
    if (data.erreur) {
        erreur.text(data.erreur);
        erreur.show();
    } else {
        erreur.hide();
    }
}

Die Aktion [/ajax-11B] hat eine JSON-Antwort in der Form {"zone1":zone1, "zone3":zone3,"saisies":saisies,"erreur":null,"zone1Active":zone1Active,"zone3Active":zone3Active,"content":content} gesendet. Wenn wir dem obigen Code folgen, sehen wir, dass:

  • das Feld [content] geändert wurde. Es enthielt zuvor Seite Nr. 2. Es enthält nun Seite Nr. 1;
  • Das Feld [Fehler] ist ausgeblendet;
  • die Bereiche [Zone 1], [Zone 3] und [Einträge] werden wie bisher angezeigt;

7.6. Verwaltung der Sitzung auf der Client-Seite

7.6.1. Einleitung

Im vorherigen Abschnitt haben wir eine Sitzung mit folgender Struktur verwaltet:


public class SessionModel1 implements Serializable {
 
    // two meters
    private int cpt1 = 0;
    private int cpt3 = 0;
    // the three zones
    private String zone1 = "xx";
    private String zone3 = "zz";
    private String saisies;
    private boolean zone1Active = true;
    private boolean zone3Active = true;
...
}

Bei einer großen Anzahl von Benutzern kann der von all diesen Benutzersitzungen belegte Speicher zu einem Problem werden. Daher gilt die Regel, die Größe dieses Speichers zu minimieren. Das SPV-Modell (Single-Page Application) ermöglicht es Ihnen, die Sitzung auf der Client-Seite zu verwalten und einen sitzungslosen Webserver zu betreiben. Tatsächlich wird die einzelne Seite zunächst vom Browser geladen. Zusammen mit ihr wird die dazugehörige JavaScript-Datei geladen. Da keine Seitenneuladung stattfindet, verbleibt diese JS-Datei dauerhaft im Browser, so wie sie ursprünglich geladen wurde. Wir können dann ihre globalen Variablen nutzen, um Informationen über die verschiedenen Aktionen des Benutzers zu speichern. Das werden wir uns nun ansehen. Wir werden nicht nur die Sitzung auf der Client-Seite verwalten, sondern auch die JS-Anwendung neu gestalten, um Serveranfragen zu minimieren.

7.6.2. Die Aktion [/ajax-12]

  

Die Aktion [/ajax-12] sieht wie folgt aus:


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

Die Ansicht [vue-12.xml] sieht wie folgt aus:

  

<!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>
  • Diese Ansicht ist identisch mit Ansicht [vue-09], mit Ausnahme des in Zeile 9 verwendeten JS-Skripts;

Die angezeigte Ansicht sieht wie folgt aus:

 

7.6.3. Der JS-Code für die Schaltfläche [Aktualisieren]

  

Der Code in der Datei [local12.js] lautet wie folgt:


// global variables
var content;
var loading;
var erreur;
var page1;
var page2;
var value1;
var value2;
var session = {
        "cpt1" : 0,
        "cpt3" : 0
    };
 
// document loading
$(document).ready(function() {
    // retrieve the references of the page's various components
    loading = $("#loading");
    loading.hide();
    erreur = $("#erreur");
    erreur.hide();
    content = $("#content");
});
  • Zeilen 17–21: Beim Laden der Master-Seite werden die Verweise auf die drei durch [loading, error, content] identifizierten Komponenten in den globalen Variablen in den Zeilen 2–4 gespeichert;
  • Zeilen 5–6: zum Speichern der beiden Seiten;
  • Zeilen 7–8: zum Speichern der beiden über den Link [Validate] übermittelten Werte;
  • Zeile 9: die Sitzung. Sie speichert die Werte der Zähler [cpt1, cpt3] auf der Client-Seite;

Die Funktion [postForm] verarbeitet den Klick auf die Schaltfläche [Refresh]:


function postForm() {
    console.log("postForm");
    // we post the session
    var post = JSON3.stringify(session);
    // make a manual Ajax call
    $.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
    })
}

Die Unterschiede zur vorherigen Version sind wie folgt:

  • Die URL in Zeile 7 ist anders;
  • Zeile 4: Es wird ein Wert gesendet, während zuvor keiner gesendet wurde. Dieser Wert ist die JSON-Zeichenkette der Sitzung. Das Prinzip ist wie folgt:
    • Der Client sendet die Sitzung an den Server,
    • der Server ändert sie und sendet sie zurück,
    • der Client speichert die neue Sitzung;
  • Zeile 10: Wir senden ein Dokument im JSON-Format (gesendeter Wert);
  • Zeile 13: Wir haben etwas zu senden;
  • Zeilen 15–20: Die Funktionen [beforeSend, error, complete] sind dieselben wie in der vorherigen Version. Nur die Funktion [success] ändert sich (Zeilen 16–18);

7.6.4. Die Aktion [/ajax-13]

  

Die Aktion [/ajax-13] läuft wie folgt ab:


    @RequestMapping(value = "/ajax-13", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
    @ResponseBody()
    public JsonResult13 ajax13(@RequestBody SessionModel2 session2, HttpServletRequest request,    HttpServletResponse response) {
    ...
}
  • Zeile 3: Der Parameter [@RequestBody SessionModel2 session2] ruft die vom Client übermittelte Sitzung ab. Diese hat den folgenden Typ [SessionModel2]:
  

package istia.st.springmvc.models;
 
import java.io.Serializable;
 
public class SessionModel2 implements Serializable {
 
    private static final long serialVersionUID = 1L;
    // two meters
    private int cpt1 = 0;
    private int cpt3 = 0;
 
    // getters and setters
    ...
}

Die [SessionModel2]-Sitzung speichert Folgendes:

  • Zeile 9: die Anzahl [cpt1], wie oft der Bereich [Zone 1] angezeigt wird;
  • Zeile 10: die Anzahl [cpt3], wie oft der Bereich [Zone 3] angezeigt wird;

Sehen wir uns den Code für die Aktion [/ajax-13] weiter an:


    @RequestMapping(value = "/ajax-13", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
    @ResponseBody()
    public JsonResult13 ajax13(@RequestBody SessionModel2 session2, HttpServletRequest request,    HttpServletResponse response) {
    ...
}
  • Zeile 3: Der Typ [JsonResult13] der Antwort lautet wie folgt:
  

package istia.st.springmvc.models;
 
public class JsonResult13 {
 
    // data
    private String page2;
    private String zone1;
    private String zone3;
    private String erreur;
    private String value1;
    private Integer value2;
 
    // session
    private SessionModel2 session;
 
    // getters and setters
    ...
}
  • Zeile 14: die Sitzung. Der Server sendet sie zur Speicherung an den Client zurück;
  • Zeile 6: der HTML-Inhalt von Seite 2;
  • Zeile 7: der HTML-Inhalt des Bereichs [Zone 1];
  • Zeile 8: der HTML-Inhalt des Bereichs [Zone 3];
  • Zeile 9: etwaige Fehlermeldungen;
  • Zeilen 10–11: zwei vom Server berechnete und auf Seite 2 angezeigte Informationen;

Schauen wir uns den Code für die Aktion [/ajax-13] weiter an:


@RequestMapping(value = "/ajax-13", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
    @ResponseBody()
    public JsonResult13 ajax13(@RequestBody SessionModel2 session2, HttpServletRequest request,
            HttpServletResponse response) {
        // thymeleaf context
        WebContext thymeleafContext = new WebContext(request, response, request.getServletContext());
        // answer
        JsonResult13 result = new JsonResult13();
        result.setSession(session2);
        // randomize an answer
        int cas = new Random().nextInt(3);
        switch (cas) {
        case 0:
            // zone 1 active
            setZone1B(thymeleafContext, result);
            return result;
        case 1:
            // zone 3 active
            setZone3B(thymeleafContext, result);
            return result;
        case 2:
            // active zones 1 and 3
            setZone1B(thymeleafContext, result);
            setZone3B(thymeleafContext, result);
            return result;
        }
        return null;
    }
  • Zeile 9: Die Sitzung wird in das Ergebnis der Aktion gesetzt;

Die Methode [setZone1B], die die Zone [Zone 1] aktiviert, lautet wie folgt:


    private void setZone1B(WebContext thymeleafContext, JsonResult13 result) {
        // retrieve the session
        SessionModel2 session = result.getSession();
        // zone 1 active
        // flow 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);
        // session
        session.setCpt1(cpt1);
}
  • Zeile 3: Wir rufen die Sitzung ab. Sie wird in Zeile 12 mit dem neuen Zähler [cpt1] geändert. Beachten Sie, dass diese Sitzung an den Client zurückgesendet wird;
  • Zeile 10: die neue Zone [Zone 1];

Die Methode [setZone3B], die die Zone [Zone 3] aktiviert, ist ähnlich:


private void setZone3B(WebContext thymeleafContext, JsonResult13 result) {
        // retrieve the session
        SessionModel2 session = result.getSession();
        // zone 3 active
        // flow 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);
        // session
        session.setCpt3(cpt3);
    }

7.6.5. Verarbeitung der Antwort der Aktion [/ajax-13]

Auf der Client-Seite wird die JSON-Antwort der Aktion [/ajax-13] durch die folgende [onSuccess]-Funktion verarbeitet:


function postForm() {
    console.log("postForm");
    // we post the session
    var post = JSON3.stringify(session);
    // make a manual Ajax call
    $.ajax({
    ...
        success : function(data) {
            // save the session
            session = data.session;
            // update both zones
            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();
            }
        },
...
    })
}
  • Zeilen 12–17: Wenn der Server etwas in das Feld [zone1] der Antwort geschrieben hat, muss der Bereich [Zone 1] neu generiert und angezeigt werden; andernfalls muss er ausgeblendet werden;
  • Zeilen 18–23: Die gleiche Logik gilt für den Bereich [Zone 3];

7.6.6. Anzeigen der Seite [Seite 2]

Der HTML-Code für den Link [Submit] lautet wie folgt:


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

Die JS-Funktion [validate] lautet wie folgt:


// validation of entered values
function valider() {
    // memorize page 1
    page1 = content.html();
    // store the values entered
    value1 = $("#text1").val().trim();
    value2 = $("#text2").val().trim();
    // posted value
    var post = JSON3.stringify({
        "value1" : value1,
        "value2" : value2,
        "pageRequired" : page2 ? false : true
    });
    // make a manual Ajax call
    $.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
    })
}
  • Wir senden eine POST-Anfrage, die uns normalerweise zu Seite 2 führen sollte;
  • Zeile 4: Wir speichern Seite 1, damit wir später dorthin zurückkehren können;
  • Zeilen 6–7: Der vorherige Vorgang speichert nicht die eingegebenen Werte, sondern nur den HTML-Code der Seite. Deshalb speichern wir nun die beiden im Formular eingegebenen Werte;
  • Zeilen 9–13: Die beiden eingegebenen Werte werden in eine JSON-Zeichenkette gesetzt. Diese wird gesendet;
  • Zeile 12: Ein Parameter, der dem Server mitteilt, ob wir Seite 2 benötigen. Wir gehen wie folgt vor: Wir rufen Seite 2 einmal ab und speichern sie dann in der JS-Variablen `[page2]`. Danach rufen wir sie nicht erneut ab. Wir verwenden die zwischengespeicherte Seite. Zeile 2: `[pageRequired]` ist `true`, wenn die Variable `[page2]` leer ist, andernfalls `false`;
  • Beachten Sie, dass die Sitzung nicht gesendet wird. Tatsächlich speichert sie Zähler, die die Aktion [/ajax-14] in Zeile 20 nicht verändert;

7.6.7. Die Aktion [/ajax-14]

Die Aktion [/ajax-14] sieht wie folgt aus:


@RequestMapping(value = "/ajax-14", method = RequestMethod.POST)
    @ResponseBody
    public JsonResult13 ajax14(@RequestBody @Valid PostAjax14 post, BindingResult bindingResult, Locale locale,    HttpServletRequest request, HttpServletResponse response) {
        ...
    }
  • Zeile 3: Die Antwort ist immer vom Typ [JsonResult13];
  • Zeile 3: Der gesendete Wert ist im folgenden Typ [PostAjax14] gekapselt:

package istia.st.springmvc.models;
 
public class PostAjax14 extends PostAjax11A {
 
    // page 2
    private boolean pageRequired;
 
    // getters and setters
    ...
}
  • Zeile 3: Die Klasse [PostAjax14] erweitert die Klasse [PostAjax11A] aus der vorherigen Version. Sie hat daher die Struktur [value1, value2, pageRequired];

Die Aktion [/ajax-14] wird wie folgt fortgesetzt:


    @RequestMapping(value = "/ajax-14", method = RequestMethod.POST)
    @ResponseBody
public JsonResult13 ajax14(@RequestBody @Valid PostAjax14 post, BindingResult bindingResult, Locale locale,    HttpServletRequest request, HttpServletResponse response) {
        // thymeleaf context
        WebContext thymeleafContext = new WebContext(request, response, request.getServletContext());
        // answer
        JsonResult13 result = new JsonResult13();
        // valid post?
        if (bindingResult.hasErrors()) {
            // an error is returned
            result.setErreur(getErreursForModel(bindingResult));
            return result;
        }
        // send page 2
        result.setValue1(post.getValue1());
        result.setValue2(post.getValue2());
        // page required?
        if (post.isPageRequired()) {
            result.setPage2(engine.process("vue-12-page2", thymeleafContext));
        }
        return result;
}
  • Zeilen 9–13: Sind die übermittelten Werte [value1, value2] ungültig, wird eine Fehlermeldung zurückgegeben;
  • Zeilen 15–16: Normalerweise sollte der Server eine Berechnung anhand der übermittelten Werte durchführen. Hier gibt er sie lediglich zurück, um zu zeigen, dass er sie empfangen hat;
  • Zeilen 18–20: Seite #2 wird nur zurückgegeben, wenn sie vom Client angefordert wurde. In Zeile 19 ist die Ansicht [view-12-page2] neu:
 

<!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>
  • Der XML-Code enthält nicht mehr wie zuvor von Thymeleaf ausgewertete Werte;
  • wir haben die Stellen identifiziert, an denen die vom Server zurückgegebenen Werte [value1, value2] platziert werden sollen. Zeile 9, [id='value1'], gibt an, wo [value1] platziert werden soll. Zeile 13, dasselbe gilt für [value2];

7.6.8. Verarbeitung der Antwort der Aktion [/ajax-14]

Die Antwort der Aktion [/ajax-14] wird von der folgenden [success]-Funktion verarbeitet:


// validation des valeurs saisies
function valider() {
    ...
    // on fait un appel Ajax à la main
    $.ajax({
        ...
        success : function(data) {
            // erreur ?
            if (data.erreur) {
                // affichage erreur
                erreur.html(data.erreur);
                erreur.show();
            } else {
                // pas d'erreur
                erreur.hide();
                // page 2
                if (page2) {
                    // on utilise la page en cache
                    content.html(page2);
                } else {
                    // on mémorise la page 2
                    page2 = data.page2;
                    // on l'affiche
                    content.html(data.page2);
                }
                // on la met à jour avec les infos du serveur
                $("#value1").text(data.value1);
                $("#value2").text(data.value2);
            }
        },
...
    })
}
  • Zeilen 9–13: Wenn der Server einen Fehler zurückgegeben hat, diesen anzeigen;
  • Zeilen 14–29: der Fall, in dem kein Fehler aufgetreten ist. Wir müssen dann Seite 2 anzeigen;
  • Zeile 17: Wir prüfen, ob Seite 2 bereits in der Variablen [page2] gespeichert ist;
  • Zeile 19: In diesem Fall verwenden wir die Variable [page2], um Seite 2 anzuzeigen;
  • Zeile 24: Andernfalls verwenden wir das vom Server bereitgestellte Feld [data.page2];
  • Zeile 22: Wir stellen sicher, dass Seite 2 gespeichert wird, damit wir sie später nicht erneut anfordern müssen;
  • Zeilen 27–28: Auf Seite 2 zeigen wir die beiden vom Server gesendeten Informationen [value1, value2] an;

7.6.9. Zurück zu Seite 1

Der Link [Zurück zu Seite 1] auf Seite 2 lautet wie folgt:


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

Die JS-Methode [returnPage1] lautet wie folgt:


// back to page 1
function retourPage1() {
    // regenerate page 1
    content.html(page1);
    // regenerate foreclosures
    $("#text1").val(value1);
    $("#text2").val(value2);
}
  • Dies ist eine JavaScript-Aktion, die nicht mit dem Server interagiert, da Seite 1 lokal in der Variablen [page1] gespeichert wurde;
  • Zeile 4: Wir laden Seite 1 neu;
  • Zeilen 6–7: Nur der HTML-Teil von Seite 1 wurde zwischengespeichert. Nicht die Benutzereingaben. Wir müssen daher die Benutzereingaben neu laden;

7.6.10. Fazit

Durch die Nutzung der Möglichkeiten des APU-Modells ist es uns gelungen, den Webserver zu vereinfachen, der nun zustandslos (keine Sitzungen) und weniger stark ausgelastet ist:

  • Wir haben die Interaktion mit dem Server in der JS-Funktion [returnPage1] entfernt;
  • Der Server generiert Seite 2 nur einmal;

7.7. Strukturierung von JavaScript-Code in Schichten

7.7.1. Einleitung

Der JavaScript-Code aus der vorherigen Anwendung wird langsam komplex. Es ist an der Zeit, ihn in Schichten zu strukturieren. Die Anwendung bleibt unverändert. Wir nehmen keine Änderungen am Server vor, außer der Definition einer neuen Landingpage. Wir werden den JS-Code umgestalten.

Die neue Architektur sieht wie folgt aus:

7.7.2. Die Startseite

Die Aktion, die die Anwendung startet, ist die folgende [/ajax-16]-Aktion:


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

Es wird die folgende Ansicht [vue-16.xml] angezeigt:


<!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>
  • Zeilen 9–10: Der JS-Code wurde in zwei verschiedene Dateien ausgelagert:
    • [local-ui] implementiert die [Präsentationsschicht],
    • [local-dao] implementiert die [DAO]-Schicht;
  

7.7.3. Implementierung der [DAO]-Schicht

7.7.4. Schnittstelle

Die [DAO]-Schicht in [local-dao.js] stellt der [Präsentationsschicht] die folgende Schnittstelle zur Verfügung:


function updatePage1(deferred, sendMeBack)
um Seite 1 über die Schaltfläche [Refresh] zu aktualisieren

function getPage2(deferred, sendMeBack, value1, value2, pageRequired)
um Seite 2 mit der Schaltfläche [Absenden] anzuzeigen

JavaScript kennt das Konzept einer Schnittstelle nicht. Ich habe diesen Begriff lediglich verwendet, um darauf hinzuweisen, dass die [Präsentationsschicht] vereinbart hat, mit der [DAO-Schicht] ausschließlich über die beiden oben genannten Funktionen zu kommunizieren.

7.7.5. Implementierung der Schnittstelle

Das Grundgerüst der Implementierung sieht wie folgt aus:


var session = {
    "cpt1" : 0,
    "cpt3" : 0
};
 
// update Page 1
function updatePage1(deferred, sendMeBack) {
...
}
 
// page 2
function getPage2(deferred, sendMeBack, value1, value2, pageRequired) {
...
}

Der Zweck der [DAO]-Schicht besteht darin, die Details der an den Webserver gerichteten HTTP-Anfragen vor der [Präsentationsschicht] zu verbergen. Die Sitzung ist Teil dieser Details. Sie wird daher nun von der [DAO]-Schicht verwaltet.

7.7.5.1. Die Funktion [updatePage1]

Die Funktion [updatePage1] ist die Funktion, die von der [Präsentationsschicht] aufgerufen wird, um Seite 1 zu aktualisieren. Ihr Code lautet wie folgt:


// update Page 1
function updatePage1(deferred, sendMeBack) {
    // requête HTTP
    executePost(deferred, sendMeBack, '/ajax-13', session);
}
  • Zeile 1: Die Funktion [updatePage1] erhält zwei Parameter:
    1. ein Objekt vom Typ [jQuery.Deferred]. Dieser Objekttyp speichert einen Status, der drei Werte annehmen kann: ['pending', 'resolved', 'rejected']. Wenn es in der Funktion [updatePage1] ankommt, befindet es sich im Status [pending];
    2. ein JS-Objekt, das an die [presentation]-Ebene zurückgegeben werden soll;

Alle HTTP-Anfragen werden von der folgenden Funktion [executePost] gestellt:


// requête HTTP
function executePost(deferred, sendMeBack, url, post) {
    // on fait un appel Ajax à la main
    $.ajax({
        headers : {
            'Accept' : 'application/json',
            'Content-Type' : 'application/json'
        },
        url : url,
        type : 'POST',
        data : JSON3.stringify(post),
        dataType : 'json',
        success : function(data) {
            // on mémorise la session
            if (data.session) {
                session = data.session;
            }
            // on rend le résultat
            deferred.resolve({
                "status" : 1,
                "data" : data,
                "sendMeBack" : sendMeBack
            });
        },
        error : function(jqXHR) {
            // on rend l'erreur
            deferred.resolve({
                "status" : 2,
                "data" : jqXHR.responseText,
                "sendMeBack" : sendMeBack
            });
        }
    });
}
  • Zeile 1: Die Funktion [executePost] führt einen Ajax-Aufruf vom Typ POST aus. Sie erwartet vier Parameter:
    1. ein [jQuery.Deferred]-Objekt im Status [pending];
    2. ein JS-Objekt, das an die [Präsentationsschicht] zurückgegeben werden soll;
    3. die POST-URL;
    4. den Wert, der als JS-Objekt gesendet werden soll;
  • Zeilen 5–8: Die Funktion sendet JSON (Zeile 7) und empfängt JSON (Zeile 6);
  • Zeile 11: Der zu sendende Wert wird in JSON konvertiert;
  • Zeilen 13–24: Die Funktion, die ausgeführt wird, wenn der Ajax-Aufruf erfolgreich ist;
  • Zeilen 19–23: Wenn der Server eine Sitzung zurückgegeben hat, wird diese gespeichert;
  • Zeilen 13–18: Setze das [deferred]-Objekt in den Status [resolved] und übergebe ein Ergebnis mit den folgenden Feldern:
    • [status]: 1 bei Erfolg, 2 bei Fehler,
    • [data]: die JSON-Antwort des Servers,
    • [sendMeBack]: der zweite Parameter der Funktion, bei dem es sich um ein Objekt handelt, das der Aufrufer abrufen möchte;
  • Zeilen 17–31: Die Funktion, die ausgeführt wird, wenn der Ajax-Aufruf fehlschlägt. Wir verfahren wie zuvor, mit zwei Unterschieden:
    • [status] wird auf 2 gesetzt, um einen Fehler anzuzeigen;
    • [data] ist wieder die JSON-Antwort des Servers, wird jedoch auf andere Weise abgerufen;

7.7.5.2. Die Funktion [getPage2]

Die Funktion [getPage2] lautet wie folgt:


// page 2
function getPage2(deferred, sendMeBack, value1, value2, pageRequired) {
    // requête HTTP
    executePost(deferred, sendMeBack, '/ajax-14', {
        "value1" : value1,
        "value2" : value2,
        "pageRequired" : pageRequired,
    });
}
  • Die Funktion erhält die folgenden Parameter:
    1. [deferred]: ein Objekt vom Typ [jQuery.Deferred] im Status [pending],
    2. [sendMeBack]: ein JS-Objekt, das an die [Präsentationsschicht] zurückgegeben werden soll,
    3. [value1]: die erste Eingabe auf Seite 1,
    4. [value2]: die zweite Eingabe auf Seite 2,
    5. [pageRequired]: ein boolescher Wert, der dem Server mitteilt, ob der HTML-Stream für Seite 2 gesendet werden soll oder nicht;
  • die Funktion [executePost] wird aufgerufen, um die erforderliche HTTP-Anfrage auszuführen;

7.7.6. Die [Präsentationsschicht]

Die [Präsentationsschicht] wird durch die Datei [local-ui.js] implementiert. Diese Datei verwendet den Code aus der Datei [local12.js] wieder, der so überarbeitet wurde, dass er die vorgelagerte [DAO]-Schicht nutzt. Es haben sich nur zwei Funktionen geändert: [postForm] und [valider].

7.7.6.1. Die Funktion [postForm]

Die Funktion [postForm] lautet wie folgt:


// update Page 1
function postForm() {
    // on met à jour la page 1
    var deferred = $.Deferred();
    loading.show();
    updatePage1(deferred, {
        'sender' : "postForm",
        'info' : 10
    });
    // affichage résultats
    deferred.done(postFormDone);
}
  • Zeile 4: Wir erstellen ein [jQuery.Deferred]-Objekt. Standardmäßig befindet es sich im Status [pending];
  • Zeile 5: Das Lade-Bild wird angezeigt
  • Zeilen 6–9: Die Funktion [updatePage1] wird ausgeführt. Wir übergeben ein Dummy-Objekt [sendMeBack], nur um zu zeigen, wofür es verwendet werden kann;
  • Zeile 11: Der Parameter der Funktion [deferred.done] ist selbst eine Funktion. Dies ist die Funktion, die ausgeführt werden soll, wenn sich der Status des [deferred]-Objekts in [resolved] ändert. Wir haben gerade gesehen, dass die DAO-Funktion [executePost] den Status dieses Objekts nach Erhalt der Serverantwort auf [resolved] gesetzt hat. Das bedeutet, dass die Serverantwort bereits empfangen wurde, wenn die Funktion [postFormDone] ausgeführt wird;

Die Funktion [postFormDone] sieht wie folgt aus:


function postFormDone(result) {
    // end waiting
    loading.hide();
    // data recovery
    var data = result.data
    // for demo
    console.log(JSON3.stringify(result.sendMeBack));
    // status analysis
    switch (result.status) {
    case 1:
        // update both zones
        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:
        // error display
        erreur.html(data);
        break;
    }
}
  • Zeile 1: Der empfangene Parameter [result] ist der Parameter, der an die Methode [deferred.resolve] in der Funktion [executePost] übergeben wurde, zum Beispiel:

            // on rend le résultat
            deferred.resolve({
                "status" : 1,
                "data" : data,
                "sendMeBack" : sendMeBack
});
  • Zeile 5: Wir rufen die Antwort vom Server ab;
  • Zeilen 10–24: Dies ist der Code, der in der vorherigen Version in der [onSuccess]-Funktion der [postForm]-Funktion stand;
  • Zeilen 25–28: Dies ist der Code, der zuvor in der Funktion [onError] der Funktion [postForm] enthalten war;

7.7.6.2. Die Rolle des Parameters [sendMeBack]

Wozu dient der Parameter [sendMeBack]? Sehen wir uns den Code an, der die Funktion [updatePage1] aufruft:


// update Page 1
function postForm() {
    // on met à jour la page 1
    var deferred = $.Deferred();
    loading.show();
    updatePage1(deferred, {
        'sender' : "postForm",
        'info' : 10
    });
    // affichage résultats
    deferred.done(postFormDone);
}

und die Signatur der Funktion [validerDone]:


function postFormDone(result) {
}

Wie kann die Funktion [postForm] Informationen an die Funktion [postFormDone] übergeben? Letztere hat nur einen Parameter, [result]. Dieser wird von der Funktion [executePost] in der [DAO]-Schicht erstellt. Um Informationen an die Funktion [postFormDone] zu übergeben, muss die Funktion [postForm] diese zunächst an die Funktion [updatePage1] übergeben. Dies ist die Aufgabe des Parameters [sendMeBack]. Er wird wie folgt verwendet:


function postFormDone(result) {
    // end waiting
    loading.hide();
    // data recovery
    var data = result.data
    // for demo
    console.log(JSON3.stringify(result.sendMeBack));
    // status analysis
    switch (result.status) {
...
  • Zeile 7: Die Funktion [postFormDone] hat den Parameter [sendMeBack] abgerufen, der ursprünglich von der Funktion [postForm] an die DAO-Funktion [updatePage1] übergeben wurde;

7.7.7. Die Funktion [valider]

Die Funktion [valider] lautet wie folgt:


// validation valeurs saisies
function valider() {
    // on mémorise la page 1
    page1 = content.html();
    // on mémorise les valeurs saisies
    value1 = $("#text1").val().trim();
    value2 = $("#text2").val().trim();
    // pas d'erreur
    erreur.hide();
    // on demande la page 2
    var deferred = $.Deferred();
    loading.show();
    getPage2(deferred, {
        'sender' : 'valider',
        'info' : 20
    }, value1, value2, page2 ? false : true);
    // affichage résultats
    deferred.done(validerDone);
}

und die Funktion [validerDone] (Zeile 18) wie folgt:


function validerDone(result) {
    // end waiting
    loading.hide();
    // data recovery
    var data = result.data
    // for demo
    console.log(JSON3.stringify(result.sendMeBack));
    // status analysis
    switch (result.status) {
    case 1:
        // mistake?
        if (data.erreur) {
            // error display
            erreur.html(data.erreur);
            erreur.show();
        } else {
            // no error
            erreur.hide();
            // page 2
            if (page2) {
                // use the cached page
                content.html(page2);
            } else {
                // memorize page 2
                page2 = data.page2;
                // we display it
                content.html(data.page2);
            }
            // we update it with server info
            $("#value1").text(data.value1);
            $("#value2").text(data.value2);
        }
        break;
    case 2:
        // error display
        erreur.html(data);
        erreur.show();
        break;
    }
}
  • Zeile 5: Wir rufen die Antwort vom Server ab;
  • Zeilen 10–32: Dies ist der Code, der in der vorherigen Version in der [onSuccess]-Funktion der [validate]-Funktion enthalten war;
  • Zeilen 34–38: Dies ist der Code, der zuvor in der Funktion [onError] der Funktion [validate] enthalten war;

7.7.8. Tests

Die Anwendung funktioniert weiterhin wie zuvor, und in der Chrome-Konsole können Sie die [sendMeBack]-Parameter der Funktionen [postForm] und [validate] sehen:

 

7.8. Fazit

Kehren wir zur allgemeinen Architektur einer Spring MVC-Anwendung zurück:

Dank des in die HTML-Seiten eingebetteten und im Browser ausgeführten JavaScripts sowie dank des APU-Modells können wir Code auf den Browser auslagern und die folgende Architektur realisieren:

  • Wir haben eine Client-[2]/Server-[1]-Architektur, bei der Client und Server über JSON kommunizieren;
  • in [1] liefert die Spring-MVC-Webschicht Ansichten, Ansichtsfragmente und Daten im JSON-Format;
  • in [2]: Der JavaScript-Code, der in die beim Start der Anwendung geladene Ansicht eingebettet ist, kann in Schichten strukturiert werden:
    • Die [Präsentations-]Schicht verarbeitet Benutzerinteraktionen,
    • die [DAO]-Schicht übernimmt den Datenzugriff über den Webserver [1],
    • die [Business]-Schicht existiert möglicherweise nicht oder übernimmt bestimmte nicht vertrauliche Funktionen von der [Business]-Schicht des Servers, um diesen zu entlasten;
  • der Client [2] kann bestimmte Ansichten zwischenspeichern, um den Server weiter zu entlasten. Er verwaltet die Sitzung;