Skip to content

7. 将 Spring MVC 应用程序 AJAX 化

7.1. AJAX在Web应用程序中的作用

到目前为止,我们学习过的示例都采用了以下架构:

要从视图 [View1] 切换到视图 [View2],浏览器会:

  • 向 Web 应用程序发送请求;
  • 接收视图 [View2] 并将其显示在视图 [View1] 的位置上。

这是经典模式:

  • 浏览器发出请求;
  • Web 服务器生成视图并响应给客户端;
  • 浏览器显示该新视图。

近年来,浏览器与Web服务器之间出现了一种新的交互模式:AJAX(异步JavaScript和XML)。这种模式涉及浏览器显示的视图与Web服务器之间的交互。浏览器继续发挥其最擅长的功能——显示HTML视图——但现在由嵌入在显示的HTML视图中的JavaScript进行控制。示意图如下:

  • 在[1]中,浏览器显示的页面上发生了一个事件(按钮点击、文本更改等)。该事件被页面中嵌入的JavaScript(JS)拦截;
  • 在[2]中,JavaScript代码会像浏览器原本会做的那样发起HTTP请求。该请求是异步的:用户在等待HTTP响应时不会被阻塞,可以继续与页面交互。该请求遵循标准的处理流程。它与标准请求没有任何(或几乎没有)区别;
  • 在[3]中,响应被发送至JS客户端。通常发送的并非完整的HTML视图,而是部分HTML视图、XML源或JSON(JavaScript对象表示法);
  • 在[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) {
        // valid tempo?
        if (tempo != null) {
            boolean valide = false;
            int valueTempo = 0;
            try {
                valueTempo = Integer.parseInt(tempo);
                valide = valueTempo >= 0;
            } catch (NumberFormatException e) {
 
            }
            if (valide) {
                session.setAttribute("tempo", new Integer(valueTempo));
            }
        }
        // prepare the view model [view-01]
        ...
}
  • 第 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) {
        // valid tempo?
...
        // prepare the view model [view-01]
        modèle.addAttribute("actionModel01", new ActionModel01());
...
        // view
        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 {
 
    // posted data
    @NotNull
    @DecimalMin(value = "0.0")
    private Double a;
 
    @NotNull
    @DecimalMin(value = "0.0")
    private Double b;
 
    // getters and setters
    ...
}
  • 第 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) {
...
        // prepare the view model [view-01]
        modèle.addAttribute("actionModel01", new ActionModel01());
        Resultats résultats = new Resultats();
        modèle.addAttribute("resultats", résultats);
...
        // view
        return "vue-01";
}
  • 第 6-7 行:我们将一个类型为 [Results] 的实例添加到模型中;

模型中放置的 [Results] 类型如下:

  

package istia.st.springmvc.models;
 
public class Resultats {
 
    // data
    private String aplusb;
    private String amoinsb;
    private String amultiplieparb;
    private String adiviseparb;
    private String heureGet;
    private String heurePost;
    private String erreur;
    private String vue;
    private String culture;
 
    // getters and setters
    ...
}
  • 第 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) {
        ...
        // local
        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) {
        // we only manage fr-FR, en-US locales
        String language = locale.getLanguage();
        String country = null;
        switch (language) {
        case "fr":
            country = "FR";
            break;
        default:
            language = "en";
            country = "US";
            break;
        }
        // culture
        résultats.setCulture(String.format("%s-%s", language, country));
}

在模板中,字符串 [${results.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) {
...
        // local
        setLocale(locale, modèle, résultats);
        // hour
        résultats.setHeureGet(new SimpleDateFormat("hh:mm:ss").format(new Date()));
        // view
        return "vue-01";
    }
  • 第 7 行:在模板中设置 GET 请求中的时间;
  • 第 9 行:显示视图 [vue-01.xml]:

7.2.3. 视图 [view-01.xml]

视图 [view-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 行:第 6.3 节中构建的 [client-validation] 库;
  • 第 14 行:[client-validation] 库所使用的 JSON 库。如果已禁用验证日志,则此项为可选;
  • 第 13 行:微软的 [Unobtrusive Ajax] 库。该库有时可帮助您避免编写 JavaScript 代码;
  • 第 16 行:用于满足我们自身需求的 JavaScript 文件;
  • 第 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] 属性由 [unobtrusive-ajax] JavaScript 库处理,该库由 [vue-01.xml] 视图导入:


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

当存在 [data-ajax-xxx] 属性时,表单的 [submit] 按钮将通过 [unobtrusive-ajax] 库发起的 Ajax 调用来执行。参数的含义如下:

  • [data-ajax="true"]: 存在此属性将导致表单的 [submit] 通过 Ajax 执行;
  • [data-ajax-method="post"]: [submit] 的请求方法。POST 请求的 URL 将采用 [action="/ajax-02.html"] 属性指定的地址;
  • [data-ajax-loading="#loading"]: 等待服务器响应时显示的区域 ID。[vue-01.xml] 视图中由 [loading] 标识的区域如下:

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

这是一张动画加载图片,将在收到服务器响应前持续显示;

  • [data-ajax-loading-duration="0"]: 在显示 [data-ajax-loading="#loading"] 区域之前所需的等待时间(以毫秒为单位)。在此情况下,该区域将在等待开始时立即显示;
  • [data-ajax-begin="beforeSend"]: 提交前需执行的 JavaScript 函数;
  • [data-ajax-complete="afterComplete"]:收到响应时执行的 JavaScript 函数;
  • [data-ajax-update="#resultats"]:服务器发送的结果将放置在此区域的 ID。[vue-01.xml] 视图包含以下区域:

<div id="resultats" />
  • [data-ajax-mode="replace"]: 将结果插入到该区域的模式。[replace] 模式会使结果“覆盖”ID 为 [resultats] 的区域中先前存在的任何内容;

请注意,只有当验证器判定被测试的值有效时,JavaScript [submit] 操作才会触发。

[unobtrusive-ajax] JavaScript 库有两个目标:

  • 确保表单能正确适应两种情况:无论浏览器中是否启用了 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 {
        // tempo?
        Integer tempo = (Integer) session.getAttribute("tempo");
        if (tempo != null && tempo > 0) {
            Thread.sleep(tempo);
        }
        // prepare the model for the next view
        Resultats résultats = new Resultats();
        modèle.addAttribute("resultats", résultats);
        // we set the locale
        setLocale(locale, modèle, résultats);
        // hour
        résultats.setHeurePost(new SimpleDateFormat("hh:mm:ss").format(new Date()));
        ...
}
  • 我们先简化一下情况:假设这个 POST 请求确实是由视图 [vue-01.xml] 中的 JavaScript 发出的。稍后我们会重新审视这一假设;
  • 第 2 行:提交的值 [a,b] 被放入模型 [ActionModel01] 中;
  • 第 4–7 行:如果用户在之前的 GET 请求中设置了超时,则从会话中检索该超时并应用(第 6 行)。这样做的目的是让用户能够看到表单中 [data-ajax-loading="#loading"] 属性的效果;
  • 第 9–10 行:向模型添加 [results] 属性;
  • 第 12 行:将区域设置 [fr-FR] 或 [en-US] 添加到模型中;
  • 第 14 行:我们在模型中设置 POST 时间;

回顾之前添加到模型中的 [Resultats] 类型:


public class Resultats {
 
    // data
    private String aplusb;
    private String amoinsb;
    private String amultiplieparb;
    private String adiviseparb;
    private String heureGet;
    private String heurePost;
    private String erreur;
    private String vue;
    private String culture;
 
    // getters and setters
...
}

[/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()));
        // we generate an error every other time
        int val = new Random().nextInt(2);
        if (val == 0) {
            // an error message is returned
            résultats.setErreur("erreur.aleatoire");
            return "vue-03";
        }
...
    }
  • 第 6–11 行:在此示例中,我们将演示如何向 JavaScript 客户端返回错误页面。一半情况下,我们会返回以下视图 [view-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="error" 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 {
...
        // retrieve posted values
        double a = formulaire.getA();
        double b = formulaire.getB();
        // we build the model
        résultats.setAplusb(String.valueOf(a + b));
        résultats.setAmoinsb(String.valueOf(a - b));
        résultats.setAmultiplieparb(String.valueOf(a * b));
        try {
            résultats.setAdiviseparb(String.valueOf(a / b));
        } catch (RuntimeException e) {
            résultats.setAdiviseparb("NaN");
        }
        // the view is displayed
        return "vue-02";
    }
  • 第 5–15 行:对数字 [a, b] 执行四则运算,并将结果封装在模型的 [Resultats] 实例中;
  • 第 17 行:返回以下视图 [view-02.xml]:

视图 [view-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],由于表单的 [data-ajax-update="#resultats"] 属性,该 HTML 结果都会被放置在视图 [vue-01.xml] 中由 [resultats] 标识的区域内。

7.2.6. 提交输入的值

在此,我们遇到了与提交值相关的一个挑战。 我们正在处理 [fr-FR] 和 [en-US] 这两种区域设置,它们对实数的表示方式不同。我们在第 6.3 节第 190 页)中曾解决过这个问题,当时我们需要以两种不同的区域设置提交实数。我们将复用当时使用的工具。然而,我们面临一个额外的挑战:我们无法访问处理提交输入值的方法。这就是为什么我们在表单标签中添加了以下属性:

  • [data-ajax-begin="beforeSend"]:表单提交前需执行的 JavaScript 函数;
  • [data-ajax-complete="afterComplete"]: 收到响应时执行的 JavaScript 函数;

虽然我们无法访问负责提交输入值的 JavaScript 函数,但我们可以编写两个 JavaScript 函数:

  • [beforeSend]:在 POST 请求发送前执行的 JavaScript 函数;
  • [afterComplete]:在收到 POST 响应时执行的 JavaScript 函数;

