Skip to content

7. Аджаксифікація Spring-додатку MVC

7.1. Місце AJAX у веб-додатку

Наразі розглянуті навчальні приклади мали таку архітектуру:

Щоб перейти з подання [Vue1] до подання [Vue2], браузер:

  • надсилає запит до веб-додатку;
  • отримує вигляд [Vue2] і відображає його замість вигляду [Vue1].

Це класична схема:

  • запит від браузера;
  • створення веб-сервером сторінки у відповідь клієнту;
  • відображення цього нового виду браузером.

Останні кілька років існує інший спосіб взаємодії між браузером і веб-сервером: AJAX (Asynchronous Javascript And Xml). Фактично це взаємодія між видом, що відображається браузером, та веб-сервером. Браузер продовжує робити те, що вміє, — відображати сторінку HTML, але тепер ним керує JavaScript, вбудований у відображену сторінку HTML. Схема така:

  • у [1] на сторінці, що відображається в браузері, відбувається подія (натискання кнопки, зміна тексту тощо). Ця подія перехоплюється JavaScript-кодом (jS), вбудованим у сторінку;
  • у [2] код JavaScript надсилає запит HTTP так, як це зробив би браузер. Запит є асинхронним: користувач може продовжувати взаємодіяти зі сторінкою, не затримуючись на очікуванні відповіді на запит HTTP. Запит проходить стандартний процес обробки. Нічим (або майже нічим) він не відрізняється від звичайного запиту;
  • у [3] клієнту надсилається відповідь jS. Замість повного подання HTML надсилається часткове подання HTML, потік XML або jSON (JavaScript Object Notation);
  • у [4] JavaScript отримує цю відповідь і використовує її для оновлення певної області відображеної сторінки HTML.

Для користувача відбувається зміна вигляду, оскільки те, що він бачить, змінилося. Однак повного перезавантаження сторінки не відбувається, а лише часткова модифікація відображуваної сторінки. Це сприяє плавності та інтерактивності сторінки: оскільки не відбувається повного перезавантаження сторінки, можна обробляти події, які раніше не оброблялися. Наприклад, пропонувати користувачеві список варіантів у міру того, як він вводить символи у поле введення. З кожним новим введеним символом на сервер надсилається запит AJAX, який у відповідь повертає нові варіанти. Без Ajax такого роду допомога при введенні раніше була неможливою. Неможливо було перезавантажувати нову сторінку після кожного введеного символу.

7.2. Оновлення сторінки за допомогою потоку HTML

7.2.1. Види

Пропонуємо розглянути такий приклад:

  • у [1] — час завантаження сторінки;
  • у [2] виконуються чотири арифметичні операції над двома дійсними числами A та B;
  • у [3] відповідь сервера записується в певну область сторінки;
  • у [4] — час обчислення. Він відрізняється від часу завантаження сторінки [5]. Останній дорівнює [1], що свідчить про те, що область [6] не була перезавантажена. Крім того, URL та [7] на сторінці не змінилися.

7.2.2. Дія [/ajax-01]

  

Контролер [Ajax.java] визначає наступну дію [/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) {
        // Чи є час дійсним?
        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));
            }
        }
        // підготовка шаблону подання [vue-01]
        ...
}
  • рядок 2: дія [/ajax-01] приймає лише один параметр [tempo]. Це час у мілісекундах, протягом якого сервер повинен чекати, перш ніж надіслати результати арифметичних операцій;
  • рядок 4: параметр [tempo] є необов’язковим;
  • рядки 5–12: перевіряється, чи є значення параметра [tempo] прийнятним;
  • рядки 13–15: якщо це так, значення затримки зберігається в сесії. Це означає, що воно діятиме, доки його не змінять;

Код дії [/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) {
        // термін дії дійсний?
...
        // готується шаблон подання [vue-01]
        modèle.addAttribute("actionModel01", new ActionModel01());
...
        // вигляд
        return "vue-01";
}

Клас [ActionModel01] в основному слугує для інкапсуляції значень, відправлених дією [/ajax-01]. Тут нічого не відправляється. Ми створюємо порожній клас, який додаємо до моделі, оскільки його використовує представлення [vue-01.xml]. Клас [ActionModel01] має такий вигляд:


package istia.st.springmvc.models;

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

public class ActionModel01 {

    // дані, що передаються
    @NotNull
    @DecimalMin(value = "0.0")
    private Double a;

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

    // методи getter та setter
    ...
}
  • рядки 11 і 15: два числа типу «реальне» [a,b], які будуть відправлені через форму;

Повернемося до коду дії:


    @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) {
...
        // підготовка моделі подання [vue-01]
        modèle.addAttribute("actionModel01", new ActionModel01());
        Resultats résultats = new Resultats();
        modèle.addAttribute("resultats", résultats);
...
        // представлення
        return "vue-01";
}
  • рядки 6–7: в шаблон вставляється екземпляр типу [Resultats];

Тип [Resultats], доданий до моделі, має такий вигляд:

  

package istia.st.springmvc.models;

public class Resultats {

    // дані
    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;

    // геттери та сеттери
    ...
}
  • рядки 6–9: результат чотирьох арифметичних операцій над числами [a,b];
  • рядок 10: час початкового завантаження сторінки;
  • рядок 11: час виконання чотирьох арифметичних операцій;
  • рядок 12: можливе повідомлення про помилку;
  • рядок 13: можливий вигляд, який має бути відображений;
  • рядок 14: культура подання, [fr-FR] або [en-US];

Код дії [/ajax-01] продовжується так:


    @RequestMapping(value = "/ajax-01", method = RequestMethod.GET, produces = "text/html; charset=UTF-8")
    public String ajax01(ActionModel01 formulaire, Locale locale, Model modèle, HttpSession session) {
        ...
        // локаль
        setLocale(locale, modèle, résultats);
...
}
  • рядок 5: метод [setLocale] служить для введення в шаблон подання культури, яку слід використовувати, [fr-FR] або [en-US]. Ця культура призначена для вбудованого в подання JavaScript;

Метод [setLocale] виглядає наступним чином:


    private void setLocale(Locale locale, Model modèle, Resultats résultats) {
        // підтримуються лише локалі fr-FR та en-US
        String language = locale.getLanguage();
        String country = null;
        switch (language) {
        case "fr":
            country = "FR";
            break;
        default:
            language = "en";
            country = "US";
            break;
        }
        // культура
        résultats.setCulture(String.format("%s-%s", language, country));
}

У шаблоні буде рядок [${resultats.culture}], що дорівнює 'fr-FR' або 'en-US'.

Повернемося до дії [/ajax-01]:


@RequestMapping(value = "/ajax-01", method = RequestMethod.GET, produces = "text/html; charset=UTF-8")
    public String ajax01(ActionModel01 formulaire, Locale locale, Model modèle, HttpSession session) {
...
        // локаль
        setLocale(locale, modèle, résultats);
        // час
        résultats.setHeureGet(new SimpleDateFormat("hh:mm:ss").format(new Date()));
        // вигляд
        return "vue-01";
    }
  • рядок 7: в шаблон вноситься час з GET;
  • рядок 9: відображається вид [vue-01.xml]:

7.2.3. Вигляд [vue-01.xml]

Вигляд [vue-01.xml] має такий вигляд:


<!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>
  • рядки 7–12: бібліотеки jQuery для валідації та інтернаціоналізації (культури);
  • рядок 15: бібліотека [client-validation], створена в розділі 6.3;
  • рядок 14: бібліотека jSON, яку використовує бібліотека [client-validation]. Вона є необов’язковою, якщо журнали валідації вимкнено;
  • рядок 13: бібліотека [Unobtrusive Ajax] від Microsoft. Ця бібліотека іноді дозволяє уникнути написання коду JavaScript;
  • рядок 16: файл jS для наших власних потреб;
  • рядки 17–22: для управління на стороні клієнта форматами [fr-FR] та [en-US]. Ми вже зустрічали цей код;
  • рядок 27: налаштоване повідомлення. Ми розглядали їх у розділі 5.18;
  • рядки 36–38: форма, до якої ми ще повернемося;
  • рядок 40: область документа, в яку JavaScript розмістить відповідь сервера;

7.2.4. Форма

 

У поданні [vue-01.xml] форма має такий вигляд:


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

яка генерує такий HTML:


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

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

                </td>
            </tr>
        </tbody>
    </table>
    <p>
        <input type="submit" value="Calculer" />
        <img id="loading" style="display: none" src="/images/loading.gif" />
        <a href="javascript:postForm()">Calculer</a>
    </p>
</form>
  • рядок 16: до поля [a] прив’язані валідатори [required], [number] та [min];
  • рядок 19: те саме стосується поля [b];

Різні повідомлення містяться у файлах [messages.properties] цього проєкту:

  

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

Тепер розглянемо атрибути тегу [form]:


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

Ми бачимо класичні атрибути тегу [form]:


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

Відразу можна помітити, що якщо в браузері, який відображає сторінку, вимкнено JavaScript, то форма буде відправлена на URL [/ajax-02.html]. Тепер проаналізуємо інші атрибути:


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

Атрибути [data-ajax-xxx] обробляються бібліотекою jS [unobtrusive-ajax], яка була імпортована поданням [vue-01.xml]:


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

Коли присутні атрибути [data-ajax-xxx], [submit] форми буде виконано за допомогою виклику Ajax із бібліотеки [unobtrusive-ajax]. Значення параметрів такі:

  • [data-ajax="true"]: саме наявність цього атрибута зумовлює те, що [submit] у формі буде виконано за допомогою AJAX;
  • [data-ajax-method="post"]: метод [submit]. Значення URL у запиті POST буде відповідати значенню атрибута [action="/ajax-02.html"];
  • [data-ajax-loading="#loading"]: ідентифікатор поля, яке потрібно відобразити під час очікування відповіді від сервера. Поле, ідентифіковане за допомогою [loading] у поданні [vue-01.xml], є таким:

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

Це анімоване зображення очікування, яке відображатиметься доти, доки не буде отримано відповідь від сервера;

  • [data-ajax-loading-duration="0"]: час очікування в мілісекундах, після якого буде відображено область [data-ajax-loading="#loading"]. У цьому випадку вона відобразиться одразу після початку очікування;
  • [data-ajax-begin="beforeSend"]: функція jS, яку потрібно виконати перед виконанням [submit];
  • [data-ajax-complete="afterComplete"]: функція jS, яку слід виконати після отримання відповіді;
  • [data-ajax-update="#resultats"]: ідентифікатор області, в яку буде розміщено результат, надісланий сервером. Погляд [vue-01.xml] містить таку область:

<div id="resultats" />
  • [data-ajax-mode="replace"]: режим вставки результату в попередню зону. Режим [replace] призведе до того, що результат «перезапише» те, що було раніше в зоні з ідентифікатором [resultats];

Слід зауважити, що виконання JavaScript-коду [submit] відбудеться лише в тому випадку, якщо валідатори визнали перевірені значення правильними.

Бібліотека jS [unobtrusive-ajax] має дві мети:

  • забезпечити правильну адаптацію форми до обох варіантів: з увімкненим або вимкненим JavaScript у браузері;
  • уникнути написання коду JavaScript. Ми побачимо, що в даному випадку цього уникнути не вдалося.

7.2.5. Акція [/ajax-02]

Ми бачили, що відправлені значення надсилалися до дії [/ajax-02]. Вона має такий вигляд:


@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 {
        // темп?
        Integer tempo = (Integer) session.getAttribute("tempo");
        if (tempo != null && tempo > 0) {
            Thread.sleep(tempo);
        }
        // готуємо шаблон наступного виду
        Resultats résultats = new Resultats();
        modèle.addAttribute("resultats", résultats);
        // встановлюємо локаль
        setLocale(locale, modèle, résultats);
        // час
        résultats.setHeurePost(new SimpleDateFormat("hh:mm:ss").format(new Date()));
        ...
}
  • Спочатку спростимо ситуацію: припустимо, що виклик POST дійсно був здійснений JavaScript-кодом у вигляді [vue-01.xml]. До цієї гіпотези ми повернемося трохи пізніше;
  • рядок 2: відправлені значення [a,b] заносяться до шаблону [ActionModel01];
  • рядки 4–7: якщо користувач встановив затримку під час попереднього GET, вона витягується із сесії, і виконується затримка (рядок 6). Мета цього — дати користувачеві змогу побачити ефект атрибута [data-ajax-loading="#loading"] у формі;
  • рядки 9–10: у шаблон додається атрибут [resultats];
  • рядок 12: у модель додається культура [fr-FR] або [en-US];
  • рядок 14: в шаблон вводиться час POST;

Нагадаємо, що у шаблон введено тип [Resultats]:


public class Resultats {

    // дані
    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;

    // гетери та сеттери
...
}

Код дії [/ajax-02] виглядає так:


@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()));
        // генерується помилка раз на два
        int val = new Random().nextInt(2);
        if (val == 0) {
            // повертається повідомлення про помилку
            résultats.setErreur("erreur.aleatoire");
            return "vue-03";
        }
...
    }
  • рядки 6–11: у цьому прикладі показано, як повернути клієнту сторінку помилки jS. У половині випадків повертається такий вигляд [vue-03.xml]:

Зверніть увагу на рядок 9: у шаблон вказується не саме повідомлення, а ключ повідомлення:

[messages_fr.properties]


erreur.aleatoire=erreur aléatoire

[messages_fr.properties]


erreur.aleatoire=randomly generated error

Код подання [vue-03.xml] має такий вигляд:


<!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="помилка" th:text="${resultats.erreur}"></span> -->
        </p>
    </body>