这两个函数被放置在一个名为 [local1.js] 的文件中:

  

[local1.js] 文件通过以下方式初始化 [vue-01.xml] 视图的 JavaScript 环境:


// global data
var loading;
var formulaire;
var résultats;
var a, b;
 
// document loading
$(document).ready(function() {
    // retrieve the references of the page's various components
    loading = $("#loading");
    formulaire = $("#formulaire");
    resultats = $('#resultats');
    a = $("#a");
    b = $("#b");
    // we hide certain elements
    loading.hide();
    // parse the form validators
    $.validator.unobtrusive.parse(formulaire);
    // we manage two locales [fr_FR, en_US]
    // the reals [a,b] are sent by the server in Anglo-Saxon format
    // we put them in French format if necessary
    checkCulture(2);
});
  • 第 22 行:[checkCulture] 函数将在稍后部分进行说明;

JavaScript 函数 [beforeSend] 将如下所示:


function beforeSend(jqXHR, settings) {
    // before POST
    // numbers must be posted in Anglo-Saxon format
    var culture = Globalize.culture().name;
    if (culture === 'fr-FR') {
        checkCulture(1);
        settings.data = formulaire.serialize();
    }
}
 
function afterComplete(jqXHR, settings) {
    ...
}
 
function checkCulture(mode) {
    if (mode == 1) {
        // we put the numbers [a,b] in Anglo-Saxon format
        var value1 = a.val().replace(",", ".");
        a.val(value1);
        var value2 = b.val().replace(",", ".");
        b.val(value2);
    }
    if (mode == 2) {
...
    }
}
  • 第4-6行:我们检查视图的区域设置是否为 [fr-FR]。如果是这种情况,则必须修改提交的值。 事实上,如果用户输入了 [1,6],则必须提交值 [1.6];否则,值 [1,6] 将在服务器端被拒绝。要实现这一点,只需将提交值中的逗号改为小数点(第 18–21 行);
  • 但我们不能止步于此。当调用 [beforeSend] 函数时,提交值的字符串 [a=val1&b=valB] 已经构建完成。因此我们需要对其进行修改。这通过该函数的第二个参数 [settings] 来实现;
  • 第 7 行:[settings.data](settings 是函数参数)代表要提交的字符串。我们使用表达式 [form.serialize()] 重新构建该字符串。该表达式会遍历表单以查找待提交的值,并构建 POST 字符串。随后,它将采用带有小数点的 [a,b] 新值;

如果我们不再进行其他操作,服务器将发送响应,且结果会正确显示。然而,尽管我们仍处于 [fr-FR] 区域设置中,[a,b] 的值现在却带有小数点。因此,如果用户没有注意到这一点并再次点击 [Calculate],验证器会提示 [a,b] 的值无效。这是正确的。 此时就需要用到 [afterComplete] 函数,该函数会在收到结果时执行:


function beforeSend(jqXHR, settings) {
    // before POST
...
}
 
function afterComplete(jqXHR, settings) {
    // after POST
    // numbers must be supplied in French format if necessary
    var culture = Globalize.culture().name;
    if (culture === 'fr-FR') {
        checkCulture(2);
    }
}
 
function checkCulture(mode) {
    if (mode == 1) {
...
    }
    if (mode == 2) {
        // put the numbers in French format
        var value1 = a.val().replace(".", ",");
        a.val(value1);
        var value2 = b.val().replace(".", ",");
        b.val(value2);
    }
}
  • 第 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. 使用 [en-US] 语言环境禁用 JavaScript

如果我们在浏览器中禁用 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 request?
        boolean isAjax = "XMLHttpRequest".equals(request.getHeader("X-Requested-With"));
        ...
    }
  • 第 4 行:现在可以通过 Ajax POST 或标准 POST 调用 [/ajax-02] 操作。我们需要能够区分这两种情况。我们通过客户端浏览器发送的 HTTP 头部来实现这一点;

当我们在启用 JavaScript 的 Chrome 开发者工具(Ctrl-Shift-I)中查看网络流量时,可以看到客户端在 POST 请求中发送了以下头部信息:

如上所示:

  • 发送了一个 [X-Requested-With] 标头 [1];
  • 已将 [X-Requested-With] 参数添加到表单提交值中 [2];

在标准的 POST 请求中不会这样做。因此,我们有两种方式来获取该信息:从 HTTP 头部获取,或者从提交的值中获取。[ajax-02] 操作的第 4 行选择了第一种方案。

让我们继续查看该操作的代码:


@RequestMapping(value = "/ajax-02", method = RequestMethod.POST, produces = "text/html; charset=UTF-8")
    public String ajax02(@Valid ActionModel01 formulaire, BindingResult result, Locale locale, Model modèle, HttpSession session, HttpServletRequest request) throws InterruptedException {
        // ajax request?
        boolean isAjax = "XMLHttpRequest".equals(request.getHeader("X-Requested-With"));
        // tempo?
        Integer tempo = (Integer) session.getAttribute("tempo");
        if (tempo != null && tempo > 0) {
            Thread.sleep(tempo);
        }
        // prepare the model for the next view
        Resultats résultats = new Resultats();
        modèle.addAttribute("resultats", résultats);
        // we set the locale
        setLocale(locale, modèle, résultats);
        // hour
        String heure = new SimpleDateFormat("hh:mm:ss").format(new Date());
        résultats.setHeurePost(heure);
        résultats.setHeureGet(heure);
        // valid request?
        if (!isAjax && result.hasErrors()) {
            return "vue-01";
        }
...
  • 第 2 行:[@Valid ActionModel01 form] 参数会触发服务器端的验证器;
  • 第 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 request?
        boolean isAjax = "XMLHttpRequest".equals(request.getHeader("X-Requested-With"));
...
        // valid request?
        if (!isAjax && result.hasErrors()) {
            return "vue-01";
        }
        // we generate an error every other time
        int val = new Random().nextInt(2);
        if (val == 0) {
            // an error message is returned
            résultats.setErreur("erreur.aleatoire");
            if (isAjax) {
                return "vue-03";
            } else {
                résultats.setVue("vue-03");
                return "vue-01";
            }
        }
...
  • 第 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 行:视图 [view-03.xml] 将插入到 [results] 区域下方;

以下是一个示例:

请注意,时间点 [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 request?
        boolean isAjax = "XMLHttpRequest".equals(request.getHeader("X-Requested-With"));
...
        // retrieve posted values
        double a = formulaire.getA();
        double b = formulaire.getB();
        // we build the model
        résultats.setAplusb(String.valueOf(a + b));
        résultats.setAmoinsb(String.valueOf(a - b));
        résultats.setAmultiplieparb(String.valueOf(a * b));
        try {
            résultats.setAdiviseparb(String.valueOf(a / b));
        } catch (RuntimeException e) {
            résultats.setAdiviseparb("NaN");
        }
        // the view is displayed
        if (isAjax) {
            return "vue-02";
        } else {
            résultats.setVue("vue-02");
            return "vue-01";
        }
}
  • 第 7–17 行:四则运算的结果被放入模板中;
  • 第22–23行:通过插入视图 [vue-02.xml](第22行)来渲染视图 [vue-01.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. 在 [fr-FR] 语言环境中禁用 JavaScript

在 [fr-FR] 语言环境中,我们遇到了以下问题:

以法语格式输入的数值被判定为无效。这是因为服务器期望接收采用英美格式的实数。解决方案相当复杂。我们将创建一个过滤器,该过滤器将:

  • 拦截请求;
  • 将提交值 [a] 和 [b] 中的逗号替换为小数点;
  • 然后将新的请求传递给需要处理它的操作;

首先,我们在视图 [vue-01.xml] 中添加一个隐藏字段:


<form ...>
...
</p>
    <!-- hidden fields -->
    <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 行:由于 [cultureFilter] Bean 返回 [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 {
        // next handler
        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) {
        // posted values a and b
        if (name != null && (name.equals("a") || name.equals("b"))) {
            String[] values = super.getParameterValues(name);
            String[] newValues = values.clone();
            newValues[0] = newValues[0].replace(",", ".");
            return newValues;
        }
        // other cases
        return super.getParameterValues(name);
    }
 
}
  • 第 6 行:[CultureRequestWrapper] 类继承自 [HttpServletRequestWrapper] 类,并将重写其中的一些方法;
  • 第 8–10 行:构造函数接收待过滤的请求,并将其传递给父类;
  • 这里需要理解的是,经过过滤的请求最终将成为一个名为 servlet 的类的输入参数。在 Spring MVC 中,该 servlet 的类型为 [DispatcherServlet]。该类提供了多种用于获取请求参数的方法:[getParameter, getParameterMap, getParameterNames, getParameterValues, ...]。 Servlet 所使用的方法必须被重写。要做到这一点,需要阅读 [DispatcherServlet] 类的源代码。我并未这样做,而是直接重写了多个方法。最终,被重写的是 [getParameterValues] 方法;
  • 第 13 行:[getParameterValues] 方法接受 [getParameterNames] 方法返回的某个参数名称作为参数,并必须返回该参数值的数组。事实上,我们知道一个参数可能在请求中出现多次;
  • 第 18 行:将逗号替换为小数点;

以下是一个执行示例:

  • 在 [1] 中,值 [a,b] 以法语格式输入;
  • 在 [2] 中,显示结果;
  • 在[3]中,服务器返回的页面中数字采用英美格式。

在视图 [vue-01.xml] 中,可通过 Thymeleaf 按以下方式解决此问题:


<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] 用于设置生成的 HTML [input] 标签的 [id、name、value] 属性。在此,我们希望自行管理 [value] 属性,因此也需自行设置 [id、name] 属性;
  • [th:value] 属性使用三元运算符 ? 来评估表达式。我们测试表达式 [${resultats.culture}=='fr-FR' and ${actionModel01.b}!=null]。如果为真,我们将 [value] 属性设置为 [actionModel01.a] 的值,其中小数点被逗号替换。 若为假,则将 [value] 属性设置为 [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. 处理 [Calculate] 链接

让我们来分析主页面 [vue-01.xml] 上的 [计算] 链接:

[vue-01.xml]视图中[计算]链接的代码如下:


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

JavaScript 函数 [postForm] 在文件 [local1.js] 中定义如下:


// global data
var loading;
var formulaire;
var résultats;
var a, b;
 
function postForm() {
    // valid form?
    if (!formulaire.validate().form()) {
        // invalid form - terminated
        return;
    }
    // we manage two locales [fr_FR, en_US]
    // the real [a,b] must be posted in Anglo-Saxon format in all cases
    // they will be filtered by [CultureFilter]
 
    // make a manual Ajax call
    $.ajax({
        url : '/ajax-02',
        headers : {
            'X-Requested-With' : 'XMLHttpRequest'
        },
        type : 'POST',
        data : formulaire.serialize(),
        dataType : 'html',
        beforeSend : function() {
            loading.show();
        },
        success : function(data) {
            resultats.html(data);
        },
        complete : function() {
            loading.hide();
        },
        error : function(jqXHR) {
            résultats.html(jqXHR.responseText);
        }
    })
}
  • 第 2–5 行:请注意,这些元素是由 [$(document).ready] 函数初始化的;
  • 第 9–12 行:我们运行表单的 JavaScript 验证器。如果任何值无效,表达式 [form.validate().form()] 将返回 false。在这种情况下,表单的 [submit] 操作将被取消;
  • 第 18–38 行:我们发起一次手动 Ajax 请求;
  • 第 19 行:Ajax 调用的目标 URL;
  • 第 20–22 行:一个 HTTP 头数组,用于补充 HTTP 请求中默认包含的头部。此处,我们添加了一个 HTTP 头,用于向服务器表明我们正在进行 Ajax 调用;
  • 第 23 行:使用的 HTTP 方法;
  • 第 24 行:要提交的数据。[formulaire.serialize] 根据 ID 为 [formulaire] 的表单生成待提交的字符串 [culture=fr-FR&a=12,7&b=20,89]。这里我们遇到了之前讨论过的问题:值 [a,b] 必须以英美格式提交。 我们知道,随着 [cultureFilter] 过滤器的创建,这个问题现已得到解决;
  • 第 25 行:预期的返回数据类型。我们知道服务器将返回一个 HTML 流;
  • 第 26 行:请求开始时执行的方法。在此,我们指定必须显示 id 为 [loading] 组件。这是加载动画图片;
  • 第 29 行:Ajax 请求成功时执行的方法。[data] 参数是来自服务器的完整响应。我们知道这是一个 HTML 流;
  • 第 30 行:我们将 [data] 参数中的 HTML 内容用于更新 ID 为 [results] 的组件。
  • 第 33 行:隐藏加载指示器;
  • 第 35 行:无论请求成功与否,在收到服务器响应时执行的函数;
  • 第 35–37 行:如果发生错误(服务器返回的 HTTP 响应状态码表明存在服务器端错误),则在 [results] 区域显示服务器的 HTML 响应;

以下是一个执行示例:

7.3. 使用 JSON 数据流更新 HTML 页面

在上一个示例中,Web 服务器通过一个 HTML 数据流响应了 Ajax HTTP 请求。该数据流包含数据以及相应的 HTML 格式。我们将重新审视上一个示例,这次使用仅包含数据的 JSON(JavaScript 对象表示法)响应。其优势在于传输的字节数更少。我们假设浏览器已启用 JavaScript。

7.3.1. [/ajax-04] 操作

[/ajax-04] 操作与 [/ajax-01] 操作完全相同,唯一的区别在于它显示的是 [vue-04.xml] 视图,而不是 [vue-01.xml] 视图:


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

7.3.2. 视图 [view-04.xml]

 

视图 [view-04.xml] 采用了视图 [view-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>
            <!-- hidden fields -->
            <input type="hidden" id="culture" name="culture" th:value="${resultats.culture}"></input>
</form>
        <hr />
        <div id="entete">
            <h4 id="titre">Résultats</h4>
            <p>
                <strong>
                    <span id="labelHeureCalcul">Heure de calcul :</span>
                    <span id="heureCalcul">12:10:87</span>
                </strong>
            </p>
        </div>
        <div id="résultats">
            <p>
                A+B=
                <span id="aplusb">16,7</span>
            </p>
            <p>
                A-B=
                <span id="amoinsb">16,7</span>
            </p>
            <p>
                A*B=
                <span id="afoisb">16,7</span>
            </p>
            <p>
                A/B=
                <span id="adivb">16,7</span>
            </p>
        </div>
        <div id="erreur">
            <p style="color: red;">
                <span id="msgErreur">xx</span>
            </p>
        </div>
    </body>
</html>
  • 第 5 行:视图的 JavaScript 现已移至 [local4.js] 文件中;
  • 第 16 行:[form] 标签不再包含来自 [Unobtrusive Ajax] 库的 [data-ajax-attr] 参数。我们在此处不会使用它。 [form] 标签也不再包含 [method] 和 [action] 属性,这些属性原本用于指定如何以及向何处提交表单中输入的值。这是因为表单将由一个 JavaScript 函数(第 20 行)来提交;
  • 第 26–57 行:ID 为 [resultats] 的区域,该区域之前为空,现在包含用于显示结果的 HTML 代码;
  • 第 26–34 行:显示计算时间的“结果”标题;
  • 第 35–52 行:四则运算的结果;
  • 第 53–57 行:服务器发送的任何错误信息;

视图 [vue-04.xm] 加载时执行的 JavaScript 代码位于文件 [local4.js] 中。内容如下:


// global data
    var loading;
    var formulaire;
    var résultats;
    var titre;
    var labelHeureCalcul;
    var heureCalcul;
    var aplusb;
    var amoinsb;
    var afoisb;
    var adivb;
    var msgErreur;
 
// document loading
$(document).ready(function() {
    // retrieve the references of the page's various components
    loading = $("#loading");
    formulaire = $("#formulaire");
    résultats = $('#résultats');
    titre=$("#titre");
    labelHeureCalcul=$("#labelHeureCalcul");
    heureCalcul=$("#heureCalcul");
    aplusb=$("#aplusb");
    amoinsb=$("#amoinsb");
    afoisb=$("#afoisb");
    adivb=$("#adivb");
    msgErreur=$("#msgErreur");
    // we hide certain elements
    résultats.hide();
    erreur.hide();
    loading.hide();
});
  • 第 17–27 行:获取页面上所有元素的 jQuery 引用;
  • 第 29 行:隐藏结果区域;
  • 第 30 行:同时隐藏错误区域;
  • 第 31 行:同时隐藏动画加载图标;
  • 第 2–12 行:将获取的引用声明为全局变量,以便其他函数可以访问它们;

7.3.3. jS [postForm] 函数

[Calculate] 链接如下:


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

JavaScript 函数 [postForm] 在 [local.js] 文件中定义如下:


function postForm() {
    // valid form?
    if (!formulaire.validate().form()) {
        // invalid form - terminated
        return;
    }
    // make a manual Ajax call
    $.ajax({
        url : '/ajax-05',
        headers : {
            'Accept' : 'application/json'
        },
        type : 'POST',
        data : formulaire.serialize(),
        dataType : 'json',
        beforeSend : onBegin,
        success : onSuccess,
        error : onError,
        complete : onComplete
    })
}
 
// before the Ajax call
function onBegin() {
...
}
 
// on receipt of the server response
// in case of success
function onSuccess(data) {
...
}
 
// on receipt of the server response
// in case of failure
function onError(jqXHR) {
...
}
 
// after [onSuccess, onError]
function onComplete() {
...
}
  • 第 3–6 行:在提交输入的值之前,我们会对其进行验证。如果值不正确,则不会提交表单;
  • 第 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] 函数如下:


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

在探索 Ajax 调用的其他 JavaScript 函数之前,我们需要了解 [/ajax-05] 操作发送的响应。

7.3.4. [/ajax-05] 操作

[/ajax-05] 操作如下:


    @RequestMapping(value = "/ajax-05", method = RequestMethod.POST)
    @ResponseBody()
    // processes the POST of view [view-04]
    public JsonResults ajax05(@Valid ActionModel01 formulaire, BindingResult result, Locale locale,    HttpServletRequest request, HttpSession session) throws InterruptedException {
        if(result.hasErrors()){
            // abnormal case - nothing returned
            return null;
        }
        ...
}
  • 第 2 行:[ResponseBody] 属性表示 [/ajax-05] 操作本身会向客户端返回响应。由于项目依赖中包含了 JSON 库,Spring Boot 会自动 将此类操作配置为返回 JSON。因此,类型为 [JsonResults] 的 JSON 字符串(第 4 行)将被发送给客户端;
  • 第 2 行:提交的值 [a, b, culture] 将封装在 [ActionModel01] 类型中,我们对其进行验证 [@Valid ActionModel01]。这只是为了符合规范。 我们假设客户端浏览器已启用 JavaScript,因此当数据提交时,这些值已在客户端经过验证。然而,我们仍需考虑未授权的 POST 请求(即未使用我们的 JavaScript 客户端)的情况。在此情况下,验证可能会失败;
  • 第 5–7 行:若发生错误,则返回一个空的 JSON 对象;

让我们继续分析 [/ajax-05] 操作:


    @RequestMapping(value = "/ajax-05", method = RequestMethod.POST)
    @ResponseBody()
    // processes the POST of view [view-04]
    public JsonResults ajax05(@Valid ActionModel01 formulaire, BindingResult result, Locale locale,
            HttpServletRequest request, HttpSession session) throws InterruptedException {
...
        // spring application context
        WebApplicationContext ctx = WebApplicationContextUtils.getWebApplicationContext(request.getServletContext());
        // tempo?
        Integer tempo = (Integer) session.getAttribute("tempo");
        if (tempo != null && tempo > 0) {
            Thread.sleep(tempo);
        }
    ...
        // we return the result
        return résultats;
}
  • 第 8 行:我们从 Spring 应用程序中获取上下文 [ctx]。我们需要它来根据消息键和区域设置从 [messages.properties] 文件中检索消息。这通过以下语法实现:

ctx.getMessage(clé_message, tableau_de_paramètres, locale)
    • [message_key]:要查找的消息的键;
    • [locale]:使用的语言环境。因此,如果该语言环境为 [en_US],则将使用 [messages_en.properties] 文件;
    • [parameter_array]:检索到的消息可以像 [key=message {0} {1}] 那样进行参数化。该消息包含两个参数 [{0} {1}]。您必须提供一个包含两个值的数组作为 [ctx.getMessage] 的第二个参数;
  • 第 10-13 行:如果会话超时,则当前线程将在超时期间暂停;

[/ajax-05] 操作随后继续如下:


        // on prépare le modèle de la prochaine vue
        JsonResults résultats = new JsonResults();
        ...
}
  • 第 2 行:创建发送给客户端的 JSON 字符串模板;

[JsonResults] 模型如下:

 

package istia.st.springmvc.models;
 
public class JsonResults {
 
    // data
    private String titre;
    private String labelHeureCalcul;
    private String heureCalcul;
    private String aplusb;
    private String amoinsb;
    private String afoisb;
    private String adivb;
    private String msgErreur;
 
    // getters and setters
...
 
}
  • 第 6–13 行:[JsonResult] 类中的每个字段都对应 [vue-04.xml] 视图中具有相同 [id] 的字段:

[/ajax-05] 操作继续如下:


        // on prépare le modèle de la prochaine vue
        JsonResults résultats = new JsonResults();
        // entête
        résultats.setTitre(ctx.getMessage("resultats.titre", null, locale));
        résultats.setLabelHeureCalcul(ctx.getMessage("labelHeureCalcul", null, locale));
        résultats.setHeureCalcul(new SimpleDateFormat("hh:mm:ss").format(new Date()));
        // on génère une erreur une fois sur deux
        int val = new Random().nextInt(2);
        if (val == 0) {
            // on renvoie un message d'erreur
            résultats.setMsgErreur(ctx.getMessage("resultats.erreur",
                    new Object[] { ctx.getMessage("erreur.aleatoire", null, locale) }, locale));
            return résultats;
}
  • 第 2 行:创建发送给客户端的 JSON 字符串模板;
  • 第4–6行:创建结果标头中的消息;
  • 第8–14行:平均每两次尝试会生成一条错误消息。此时,流程在此终止,并将JSON字符串返回给客户端(第13行);
  • 第 11 行:此处是一个参数化消息的示例:

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

[/ajax-05] 操作继续如下:


        // on récupère les valeurs postées
        double a = formulaire.getA();
        double b = formulaire.getB();
        // on construit le modèle
        résultats.setAplusb(String.valueOf(a + b));
        résultats.setAmoinsb(String.valueOf(a - b));
        résultats.setAfoisb(String.valueOf(a * b));
        try {
            résultats.setAdivb(String.valueOf(a / b));
        } catch (RuntimeException e) {
            résultats.setAdivb("NaN");
        }
        // on rend le résultat
return résultats;
  • 第 2-3 行:获取 [a] 和 [b] 的值;
  • 第 5-12 行:构建四个结果;
  • 第 14 行:将 JSON 字符串 [JsonResults] 发送给客户端;

让我们看看这在 [Advanced Rest Client] 中是如何工作的:

  • 在 [1-2] 中,我们向 [/ajax-05] 操作发送 POST 请求;
  • 在 [3] 中,我们提交了错误的值;
  • 在 [4] 中,服务器返回了一个空响应;
  • 在 [1] 中,我们提交了正确的值;
  • 在 [2] 中,服务器返回的 JSON 对象,此处包含一条错误信息;
  • 在 [1] 中,我们提交了正确的值;
  • 在[2]中,服务器返回的JSON对象,显示了四个结果;
  • 在 [1] 中,我们发送了正确的值;
  • 在 [2] 中,我们触发了服务器端异常。我们可以看到服务器仍然返回了一个 JSON 对象。在此消息中,我们看到响应的 HTTP 状态码为 [500],这表明发生了服务器端错误;

7.3.5. jS [postForm] 函数 - 2

既然我们已经了解了服务器返回的 JSON 对象,就可以在 JavaScript 中使用它。当服务器发送 HTTP 状态码为 [200] 的响应时,[onSuccess] 方法将执行,其实现如下:


// on receipt of the server response
// in case of success
function onSuccess(data) {
    console.log("onSuccess");
    // fill in the results area
    titre.text(data.titre);
    labelHeureCalcul.text(data.labelHeureCalcul);
    heureCalcul.text(data.heureCalcul);
    entete.show();
    // error-free results
    if (!data.msgErreur) {
        aplusb.text(data.aplusb);
        amoinsb.text(data.amoinsb);
        afoisb.text(data.afoisb);
        adivb.text(data.adivb);
        résultats.show();
        return;
    }
    // results with error
    msgErreur.text(data.msgErreur);
    erreur.show();
}
  • 第 3 行:[data] 参数是服务器返回的 JSON 对象:
 

当 HTTP 响应状态为 [500] 时执行的 [onError] 方法如下:


// on receipt of the server response
// in case of failure
function onError(jqXHR) {
    console.log("onError");
    // system error
    msgErreur.text(jqXHR.responseText);
    erreur.show();
}
  • 第 3 行:jQuery 对象 [jqXHR] 具有以下属性:
    • responseText:服务器的响应文本,
    • status:服务器返回的错误代码,
    • statusText:与该错误代码关联的文本;
  • 第 6 行:对象 [jqXHR.responseText] 是以下 JSON 对象:
 

7.3.6. 测试

让我们来看一些该 Web 应用程序运行时的截图:

 
 
 

7.4. 单页Web应用程序

7.4.1. 简介

Ajax 技术使您能够构建单页应用程序:

  • 初始页面通过标准的浏览器请求加载;
  • 后续页面通过 Ajax 调用加载。因此,浏览器的 URL 始终保持不变,也不会加载新页面。此类应用程序被称为单页应用程序(SPA)。

以下是一个此类应用程序的基本示例。新应用程序将包含两个视图:

  • 在 [1] 中,操作 [/ajax-06] 会调出第一页,即第 1 页
  • 在 [2] 中,通过一个链接,我们可以借助 Ajax 调用跳转到第 2 页
  • 在 [3] 中,URL 未发生变化。显示的页面是第 2 页
  • 在[4]中,有一个链接可通过Ajax调用让我们返回第1页
  • 在[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] 被包含在视图 [vue-06.xml] 的 [content] ID 区域中;

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]