</html>

  • у рядку 12 можна помітити повідомлення, параметри якого задаються ключем повідомлення, що, у свою чергу, обчислюється. Ми ввели це поняття в параграфі 5.18 на сторінці 170.

Код дії [/ajax-02] продовжується так:


@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 {
...
        // отримуються відправлені значення
        double a = formulaire.getA();
        double b = formulaire.getB();
        // будується модель
        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");
        }
        // відображається представлення
        return "vue-02";
    }
  • рядки 5–15: чотири арифметичні операції виконуються над числами [a,b] і інкапсулюються в екземпляр [Resultats] моделі;
  • рядок 17: повертається наступний вигляд [vue-02.xml]:

Вигляд [vue-02.xml] має такий вигляд:


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

Незалежно від того, чи результатом є вигляд [vue-02.xml], чи вигляд [vue-03.xml], цей результат HTML розміщується в полі, позначеному [resultats], у поданні [vue-01.xml], що зумовлено атрибутом [data-ajax-update="#resultats"] форми.

7.2.6. POST введених значень

Тут виникає складність із занесеними значеннями. Ми працюємо з двома культурами [fr-FR] та [en-US], які по-різному записують дійсні числа. Ми вже стикалися з цією проблемою, коли в параграфі 6.3 на сторінці 190 потрібно було занести дійсні числа у двох різних культурах. Тут ми знову скористаємося тими ж інструментами, що й тоді. Однак у нас є додаткова складність: ми не маємо доступу до методу, який перетворює введені значення у формат POST. Саме тому ми додали такі атрибути до тегу форми:

  • [data-ajax-begin="beforeSend"]: функція jS, яку слід виконати перед виконанням [submit];
  • [data-ajax-complete="afterComplete"]: функція jS, яку слід виконати після отримання відповіді;

Ми не маємо доступу до функції jS, яка відправлятиме введені значення, але можемо написати дві функції jS:

  • [beforeSend]: функція jS, що виконується перед POST;
  • [afterComplete]: функція jS, що виконується після отримання відповіді на POST;

Ці дві функції розміщені у файлі [local1.js]:

  

Файл [local1.js] ініціалізує середовище jS подання [vue-01.xml] наступним чином:


// глобальні дані
var loading;
var formulaire;
var résultats;
var a, b;

// під час завантаження документа
$(document).ready(function() {
    // отримуємо посилання на різні компоненти сторінки
    loading = $("#loading");
    formulaire = $("#formulaire");
    resultats = $('#результати');
    a = $("#a");
    b = $("#b");
    // приховуються певні елементи
    loading.hide();
    // аналізуються валідатори форми
    $.validator.unobtrusive.parse(formulaire);
    // обробляються дві локалі [fr_FR, en_US]
    // фактичні значення [a,b] надсилаються сервером у англосаксонському форматі
    // за необхідності їх перетворюють у французький формат
    checkCulture(2);
});
  • рядок 22: функція [checkCulture] представлена трохи далі;

Функція jS [beforeSend] матиме такий вигляд:


function beforeSend(jqXHR, settings) {
    // перед POST
    // числа мають бути надіслані в англосаксонському форматі
    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) {
        // числа [a,b] переводимо в англо-саксонський формат
        var value1 = a.val().replace(",", ".");
        a.val(value1);
        var value2 = b.val().replace(",", ".");
        b.val(value2);
    }
    if (mode == 2) {
...
    }
}
  • рядки 4–6: перевіряється, чи культура подання — це [fr-FR]. У цьому випадку потрібно змінити відправлені значення. Адже якщо користувач ввів [1,6], потрібно відправити значення [1.6], інакше значення [1,6] буде відхилено на стороні сервера. Для цього достатньо замінити кому в переданих значеннях на десяткову крапку (рядки 18–21);
  • але на цьому не можна зупинятися. Адже під час виклику функції [beforeSend] рядок відправлених значень [a=val1&b=valB] вже сформовано. Тому нам потрібно її змінити. Це робиться за допомогою другого параметра функції [settings];
  • рядок 7: [settings.data] (settings — це параметр функції) представляє рядок, що відправляється. Цей рядок відтворюється за допомогою виразу [formulaire.serialize()]. Цей вираз проходить по формі у пошуках значень, які потрібно відправити, і формує рядок POST. Потім він бере нові значення [a,b] із десятковими крапками;

Якщо нічого більше не робити, сервер надішле свою відповідь, яка відобразиться правильно. Тільки тепер значення [a,b] містять десяткову крапку, хоча ми все ще перебуваємо в культурному форматі [fr-FR]. Тож якщо користувач цього не помітить і знову натисне на [Calculer], валідатори повідомлять йому, що значення [a,b] є недійсними. Що є правильним. Саме тут вступає в дію функція [afterComplete], яка виконується після отримання результату:


function beforeSend(jqXHR, settings) {
    // перед POST
...
}

function afterComplete(jqXHR, settings) {
    // після POST
    // числа слід перевести у французький формат, якщо це необхідно
    var culture = Globalize.culture().name;
    if (culture === 'fr-FR') {
        checkCulture(2);
    }
}

function checkCulture(mode) {
    if (mode == 1) {
...
    }
    if (mode == 2) {
        // числа переводимо у французький формат
        var value1 = a.val().replace(".", ",");
        a.val(value1);
        var value2 = b.val().replace(".", ",");
        b.val(value2);
    }
}
  • рядки 9–12: якщо культура подання — [fr-FR], числа [a,b] перетворюються у французький формат.

7.2.7. Тестування

Ось кілька знімків екрана з тестів:

  • у [1] — відповідь сервера;
  • у [2] — відповідь сервера з повідомленням про помилку;
  • у [3] встановлюється затримка в 5 секунд. Це означає, що сервер чекатиме 5 секунд, перш ніж надіслати свою відповідь. У тезі [form] ми використали атрибут [data-ajax-loading='#loading']. Параметр [loading] — це ідентифікатор зони, яка:
    • відображається протягом усього часу очікування;
    • приховується після отримання відповіді від сервера;

Тут [loading] — це ідентифікатор анімованого зображення, яке видно в [4].

7.2.8. Вимкнення JavaScript у контексті [en-US]

Що відбувається, якщо вимкнути JavaScript у браузері?

POST введених значень відбуватиметься відповідно до тегу [form], атрибути якого [data-ajax-attr] не використовуватимуться. Все відбувається так, ніби ми маємо такий тег [form]:


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

Отже, введені значення будуть передані до дії [/ajax-02]. Вони не будуть перевірені на стороні клієнта. Тому в дію вступатимуть серверні валідатори. Вони вже діяли раніше, але щодо значень, які були попередньо перевірені на стороні клієнта, а отже, були правильними. Тепер це вже не так.

Ми змінюємо дію [/ajax-02] наступним чином:


@RequestMapping(value = "/ajax-02", method = RequestMethod.POST, produces = "text/html; charset=UTF-8")
    public String ajax02(@Valid ActionModel01 formulaire, BindingResult result, Locale locale, Model modèle,    HttpSession session, HttpServletRequest request) throws InterruptedException {
        // запит Ajax?
        boolean isAjax = "XMLHttpRequest".equals(request.getHeader("X-Requested-With"));
        ...
    }
  • рядок 4: отже, дію [/ajax-02] тепер можна викликати за допомогою POST Ajax або за допомогою класичного POST. Нам потрібно вміти розрізняти ці два випадки. Це робиться за допомогою заголовків HTTP, що надсилаються клієнтським браузером;

Якщо переглянути мережевий трафік у консолі розробника Chrome (Ctrl+Shift+I) при увімкненому JavaScript, можна побачити, що клієнт надсилає такі заголовки під час виконання POST:

З наведеного вище видно, що:

  • був надісланий заголовок [X-Requested-With] [1];
  • параметр [X-Requested-With] було додано до значень, надісланих у [2];

У випадку класичного POST цього не відбувається. Отже, є два варіанти отримання інформації: з заголовків HTTP або з опублікованих значень. У рядку 4 дії [/ajax-02] обрано перший варіант.

Продовжимо розгляд коду цієї дії:


@RequestMapping(value = "/ajax-02", method = RequestMethod.POST, produces = "text/html; charset=UTF-8")
    public String ajax02(@Valid ActionModel01 formulaire, BindingResult result, Locale locale, Model modèle, HttpSession session, HttpServletRequest request) throws InterruptedException {
        // запит Ajax?
        boolean isAjax = "XMLHttpRequest".equals(request.getHeader("X-Requested-With"));
        // темп?
        Integer tempo = (Integer) session.getAttribute("tempo");
        if (tempo != null && tempo > 0) {
            Thread.sleep(tempo);
        }
        // готуємо шаблон наступного вікна
        Resultats résultats = new Resultats();
        modèle.addAttribute("resultats", résultats);
        // встановлюємо локаль
        setLocale(locale, modèle, résultats);
        // час
        String heure = new SimpleDateFormat("hh:mm:ss").format(new Date());
        résultats.setHeurePost(heure);
        résultats.setHeureGet(heure);
        // запит дійсний?
        if (!isAjax && result.hasErrors()) {
            return "vue-01";
        }
...
  • рядок 2: параметр [@Valid ActionModel01 formulaire] запускає серверні валідатори;
  • рядки 20–22: якщо виклик не є Ajax-викликом і перевірка завершилася невдало, то повертається представлення [vue-01.xml] із повідомленнями про помилки.

Ось приклад:

Продовжимо розгляд дії [/ajax-02]:


@RequestMapping(value = "/ajax-02", method = RequestMethod.POST, produces = "text/html; charset=UTF-8")
    public String ajax02(@Valid ActionModel01 formulaire, BindingResult result, Locale locale, Model modèle,    HttpSession session, HttpServletRequest request) throws InterruptedException {
        // запит Ajax?
        boolean isAjax = "XMLHttpRequest".equals(request.getHeader("X-Requested-With"));
...
        // запит дійсний?
        if (!isAjax && result.hasErrors()) {
            return "vue-01";
        }
        // генерується помилка в кожному другому випадку
        int val = new Random().nextInt(2);
        if (val == 0) {
            // повертається повідомлення про помилку
            résultats.setErreur("erreur.aleatoire");
            if (isAjax) {
                return "vue-03";
            } else {
                résultats.setVue("vue-03");
                return "vue-01";
            }
        }
...
  • рядок 14: генерується випадкова помилка;
  • рядок 16: у разі виклику Ajax повертається представлення [vue-03.xml], яке буде розміщено в області, позначеній [resultats];
  • рядок 18: у разі виклику, що не є Ajax, вигляд, який потрібно відобразити, поміщається в модель типу [Resultats];
  • рядок 19: знову повертається представлення [vue-01.xml];

Вигляд [vue-01.xml] змінюється наступним чином:


<div id="resultats" />
<div th:if="${resultats.vue}=='vue-02'" th:include="vue-02" />
<div th:if="${resultats.vue}=='vue-03'" th:include="vue-03" />
  • рядок 3: погляд [vue-03.xml] буде вставлено під зоною [resultats];

Ось приклад:

Слід зауважити, що відтепер записи [1] та [2] є ідентичними.

Продовжимо розгляд операції [/ajax-02]:


    @RequestMapping(value = "/ajax-02", method = RequestMethod.POST, produces = "text/html; charset=UTF-8")
    public String ajax02(@Valid ActionModel01 formulaire, BindingResult result, Locale locale, Model modèle, HttpSession session, HttpServletRequest request) throws InterruptedException {
        // запит Ajax?
        boolean isAjax = "XMLHttpRequest".equals(request.getHeader("X-Requested-With"));
...
        // отримуємо надіслані значення
        double a = formulaire.getA();
        double b = formulaire.getB();
        // побудовано модель
        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");
        }
        // відображення подання
        if (isAjax) {
            return "vue-02";
        } else {
            résultats.setVue("vue-02");
            return "vue-01";
        }
}
  • рядки 7–17: результати чотирьох арифметичних операцій вводяться в модель;
  • рядки 22–23: формується представлення [vue-01.xml] (рядок 22) шляхом вставки в нього представлення [vue-02.xml] (рядок 22);

Це вставлення здійснюється в [vue-01.xml] наступним чином:


<div id="resultats" />
<div th:if="${resultats.vue}=='vue-02'" th:include="vue-02" />
<div th:if="${resultats.vue}=='vue-03'" th:include="vue-03" />
  • рядок 2: погляд [vue-02.xml] буде вставлено під зоною [resultats];

Ось приклад виконання:

 

7.2.9. Вимкнення JavaScript із культурою [fr-FR]

З культурою [fr-FR] виникає така проблема:

Значення, введені у французькому форматі, були визнані недійсними. Справа в тому, що сервер очікує чисел у англосаксонському форматі. Рішення є досить складним. Ми створимо фільтр, який:

  • перехоплювати запит;
  • замінить коми у надісланих значеннях [a] та [b] на десяткову крапку;
  • а потім передасть новий запит до дії, яка має його обробити;

Насамперед додаємо приховане поле у вікно [vue-01.xml]:


<form ...>
...
</p>
    <!-- приховані поля -->
    <input type="hidden" id="culture" name="culture" th:value="${resultats.culture}"></input>
</form>
  • рядок 5: значення [fr-FR] або [en-US] вноситься в поле атрибута [name=culture]. Оскільки тег [input] є у формі, його значення буде відправлено разом із значеннями [a] та [b]. У результаті отримаємо відправлений рядок такого вигляду:
culture=fr-FR&a=12,7&b=20,78

Важливо зрозуміти цей момент.

Далі ми додаємо фільтр у конфігурацію додатка:

  

Файл [Config] змінюється наступним чином:


@Configuration
@ComponentScan({ "istia.st.springmvc.controllers", "istia.st.springmvc.models" })
@EnableAutoConfiguration
public class Config extends WebMvcConfigurerAdapter {
...
    @Bean
    public Filter cultureFilter() {
        return new CultureFilter();
    }
}
  • рядок 7: те, що bean [cultureFilter] повертає тип [Filter], робить його фільтром. Сам bean може мати будь-яку назву;

Наступним кроком є створення самого фільтра:

  

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 {
        // наступний обробник
        filterChain.doFilter(new CultureRequestWrapper(request), response);
    }
}
  • рядок 12: ми розширюємо клас [OncePerRequestFilter], який є класом Spring, і нам потрібно перевизначити метод [doFilterInternal] цього класу;
  • рядок 15: метод [doFilterInternal] отримує три параметри:
    • [HttpServletRequest request]: запит, який потрібно відфільтрувати. Його не можна змінювати,
    • [HttpServletResponse response]: відповідь, яка буде надіслана серверу. Фільтр може вирішити надіслати її самостійно,
    • [FilterChain filterChain]: ланцюжок фільтрів. Після того як метод [doFilterInternal] завершив свою роботу, він повинен передати запит до наступного фільтра в ланцюжку фільтрів;
  • рядок 18: створюється новий запит на основі отриманого запиту [new CultureRequestWrapper(request)] і передається до наступного фільтра. Оскільки початковий запит [HttpServletRequest request] не можна змінити, створюється новий;