[vue-07.xml] 视图中的 [Page 2] 链接使用了在以下 [local6.js] 文件中定义的 jS [gotoPage] 函数:


// global data
var content;
 
function gotoPage(num) {
    // make a manual Ajax call
    $.ajax({
        url : '/ajax-07',
        type : 'POST',
        data : 'num=' + num,
        dataType : 'html',
        beforeSend : function() {
        },
        success : function(data) {
            content.html(data)
        },
        complete : function() {
        },
        error : function(jqXHR) {
            // system error
            content.html(jqXHR.responseText);
        }
    })
}
 
// document loading
$(document).ready(function() {
    // retrieve the references of the page's various components
    content = $("#content");
});
  • 第 28 行:页面加载时,我们获取 ID 为 [content] 的元素并将其赋值给全局变量(第 2 行);
  • 第 4 行:[gotoPage] 函数接收一个参数,即要在当前视图中显示的页码(1 或 2);
  • 第 7 行:POST 请求的目标 URL;
  • 第 8 行:通过 POST 方法向第 7 行中的 URL 发送请求;
  • 第 9 行:POST 提交的字符串。提交了一个名为 [num] 的参数,其值为当前视图中要显示的页码(第 4 行);
  • 第 10 行:服务器将返回 HTML,即待显示页面的 HTML 内容;
  • 第 13–15 行:若请求成功(HTTP 状态码 200),将服务器发送的 HTML 放入 ID 为 [content] 的元素中;
  • 第 18–20 行:如果请求失败(HTTP 状态码 500),则将服务器发送的 HTML 放入 ID 为 [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 : page number
        switch (num) {
        case 1:
            return "vue-07";
        case 2:
            return "vue-08";
        default:
            return "vue-07";
        }
    }
  • 第 2 行:我们获取名为 [num] 的提交参数。请注意,第 2 行中的参数名称必须与提交的参数名称一致,本例中即为 [num]。[num] 表示页面或视图编号;
  • 第 5-6 行:如果 [num==1],则返回视图 [vue-07.xml];
  • 第 7-8 行:如果 [num==2],则返回视图 [vue-08.xml];
  • 第 9-10 行:在所有其他情况下(通常不可能发生),返回视图 [vue-07.xml];

7.4.7. 视图 [view-08.xml]