Клас [CultureRequestWrapper] має такий вигляд:

  

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) {
        // передані значення a та 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;
        }
        // інші випадки
        return super.getParameterValues(name);
    }

}
  • рядок 6: клас [CultureRequestWrapper] успадковує клас [HttpServletRequestWrapper] і переопределяє деякі з його методів;
  • рядки 8–10: конструктор, який отримує запит, що підлягає фільтрації, і передає його батьківському класу;
  • тут слід розуміти, що відфільтрований запит зрештою стане вхідним параметром класу, який називається сервлетом. У Spring MVC цей сервлет має тип [DispatcherServlet]. Цей клас має різні методи для отримання параметрів запиту: [getParameter, getParameterMap, getParameterNames, getParameterValues, ...]. Необхідно перевизначити метод, який використовує сервлет. Для цього слід ознайомитися з кодом класу [DispatcherServlet]. Я цього не зробив і перевизначив різні методи. Зрештою було перевизначено метод [getParameterValues];
  • рядок 13: метод [getParameterValues] отримує як параметр ім’я одного з параметрів, повернутих методом [getParameterNames], і повинен повернути масив його значень. Адже відомо, що один параметр може бути присутнім у запиті кілька разів;
  • рядок 18: замінюємо кому на десяткову крапку;

Ось приклад виконання:

  • у [1] значення [a,b] вводяться у французькому форматі;
  • у [2] — результати;
  • у [3] сервер повернув сторінку з числами в англосаксонському форматі.

Цю останню проблему можна вирішити за допомогою Thymeleaf наступним чином у подання [vue-01.xml]


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

У рядках 3 і 6 потрібно внести кілька змін. Розглянемо рядок 3:

  • ми написали [th:field="*{a}"]. Параметр [th:field] визначає атрибути [id, name, value] тегу HTML [input], що генерується. У цьому випадку ми хочемо самостійно керувати атрибутом [value]. Тому ми також самостійно встановлюємо атрибути [id, name];
  • атрибут [th:value] обчислює вираз, що використовує тернарний оператор ?. Ми перевіряємо вираз [${resultats.culture}=='fr-FR' and ${actionModel01.b}!=null]. Якщо він істинний, то атрибуту [value] присвоюється значення a [actionModel01.a], де десяткова крапка замінюється комою. Якщо він є хибним, атрибуту [value] присвоюється значення a [actionModel01.a] без змін;
  • рядок 6: те саме повторюється для поля [b];

Ось приклад виконання:

  • у [1] числа [a,b] зберегли французьку систему позначення. У [2] це не так;

Цю нову проблему вирішують так само, як і попередню. Змінюємо вигляд [vue-03.xml] наступним чином:


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

Ось приклад:

Тепер ми маємо додаток, який коректно обробляє два формати в середовищі як з використанням, так і без використання JavaScript. Для цього довелося значно ускладнити серверний код. Надалі ми завжди будемо припускати, що JavaScript у браузері увімкнено. Це дозволяє реалізувати функції, які неможливі в суто серверному режимі.

7.2.10. Обробка посилання [Calculer]

Розглянемо посилання [Calculer] на головній сторінці [vue-01.xml]:

Код посилання [Calculer] у поданні [vue-01.xml] такий:


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

Функція jS [postForm] визначена у файлі [local1.js] наступним чином:


// глобальні дані
var loading;
var formulaire;
var résultats;
var a, b;

function postForm() {
    // форма дійсна?
    if (!formulaire.validate().form()) {
        // форма недійсна — завершено
        return;
    }
    // обробляються дві локалі [fr_FR, en_US]
    // фактичні значення [a,b] у будь-якому випадку мають бути вказані в англосаксонському форматі
    // це буде зроблено за допомогою фільтра [CultureFilter]

    // Ajax-запит виконується вручну
    $.ajax({
        url : '/ajax-02',
        headers : {
            'X-Requested-With: 'XMLHttpRequest'
        },
        type : 'POST',
        data : formulaire.serialize(),
        dataType : 'html',
        beforeSend : function() {
            loading.show();
        },
        success : function(data) {
            resultats.html(data);
        },
        complete : function() {
            loading.hide();
        },
        error : function(jqXHR) {
            résultats.html(jqXHR.responseText);
        }
    })
}
  • рядки 2–5: нагадаємо, що ці елементи були ініціалізовані функцією [$(document).ready];
  • рядки 9–12: виконуються валідатори jS форми. Якщо одне зі значень є недійсним, вираз [formulaire.validate().form()] повертає значення false. У цьому випадку валідатор [submit] форми скасовується;
  • рядки 18–38: вручну виконується виклик Ajax;
  • рядок 19: цільове значення URL для виклику Ajax;
  • рядки 20–22: масив заголовків HTTP, який додається до тих, що присутні за замовчуванням у запиті HTTP. Тут додається заголовок HTTP, який вказує серверу, що виконується виклик Ajax;
  • рядок 23: використовуваний метод HTTP;
  • рядок 24: дані, що надсилаються. [formulaire.serialize] створює рядок для надсилання [culture=fr-FR&a=12,7&b=20,89] з форми з ідентифікатором [formulaire]. Тут ми знову стикаємося з проблемою, яку розглядали раніше: значення [a,b] мають надсилатися в англосаксонському форматі. Ми знаємо, що ця проблема вже вирішена завдяки створенню фільтра [cultureFilter];
  • рядок 25: очікуваний тип даних у відповіді. Відомо, що сервер поверне потік HTML;
  • рядок 26: метод, який слід виконати при запуску запиту. Тут вказано, що потрібно відобразити компонент з ідентифікатором [loading]. Це анімована картинка очікування;
  • рядок 29: метод, який потрібно виконати у разі успішного виконання запиту Ajax. Параметр [data] — це повна відповідь сервера. Відомо, що це потік HTML;
  • рядок 30: оновлюємо компонент з ідентифікатором [résultats], використовуючи значення HTML з параметра [data].
  • рядок 33: приховується сигнал очікування;
  • рядок 35: функція, що виконується після отримання відповіді від сервера, незалежно від того, чи це успіх, чи помилка;
  • рядки 35–37: у разі помилки (сервер надіслав відповідь HTTP зі статусом, що вказує на помилку на стороні сервера), у полі [resultats] відображається відповідь HTML від сервера;

Ось приклад виконання:

7.3. Оновлення сторінки HTML за допомогою потоку jSON

У попередньому прикладі веб-сервер відповідав на запит HTTP Ajax потоком HTML. У цьому потоці містилися дані з форматуванням HTML. Пропонуємо повернутися до попереднього прикладу, але цього разу з відповідями jSON (JavaScript Object Notation), що містять лише дані. Перевага полягає в тому, що таким чином передається менше байтів. Припускаємо, що в браузері увімкнено JavaScript.

7.3.1. Дія [/ajax-04]

Дія [/ajax-04] ідентична дії [/ajax-01], за винятком того, що замість подання [vue-01.xml] відображається подання [vue-04.xml]:


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

7.3.2. Вигляд [vue-04.xml]

 

Вигляд [vue-04.xml] повторює основну частину вигляду [vue-01.xml] із такими відмінностями:


<!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>
            <!-- приховані поля -->
            <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>
  • рядок 5: код JavaScript цього виду тепер знаходиться у файлі [local4.js];
  • рядок 16: тег [form] більше не має параметрів [data-ajax-attr] з бібліотеки [Unobtrusive Ajax]. Ми не будемо його тут використовувати. Тег [form] також не має атрибутів [method] та [action], які вказують, як і куди відправляти значення, введені у форму. Це пов’язано з тим, що ці дані будуть відправлені за допомогою функції jS (рядок 20);
  • рядки 26–57: поле ідентифікатора [resultats], яке раніше було порожнім, тепер містить код HTML для відображення результатів;
  • рядки 26–34: заголовок результатів, де відображається час обчислення;
  • рядки 35–52: результати чотирьох арифметичних операцій;
  • рядки 53–57: можливе повідомлення про помилку, надіслане сервером;

Код jS, що виконується під час завантаження подання [vue-04.xm], міститься у файлі [local4.js]. Він має такий вигляд:


// загальні дані
    var loading;
    var formulaire;
    var résultats;
    var titre;
    var labelHeureCalcul;
    var heureCalcul;
    var aplusb;
    var amoinsb;
    var afoisb;
    var adivb;
    var msgErreur;

// під час завантаження документа
$(document).ready(function() {
    // отримуємо посилання на різні компоненти сторінки
    loading = $("#loading");
    formulaire = $("#formulaire");
    résultats = $('#результати');
    titre=$("#titre");
    labelHeureCalcul=$("#labelHeureCalcul");
    heureCalcul=$("#heureCalcul");
    aplusb=$("#aplusb");
    amoinsb=$("#amoinsb");
    afoisb=$("#afoisb");
    adivb=$("#adivb");
    msgErreur=$("#msgErreur");
    // приховування деяких елементів
    résultats.hide();
    erreur.hide();
    loading.hide();
});
  • рядки 17–27: отримуються посилання jQuery на всі елементи сторінки;
  • рядок 29: область результатів приховано;
  • рядок 30: так само як і область помилки;
  • рядок 31: так само як і анімоване зображення очікування;
  • рядки 2–12: отримані посилання оголошуються глобальними, щоб інші функції могли ними користуватися;

7.3.3. Функція jS [postForm]

Посилання [Calculer] виглядає так:


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

Функція jS [postForm] визначена у файлі [local.js] наступним чином:


function postForm() {
    // форма правильна?
    if (!formulaire.validate().form()) {
        // форма недійсна — завершено
        return;
    }
    // виконується ручний виклик Ajax
    $.ajax({
        url : '/ajax-05',
        headers : {
            'Accept: 'application/json'
        },
        type : 'POST',
        data : formulaire.serialize(),
        dataType : 'json',
        beforeSend : onBegin,
        success : onSuccess,
        error : onError,
        complete : onComplete
    })
}

// перед викликом Ajax
function onBegin() {
...
}

// після отримання відповіді від сервера
// у разі успіху
function onSuccess(data) {
...
}

// після отримання відповіді від сервера
// у разі невдачі
function onError(jqXHR) {
...
}

// після [onSuccess, onError]
function onComplete() {
...
}
  • рядки 3–6: перед відправкою введених значень їх перевіряють. Якщо вони неправильні, не виконується POST форми;
  • рядок 9: введені значення надсилаються до дії [/ajax-05], яку ми детально розглянемо трохи далі;
  • рядки 10–12: заголовок HTTP, щоб повідомити серверу, що очікується відповідь у форматі jSON;
  • рядок 13: введені значення будуть відправлені;
  • рядок 14: серіалізація введених значень у рядок, готовий до відправлення, [a=1,6&b=2,4&culture=fr-FR];
  • рядок 15: тип відповіді, надісланої сервером. Це буде jSON;
  • рядок 16: функція, яку потрібно виконати перед POST;
  • рядок 17: функція, яку слід виконати після отримання відповіді від сервера, якщо запит завершився успішно. «Успіх» запиту HTTP визначається за статусом відповіді HTTP від сервера. Відповідь [HTTP/1.1 200 OK ] є успішною. Відповідь [HTTP/1.1 500 Internal Server Error] є невдалою. Те, що називається статусом відповіді HTTP, — це код [200] або [500]. Деякі з цих кодів пов’язані з «успіхом», тоді як інші — з «невдачею»;
  • рядок 18: функція, яку слід виконати після отримання відповіді від сервера, якщо статус HTTP цієї відповіді вказує на невдачу;
  • рядок 18: функція, яку слід виконати останньою, після попередніх функцій [onSuccess, onError];

Функція [onBegin] має такий вигляд:


// перед викликом Ajax
function onBegin() {
    console.log("onBegin");
    // відображається анімований малюнок
    loading.show();
    // приховуються певні елементи подання
    entete.hide();
    résultats.hide();
    erreur.hide();
}

Перш ніж розглянути інші функції jS виклику Ajax, нам потрібно дізнатися, яку відповідь надсилає дія [/ajax-05].

7.3.4. Дія [/ajax-05]