视图 [view-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. 在 JSON 响应中嵌入多个 HTML 流

7.5.1. 简介

考虑以下应用程序:

第 [1] 页包含四个区域:

  • [区域 1] 和 [区域 3] 是点击 [刷新] 按钮时会显示或隐藏的区域。我们统计这两个区域各自显示的次数 [2]。[区域 1] 使用法语,而 [区域 3] 使用英语;
  • [区域 2] 始终存在;
  • [条目] 部分始终可见;

[提交] 链接将显示下一页 [3]:

  • [返回第 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>
        <!-- zone 1 -->
        <fieldset id="zone1" style="background-color:pink">
            <legend>Zone 1</legend>
            <span id="zone1-content" th:text="xx">xx</span>
        </fieldset>
        <!-- zone 2 -->
        <fieldset id="zone2" style="background-color:lightgreen">
            <legend>Zone 2</legend>
            <span>Ce texte reste toujours présent</span>
        </fieldset>
        <!-- zone 3 -->
        <fieldset id="zone3" style="background-color:yellow">
            <legend>Zone 3</legend>
            <span id="zone3-content" th:text="zz">zz</span>
        </fieldset>
        <br />
        <p>
            <button onclick="javascript:postForm()">Rafraîchir</button>
        </p>
        <hr />
        <div id="saisies" th:include="vue-09-saisies">
        </div>
    </body>
</html>
  • 第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 页有两个功能:

  • [刷新]:用于刷新第 1 和第 3 区域。此操作由服务器处理,服务器会随机返回:
    • 字段 1 及其访问计数器,而字段 3 则为空,
    • 区域 3 及其访问计数器,而区域 1 则无数据,
    • 两个区域均显示其访问计数;
  • [提交]:显示包含输入值的第2页,若输入数据无效则显示错误信息;

我们将首先关注 [刷新] 按钮。

7.5.4. [刷新]按钮的JS代码

  

[local9.js] 文件中的代码如下:


// global variables
var content;
var loading;
var erreur;
 
// document loading
$(document).ready(function() {
    // retrieve the references of the page's various components
    loading = $("#loading");
    loading.hide();
    erreur = $("#erreur");
    erreur.hide();
    content = $("#content");
});
  • 第 9-13 行:当主页面加载时,会存储对由 [loading, error, content] 标识的三个组件的引用;
  • 第 2-4 行:将这三个组件的引用存储在全局变量中。它们保持不变,因为这三个区域始终存在于显示的页面上,无论何时。正因为它们保持不变,因此可以在 [$(document).ready] 中进行计算,并与 JS 文件中的其他函数共享;

[postForm] 函数负责处理 [Refresh] 按钮的点击事件:


function postForm() {
    console.log("postForm");
    // make a manual Ajax call
    $.ajax({
        url : '/ajax-10',
        headers : {
            'Accept' : 'application/json'
        },
        type : 'POST',
        dataType : 'json',
        beforeSend : onBegin,
        success : onSuccess,
        error : onError,
        complete : onComplete
    })
}
  • 第 4–15 行:向服务器发送的 Ajax 请求;
  • 第 5 行:[ajax-10] 操作将处理 POST 请求;
  • 第 6–8 行:响应将采用 JSON 格式。JS 客户端表明其接受 JSON 文档;
  • 第 9 行:通过 POST 操作调用 [ajax-10] 操作;
  • 第 10 行:我们将接收 JSON 数据;
  • 第 11 行:Ajax 调用前执行的函数;
  • 第 12 行:在成功收到服务器响应时执行的函数 [200 OK];
  • 第 13 行:在收到服务器响应且请求失败时 [500 Internal Server Error, ...] 执行的函数;
  • 第 14 行:接收响应后执行的函数;

[onBegin] 函数如下:


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

它仅在等待服务器响应时显示动画加载图片。

7.5.5. [/ajax-10] 操作

  

[/ajax-10] 操作如下:


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

会话 [SessionModel1] 存储以下内容:

  • 第 15 行:[Zone 1] 区域显示的次数 [cpt1];
  • 第 16 行:[Zone 3] 区域显示的次数 [cpt3];
  • 第 18–20 行:[Zone 1]、[Zone 3] 和 [Inputs] 区域的 HTML 流。这在 [Page 1] --> [Page 2] --> [Page 1] 的序列中是必要的。从 [Page 2] 跳转到 [Page 1] 时,必须恢复 [Page 1] 及其三个区域;
  • 第 21–22 行:两个布尔值,用于指示 [Zone 1] 和 [Zone 3] 区域是否显示(可见);

注入到 [AjaxController] 中的另一个元素如下:


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

[SpringTemplateEngine] Bean 在 [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] Bean,它允许我们定义视图的某些特性;
  • 第 13–17 行:[SpringTemplateEngine] Bean 允许我们定义视图“引擎”,即负责向客户端生成 [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 {
 
    // data
    private String content;
    private String zone1;
    private String zone3;
    private String erreur;
    private String saisies;
    private boolean zone1Active;
    private boolean zone3Active;
 
    public JsonResult10() {
    }
 
    // getters and setters
...
}
  • 第 6 行:由 [content] 标识的区域的 HTML 内容;
  • 第 7 行:[Zone 1] 区域的 HTML 内容;
  • 第 8 行:[Zone 3] 区域的 HTML 内容;
  • 第 9 行:[Error] 区域的 HTML 内容;
  • 第 10 行:[Inputs] 区域的 HTML 内容;
  • 第 11 行:一个布尔值,用于指示是否应显示 [Zone 1] 区域;
  • 第 12 行:一个布尔值,用于指示是否应显示 [Zone 3] 区域;

[/ajax-10] 操作的代码如下:


@RequestMapping(value = "/ajax-10", method = RequestMethod.POST)
    @ResponseBody()
    public JsonResult10 ajax10(HttpServletRequest request, HttpServletResponse response) {
        // thymeleaf context
        WebContext thymeleafContext = new WebContext(request, response, request.getServletContext());
        // answer
        JsonResult10 result = new JsonResult10();
        // session
        session.setZone1(null);
        session.setZone3(null);
        session.setZone1Active(false);
        session.setZone3Active(false);
        // randomize an answer
        int cas = new Random().nextInt(3);
        switch (cas) {
        case 0:
            // zone 1 active
            setZone1(thymeleafContext, result);
            return result;
        case 1:
            // zone 3 active
            setZone3(thymeleafContext, result);
            return result;
        case 2:
            // active zones 1 and 3
            setZone1(thymeleafContext, result);
            setZone3(thymeleafContext, result);
            return result;
        }
        return null;
    }
  • 第 5 行:我们获取 [Thymeleaf] 上下文。稍后我们将了解它的用途;
  • 第 7 行:目前我们创建一个空响应;
  • 第 9–12 行:我们将会话中的两个字段设置为 [null],并指定它们不应显示。这两个字段稍后将被生成,但可能只有其中一个会被生成;
  • 第 14–29 行:两个字段均被生成;
  • 第 17–19 行:仅生成 [Zone 1] 区域;
  • 第 21–23 行:仅生成 [Zone 3] 区域;
  • 第 25–28 行:[Zone 1] 和 [Zone 3] 均被生成;

[Zone 1] 区域的 HTML 流通过以下方法生成:


    private void setZone1(WebContext thymeleafContext, JsonResult10 result) {
        // zone 1 active
        // flow HTML
        int cpt1 = session.getCpt1() + 1;
        thymeleafContext.setVariable("cpt1", cpt1);
        thymeleafContext.setLocale(new Locale("fr", "FR"));
        String zone1 = engine.process("vue-09-zone1", thymeleafContext);
        result.setZone1(zone1);
        result.setZone1Active(true);
        // session
        session.setCpt1(cpt1);
        session.setZone1(zone1);
        session.setZone1Active(true);
}
  • 第 1 行:参数为:
    • 类型为 [WebContext] 的 [Thymeleaf] 上下文,
    • 当前正在构建的发给客户端的响应,类型为 [JsonResult10];
  • 第 3 行:我们递增会话计数器 [cpt1],该计数器记录区域 [Zone 1] 的显示次数;
  • 第 4 行:类型为 [WebContext] 的 [Thymeleaf] 上下文的行为与 Spring MVC 中的 [Model] 有些类似。要向模型添加元素,我们使用 [WebContext.setVariable]。在此,我们将计数器 [cpt1] 放入 [Thymeleaf] 模型中。这将允许 Thymeleaf 表达式 [${cpt1}] 被求值
  • 第 5 行:[Thymeleaf] 上下文具有语言环境。这使其能够求解 [#{key_msg}] 类型的表达式。此处,我们将 Thymeleaf 上下文与法语语言环境关联;
  • 第 6 行:这是最关键的指令。Thymeleaf 引擎将使用我们刚刚计算出的模板和区域设置来处理视图 [vue-09-zone1.xml],并且不将生成的 HTML 输出发送给客户端,而是将其作为字符串返回;
  • 第 7–9 行:刚刚计算出的 [Zone 1] 区域的 HTML 输出被存储在会话中,并作为将发送给客户端的结果。此外,我们指定必须显示 [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: 

[Zone 3] 区域的 HTML 流程通过类似的方法生成:


    private void setZone3(WebContext thymeleafContext, JsonResult10 result) {
        // zone 3 active
        // flow HTML
        int cpt3 = session.getCpt3() + 1;
        thymeleafContext.setVariable("cpt3", cpt3);
        thymeleafContext.setLocale(new Locale("en", "US"));
        String zone3 = engine.process("vue-09-zone3", thymeleafContext);
        result.setZone3(zone3);
        result.setZone3Active(true);
        // session
        session.setCpt3(cpt3);
        session.setZone3(zone3);
        session.setZone3Active(true);
}
  • 第 6 行:区域 [Zone 3] 的语言环境为英语;

7.5.6. 处理来自 [/ajax-10] 操作的响应

让我们回到 [local9.js] 中的 JS 代码,该代码将处理服务器的响应:


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

让我们回顾一下第 3 行中 [data] 变量接收到的响应的 Java 结构:


public class JsonResult10 {
 
    // data
    private String content;
    private String zone1;
    private String zone3;
    private String erreur;
    private String saisies;
    private boolean zone1Active;
    private boolean zone3Active;
 
}
  • 第 6–8 行:如果 [data.content] 不为空,则 [id=content] 字段将使用该值进行初始化。该字段代表完整的 [Page 1] 或 [Page 2]。在此示例中,[data.content == null],因此 [id=content] 区域不会被修改,并将继续显示 [Page 1];
  • 第 10–17 行:如果 [data.zone1Active==true],则显示 [Zone 1]。此外,如果 [data.zone1!=null],则 [Zone 1] 的内容会被修改;否则,内容保持不变;
  • 第 19–26 行:[Zone 3] 同样适用此规则;
  • 第 28–30 行:若 [data.saisies!=null],则刷新 [Saisies] 区域。在此演示中,[data.saisies==null],因此 [Saisies] 区域保持不变;
  • 第 32–37 行:[Error] 字段的处理逻辑类似,但有以下细微差别:
    • 第 33 行:[data.error] 将是一个文本格式的错误消息;
    • 第 36 行:如果 [data.error] 为空,则隐藏 [Error] 字段。这是因为该字段可能已在之前的请求中显示过;

若发生服务器端错误(如 500 内部服务器错误等 HTTP 状态码),将执行以下函数:


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

要查看此类错误,请按以下方式修改 [postForm] 函数:


function postForm() {
    console.log("postForm");
    // retrieve references to the current page
    ...
    // make a manual Ajax call
    $.ajax({
        url : '/ajax-10x',
        ...
    })
}
  • 第 7 行:我们输入了一个不存在的 URL;

点击 [刷新] 按钮后的结果如下:

值得注意的是,该错误也以 JSON 字符串的形式发送了过来。

收到服务器响应后执行的方法如下:


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

我们只是隐藏了动画加载图片。

7.5.7. 显示 [第 2 页] 页面

[提交]链接的HTML代码如下:


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

JS 函数 [validate] 如下:


// validation of entered values
function valider() {
    // posted value
    var post = JSON3.stringify({
        "value1" : $("#text1").val().trim(),
        "value2" : $("#text2").val().trim()
    });
    // make a manual Ajax call
    $.ajax({
        url : '/ajax-11A',
        headers : {
            'Accept' : 'application/json',
            'Content-Type' : 'application/json'
        },
        type : 'POST',
        data : post,
        dataType : 'json',
        beforeSend : onBegin,
        success : onSuccess,
        error : onError,
        complete : onComplete
    })
}
  • 第 4-7 行:我们有两个值 v1 和 v2 需要提交:它们来自标识为 [#text1] 和 [#text2] 的输入组件。我们将尝试一种新方法。我们将把这两个值作为 JSON 字符串 {"value1":v1,"value2":v2} 进行提交;
  • 第 10 行:这些 POST 值将被发送至 [ajax-11A] 操作;
  • 第 12 行:由于我们知道将收到 JSON 响应,因此指定服务器支持接收 JSON;
  • 第 13 行:告知服务器我们将以 JSON 字符串的形式发送提交的值;
  • 第 15-16 行:我们通过 POST 方法发送待传输的值;
  • 第 17 行:我们将接收 JSON 响应;

7.5.8. [ajax-11A] 操作

处理提交的 JSON 字符串的 [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) {
        ...
    }
  • 第 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 {
 
    // data
    @Size(min = 4, max = 6)
    @NotNull
    private String value1;
    @Range(min = 10, max = 14)
    @NotNull
    private Integer value2;
 
    // getters and setters
    ...
}
  • [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 context
        WebContext thymeleafContext = new WebContext(request, response, request.getServletContext());
        // answer
        JsonResult10 result = new JsonResult10();
        // valid post?
        if (bindingResult.hasErrors()) {
            // page 1 is returned with an error
            result.setZone1Active(session.isZone1Active());
            result.setZone3Active(session.isZone3Active());
            result.setErreur(getErreursForModel(bindingResult));
            return result;
        }
        ...
}
  • 第 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 context
        WebContext thymeleafContext = new WebContext(request, response, request.getServletContext());
        // answer
        JsonResult10 result = new JsonResult10();
        // valid post?
        if (bindingResult.hasErrors()) {
    ...
        }
        // the input field is saved
        thymeleafContext.setVariable("value1", post.getValue1());
        thymeleafContext.setVariable("value2", post.getValue2());
        session.setSaisies(engine.process("vue-09-saisies", thymeleafContext));
        // send page 2
        result.setContent(engine.process("vue-09-page2", thymeleafContext));
        return result;
}
  • 第 13-14 行:将表单提交的值放入 Thymeleaf 上下文中;
  • 第 15 行:利用此上下文,我们计算视图 [vue-09-saisies] 并将其存储在会话中,以便稍后重新生成;
  • 第 17 行:将第 2 页放入将发送给客户端的结果中;

视图 [view-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 行显示了操作 [/ajax-11A] 放置在 Thymeleaf 上下文中的值 [value1, value2];

7.5.9. 处理来自 [/ajax-11A] 操作的响应

在客户端,来自 [/ajax-10] 操作的响应由 [onSuccess] 函数进行处理:


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

我们已经对这段代码进行了说明。让我们来考虑两种情况:带有错误的响应和不带错误的响应:

包含错误

在此情况下,[/ajax-11A] 操作发送了一条格式为 {"zone1":null, "zone3":null,"saisies":null,"erreur":erreur,"zone1Active":zone1Active,"zone3Active":zone3Active,"content":null} 的 JSON 响应。如果我们参照上面的代码,会发现:

  • [content] 字段未发生变化。其内容为第 1 页;
  • [Error] 字段被显示出来;
  • [Zone 1]、[Zone 3] 和 [Entries] 字段保持不变;

无错误

在此情况下,[/ajax-11A] 操作发送的 JSON 响应格式为 {"zone1":null, "zone3":null,"saisies":null,"erreur":null,"zone1Active":false,"zone3Active":false,"content":content}。根据上述代码,我们可以看到:

  • [content] 字段被显示出来。其中包含第 2 页;

以下是三个执行示例:

一个包含验证错误的案例:

出现POST错误的案例:

这种错误有所不同。由于 Spring 无法将 JSON 字符串转换为 [PostAjax11A] 类型,因此返回了一个 [status=400] 的 HTTP 响应。[ajax-11A] 操作未被执行;

无错误的情况:

7.5.10. 返回第1页

第2页上的[返回第1页]链接如下:


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

JS方法 [returnPage1] 如下:


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

它向 [/ajax-11B] 操作发送一个 POST 请求,且不包含任何提交的数据。

7.5.11. [/ajax-11B] 操作

[/ajax-11B] 操作如下:


    @RequestMapping(value = "/ajax-11B", method = RequestMethod.POST)
    @ResponseBody
    public JsonResult10 ajax11B(HttpServletRequest request, HttpServletResponse response) {
        // thymeleaf context
        WebContext thymeleafContext = new WebContext(request, response, request.getServletContext());
        // answer
        JsonResult10 result = new JsonResult10();
        // we return page 1 in its original state
        result.setContent(engine.process("vue-09-page1", thymeleafContext));
        result.setSaisies(session.getSaisies());
        result.setZone1(session.getZone1());
        result.setZone3(session.getZone3());
        result.setZone1Active(session.isZone1Active());
        result.setZone3Active(session.isZone3Active());
        return result;
}

该操作必须重新生成第 1 页及其三个区域 [Zone1、Zone3、Error]:

  • 第 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");
    // content
    if (data.content) {
        content.html(data.content);
    }
    // zone 1
    if (data.zone1Active) {
        $("#zone1").show();
        if (data.zone1) {
            $("#zone1-content").html(data.zone1);
        }
    } else {
        $("#zone1").hide();
    }
    // zone 3 active?
    if (data.zone3Active) {
        $("#zone3").show();
        if (data.zone3) {
            $("#zone3-content").html(data.zone3);
        }
    } else {
        $("#zone3").hide();
    }
    // seized?
    if (data.saisies) {
        $("#saisies").html(data.saisies);
    }
    // mistake?
    if (data.erreur) {
        erreur.text(data.erreur);
        erreur.show();
    } else {
        erreur.hide();
    }
}

操作 [/ajax-11B] 发送了一条格式为 {"zone1":zone1, "zone3":zone3,"saisies":saisies,"erreur":null,"zone1Active":zone1Active,"zone3Active":zone3Active,"content":content} 的 JSON 响应。如果我们按照上面的代码,可以看到:

  • [content]字段已被修改。它之前包含第2页,现在将包含第1页;
  • [错误] 字段被隐藏;
  • [区域 1]、[区域 3] 和 [条目] 区域按原样显示;

7.6. 在客户端管理会话

7.6.1. 简介

在前一节中,我们管理了一个具有以下结构的会话:


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

当用户数量庞大时,所有用户会话占用的内存可能会成为问题。因此,原则是尽量缩小该内存的占用规模。SPV(单页应用程序)模型允许您在客户端管理会话,并使用无会话的 Web 服务器。 实际上,单页应用最初由浏览器加载,同时加载的还有配套的 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] 完全相同,仅第 9 行使用的 JS 脚本不同;

显示的视图如下:

 

7.6.3. [刷新]按钮的JS代码

  

[local12.js] 文件中的代码如下:


// global variables
var content;
var loading;
var erreur;
var page1;
var page2;
var value1;
var value2;
var session = {
        "cpt1" : 0,
        "cpt3" : 0
    };
 
// document loading
$(document).ready(function() {
    // retrieve the references of the page's various components
    loading = $("#loading");
    loading.hide();
    erreur = $("#erreur");
    erreur.hide();
    content = $("#content");
});
  • 第 17–21 行:当主页面加载时,第 2–4 行将由 [loading, error, content] 标识的三个组件的引用存储在全局变量中;
  • 第 5–6 行:用于存储这两个页面;
  • 第 7–8 行:用于存储通过 [Validate] 链接提交的两个值;
  • 第9行:会话。它在客户端存储计数器 [cpt1, cpt3] 的值;

[postForm] 函数负责处理 [Refresh] 按钮的点击事件:


function postForm() {
    console.log("postForm");
    // we post the session
    var post = JSON3.stringify(session);
    // make a manual Ajax call
    $.ajax({
        url : '/ajax-13',
        headers : {
            'Accept' : 'application/json',
            'Content-Type' : 'application/json'
        },
        type : 'POST',
        data : post,
        dataType : 'json',
        beforeSend : onBegin,
        success : function(data) {
            ...
        },
        error : onError,
        complete : onComplete
    })
}

与上一版本的区别如下:

  • 第 7 行的 URL 不同;
  • 第 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;
    // two meters
    private int cpt1 = 0;
    private int cpt3 = 0;
 
    // getters and setters
    ...
}

[SessionModel2] 会话存储以下内容:

  • 第 9 行:[Zone 1] 区域显示 [cpt1] 次;
  • 第 10 行:[Zone 3] 区域显示的次数 [cpt3];

让我们继续查看 [/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 {
 
    // data
    private String page2;
    private String zone1;
    private String zone3;
    private String erreur;
    private String value1;
    private Integer value2;
 
    // session
    private SessionModel2 session;
 
    // getters and setters
    ...
}
  • 第 14 行:会话。服务器将其发回客户端进行存储;
  • 第 6 行:第 2 页的 HTML 内容;
  • 第 7 行:[Zone 1] 区域的 HTML 内容;
  • 第 8 行:[Zone 3] 区域的 HTML 内容;
  • 第 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 context
        WebContext thymeleafContext = new WebContext(request, response, request.getServletContext());
        // answer
        JsonResult13 result = new JsonResult13();
        result.setSession(session2);
        // randomize an answer
        int cas = new Random().nextInt(3);
        switch (cas) {
        case 0:
            // zone 1 active
            setZone1B(thymeleafContext, result);
            return result;
        case 1:
            // zone 3 active
            setZone3B(thymeleafContext, result);
            return result;
        case 2:
            // active zones 1 and 3
            setZone1B(thymeleafContext, result);
            setZone3B(thymeleafContext, result);
            return result;
        }
        return null;
    }
  • 第 9 行:会话被放入操作的结果中;

激活 [Zone 1] 区域的 [setZone1B] 方法如下:


    private void setZone1B(WebContext thymeleafContext, JsonResult13 result) {
        // retrieve the session
        SessionModel2 session = result.getSession();
        // zone 1 active
        // flow HTML
        int cpt1 = session.getCpt1() + 1;
        thymeleafContext.setVariable("cpt1", cpt1);
        thymeleafContext.setLocale(new Locale("fr", "FR"));
        String zone1 = engine.process("vue-09-zone1", thymeleafContext);
        result.setZone1(zone1);
        // session
        session.setCpt1(cpt1);
}
  • 第 3 行:我们获取会话。该会话将在第 12 行使用新的计数器 [cpt1] 进行修改。请注意,该会话将被发回给客户端;
  • 第 10 行:新的区域 [Zone 1];

激活区域 [Zone 3] 的方法 [setZone3B] 类似:


private void setZone3B(WebContext thymeleafContext, JsonResult13 result) {
        // retrieve the session
        SessionModel2 session = result.getSession();
        // zone 3 active
        // flow HTML
        int cpt3 = session.getCpt3() + 1;
        thymeleafContext.setVariable("cpt3", cpt3);
        thymeleafContext.setLocale(new Locale("en", "US"));
        String zone3 = engine.process("vue-09-zone3", thymeleafContext);
        result.setZone3(zone3);
        // session
        session.setCpt3(cpt3);
    }

7.6.5. 处理来自 [/ajax-13] 操作的响应

在客户端,来自 [/ajax-13] 操作的 JSON 响应由以下 [onSuccess] 函数进行处理:


function postForm() {
    console.log("postForm");
    // we post the session
    var post = JSON3.stringify(session);
    // make a manual Ajax call
    $.ajax({
    ...
        success : function(data) {
            // save the session
            session = data.session;
            // update both zones
            if (data.zone1) {
                $("#zone1-content").html(data.zone1);
                $("#zone1").show();
            } else {
                $("#zone1").hide();
            }
            if (data.zone3) {
                $("#zone3").show();
                $("#zone3-content").html(data.zone3);
            } else {
                $("#zone3").hide();
            }
        },
...
    })
}
  • 第 12–17 行:如果服务器在响应的 [zone1] 字段中填充了内容,则必须重新生成并显示 [Zone 1] 区域;否则,必须将其隐藏;
  • 第 18–23 行:[Zone 3] 区域也适用相同的逻辑;

7.6.6. 显示 [第 2 页] 页面

[提交]链接的HTML代码如下:


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

JS 函数 [validate] 如下:


// validation of entered values
function valider() {
    // memorize page 1
    page1 = content.html();
    // store the values entered
    value1 = $("#text1").val().trim();
    value2 = $("#text2").val().trim();
    // posted value
    var post = JSON3.stringify({
        "value1" : value1,
        "value2" : value2,
        "pageRequired" : page2 ? false : true
    });
    // make a manual Ajax call
    $.ajax({
        url : '/ajax-14',
        headers : {
            'Accept' : 'application/json',
            'Content-Type' : 'application/json'
        },
        type : 'POST',
        data : post,
        dataType : 'json',
        beforeSend : onBegin,
        success : function(data) {
        ...
        },
        error : onError,
        complete : onComplete
    })
}
  • 我们将发送一个 POST 请求,这通常会将我们带到第 2 页;
  • 第 4 行:我们保存第 1 页,以便稍后返回;
  • 第6–7行:前面的操作并未保存输入的值,仅保存了页面的HTML代码。因此现在我们需要保存表单中输入的两个值;
  • 第9–13行:将这两个输入值放入一个JSON字符串中。这就是即将被提交的内容;
  • 第 12 行:一个参数用于告知服务器是否需要第 2 页。我们将按以下方式操作:先请求一次第 2 页,然后将其存储在 JS 变量 `[page2]` 中。此后,我们将不再重新请求该页面,而是使用缓存的页面。第 2 行:如果变量 `[page2]` 为空,则 `[pageRequired]` 为 `true`,否则为 `false`;
  • 请注意,会话数据并未被提交。实际上,它存储的计数器不会被第20行的 [/ajax-14] 操作修改;

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 {
 
    // page 2
    private boolean pageRequired;
 
    // getters and setters
    ...
}
  • 第 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 context
        WebContext thymeleafContext = new WebContext(request, response, request.getServletContext());
        // answer
        JsonResult13 result = new JsonResult13();
        // valid post?
        if (bindingResult.hasErrors()) {
            // an error is returned
            result.setErreur(getErreursForModel(bindingResult));
            return result;
        }
        // send page 2
        result.setValue1(post.getValue1());
        result.setValue2(post.getValue2());
        // page required?
        if (post.isPageRequired()) {
            result.setPage2(engine.process("vue-12-page2", thymeleafContext));
        }
        return result;
}
  • 第 9–13 行:如果提交的值 [value1, value2] 无效,则返回一条错误消息;
  • 第 15–16 行:通常,服务器应使用提交的值进行计算。此处仅将其返回,以表明已收到这些值;
  • 第 18–20 行:仅当客户端请求时才返回第 2 页。第 19 行,视图 [view-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] 函数进行处理:


// validation des valeurs saisies
function valider() {
    ...
    // on fait un appel Ajax à la main
    $.ajax({
        ...
        success : function(data) {
            // erreur ?
            if (data.erreur) {
                // affichage erreur
                erreur.html(data.erreur);
                erreur.show();
            } else {
                // pas d'erreur
                erreur.hide();
                // page 2
                if (page2) {
                    // on utilise la page en cache
                    content.html(page2);
                } else {
                    // on mémorise la page 2
                    page2 = data.page2;
                    // on l'affiche
                    content.html(data.page2);
                }
                // on la met à jour avec les infos du serveur
                $("#value1").text(data.value1);
                $("#value2").text(data.value2);
            }
        },
...
    })
}
  • 第 9–13 行:如果服务器返回了错误,则显示该错误;
  • 第 14–29 行:无错误的情况。此时必须显示第 2 页;
  • 第 17 行:检查变量 [page2] 中是否已存储第 2 页;
  • 第 19 行:若已存储,则使用变量 [page2] 显示第 2 页;
  • 第 24 行:否则,使用服务器提供的 [data.page2] 字段;
  • 第 22 行:确保将第 2 页存储起来,以免后续再次请求;
  • 第27–28行:在第2页上,显示服务器发送的两条信息 [value1, value2];

7.6.9. 返回第 1 页

第 2 页上的 [返回第 1 页] 链接如下:


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

JS方法 [returnPage1] 如下:


// back to page 1
function retourPage1() {
    // regenerate page 1
    content.html(page1);
    // regenerate foreclosures
    $("#text1").val(value1);
    $("#text2").val(value2);
}
  • 这是一个不与服务器交互的 JavaScript 操作,因为页面 1 已被保存在变量 [page1] 中;
  • 第 4 行:我们重新加载第 1 页;
  • 第6-7行:仅缓存了第1页的HTML部分,未缓存用户输入。因此我们必须重新加载用户输入;

7.6.10. 结论

通过利用 APU 模型的功能,我们成功简化了 Web 服务器,使其现在成为无状态(无会话)且负载较轻的系统:

  • 我们移除了JS函数[returnPage1]中与服务器的交互;
  • 服务器仅生成一次第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] 实现 [presentation] 层,
    • [local-dao] 实现 [DAO] 层;
  

7.7.3. [DAO] 层的实现

7.7.4. 接口

[local-dao.js] 中的 [DAO] 层将向 [展示] 层提供以下接口:


function updatePage1(deferred, sendMeBack)
用于通过 [Refresh] 按钮更新第 1 页

function getPage2(deferred, sendMeBack, value1, value2, pageRequired)
通过 [提交] 按钮显示第 2 页

JavaScript 没有接口的概念。我使用这个术语只是为了表明,[展示]层同意仅通过上述两个函数与[DAO]层进行通信。

7.7.5. 接口的实现

实现的骨架如下:


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

[DAO] 层的目的是将向 Web 服务器发出的 HTTP 请求的细节隐藏起来,使其不被 [presentation] 层所知。会话是这些细节的一部分。因此,会话现在由 [DAO] 层进行管理。

7.7.5.1. [updatePage1] 函数

[updatePage1] 函数是由 [presentation] 层调用以刷新第 1 页的函数。其代码如下:


// update Page 1
function updatePage1(deferred, sendMeBack) {
    // requête HTTP
    executePost(deferred, sendMeBack, '/ajax-13', session);
}
  • 第 1 行:[updatePage1] 函数接收两个参数:
    1. 一个 [jQuery.Deferred] 类型的对象。此类对象存储的状态可能有三个值:['pending'、'resolved'、'rejected']。当它传入 [updatePage1] 函数时,处于 [pending] 状态;
    2. 一个将返回给 [presentation] 层的 JS 对象;

所有 HTTP 请求均由以下 [executePost] 函数发起:


// requête HTTP
function executePost(deferred, sendMeBack, url, post) {
    // on fait un appel Ajax à la main
    $.ajax({
        headers : {
            'Accept' : 'application/json',
            'Content-Type' : 'application/json'
        },
        url : url,
        type : 'POST',
        data : JSON3.stringify(post),
        dataType : 'json',
        success : function(data) {
            // on mémorise la session
            if (data.session) {
                session = data.session;
            }
            // on rend le résultat
            deferred.resolve({
                "status" : 1,
                "data" : data,
                "sendMeBack" : sendMeBack
            });
        },
        error : function(jqXHR) {
            // on rend l'erreur
            deferred.resolve({
                "status" : 2,
                "data" : jqXHR.responseText,
                "sendMeBack" : sendMeBack
            });
        }
    });
}
  • 第 1 行:[executePost] 函数执行一个 POST 类型的 Ajax 调用。它期望四个参数:
    1. 一个处于 [pending] 状态的 [jQuery.Deferred] 对象;
    2. 一个将返回给 [presentation] 层的 JS 对象;
    3. POST 请求的 URL;
    4. 作为 JavaScript 对象发送的值;
  • 第 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] 函数如下:


// page 2
function getPage2(deferred, sendMeBack, value1, value2, pageRequired) {
    // requête HTTP
    executePost(deferred, sendMeBack, '/ajax-14', {
        "value1" : value1,
        "value2" : value2,
        "pageRequired" : pageRequired,
    });
}
  • 该函数接收以下参数:
    1. [deferred]:处于 [pending] 状态的 [jQuery.Deferred] 类型对象,
    2. [sendMeBack]:一个将返回给 [presentation] 层的 JS 对象,
    3. [value1]:第 1 页上的第一个输入项,
    4. [value2]:第 2 页上的第二个输入字段,
    5. [pageRequired]:一个布尔值,用于指示服务器是否发送第 2 页的 HTML 数据流;
  • 调用 [executePost] 函数以执行必要的 HTTP 请求;

7.7.6. [呈现]层

[presentation] 层由 [local-ui.js] 文件实现。该文件复用了 [local12.js] 文件中的代码,并进行了修改以调用前面的 [DAO] 层。仅有两个函数发生了变化:[postForm] 和 [valider]。

7.7.6.1. [postForm] 函数

[postForm] 函数如下:


// update Page 1
function postForm() {
    // on met à jour la page 1
    var deferred = $.Deferred();
    loading.show();
    updatePage1(deferred, {
        'sender' : "postForm",
        'info' : 10
    });
    // affichage résultats
    deferred.done(postFormDone);
}
  • 第 4 行:我们创建了一个 [jQuery.Deferred] 对象。默认情况下,它处于 [pending] 状态;
  • 第 5 行:显示加载图片
  • 第 6–9 行:执行 [updatePage1] 函数。我们传递一个虚拟的 [sendMeBack] 对象,仅用于演示其用途;
  • 第 11 行:[deferred.done] 函数的参数本身是一个函数。当 [deferred] 对象的状态变为 [resolved] 时,将执行此函数。 我们刚才看到,DAO 函数 [executePost] 在收到服务器响应后,将该对象的状态设置为 [resolved]。这意味着当 [postFormDone] 函数执行时,服务器响应已经收到;