Дія [/ajax-05] виглядає так:


    @RequestMapping(value = "/ajax-05", method = RequestMethod.POST)
    @ResponseBody()
    // обробляє POST з вікна [vue-04]
    public JsonResults ajax05(@Valid ActionModel01 formulaire, BindingResult result, Locale locale,    HttpServletRequest request, HttpSession session) throws InterruptedException {
        if(result.hasErrors()){
            // аномальний випадок — нічого не відображається
            return null;
        }
        ...
}
  • рядок 2: атрибут [ResponseBody] вказує, що дія [/ajax-05] сама повертає відповідь клієнту. Оскільки бібліотека jSON входить до залежностей проєкту, Spring Boot автоматично налаштовує дії цього типу так, щоб вони повертали jSON. Отже, саме ланцюжок jSON типу [JsonResults] (рядок 4) буде надіслано клієнту;
  • рядок 2: відправлені значення [a, b, culture] будуть інкапсульовані в тип [ActionModel01], для якого запитується перевірка [@Valid ActionModel01]. Це робиться для дотримання формальності. Ми виходили з припущення, що JavaScript у браузері клієнта увімкнено, а отже, коли значення надходять, вони вже перевірені на стороні клієнта. Проте можна передбачити випадок, коли з’явиться «дикий» POST, який не використовуватиме нашого клієнта jS. У цьому випадку перевірка може завершитися невдачею;
  • рядки 5–7: у разі помилки повертається порожній потік jSON;

Продовжимо розгляд дії [/ajax-05]:


    @RequestMapping(value = "/ajax-05", method = RequestMethod.POST)
    @ResponseBody()
    // обробляє POST з подання [vue-04]
    public JsonResults ajax05(@Valid ActionModel01 formulaire, BindingResult result, Locale locale,
            HttpServletRequest request, HttpSession session) throws InterruptedException {
...
        // контекст додатка Spring
        WebApplicationContext ctx = WebApplicationContextUtils.getWebApplicationContext(request.getServletContext());
        // темп?
        Integer tempo = (Integer) session.getAttribute("tempo");
        if (tempo != null && tempo > 0) {
            Thread.sleep(tempo);
        }
    ...
        // виводимо результат
        return résultats;
}
  • рядок 8: отримуємо контекст [ctx] з додатка Spring. Він потрібен для отримання повідомлень із файлів [messages.properties] за ключем повідомлення та локаллю. Це робиться за допомогою такого синтаксису:

ctx.getMessage(clé_message, tableau_de_paramètres, locale)
    • [clé_message]: ключ повідомлення, яке шукається;
    • [locale] — використовувана локаль. Отже, якщо ця локаль — [en_US], то буде використано файл [messages_en.properties];
    • [tableau_de_paramètres]: отримане повідомлення можна налаштувати так, як у [clé=message {0} {1}]. У цьому повідомленні є два параметри [{0} {1}]. Як другий параметр [ctx.getMessage] потрібно вказати масив із двох значень;
  • рядки 10–13: якщо у сесії є затримка, поточний потік зупиняється на час її тривалості;

Дія [/ajax-05] продовжується наступним чином:


        // готуємо модель наступного виду
        JsonResults résultats = new JsonResults();
        ...
}
  • рядок 2: створення шаблону рядка jSON, що надсилається клієнту;

Шаблон [JsonResults] має такий вигляд:

 

package istia.st.springmvc.models;

public class JsonResults {

    // дані
    private String titre;
    private String labelHeureCalcul;
    private String heureCalcul;
    private String aplusb;
    private String amoinsb;
    private String afoisb;
    private String adivb;
    private String msgErreur;

    // гетери та сеттери
...

}
  • рядки 6–13: кожне з полів класу [JsonResult] відповідає полю з таким самим номером [id] у поданні [vue-04.xml]:

Дія [/ajax-05] продовжується наступним чином:


        // готуємо модель наступного виду
        JsonResults résultats = new JsonResults();
        // заголовок
        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()));
        // генерується помилка раз на два
        int val = new Random().nextInt(2);
        if (val == 0) {
            // повертається повідомлення про помилку
            résultats.setMsgErreur(ctx.getMessage("resultats.erreur",
                    new Object[] { ctx.getMessage("erreur.aleatoire", null, locale) }, locale));
            return résultats;
}
  • рядок 2: створення шаблону рядка jSON, що надсилається клієнту;
  • рядки 4–6: створюються повідомлення заголовка результатів;
  • рядки 8–14: в середньому раз на два генерується повідомлення про помилку. У цьому випадку процес не продовжується, і рядок jSON повертається клієнту (рядок 13);
  • рядок 11: тут наведено приклад налаштованого повідомлення:

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

Дія [/ajax-05] продовжується наступним чином:


        // отримуємо відправлені значення
        double a = formulaire.getA();
        double b = formulaire.getB();
        // побудовується модель
        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");
        }
        // повертається результат
return résultats;
  • рядки 2–3: отримуємо значення [a] та [b];
  • рядки 5–12: формуються чотири результати;
  • рядок 14: рядок jSON [JsonResults] надсилається клієнту;

Давайте подивимося, що вийде з клієнтом [Advanced Rest Client]:

  • у [1-2] надсилається запит POST до дії [/ajax-05];
  • у [3] надсилаються неправильні значення;
  • у [4] сервер повернув порожній потік;
  • у [1] відправляються правильні значення;
  • у [2] — об’єкт jSON, повернутий сервером, з повідомленням про помилку;
  • у [1] надсилаються правильні значення;
  • у [2] — об’єкт jSON, повернутий сервером, тут містяться чотири результати;
  • у [1] надсилаються правильні значення;
  • у [2] вдалося спровокувати виняток на стороні сервера. Бачимо, що сервер знову надсилає об’єкт jSON. У цьому повідомленні бачимо, що статус HTTP відповіді дорівнює [500], що вказує на те, що сталася помилка на стороні сервера;

7.3.5. Функція jS [postForm] — 2

Тепер, коли ми знаємо об’єкт jSON, повернутий сервером, ми можемо використовувати його в JavaScript. Метод [onSuccess], що виконується, коли сервер надсилає відповідь зі статусом HTTP [200], виглядає наступним чином:


// після отримання відповіді від сервера
// у разі успіху
function onSuccess(data) {
    console.log("onSuccess");
    // заповнюється область результатів
    titre.text(data.titre);
    labelHeureCalcul.text(data.labelHeureCalcul);
    heureCalcul.text(data.heureCalcul);
    entete.show();
    // результати без помилок
    if (!data.msgErreur) {
        aplusb.text(data.aplusb);
        amoinsb.text(data.amoinsb);
        afoisb.text(data.afoisb);
        adivb.text(data.adivb);
        résultats.show();
        return;
    }
    // результати з помилкою
    msgErreur.text(data.msgErreur);
    erreur.show();
}
  • рядок 3: параметр [data] — це об’єкт jSON, повернутий сервером:
 

Метод [onError], що виконується, коли статус відповіді HTTP дорівнює [500], має такий вигляд:


// після отримання відповіді від сервера
// у разі невдачі
function onError(jqXHR) {
    console.log("onError");
    // системна помилка
    msgErreur.text(jqXHR.responseText);
    erreur.show();
}
  • рядок 3: об’єкт JQuery [jqXHR] має серед своїх властивостей такі:
    • responseText: текст відповіді сервера,
    • status: код помилки, повернутий сервером,
    • statusText: текст, пов’язаний із цим кодом помилки;
  • рядок 6: об’єкт [jqXHR.responseText] є наступним об’єктом jSON:
 

7.3.6. Тестування

Розглянемо кілька знімків екрана під час роботи веб-додатку:

 
 
 

7.4. Веб-додаток з однією сторінкою

7.4.1. Вступ

Технологія Ajax дозволяє створювати односторінкові додатки:

  • перша сторінка завантажується за допомогою звичайного запиту браузера;
  • наступні сторінки отримуються за допомогою викликів Ajax. Таким чином, у підсумку браузер ніколи не змінює URL і ніколи не завантажує нову сторінку. Цей тип додатків називають односторінковим додатком (APU) або, англійською, Single Page Application (SPA).

Ось простий приклад такого додатка. Новий додаток матиме два види:

  • у [1] дія [/ajax-06] дозволяє нам отримати першу сторінку, сторінку 1;
  • у [2] посилання дозволяє перейти на сторінку 2 за допомогою виклику Ajax;
  • у [3], URL не змінився. Відображається сторінка 2;
  • у [4] посилання дозволяє повернутися на сторінку 1 за допомогою виклику Ajax;
  • у [5] URL не змінився. Відображається сторінка 1.

7.4.2. Дія [/ajax-06]

Код дії [/ajax-06] такий:


    @RequestMapping(value = "/ajax-06", method = RequestMethod.GET, produces = "text/html; charset=UTF-8")
    public String ajax06() {
        return "vue-06";
}
  • рядки 1–4: дія [/ajax-06] просто відображає вигляд [vue-06.xml];

7.4.3. Вигляд [vue-06.xml]

Вигляд [vue-06.xml] має такий вигляд:


<!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>
  • рядок 8: подання використовує скрипт [local6.js];
  • рядок 12: вигляд [vue-07.xml] включено в область з ідентифікатором [content] вигляду [vue-06.xml];

7.4.4. Вигляд [vue-07.xml]

Вигляд [vue-07.xml] має такий вигляд:


<!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. Функція jS [gotoPage]

Посилання [Page 2] з подання [vue-07.xml] використовує функцію jS [gotoPage], визначену в наступному файлі [local6.js]:


// загальні дані
var content;

function gotoPage(num) {
    // Ajax-запит виконується вручну
    $.ajax({
        url : '/ajax-07',
        type : 'POST',
        data : 'num=' + num,
        dataType : 'html',
        beforeSend : function() {
        },
        success : function(data) {
            content.html(data)
        },
        complete : function() {
        },
        error : function(jqXHR) {
            // системна помилка
            content.html(jqXHR.responseText);
        }
    })
}

// під час завантаження документа
$(document).ready(function() {
    // отримано посилання на різні компоненти сторінки
    content = $("#content");
});
  • рядок 28: під час завантаження сторінки запам'ятовується область з ідентифікатором [content] і перетворюється на глобальну змінну (рядок 2);
  • рядок 4: функція [gotoPage] отримує як параметр номер сторінки (1 або 2), яку потрібно відобразити в поточному вікні;
  • рядок 7: URL є цільовою функцією для POST;
  • рядок 8: функція URL з рядка 7 викликається за допомогою функції POST;
  • рядок 9: відправлений рядок. Відправляється параметр із назвою [num]. Його значенням є номер сторінки (рядок 4), яку потрібно відобразити в поточному вікні;
  • рядок 10: сервер поверне HTML — код сторінки, яку потрібно відобразити;
  • рядки 13–15: у разі успіху (статус HTTP дорівнює 200) значення HTML, надіслане сервером, записується в поле ідентифікатора [content];
  • рядки 18–20: у разі невдачі (статус HTTP дорівнює 500) значення HTML, надіслане сервером, записується в поле ідентифікатора [content];

7.4.6. Дія [/ajax-07]

Код дії [/ajax-07] такий:


@RequestMapping(value = "/ajax-07", method = RequestMethod.POST, produces = "text/html; charset=UTF-8")
    public String ajax07(int num) {
        // num: номер сторінки
        switch (num) {
        case 1:
            return "vue-07";
        case 2:
            return "vue-08";
        default:
            return "vue-07";
        }
    }
  • рядок 2: отримуємо параметр, надісланий методом POST, який називається [num]. Нагадуємо, що параметр у рядку 2 повинен мати назву параметра, надісланого методом POST, у даному випадку [num]. [num] — це номер сторінки або подання;
  • рядки 5–6: якщо [num==1], то повертається перегляд [vue-07.xml];
  • рядки 7–8: якщо [num==2], повертається перегляд [vue-08.xml];
  • рядки 9–10: в інших випадках (що зазвичай неможливо) повертається представлення [vue-07.xml];

7.4.7. Вигляд [vue-08.xml]

Вигляд [vue-08.xml] утворює сторінку № 2 додатка:


<!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. Включення декількох потоків HTML у відповідь jSON

7.5.1. Вступ

Розглянемо такий додаток:

Сторінка [1] має чотири зони:

  • [Zone 1, Zone 3] — це області, які з’являються/зникають при натисканні кнопки [Rafraîchir]. Підраховується кількість появи кожної з цих двох зон [2]. Зона [Zone 1] використовує французьку мову, тоді як зона [Zone 3] використовує англійську мову;
  • зона [Zone 2] присутня постійно;
  • зона [Saisies] присутня постійно;

Посилання [Valider] відкриває наступну сторінку [3]:

  • посилання [Retour à la page 1] повертає сторінку № 1 до того стану, в якому вона перебувала раніше ([4]);

Додаток складається з однієї сторінки. Браузер запитує першу сторінку у сервера. Наступні сторінки отримуються з сервера за допомогою викликів Ajax.

7.5.2. Дія [/ajax-09]

  

Дія [/ajax-09] виглядає наступним чином:


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

Вона просто відображає вигляд [vue-09.xml].

7.5.3. Види XML

  

Вигляд [vue-09.xml] є головною сторінкою додатка:


<!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>
  • рядок 9: файл JS, що використовується в додатку;
  • рядок 15: вміст головної сторінки;
  • рядок 16: анімоване зображення очікування:
  • рядок 17: область для відображення можливої помилки;

Вигляд [vue-09-page1.xml] — це сторінка 1 додатка:


<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
    <body>
        <h2>Page 1</h2>
        <!-- зона 1 -->
        <fieldset id="zone1" style="background-color:pink">
            <legend>Zone 1</legend>
            <span id="zone1-content" th:text="xx">xx</span>
        </fieldset>
        <!-- зона 2 -->
        <fieldset id="zone2" style="background-color:lightgreen">
            <legend>Zone 2</legend>
            <span>Ce texte reste toujours présent</span>
        </fieldset>
        <!-- зона 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>
  • рядки 6–9: область [Zone 1]. Її вміст розміщується в компоненті [id="zone1-content"];
  • рядки 11–14: область [Zone 2], яка не змінюється;
  • рядки 16–19: поле [Zone 3]. Його вміст розміщується в компоненті [id="zone3-content"];
  • рядок 22: функція JS, яка відправляє форму;
  • рядок 25: включення поля для введення даних;

Слід зауважити, що сторінка 1 не має тегу [form]. Усе оброблятиметься за допомогою JavaScript.

Вигляд [vue-09-saisies.xml] такий:


<!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>
  • рядки 5–8: введення символьного рядка;
  • рядки 13–16: введення цілого числа;
  • рядок 14: функція JS, яка відправляє введені значення;

Знову ж таки, слід зауважити, що поле введення не має тегу [form].

Загалом сторінка № 1 має дві функції:

  • [Rafraîchir]: яка оновлює поля 1 і 3. Ця дія обробляється сервером, який випадковим чином повертає:
    • зону 1 із лічильником відвідувань і нічого для зони 3,
    • зону 3 з її лічильником відвідувань і нічого для зони 1,
    • обидві зони з їх лічильниками відвідувань;
  • [Valider]: яка відображає сторінку 2 із введеними значеннями або повідомлення про помилку, якщо введені дані є недійсними;

Спочатку розглянемо кнопку [Rafraîchir].

7.5.4. Код JS для управління кнопкою [Rafraîchir]

  

Код файлу [local9.js] такий:


// глобальні змінні
var content;
var loading;
var erreur;

// під час завантаження документа
$(document).ready(function() {
    // отримуємо посилання на різні компоненти сторінки
    loading = $("#loading");
    loading.hide();
    erreur = $("#erreur");
    erreur.hide();
    content = $("#content");
});
  • рядки 9–13: під час завантаження головної сторінки зберігаються посилання на три компоненти, ідентифіковані за кодом [loading, erreur, content];
  • рядки 2–4: посилання на ці три компоненти зберігаються у глобальних змінних. Вони залишаються незмінними, оскільки три відповідні області завжди присутні на відображуваній сторінці, незалежно від моменту часу. Оскільки вони залишаються незмінними, їх можна обчислити у функції [$(document).ready] та використовувати в інших функціях файлу JS;

Функція [postForm] обробляє натискання кнопки [Rafraîchir]:


function postForm() {
    console.log("postForm");
    // виконується ручний виклик Ajax
    $.ajax({
        url : '/ajax-10',
        headers : {
            'Accept' : 'application/json'
        },
        type : 'POST',
        dataType : 'json',
        beforeSend : onBegin,
        success : onSuccess,
        error : onError,
        complete : onComplete
    })
}
  • рядки 4–15: виклик Ajax на сервер;
  • рядок 5: саме дія [ajax-10] оброблятиме POST;
  • рядки 6–8: відповіддю буде jSON. Клієнт JS вказує, що приймає документи jSON;
  • рядок 9: дія [ajax-10] викликається з операцією POST;
  • рядок 10: буде отримано jSON;
  • рядок 11: функція, що виконується перед викликом Ajax;
  • рядок 12: функція, що виконується після отримання відповіді від сервера, якщо виклик завершився успішно — [200 OK];
  • рядок 13: функція, що виконується після отримання відповіді від сервера, якщо відповідь невдала: [500 Internal server error, ...];
  • рядок 14: функція, що виконується після отримання відповіді;

Функція [onBegin] має такий вигляд:


// перед викликом Ajax
function onBegin() {
    console.log("onBegin");
    // зображення очікування
    loading.show();
}

Вона просто запускає анімований малюнок, що відображає очікування відповіді від сервера.

7.5.5. Дія [/ajax-10]

  

Дія [/ajax-10] полягає в наступному:


// сесія
    @Autowired
    private SessionModel1 session;
    // движок Thymeleaf / Spring
    @Autowired
    private SpringTemplateEngine engine;

    @RequestMapping(value = "/ajax-10", method = RequestMethod.POST)
    @ResponseBody()
    public JsonResult10 ajax10(HttpServletRequest request, HttpServletResponse response) {
    ...
    }
  • рядок 3: вводиться сесія. Вона має такий тип [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;
    // два лічильники
    private int cpt1 = 0;
    private int cpt3 = 0;
    // три зони
    private String zone1 = "xx";
    private String zone3 = "zz";
    private String saisies;
    private boolean zone1Active = true;
    private boolean zone3Active = true;

    // методи getter та setter
    ...
}

Сесія [SessionModel1] зберігає такі елементи:

  • рядок 15: кількість випадків [cpt1], коли відображається поле [Zone 1];
  • рядок 16: кількість випадків [cpt3], коли відображається область [Zone 3];
  • рядки 18–20: потоки HTML із зон [Zone 1], [Zone 3] та [Saisies]. Це необхідно у послідовності [Page 1] --> [Page 2] --> [Page 1]. Під час переходу від [Page 2] до [Page 1] необхідно відновити [Page 1] і, відповідно, його три поля;
  • рядки 21–22: два логічні значення, які вказують, чи відображаються (видимі) поля [Zone 1] та [Zone 3];

Інший елемент, вставлений у контролер [AjaxController], є таким:


    // движок Thymeleaf / Spring
    @Autowired
private SpringTemplateEngine engine;

Бін типу [SpringTemplateEngine] визначено у файлі конфігурації [Config]:

  

Він визначений наступним чином:


    @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;
}
  • рядки 2–10: нам відомий бін типу [SpringResourceTemplateResolver], який дозволяє нам визначати певні характеристики представлень;
  • рядки 13–17: bean типу [SpringTemplateEngine] дозволяє нам визначити «двигун» переглядів — клас, відповідальний за генерацію відповідей [Thymeleaf] для клієнтів. [Thymeleaf] має «двигун» за замовчуванням та інший, коли використовується в середовищі [Spring]. Саме останній ми використовуємо тут;

Підпис дії [/ajax-10] має такий вигляд:


@RequestMapping(value = "/ajax-10", method = RequestMethod.POST)
    @ResponseBody()
    public JsonResult10 ajax10(HttpServletRequest request, HttpServletResponse response) {
    ...
}
  • рядок 1: дія [/ajax-10] приймає лише POST;
  • рядок 2: дія [/ajax-10] сама повертає відповідь клієнту. Вона буде автоматично перетворена на jSON;
  • рядок 3: відповідь має наступний формат [JsonResult10]:
  

package istia.st.springmvc.models;

public class JsonResult10 {

    // дані
    private String content;
    private String zone1;
    private String zone3;
    private String erreur;
    private String saisies;
    private boolean zone1Active;
    private boolean zone3Active;

    public JsonResult10() {
    }

    // методи getter та setter
...
}
  • рядок 6: вміст HTML поля, позначеного [content];
  • рядок 7: вміст HTML зони [Zone 1];
  • рядок 8: вміст HTML зони [Zone 3];
  • рядок 9: вміст HTML з поля [Erreur];
  • рядок 10: вміст HTML з поля [Saisies];
  • рядок 11: логічне значення, що вказує, чи слід відображати поле [Zone 1];
  • рядок 12: логічне значення, що вказує, чи слід відображати зону [Zone 3];

Код дії [/ajax-10] такий:


@RequestMapping(value = "/ajax-10", method = RequestMethod.POST)
    @ResponseBody()
    public JsonResult10 ajax10(HttpServletRequest request, HttpServletResponse response) {
        // контекст Thymeleaf
        WebContext thymeleafContext = new WebContext(request, response, request.getServletContext());
        // відповідь
        JsonResult10 result = new JsonResult10();
        // сесія
        session.setZone1(null);
        session.setZone3(null);
        session.setZone1Active(false);
        session.setZone3Active(false);
        // повертається випадкова відповідь
        int cas = new Random().nextInt(3);
        switch (cas) {
        case 0:
            // зона 1 активна
            setZone1(thymeleafContext, result);
            return result;
        case 1:
            // зона 3 активна
            setZone3(thymeleafContext, result);
            return result;
        case 2:
            // зони 1 і 3 активні
            setZone1(thymeleafContext, result);
            setZone3(thymeleafContext, result);
            return result;
        }
        return null;
    }
  • рядок 5: отримуємо контекст [Thymeleaf]. Пізніше ми побачимо, для чого він нам знадобиться;
  • рядок 7: поки що створюємо порожню відповідь;
  • рядки 9–12: ми передаємо в [null] обидва поля, що містяться в сесії, і вказуємо, що їх не слід відображати. Ці два поля незабаром будуть згенеровані, але можливо, що згенерується лише одне з них;
  • рядки 14–29: обидва поля згенеровано;
  • рядки 17–19: згенеровано лише поле [Zone 1];
  • рядки 21–23: генерується лише поле [Zone 3];
  • рядки 25–28: генеруються обидві зони [Zone 1] та [Zone 3];