[postFormDone] 函数如下:


function postFormDone(result) {
    // end waiting
    loading.hide();
    // data recovery
    var data = result.data
    // for demo
    console.log(JSON3.stringify(result.sendMeBack));
    // status analysis
    switch (result.status) {
    case 1:
        // update both zones
        if (data.zone1) {
            $("#zone1-content").html(data.zone1);
            $("#zone1").show();
        } else {
            $("#zone1").hide();
        }
        if (data.zone3) {
            $("#zone3").show();
            $("#zone3-content").html(data.zone3);
        } else {
            $("#zone3").hide();
        }
        break;
    case 2:
        // error display
        erreur.html(data);
        break;
    }
}
  • 第 1 行:接收到的 [result] 参数是传递给 [executePost] 函数中 [deferred.resolve] 方法的参数,例如:

            // on rend le résultat
            deferred.resolve({
                "status" : 1,
                "data" : data,
                "sendMeBack" : sendMeBack
});
  • 第 5 行:我们从服务器获取响应;
  • 第 10–24 行:这是在旧版本中位于 [postForm] 函数的 [onSuccess] 函数内的代码;
  • 第 25–28 行:这是之前位于 [postForm] 函数的 [onError] 函数中的代码;

7.7.6.2. [sendMeBack] 参数的作用