Потік HTML з зони [Zone 1] генерується за допомогою такого методу:


    private void setZone1(WebContext thymeleafContext, JsonResult10 result) {
        // зона 1 активна
        // потік 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.setCpt1(cpt1);
        session.setZone1(zone1);
        session.setZone1Active(true);
}
  • рядок 1: параметри такі:
    • контекст [Thymeleaf] типу [WebContext],
    • відповідь клієнту, що формується, типу [JsonResult10];
  • рядок 3: збільшується лічильник [cpt1] сеансу, який підраховує кількість випадків відображення поля [Zone 1];
  • рядок 4: контекст [Thymeleaf] типу [WebContext] працює приблизно так само, як модель [Model] з Spring MVC. Щоб додати елемент до шаблону, використовується [WebContext.setVariable]. Отже, тут лічильник [cpt1] вставляється в шаблон [Thymeleaf]. Це дозволить обчислити вираз Thymeleaf [${cpt1}]
  • рядок 5: контекст [Thymeleaf] має локаль. Це дозволяє йому обчислювати вирази типу [#{clé_msg}]. Тут контекст Thymeleaf пов’язується з французькою локаллю;
  • рядок 6: це найцікавіша інструкція. Механізм Thymeleaf обробить представлення [vue-09-zone1.xml] із використанням моделі та локалі, які щойно були обчислені, і замість того, щоб надіслати отриманий потік HTML клієнту, він поверне його у вигляді символьного рядка;
  • рядки 7–9: потік HTML з області [Zone 1], що щойно було обчислено, зберігається в сесії та у результаті, який буде надіслано клієнту. Крім того, вказується, що область [Zone 1] має бути відображена;
  • рядки 11–13: у сесії зберігається інформація щодо зони [Zone 1], щоб мати змогу її відтворити;

У рядку 7 обробляється наступний вигляд [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>
  • рядок 3: вираз [#{message.zone}] буде обчислено за допомогою локалі;
  • рядок 4: вираз [${cpt1}] буде обчислено за допомогою шаблону Thymeleaf;

Повідомлення з ключем [message.zone] визначено у файлах повідомлень [messages_fr.properties] та [messages_en.properties]:

  

[messages_fr.properties]


message.zone=Nombre d'accès : 

[messages_en.properties]


message.zone=Number of hits: 

Потік HTML із зони [Zone 3] генерується аналогічним методом:


    private void setZone3(WebContext thymeleafContext, JsonResult10 result) {
        // зона 3 активна
        // потік 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.setCpt3(cpt3);
        session.setZone3(zone3);
        session.setZone3Active(true);
}
  • рядок 6: локаль зони [Zone 3] — англійська;

7.5.6. Обробка відповіді дії [/ajax-10]

Повернемося до коду JS з [local9.js], який оброблятиме відповідь сервера:


// після отримання відповіді від сервера
// у разі успіху
function onSuccess(data) {
    console.log("onSuccess");
    // вміст
    if (data.content) {
        content.html(data.content);
    }
    // зона 1
    if (data.zone1Active) {
        $("#zone1").show();
        if (data.zone1) {
            $("#zone1-content").html(data.zone1);
        }
    } else {
        $("#zone1").hide();
    }
    // зона 3 активна?
    if (data.zone3Active) {
        $("#zone3").show();
        if (data.zone3) {
            $("#zone3-content").html(data.zone3);
        }
    } else {
        $("#zone3").hide();
    }
    // введені дані?
    if (data.saisies) {
        $("#saisies").html(data.saisies);
    }
    // помилка?
    if (data.erreur) {
        erreur.text(data.erreur);
        erreur.show();
    } else {
        erreur.hide();
    }
}

Нагадаємо структуру Java отриманої відповіді у рядку 3 у змінній [data]:


public class JsonResult10 {

    // дані
    private String content;
    private String zone1;
    private String zone3;
    private String erreur;
    private String saisies;
    private boolean zone1Active;
    private boolean zone3Active;

}
  • рядки 6–8: якщо [data.content!=null], то поле [id=content] ініціалізується цим значенням. Це поле повністю відповідає [Page 1] або [Page 2]. У даному прикладі маємо [data.content==null], отже, область [id=content] не змінюватиметься і продовжуватиме відображати [Page 1];
  • рядки 10–17: відображається [Zone 1], якщо [data.zone1Active==true]. Якщо, крім того, [data.zone1!=null], то вміст [Zone 1] змінюється, інакше він залишається без змін;
  • рядки 19–26: те саме для [Zone 3];
  • рядки 28–30: якщо є [data.saisies!=null], то область [Saisies] перегенерується. У даній демонстрації ми маємо [data.saisies==null], отже, зона [Saisies] залишається без змін;
  • рядки 32–37: аналогічний висновок щодо зони [Erreur] із такими нюансами:
    • рядок 33: [data.erreur] буде повідомленням про помилку у текстовому форматі;
    • рядок 36: якщо [data.erreur==null], то поле [Erreur] приховано. Адже воно могло бути відображене під час попереднього запиту;

У разі помилки на стороні сервера (HTTP, статус типу 500 Internal server error) виконується така функція:


// після отримання відповіді від сервера
// у разі невдачі
function onError(jqXHR) {
    console.log("onError");
    // системна помилка
    erreur.text(jqXHR.responseText);
    erreur.show();
}

Щоб побачити таку помилку, змінимо функцію [postForm] наступним чином:


function postForm() {
    console.log("postForm");
    // отримано посилання на поточній сторінці
    ...
    // виконується ручний виклик Ajax
    $.ajax({
        url : '/ajax-10x',
        ...
    })
}
  • рядок 7: вводимо неіснуючу функцію URL;

Ось результати, які з’являються після натискання кнопки [Rafraîchir]:

Цікаво, що помилка також була надіслана у вигляді рядка jSON.

Після отримання відповіді від сервера виконується такий метод:


// після [onSuccess, onError]
function onComplete() {
    console.log("onComplete");
    // зображення очікування
    loading.hide();
}

Ми просто приховуємо анімований зображення очікування.

7.5.7. Відображення сторінки [Page 2]

Код HTML посилання [Valider] має такий вигляд:


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

Функція JS [valider] така:


// перевірка введених значень
function valider() {
    // значення відправлено
    var post = JSON3.stringify({
        "value1" : $("#text1").val().trim(),
        "value2" : $("#text2").val().trim()
    });
    // виконується ручний виклик Ajax
    $.ajax({
        url : '/ajax-11A',
        headers : {
            'Accept: 'application/json',
            'Content-Type' : 'application/json'
        },
        type : 'POST',
        data : post,
        dataType : 'json',
        beforeSend : onBegin,
        success : onSuccess,
        error : onError,
        complete : onComplete
    })
}
  • рядки 4–7: у нас є два значення v1 і v2, які потрібно відправити: це значення полів введення, ідентифікованих за кодами [#text1] та [#text2]. Ми зробимо щось нове. Ми відправимо ці два значення у вигляді рядка jSON {"value1":v1,"value2":v2};
  • рядок 10: відправлені значення будуть надіслані до дії [ajax-11A];
  • рядок 12: оскільки ми знаємо, що отримаємо відповідь jSON, вказуємо, що можемо отримати її у форматі jSON;
  • рядок 13: серверу вказується, що йому буде надіслано значення, відправлене методом POST, у вигляді рядка jSON;
  • рядки 15–16: формуємо POST із значення, яке потрібно відправити;
  • рядок 17: ми отримаємо jSON;

7.5.8. Дія [ajax-11A]

Дія [ajax-11A], яка обробляє відправлений рядок jSON, виглядає наступним чином:


@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) {
        ...
    }
  • рядок 1: за допомогою ["application/json"] вказується, що дія очікує документ у форматі jSON. Цей документ є значенням, відправленим клієнтом;
  • рядок 3: відправлене значення буде отримано в наступному об’єкті [PostAjax11A post]:
  

package istia.st.springmvc.models;

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

import org.hibernate.validator.constraints.Range;

public class PostAjax11A {

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

    // гетери та сеттери
    ...
}
  • структура об’єкта [PostAjax11A] повинна відповідати структурі відправленого об’єкта {"value1":v1,"value2":v2}. Тому необхідні поля [value1] (рядок 13) та [value2] (рядок 16);
  • ми встановили обмеження цілісності для обох полів;

Повернемося до коду дії [ajax-11A]:


@RequestMapping(value = "/ajax-11A", method = RequestMethod.POST, consumes = "application/json")
    @ResponseBody
    public JsonResult10 ajax11A(@RequestBody @Valid PostAjax11A post, BindingResult bindingResult, Locale locale,    HttpServletRequest request, HttpServletResponse response) {
        // контекст Thymeleaf
        WebContext thymeleafContext = new WebContext(request, response, request.getServletContext());
        // відповідь
        JsonResult10 result = new JsonResult10();
        // пост дійсний?
        if (bindingResult.hasErrors()) {
            // повертаємо сторінку 1 з помилкою
            result.setZone1Active(session.isZone1Active());
            result.setZone3Active(session.isZone3Active());
            result.setErreur(getErreursForModel(bindingResult));
            return result;
        }
        ...
}
  • рядок 3: анотація [@RequestBody] позначає документ, надісланий клієнтом. Це значення, відправлене клієнтом у jSON. Отже, воно буде використано для побудови об’єкта [PostAjax11A];
  • рядок 3: анотація [@Valid] вимагає перевірки відправленого значення;
  • рядок 9: якщо перевірка не пройде:
    • рядок 13: повертається повідомлення про помилку,
    • рядки 11–12: поля 1 і 3 повертаються до попереднього стану (відображаються чи ні);

Розрахунок повідомлення про помилку здійснюється наступним чином:


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

Цю функцію ми вже зустрічали раніше.

Дія [ajax-11A] продовжується наступним чином:


@RequestMapping(value = "/ajax-11A", method = RequestMethod.POST, consumes = "application/json")
    @ResponseBody
    public JsonResult10 ajax11A(@RequestBody @Valid PostAjax11A post, BindingResult bindingResult, Locale locale,    HttpServletRequest request, HttpServletResponse response) {
        // контекст Thymeleaf
        WebContext thymeleafContext = new WebContext(request, response, request.getServletContext());
        // відповідь
        JsonResult10 result = new JsonResult10();
        // пост дійсний?
        if (bindingResult.hasErrors()) {
    ...
        }
        // зберігається поле введення
        thymeleafContext.setVariable("value1", post.getValue1());
        thymeleafContext.setVariable("value2", post.getValue2());
        session.setSaisies(engine.process("vue-09-saisies", thymeleafContext));
        // відправляємо сторінку 2
        result.setContent(engine.process("vue-09-page2", thymeleafContext));
        return result;
}
  • рядки 13–14: відправлені значення поміщаються в контекст Thymeleaf;
  • рядок 15: за допомогою цього контексту обчислюється представлення [vue-09-saisies] і поміщається в сесію, щоб можна було відтворити його пізніше;
  • рядок 17: сторінка 2 додається до результату, який буде надіслано клієнту;

Вигляд [vue-09-page2.xml] має такий вигляд:

  

<!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>
  • у рядках 9 та 13 відображаються значення [value1, value2], які дія [/ajax-11A] розмістила в контексті Thymeleaf;

7.5.9. Обробка відповіді дії [/ajax-11A]

На стороні клієнта відповідь дії [/ajax-10] обробляється функцією [onSuccess]:


function onSuccess(data) {
    console.log("onSuccess");
    // вміст
    if (data.content) {
        content.html(data.content);
    }
    // поле 1
    if (data.zone1Active) {
        $("#zone1").show();
        if (data.zone1) {
            $("#zone1-content").html(data.zone1);
        }
    } else {
        $("#zone1").hide();
    }
    // поле 3 активне?
    if (data.zone3Active) {
        $("#zone3").show();
        if (data.zone3) {
            $("#zone3-content").html(data.zone3);
        }
    } else {
        $("#zone3").hide();
    }
    // введені дані?
    if (data.saisies) {
        $("#saisies").html(data.saisies);
    }
    // помилка?
    if (data.erreur) {
        erreur.text(data.erreur);
        erreur.show();
    } else {
        erreur.hide();
    }
}

Ми вже коментували цей код. Розглянемо два випадки: відповідь з помилкою та без неї:

З помилкою

У цьому випадку дія [/ajax-11A] надіслала відповідь jSON у вигляді {"zone1":null, "zone3":null,"saisies":null,"erreur":erreur,"zone1Active":zone1Active,"zone3Active":zone3Active,"content":null}. Якщо простежити за наведеним вище кодом, можна побачити, що:

  • поле [content] не змінюється. Воно містило сторінку № 1;
  • зона [Erreur] відображається;
  • зони [Zone 1], [Zone 3], [Saisies] залишаються в тому стані, в якому вони були;

Без помилок

У цьому випадку дія [/ajax-11A] надіслала відповідь jSON у вигляді {"zone1":null, "zone3":null,"saisies":null,"erreur":null,"zone1Active":false,"zone3Active":false,"content":content}. Якщо простежити код вище, можна побачити, що:

  • зона [content] відображається. Вона містить сторінку № 2;

Ось три приклади виконання:

Випадок з помилкою перевірки:

Випадок з помилкою POST:

Цей тип помилки відрізняється. Оскільки Spring не зміг перетворити рядок jSON у тип [PostAjax11A], він повернув відповідь HTTP із [status=400]. Дія [ajax-11A] не була виконана;

Випадок без помилки:

7.5.10. Повернутися до сторінки № 1

Посилання [Retour vers la page 1] на сторінці № 2 має такий вигляд:


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

Метод JS [retourPage1] є таким:


// повернутися на сторінку 1
function retourPage1() {
    // виконуємо Ajax-запит вручну
    $.ajax({
        url : '/ajax-11B',
        headers : {
            'Accept: 'application/json',
        },
        type : 'POST',
        dataType : 'json',
        beforeSend : onBegin,
        success : onSuccess,
        error : onError,
        complete : onComplete
    })
}

Вона виконує операцію POST без переданого значення до операції [/ajax-11B].

7.5.11. Операція [/ajax-11B]

Операція [/ajax-11B] виглядає наступним чином:


    @RequestMapping(value = "/ajax-11B", method = RequestMethod.POST)
    @ResponseBody
    public JsonResult10 ajax11B(HttpServletRequest request, HttpServletResponse response) {
        // контекст Thymeleaf
        WebContext thymeleafContext = new WebContext(request, response, request.getServletContext());
        // відповідь
        JsonResult10 result = new JsonResult10();
        // сторінку 1 повертаємо до початкового стану
        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;
}

Дія повинна перегенерувати сторінку № 1 з трьома її полями [Zone1, Zone3, Erreur]:

  • рядок 9: сторінка № 1 додається до результату;
  • рядок 10: область введення даних додається до результату;
  • рядок 11: область [Zone 1] додається до результату;
  • рядок 12: область [Zone 3] додається до результату;
  • рядки 13–14: у результат додається стан полів [Zone 1] та [Zone 3];

7.5.12. Обробка відповіді дії [/ajax-11B]

Відповідь дії [/ajax-11B] обробляється функцією [onSuccess]:


function onSuccess(data) {
    console.log("onSuccess");
    // вміст
    if (data.content) {
        content.html(data.content);
    }
    // зона 1
    if (data.zone1Active) {
        $("#zone1").show();
        if (data.zone1) {
            $("#zone1-content").html(data.zone1);
        }
    } else {
        $("#zone1").hide();
    }
    // зона 3 активна?
    if (data.zone3Active) {
        $("#zone3").show();
        if (data.zone3) {
            $("#zone3-content").html(data.zone3);
        }
    } else {
        $("#zone3").hide();
    }
    // введені дані?
    if (data.saisies) {
        $("#saisies").html(data.saisies);
    }
    // помилка?
    if (data.erreur) {
        erreur.text(data.erreur);
        erreur.show();
    } else {
        erreur.hide();
    }
}

Дія [/ajax-11B] надіслала відповідь jSON у форматі {"zone1":zone1, "zone3":zone3,"saisies":saisies,"erreur":null,"zone1Active":zone1Active,"zone3Active":zone3Active,"content":content}. Якщо проаналізувати наведений вище код, можна побачити, що:

  • поле [content] змінено. Раніше воно містило сторінку № 2. Тепер воно міститиме сторінку № 1;
  • зона [Erreur] прихована;
  • зони [Zone 1], [Zone 3], [Saisies] відображаються у тому стані, в якому вони були;

7.6. Управління сесією на стороні клієнта

7.6.1. Вступ

У попередньому розділі ми керували сесією, структура якої була такою:


public class SessionModel1 implements Serializable {

    // два лічильники
    private int cpt1 = 0;
    private int cpt3 = 0;
    // три зони
    private String zone1 = "xx";
    private String zone3 = "zz";
    private String saisies;
    private boolean zone1Active = true;
    private boolean zone3Active = true;
...
}

Коли користувачів дуже багато, обсяг пам’яті, зайнятий сесіями всіх цих користувачів, може стати проблемою. Тому слід мінімізувати цей обсяг. Модель APU (односторінковий додаток) дозволяє керувати сесією на стороні клієнта та мати веб-сервер без сесій. Дійсно, односторінкова сторінка спочатку завантажується браузером. Разом із нею завантажується супутній файл JavaScript. Оскільки перезавантаження сторінки не відбувається, цей файл JS залишатиметься в браузері постійно в тому вигляді, в якому він був завантажений спочатку. Тож можна використовувати його глобальні змінні для зберігання інформації про різні дії користувача. Саме це ми й розглянемо зараз. Ми не тільки будемо керувати сесією на стороні клієнта, але й переробимо додаток JS, щоб якомога менше навантажувати сервер.

7.6.2. Дія [/ajax-12]

  

Дія [/ajax-12] виглядає так:


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

Вигляд [vue-12.xml] такий:

  

<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
    <head>
        <meta name="viewport" content="width=device-width" />
        <title>Ajax-12</title>
        <link rel="stylesheet" href="/css/ajax01.css" />
        <script type="text/javascript" src="/js/jquery/jquery-2.1.1.min.js"></script>
        <script type="text/javascript" src="/js/json3.js"></script>        
        <script type="text/javascript" src="/js/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>
  • цей вигляд ідентичний вигляду [vue-09], за винятком скрипта JS, який використовується в рядку 9;

Відображується такий вигляд:

 

7.6.3. Код JS для управління кнопкою [Rafraîchir]

  

Код файлу [local12.js] такий:


// глобальні змінні
var content;
var loading;
var erreur;
var page1;
var page2;
var value1;
var value2;
var session = {
        "cpt1" : 0,
        "cpt3" : 0
    };

// під час завантаження документа
$(document).ready(function() {
    // отримуємо посилання на різні компоненти сторінки
    loading = $("#loading");
    loading.hide();
    erreur = $("#erreur");
    erreur.hide();
    content = $("#content");
});
  • рядки 17–21: під час завантаження головної сторінки посилання на три компоненти, позначені кодом [loading, erreur, content], зберігаються у глобальних змінних рядків 2–4;
  • рядки 5–6: для збереження двох сторінок;
  • рядки 7–8: для збереження двох значень, надісланих за посиланням [Valider];
  • рядок 9: сесія. Вона зберігає на стороні клієнта значення лічильників [cpt1, cpt3];

Функція [postForm] обробляє натискання кнопки [Rafraîchir]:


function postForm() {
    console.log("postForm");
    // зберігаємо сесію
    var post = JSON3.stringify(session);
    // виконується ручний виклик Ajax
    $.ajax({
        url : '/ajax-13',
        headers : {
            'Accept: 'application/json',
            'Content-Type' : 'application/json'
        },
        type : 'POST',
        data : post,
        dataType : 'json',
        beforeSend : onBegin,
        success : function(data) {
            ...
        },
        error : onError,
        complete : onComplete
    })
}

Відмінності від попередньої версії такі:

  • URL у рядку 7 відрізняється;
  • рядок 4: відправляється значення, тоді як раніше його не відправляли. Це значення — рядок jSON сеансу. Принцип такий:
    • клієнт надсилає сесію на сервер,
    • сервер змінює її та відправляє назад,
    • клієнт запам’ятовує нову сесію;
  • рядок 10: надсилається документ у форматі jSON (поштове відправлення);
  • рядок 13: є щось, що потрібно відправити;
  • рядки 15–20: функції [beforeSend, error, complete] відповідають попередній версії. Змінюється лише функція [success] (рядки 16–18);

7.6.4. Дія [/ajax-13]

  

Дія [/ajax-13] виглядає так:


    @RequestMapping(value = "/ajax-13", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
    @ResponseBody()
    public JsonResult13 ajax13(@RequestBody SessionModel2 session2, HttpServletRequest request,    HttpServletResponse response) {
    ...
}
  • рядок 3: параметр [@RequestBody SessionModel2 session2] отримує сесію, надіслану клієнтом. Вона має такий тип [SessionModel2]:
  

package istia.st.springmvc.models;

import java.io.Serializable;

public class SessionModel2 implements Serializable {

    private static final long serialVersionUID = 1L;
    // два лічильники
    private int cpt1 = 0;
    private int cpt3 = 0;

    // гетери та сеттери
    ...
}

Сесія [SessionModel2] зберігає такі елементи:

  • рядок 9: кількість випадків [cpt1], коли відображається область [Zone 1];
  • рядок 10: кількість випадків [cpt3], коли відображається область [Zone 3];

Продовжимо розгляд коду дії [/ajax-13]:


    @RequestMapping(value = "/ajax-13", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
    @ResponseBody()
    public JsonResult13 ajax13(@RequestBody SessionModel2 session2, HttpServletRequest request,    HttpServletResponse response) {
    ...
}
  • у рядку 3 тип відповіді [JsonResult13] є таким:
  

package istia.st.springmvc.models;

public class JsonResult13 {

    // дані
    private String page2;
    private String zone1;
    private String zone3;
    private String erreur;
    private String value1;
    private Integer value2;

    // сесія
    private SessionModel2 session;

    // методи getter та setter
    ...
}
  • рядок 14: сесія. Сервер повертає її клієнту для збереження;
  • рядок 6: вміст HTML сторінки № 2;
  • рядок 7: вміст HTML зони [Zone 1];
  • рядок 8: вміст HTML зони [Zone 3];
  • рядок 9: можливе повідомлення про помилку;
  • рядки 10–11: дві інформації, обчислені сервером і відображені на сторінці № 2;

Продовжимо розгляд коду дії [/ajax-13]:


@RequestMapping(value = "/ajax-13", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
    @ResponseBody()
    public JsonResult13 ajax13(@RequestBody SessionModel2 session2, HttpServletRequest request,
            HttpServletResponse response) {
        // контекст Thymeleaf
        WebContext thymeleafContext = new WebContext(request, response, request.getServletContext());
        // відповідь
        JsonResult13 result = new JsonResult13();
        result.setSession(session2);
        // повертаємо випадкову відповідь
        int cas = new Random().nextInt(3);
        switch (cas) {
        case 0:
            // зона 1 активна
            setZone1B(thymeleafContext, result);
            return result;
        case 1:
            // зона 3 активна
            setZone3B(thymeleafContext, result);
            return result;
        case 2:
            // зони 1 і 3 активні
            setZone1B(thymeleafContext, result);
            setZone3B(thymeleafContext, result);
            return result;
        }
        return null;
    }
  • рядок 9: сесія додається до результату дії;

Метод [setZone1B], який активує зону [Zone 1], виглядає наступним чином:


    private void setZone1B(WebContext thymeleafContext, JsonResult13 result) {
        // відновлюється сесія
        SessionModel2 session = result.getSession();
        // зона 1 активна
        // потік 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.setCpt1(cpt1);
}
  • рядок 3: отримуємо сесію. Вона буде змінена в рядку 12 за допомогою нового лічильника [cpt1]. Нагадуємо, що ця сесія буде повернута клієнту;
  • рядок 10: нове поле [Zone 1];

Метод [setZone3B], який активує поле [Zone 3], є аналогічним:


private void setZone3B(WebContext thymeleafContext, JsonResult13 result) {
        // відновлюється сесія
        SessionModel2 session = result.getSession();
        // зона 3 активна
        // потік 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.setCpt3(cpt3);
    }

7.6.5. Обробка відповіді дії [/ajax-13]

На стороні клієнта відповідь jSON на дію [/ajax-13] обробляється за допомогою такої функції [onSuccess]:


function postForm() {
    console.log("postForm");
    // відправляємо сесію
    var post = JSON3.stringify(session);
    // виконується ручний виклик Ajax
    $.ajax({
    ...
        success : function(data) {
            // зберігаємо сесію
            session = data.session;
            // оновлюємо обидва поля
            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();
            }
        },
...
    })
}
  • рядки 12–17: якщо сервер вніс якісь дані в поле [zone1] відповіді, то потрібно перегенерувати поле [Zone 1] і відобразити його, інакше воно має бути приховане;
  • рядки 18–23: те саме стосується області [Zone 3];

7.6.6. Відображення сторінки [Page 2]

Код HTML посилання [Valider] має такий вигляд:


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

Функція JS [valider] має такий вигляд:


// перевірка введених значень
function valider() {
    // зберігаємо сторінку 1
    page1 = content.html();
    // зберігаються введені значення
    value1 = $("#text1").val().trim();
    value2 = $("#text2").val().trim();
    // значення відправлено
    var post = JSON3.stringify({
        "value1" : value1,
        "value2" : value2,
        "pageRequired" : page2 ? false : true
    });
    // виконується ручний виклик Ajax
    $.ajax({
        url : '/ajax-14',
        headers : {
            'Accept: 'application/json',
            'Content-Type' : 'application/json'
        },
        type : 'POST',
        data : post,
        dataType : 'json',
        beforeSend : onBegin,
        success : function(data) {
        ...
        },
        error : onError,
        complete : onComplete
    })
}
  • ми виконаємо POST, що зазвичай переведе нас на сторінку № 2;
  • рядок 4: ми запам'ятовуємо сторінку № 1, щоб мати змогу повернутися до неї пізніше;
  • рядки 6–7: попередня операція не зберігає введені значення, а лише код сторінки HTML. Тому тепер ми зберігаємо обидва значення, введені у форму;
  • рядки 9–13: обидва введені значення поміщаються в рядок jSON. Саме він буде відправлений;
  • рядок 12: параметр, що вказує серверу, чи потрібна нам сторінка № 2. Ми будемо діяти таким чином. Спочатку ми запросимо сторінку № 2, а потім збережемо її у змінній JS [page2]. Після цього ми більше не будемо її запитувати. Ми будемо використовувати сторінку з кешу. У рядку 2 значення [pageRequired] дорівнює [true], якщо змінна [page2] порожня, і [false] в іншому випадку;
  • зауважимо, що сесія не надсилається. Дійсно, вона зберігає лічильники, які дія [/ajax-14] у рядку 20 не змінює;

7.6.7. Дія [/ajax-14]

Дія [/ajax-14] виглядає так:


@RequestMapping(value = "/ajax-14", method = RequestMethod.POST)
    @ResponseBody
    public JsonResult13 ajax14(@RequestBody @Valid PostAjax14 post, BindingResult bindingResult, Locale locale,    HttpServletRequest request, HttpServletResponse response) {
        ...
    }
  • рядок 3: відповідь завжди має тип [JsonResult13];
  • рядок 3: відправлене значення інкапсульовано в наступний тип [PostAjax14]:

package istia.st.springmvc.models;

public class PostAjax14 extends PostAjax11A {

    // сторінка 2
    private boolean pageRequired;

    // гетери та сеттери
    ...
}
  • рядок 3: клас [PostAjax14] розширює клас [PostAjax11A] попередньої версії. Отже, він має структуру [value1, value2, pageRequired];

Дія [/ajax-14] продовжується наступним чином:


    @RequestMapping(value = "/ajax-14", method = RequestMethod.POST)
    @ResponseBody
public JsonResult13 ajax14(@RequestBody @Valid PostAjax14 post, BindingResult bindingResult, Locale locale,    HttpServletRequest request, HttpServletResponse response) {
        // контекст Thymeleaf
        WebContext thymeleafContext = new WebContext(request, response, request.getServletContext());
        // відповідь
        JsonResult13 result = new JsonResult13();
        // пост дійсний?
        if (bindingResult.hasErrors()) {
            // повертається помилка
            result.setErreur(getErreursForModel(bindingResult));
            return result;
        }
        // відправляється сторінка 2
        result.setValue1(post.getValue1());
        result.setValue2(post.getValue2());
        // сторінка знайдена?
        if (post.isPageRequired()) {
            result.setPage2(engine.process("vue-12-page2", thymeleafContext));
        }
        return result;
}
  • рядки 9–13: якщо надіслані значення [value1, value2] є недійсними, повертається повідомлення про помилку;
  • рядки 15–16: зазвичай сервер повинен виконати обчислення з надісланими значеннями. У цьому випадку він просто повертає їх, щоб показати, що їх було отримано;
  • рядки 18–20: сторінка № 2 повертається лише в тому випадку, якщо її запросив клієнт. У рядку 19 представлений новий вигляд [vue-12-page2]:
 

<!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>
  • код XML більше не містить значень, обчислених Thymeleaf, як це було раніше;
  • було визначено місця, куди слід розмістити значення [value1, value2], повернуті сервером. Рядок 9, [id='value1'] вказує місце, куди слід розмістити [value1]. У рядку 13 те саме стосується [value2];

7.6.8. Обробка відповіді дії [/ajax-14]

Відповідь дії [/ajax-14] обробляється за допомогою наступної функції [success]:


// перевірка введених значень
function valider() {
    ...
    // виконується ручний виклик Ajax
    $.ajax({
        ...
        success : function(data) {
            // помилка?
            if (data.erreur) {
                // відображення помилки
                erreur.html(data.erreur);
                erreur.show();
            } else {
                // помилки немає
                erreur.hide();
                // сторінка 2
                if (page2) {
                    // використовується сторінка з кешу
                    content.html(page2);
                } else {
                    // сторінка 2 зберігається в кеші
                    page2 = data.page2;
                    // сторінка відображається
                    content.html(data.page2);
                }
                // оновлюється даними з сервера
                $("#value1").text(data.value1);
                $("#value2").text(data.value2);
            }
        },
...
    })
}
  • рядки 9–13: якщо сервер повернув помилку, її відображають;
  • рядки 14–29: випадок, коли помилки не було. У цьому разі потрібно відобразити сторінку № 2;
  • рядок 17: перевіряємо, чи сторінка № 2 вже збережена у змінній [page2];
  • рядок 19: у цьому випадку використовується змінна [page2] для відображення сторінки № 2;
  • рядок 24: інакше використовується поле [data.page2], надане сервером;
  • рядок 22: слід запам’ятати сторінку № 2, щоб надалі не запитувати її знову;
  • рядки 27–28: на сторінці № 2 відображаються обидва записи [value1, value2], надіслані сервером;

7.6.9. Повернення на сторінку № 1

Посилання [Retour vers la page 1] на сторінці № 2 має такий вигляд:


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

Метод JS [retourPage1] виглядає так:


// повернення на сторінку 1
function retourPage1() {
    // сторінка 1 перегенерується
    content.html(page1);
    // перегенерувати введені дані
    $("#text1").val(value1);
    $("#text2").val(value2);
}
  • це дія JS без взаємодії з сервером, оскільки сторінка № 1 була збережена локально у змінній [page1];
  • рядок 4: відбувається повторне формування сторінки № 1;
  • рядки 6–7: було збережено лише частину HTML сторінки № 1. Введені дані не збереглися. Тому їх потрібно відтворити;

7.6.10. Висновок

Використовуючи можливості моделі APU, нам вдалося спростити роботу веб-сервера, який тепер є безстатусним (відсутність сеансу) і менш навантаженим:

  • ми прибрали взаємодію з сервером у функції JS ([retourPage1]);
  • сервер генерує сторінку № 2 лише один раз;

7.7. Структурування коду JavaScript за рівнями

7.7.1. Вступ

Код JavaScript попереднього додатка починає ставати складним. Настав час структурувати його за рівнями. Додаток залишиться таким самим, як і раніше. Ми не будемо втручатися в роботу сервера, за винятком визначення нової стартової сторінки. Ми переробимо код JS.

Нова архітектура буде такою:

7.7.2. Початкова сторінка

Дія, яка запускає додаток, — це така дія [/ajax-16]:


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

Вона відображає такий вигляд: [vue-16.xml]:


<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
    <head>
        <meta name="viewport" content="width=device-width" />
        <title>Ajax-12</title>
        <link rel="stylesheet" href="/css/ajax01.css" />
        <script type="text/javascript" src="/js/jquery/jquery-2.1.1.min.js"></script>
        <script type="text/javascript" src="/js/json3.js"></script>
        <script type="text/javascript" src="/js/local16-dao.js"></script>
        <script type="text/javascript" src="/js/local16-ui.js"></script>
    </head>
    <body>
        <h3>Ajax - 16 - Navigation dans une Application à Page Unique</h3>
        <h3>Structuration du code JS</h3>
        <hr />
        <div id="content" th:include="vue-09-page1" />
        <img id="loading" src="/images/loading.gif" />
        <div id="erreur" style="background-color:lightgrey"></div>
    </body>
</html>
  • рядки 9–10: код JS було розміщено у двох різних файлах:
    • [local-ui] реалізує рівень [présentation],
    • [local-dao] реалізує рівень [DAO];
  

7.7.3. Реалізація шару [DAO]

7.7.4. Інтерфейс

Рівень [DAO] у [local-dao.js] надаватиме наступний інтерфейс рівню [présentation]:


function updatePage1(deferred, sendMeBack)
для оновлення сторінки 1 за допомогою кнопки [Rafraîchir]

function getPage2(deferred, sendMeBack, value1, value2, pageRequired)
для відображення сторінки 2 за допомогою кнопки [Valider]

У JavaScript немає поняття інтерфейсу. Я використав цей термін лише для того, щоб вказати, що рівень [présentation] взаємодіє з рівнем [DAO] виключно за допомогою двох вищезазначених функцій.

7.7.5. Реалізація інтерфейсу

Структура реалізації така:


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

// оновлення сторінки 1
function updatePage1(deferred, sendMeBack) {
...
}

// сторінка 2
function getPage2(deferred, sendMeBack, value1, value2, pageRequired) {
...
}

Мета шару [DAO] — приховати від шару [présentation] деталі запитів HTTP, що надсилаються до веб-сервера. Сесія є частиною цих деталей. Тому відтепер вона управляється рівнем [DAO].

7.7.5.1. Функція [updatePage1]

Функція [updatePage1] — це функція, яку викликає рівень [présentation] для оновлення сторінки 1. Її код такий:


// Оновлення сторінки 1
function updatePage1(deferred, sendMeBack) {
    // запит HTTP
    executePost(deferred, sendMeBack, '/ajax-13', session);
}
  • рядок 1: функція [updatePage1] отримує два параметри:
    1. об’єкт типу [jQuery.Deferred]. Цей тип об’єкта зберігає стан, який може мати три значення ['pending', 'resolved', 'rejected']. Коли він надходить у функцію [updatePage1], він перебуває у стані [pending];
    2. об’єкт JS, який слід повернути у шар [présentation];

Усі запити HTTP виконуються наступною функцією [executePost]:


// запит HTTP
function executePost(deferred, sendMeBack, url, post) {
    // виконуємо Ajax-запит вручну
    $.ajax({
        headers : {
            'Accept: 'application/json',
            'Content-Type: 'application/json'
        },
        url : url,
        type : 'POST',
        data : JSON3.stringify(post),
        dataType : 'json',
        success : function(data) {
            // зберігаємо сесію
            if (data.session) {
                session = data.session;
            }
            // виводимо результат
            deferred.resolve({
                "status" : 1,
                "data" : data,
                "sendMeBack" : sendMeBack
            });
        },
        error : function(jqXHR) {
            // повертаємо помилку
            deferred.resolve({
                "status" : 2,
                "data" : jqXHR.responseText,
                "sendMeBack" : sendMeBack
            });
        }
    });
}
  • рядок 1: функція [executePost] виконує виклик Ajax типу POST. Вона очікує чотири параметри:
    1. об’єкт типу [jQuery.Deferred] у стані [pending];
    2. об'єкт JS, який потрібно повернути у шарі [présentation];
    3. URL з POST;
    4. значення, яке потрібно відправити як об’єкт JS;
  • рядки 5–8: функція відправлення з jSON (рядок 7) та отримання з jSON (рядок 6);
  • рядок 11: значення, яке потрібно відправити, перетворюється на jSON;
  • рядки 13–24: функція, що виконується у разі успішного виклику Ajax;
  • рядки 19–23: якщо сервер повернув сесію, її зберігають;
  • рядки 13–18: переносять об’єкт [deferred] у стан [resolved], передаючи при цьому результат із такими полями:
    • [status]: 1 — успіх, 2 — невдача,
    • [data]: відповідь сервера jSON,
    • [sendMeBack]: другий параметр функції — об’єкт, який викликаючий хоче отримати;
  • рядки 17–31: функція, що виконується у разі невдалого виклику Ajax. Ми робимо те саме, що й раніше, з двома відмінностями:
    • [status] приймає значення 2, щоб повідомити про помилку;
    • [data] — це знову відповідь jSON від сервера, але отримана іншим способом;

7.7.5.2. Функція [getPage2]

Функція [getPage2] виглядає так:


// сторінка 2
function getPage2(deferred, sendMeBack, value1, value2, pageRequired) {
    // запит HTTP
    executePost(deferred, sendMeBack, '/ajax-14', {
        "value1" : value1,
        "value2" : value2,
        "pageRequired" : pageRequired,
    });
}
  • функція приймає такі параметри:
    1. [deferred]: об’єкт типу [jQuery.Deferred] у стані [pending],
    2. [sendMeBack]: об’єкт JS, який потрібно повернути у шарі [présentation],
    3. [value1]: перший запис на сторінці 1,
    4. [value2]: другий запис на сторінці 2,
    5. [pageRequired]: логічне значення, що вказує серверу, чи слід надсилати потік HTML зі сторінки № 2;
  • функція [executePost] викликається для виконання необхідного запиту HTTP;

7.7.6. Рівень [présentation]

Рівень [présentation] реалізовано за допомогою файлу [local-ui.js]. Останній містить код із файлу [local12.js], перероблений для використання попереднього рівня [DAO]. Змінилися лише дві функції: [postForm] та [valider].

7.7.6.1. Функція [postForm]

Функція [postForm] має такий вигляд:


// оновлення сторінки 1
function postForm() {
    // оновлюється сторінка 1
    var deferred = $.Deferred();
    loading.show();
    updatePage1(deferred, {
        'відправник: «postForm»,
        'інформація: 10
    });
    // відображення результатів
    deferred.done(postFormDone);
}
  • рядок 4: створюється об’єкт [jQuery.Deferred]. За замовчуванням він перебуває у стані [pending];
  • рядок 5: відображається зображення очікування
  • рядки 6–9: виконується функція [updatePage1]. Передається фіктивний об’єкт [sendMeBack], лише для того, щоб показати, для чого це може знадобитися;
  • рядок 11: параметр функції [deferred.done] сам по собі є функцією. Це функція, яку слід виконати, коли стан об’єкта [deferred] переходить у стан [resolved]. Ми щойно побачили, що функція DAO [executePost] передавала стан цього об’єкта до [resolved] після отримання відповіді від сервера. Це означає, що на момент виконання функції [postFormDone] відповідь від сервера вже була отримана;

Функція [postFormDone] виглядає наступним чином:


function postFormDone(result) {
    // завершення очікування
    loading.hide();
    // отримання даних
    var data = result.data
    // для демонстрації
    console.log(JSON3.stringify(result.sendMeBack));
    // аналізується стан
    switch (result.status) {
    case 1:
        // оновлення обох полів
        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:
        // відображення помилки
        erreur.html(data);
        break;
    }
}
  • рядок 1: отриманий параметр [result] є параметром, переданим до методу [deferred.resolve] у функції [executePost], наприклад:

            // повертаємо результат
            deferred.resolve({
                "status" : 1,
                "data" : data,
                "sendMeBack" : sendMeBack
});
  • рядок 5: отримується відповідь від сервера;
  • рядки 10–24: тут розміщений код, який у попередній версії знаходився у функції [onSuccess] функції [postForm];
  • рядки 25–28: тут міститься код, який у попередній версії був у функції [onError] функції [postForm];

7.7.6.2. Роль параметра [sendMeBack]

Для чого призначений параметр [sendMeBack]? Розглянемо код виклику функції [updatePage1]:


// оновлення сторінки 1
function postForm() {
    // оновлення сторінки 1
    var deferred = $.Deferred();
    loading.show();
    updatePage1(deferred, {
        'відправник: «postForm»,
        'інформація: 10
    });
    // відображення результатів
    deferred.done(postFormDone);
}

та сигнатуру функції [validerDone]:


function postFormDone(result) {
}

Як функція [postForm] може передати інформацію функції [postFormDone]? Остання має лише один параметр [result]. Цей параметр створюється функцією [executePost] на рівні [DAO]. Щоб передати дані до функції [postFormDone], функція [postForm] спочатку повинна передати їх до функції [updatePage1]. Саме для цього призначений параметр [sendMeBack]. Він використовується наступним чином:


function postFormDone(result) {
    // завершення очікування
    loading.hide();
    // отримання даних
    var data = result.data
    // для демонстрації
    console.log(JSON3.stringify(result.sendMeBack));
    // аналізується стан
    switch (result.status) {
...
  • у рядку 7 функція [postFormDone] відновила параметр [sendMeBack], який спочатку був переданий функції DAO [updatePage1] функцією [postForm];

7.7.7. Функція [valider]

Функція [valider] має такий вигляд:


// перевірка введених значень
function valider() {
    // зберігання сторінки 1
    page1 = content.html();
    // зберігання введених значень
    value1 = $("#text1").val().trim();
    value2 = $("#text2").val().trim();
    // помилок немає
    erreur.hide();
    // запит на сторінку 2
    var deferred = $.Deferred();
    loading.show();
    getPage2(deferred, {
        'відправник: «підтвердити»,
        'info' : 20
    }, value1, value2, page2 ? false : true);
    // відображення результатів
    deferred.done(validerDone);
}

а функція [validerDone] (рядок 18) має такий вигляд:


function validerDone(result) {
    // завершення очікування
    loading.hide();
    // отримання даних
    var data = result.data
    // для демонстрації
    console.log(JSON3.stringify(result.sendMeBack));
    // аналізується стан
    switch (result.status) {
    case 1:
        // помилка?
        if (data.erreur) {
            // відображення помилки
            erreur.html(data.erreur);
            erreur.show();
        } else {
            // помилки немає
            erreur.hide();
            // сторінка 2
            if (page2) {
                // використовується сторінка з кешу
                content.html(page2);
            } else {
                // сторінка 2 зберігається
                page2 = data.page2;
                // сторінка відображається
                content.html(data.page2);
            }
            // оновлюється даними з сервера
            $("#value1").text(data.value1);
            $("#value2").text(data.value2);
        }
        break;
    case 2:
        // відображення помилки
        erreur.html(data);
        erreur.show();
        break;
    }
}
  • рядок 5: отримуємо відповідь від сервера;
  • рядки 10–32: тут міститься код, який у попередній версії був у функції [onSuccess] функції [valider];
  • рядки 34–38: тут міститься код, який у попередній версії був у функції [onError] функції [valider];

7.7.8. Тестування

Додаток продовжує працювати як і раніше, а в консолі Chrome можна побачити параметри [sendMeBack] функцій [postForm] та [valider]:

 

7.8. Conclusion

Повернемося до загальної схеми додатка Spring MVC:

Завдяки вбудованому в сторінки JavaScript-коду HTML, що виконується в браузері, та завдяки моделі APU можна перенести частину коду в браузер і отримати таку архітектуру:

  • ми маємо архітектуру «клієнт» [2] / «сервер» [1], де клієнт і сервер взаємодіють за допомогою jSON;
  • у [1] веб-шар Spring MVC надає представлення, фрагменти представлень та дані у форматі jSON;
  • у [2]: код JavaScript, вбудований у вигляд, що завантажується під час запуску додатка, може бути структурований у шари:
    • шар [présentation] відповідає за взаємодію з користувачем,
    • шар [DAO] відповідає за доступ до даних через веб-сервер [1],
    • шар [métier] може не існувати або переймати деякі неконфіденційні функції шару [métier] сервера з метою його розвантаження;
  • клієнт [2] може кешувати певні види, щоб також зменшити навантаження на сервер. Він керує сесією;