[sendMeBack] 参数用于什么?让我们看看调用 [updatePage1] 函数的代码:


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

以及 [validerDone] 函数的签名:


function postFormDone(result) {
}

[postForm] 函数如何将信息传递给 [postFormDone] 函数? 后者只有一个参数,即 [result]。该参数由 [DAO] 层中的 [executePost] 函数生成。要将信息传递给 [postFormDone] 函数,[postForm] 函数必须先将其传递给 [updatePage1] 函数。这就是 [sendMeBack] 参数的作用。其用法如下:


function postFormDone(result) {
    // end waiting
    loading.hide();
    // data recovery
    var data = result.data
    // for demo
    console.log(JSON3.stringify(result.sendMeBack));
    // status analysis
    switch (result.status) {
...
  • 第 7 行,[postFormDone] 函数已获取 [postForm] 函数最初传递给 DAO 函数 [updatePage1] 的 [sendMeBack] 参数;

7.7.7. [valider] 函数

[valider] 函数如下:


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

以及 [validerDone] 函数(第 18 行)如下:


function validerDone(result) {
    // end waiting
    loading.hide();
    // data recovery
    var data = result.data
    // for demo
    console.log(JSON3.stringify(result.sendMeBack));
    // status analysis
    switch (result.status) {
    case 1:
        // mistake?
        if (data.erreur) {
            // error display
            erreur.html(data.erreur);
            erreur.show();
        } else {
            // no error
            erreur.hide();
            // page 2
            if (page2) {
                // use the cached page
                content.html(page2);
            } else {
                // memorize page 2
                page2 = data.page2;
                // we display it
                content.html(data.page2);
            }
            // we update it with server info
            $("#value1").text(data.value1);
            $("#value2").text(data.value2);
        }
        break;
    case 2:
        // error display
        erreur.html(data);
        erreur.show();
        break;
    }
}
  • 第 5 行:我们从服务器获取响应;
  • 第 10–32 行:这是在旧版本中位于 [validate] 函数的 [onSuccess] 函数内的代码;
  • 第 34–38 行:这是之前位于 [validate] 函数的 [onError] 函数中的代码;

7.7.8. 测试

应用程序仍像以前一样正常运行,在 Chrome 控制台中,你可以看到 [postForm] 和 [validate] 函数的 [sendMeBack] 参数:

 

7.8. 结论

让我们回到 Spring MVC 应用程序的总体架构:

得益于嵌入在 HTML 页面中并在浏览器中执行的 JavaScript,以及 APU 模型,我们可以将代码卸载到浏览器,从而实现以下架构:

  • 我们采用客户端 [2] / 服务器 [1] 架构,其中客户端与服务器通过 JSON 进行通信;
  • 在 [1] 中,Spring MVC Web 层以 JSON 格式提供视图、视图片段和数据;
  • 在[2]中:应用程序启动时加载的视图中嵌入的 JavaScript 代码可以分层组织:
    • [展示]层处理用户交互,
    • [DAO] 层通过 Web 服务器 [1] 处理数据访问,
    • [业务]层可能不存在,也可能从服务器的[业务]层接管某些非机密功能以减轻服务器负担;
  • 客户端[2]可缓存特定视图以进一步减轻服务器负担。它负责管理会话;