7. Spring 应用程序 MVC 的 Ajax 化
7.1. AJAX在Web应用中的作用
目前,所研究的机器学习示例具有以下架构:
![]() |
要从视图 [Vue1] 切换到视图 [Vue2],浏览器:
- 向 Web 应用程序发送请求;
- 接收视图 [Vue2],并将其显示在原视图 [Vue1] 的位置。
这是典型的流程:
- 浏览器发起请求;
- Web 服务器生成视图并作为响应发送给客户端;
- 浏览器显示该新视图。
近年来,浏览器与Web服务器之间出现了一种新的交互模式:AJAX(异步JavaScript和XML)。这实际上是浏览器显示的视图与Web服务器之间的交互。 浏览器仍继续执行其本职工作——显示视图,但现在它由显示的视图中嵌入的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) {
// 时间有效吗?
if (tempo != null) {
boolean valide = false;
int valueTempo = 0;
try {
valueTempo = Integer.parseInt(tempo);
valide = valueTempo >= 0;
} catch (NumberFormatException e) {
}
if (valide) {
session.setAttribute("tempo", new Integer(valueTempo));
}
}
// 准备视图模板 [vue-01]
...
}
- 第 2 行:操作 [/ajax-01] 仅接受一个参数 [tempo]。该参数表示服务器在发送算术运算结果前需等待的时间(以毫秒为单位);
- 第 4 行:参数 [tempo] 为可选参数;
- 第 5-12 行:验证参数 [tempo] 的值是否有效;
- 第13-15行:若验证通过,则将延迟值保存至会话中。这意味着该值将一直有效,除非被修改;
操作代码 [/ajax-01] 后续内容如下:
@RequestMapping(value = "/ajax-01", method = RequestMethod.GET, produces = "text/html; charset=UTF-8")
public String ajax01(Locale locale, Model modèle, HttpSession session, String tempo) {
// 时间段有效吗?
...
// 正在准备视图模板 [vue-01]
modèle.addAttribute("actionModel01", new ActionModel01());
...
// 视图
return "vue-01";
}
类 [ActionModel01] 主要用于封装由操作 [/ajax-01] 提交的值。此处未提交任何内容。 我们创建了一个空类并将其放入模型中,因为视图 [vue-01.xml] 会使用它。类 [ActionModel01] 如下所示:
package istia.st.springmvc.models;
import javax.validation.constraints.DecimalMin;
import javax.validation.constraints.NotNull;
public class ActionModel01 {
// 提交的数据
@NotNull
@DecimalMin(value = "0.0")
private Double a;
@NotNull
@DecimalMin(value = "0.0")
private Double b;
// 获取器和设置器
...
}
- 第 11 行和第 15 行:两个实数 [a,b],将通过表单提交;
让我们回到操作代码:
@RequestMapping(value = "/ajax-01", method = RequestMethod.GET, produces = "text/html; charset=UTF-8")
public String ajax01(Locale locale, Model modèle, HttpSession session, String tempo) {
...
// 正在准备视图 [vue-01] 的模型
modèle.addAttribute("actionModel01", new ActionModel01());
Resultats résultats = new Resultats();
modèle.addAttribute("resultats", résultats);
...
// 视图
return "vue-01";
}
- 第 6-7 行:在模型中放置一个类型为 [Resultats] 的实例;
放入模型中的 [Resultats] 类型如下:
![]() |
package istia.st.springmvc.models;
public class Resultats {
// 数据
private String aplusb;
private String amoinsb;
private String amultiplieparb;
private String adiviseparb;
private String heureGet;
private String heurePost;
private String erreur;
private String vue;
private String culture;
// getter 和 setter
...
}
- 第6-9行:对数字[a,b]进行四则运算的结果;
- 第10行:页面首次加载的时间;
- 第11行:四则运算的执行时间;
- 第12行:可能出现的错误信息;
- 第13行:需显示的视图(如有);
- 第 14 行:视图的布局,[fr-FR] 或 [en-US];
操作代码 [/ajax-01] 后续内容如下:
@RequestMapping(value = "/ajax-01", method = RequestMethod.GET, produces = "text/html; charset=UTF-8")
public String ajax01(ActionModel01 formulaire, Locale locale, Model modèle, HttpSession session) {
...
// 区域设置
setLocale(locale, modèle, résultats);
...
}
- 第 5 行:方法 [setLocale] 用于将要使用的区域设置([fr-FR] 或 [en-US])放入视图模板中。该区域设置供视图中嵌入的 JavaScript 使用;
方法 [setLocale] 如下:
private void setLocale(Locale locale, Model modèle, Resultats résultats) {
// 仅支持 fr-FR 和 en-US 区域设置
String language = locale.getLanguage();
String country = null;
switch (language) {
case "fr":
country = "FR";
break;
default:
language = "en";
country = "US";
break;
}
// 文化
résultats.setCulture(String.format("%s-%s", language, country));
}
在视图模板中,字符串 [${resultats.culture}] 的值为 'fr-FR' 或 'en-US'。
让我们回到 [/ajax-01] 操作:
@RequestMapping(value = "/ajax-01", method = RequestMethod.GET, produces = "text/html; charset=UTF-8")
public String ajax01(ActionModel01 formulaire, Locale locale, Model modèle, HttpSession session) {
...
// 区域设置
setLocale(locale, modèle, résultats);
// 时间
résultats.setHeureGet(new SimpleDateFormat("hh:mm:ss").format(new Date()));
// 视图
return "vue-01";
}
- 第 7 行:将 GET 的时间填入模板;
- 第 9 行:显示视图 [vue-01.xml]:
7.2.3. 视图 [vue-01.xml]
![]() | ![]() |
视图 [vue-01.xml] 如下所示:
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta name="viewport" content="width=device-width" />
<title>Ajax-01</title>
<link rel="stylesheet" href="/css/ajax01.css" />
<script type="text/javascript" src="/js/jquery/jquery-2.1.1.min.js"></script>
<script type="text/javascript" src="/js/jquery/jquery.validate.min.js"></script>
<script type="text/javascript" src="/js/jquery/jquery.validate.unobtrusive.min.js"></script>
<script type="text/javascript" src="/js/jquery/globalize/globalize.js"></script>
<script type="text/javascript" src="/js/jquery/globalize/cultures/globalize.culture.fr-FR.js"></script>
<script type="text/javascript" src="/js/jquery/globalize/cultures/globalize.culture.en-US.js"></script>
<script type="text/javascript" src="/js/jquery/jquery.unobtrusive-ajax.js"></script>
<script type="text/javascript" src="/js/json3.js"></script>
<script type="text/javascript" src="/js/client-validation.js"></script>
<script type="text/javascript" src="/js/local1.js"></script>
<script th:inline="javascript">
/*<![CDATA[*/
var culture = [[${resultats.culture}]];
Globalize.culture(culture);
/*]]>*/
</script>
</head>
<body>
<h2>Ajax - 01</h2>
<p>
<strong th:text="#{labelHeureGetCulture(${resultats.heureGet},${resultats.culture})}">
Heure de chargement :
</strong>
</p>
<h4>
<p th:text="#{titre.part1}">
Opérations arithmétiques sur deux nombres réels A et B positifs ou nuls
</p>
</h4>
<form id="formulaire" name="formulaire" ... ">
...
</form>
<hr />
<div id="resultats" />
</body>
</html>
- 第 7-12 行:用于验证和国际化(文化)的库 jQuery;
- 第 15 行:在第 6.3 节中构建的库 [client-validation];
- 第14行:jSON库,由[client-validation]库调用。若已禁用验证日志,则该库为可选;
- 第 13 行:Microsoft 的 [Unobtrusive Ajax] 库。该库有时可避免编写 JavaScript 代码;
- 第 16 行:用于我们自身需求的 jS 文件;
- 第 17-22 行:用于在客户端管理区域设置的 [fr-FR] 和 [en-US]。我们之前已经接触过这段代码;
- 第 27 行:一个带参数的消息。我们在第 5.18 节中已对其进行了研究;
- 第36-38行:我们将稍后讨论的表单;
- 第40行:文档中的区域,JavaScript将在此处放置服务器的响应;
7.2.4. 表单
![]() |
在视图 [vue-01.xml] 中,表单如下:
<form id="formulaire" name="formulaire" th:action="@{/ajax-02.html}" method="post" th:object="${actionModel01}" th:attr="data-ajax='true',data-ajax-loading='#loading',data-ajax-loading-duration='0',data-ajax-method='post',data-ajax-mode='replace',data-ajax-update='#resultats', data-ajax-begin='beforeSend',data-ajax-complete='afterComplete' ">
<table>
<thead>
<tr>
<th>
<span th:text="#{valeur.a}"></span>
</th>
<th>
<span th:text="#{valeur.b}"></span>
</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<input type="text" th:field="*{a}" th:value="*{a}" data-val="true"
th:attr="data-val-required=#{NotNull},data-val-number=#{typeMismatch},data-val-min=#{actionModel01.a.min},data-val-min-value=#{actionModel01.a.min.value}" />
</td>
<td>
<input type="text" th:field="*{b}" th:value="*{b}" data-val="true"
th:attr="data-val-required=#{NotNull},data-val-number=#{typeMismatch},data-val-min=#{actionModel01.b.min},data-val-min-value=#{actionModel01.b.min.value}" />
</td>
</tr>
<tr>
<td>
<span class="field-validation-valid" data-valmsg-for="a" data-valmsg-replace="true"></span>
<span th:if="${#fields.hasErrors('a')}" th:errors="*{a}" class="error">Donnée
erronée
</span>
</td>
<td>
<span class="field-validation-valid" data-valmsg-for="b" data-valmsg-replace="true"></span>
<span th:if="${#fields.hasErrors('b')}" th:errors="*{b}" class="error">Donnée
erronée
</span>
</td>
</tr>
</tbody>
</table>
<p>
<input type="submit" th:value="#{action.calculer}" value="Calculer"></input>
<img id="loading" style="display: none" src="/images/loading.gif" />
<a href="javascript:postForm()" th:text="#{action.calculer}">Calculer</a>
</p>
</form>
该表单生成以下 HTML:
<form id="formulaire" name="formulaire" method="post" data-ajax-update="#resultats" data-ajax-complete="afterComplete" data-ajax-begin="beforeSend" data-ajax-loading-duration="0" data-ajax-mode="replace" data-ajax="true" data-ajax-method="post" data-ajax-loading="#loading" action="/ajax-02.html">
<table>
<thead>
<tr>
<th>
<span>valeur de A</span>
</th>
<th>
<span>valeur de B</span>
</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<input type="text" data-val="true" data-val-min="Le nombre doit être supérieur ou égal à 0" data-val-number="Format invalide" data-val-min-value="0" data-val-required="Le champ est obligatoire" value="" id="a" name="a" />
</td>
<td>
<input type="text" data-val="true" data-val-min="Le nombre doit être supérieur ou égal à 0" data-val-number="Format invalide" data-val-min-value="0" data-val-required="Le champ est obligatoire" value="" id="b" name="b" />
</td>
</tr>
<tr>
<td>
<span class="field-validation-valid" data-valmsg-for="a" data-valmsg-replace="true"></span>
</td>
<td>
<span class="field-validation-valid" data-valmsg-for="b" data-valmsg-replace="true"></span>
</td>
</tr>
</tbody>
</table>
<p>
<input type="submit" value="Calculer" />
<img id="loading" style="display: none" src="/images/loading.gif" />
<a href="javascript:postForm()">Calculer</a>
</p>
</form>
- 第 16 行:字段 [a] 关联了验证器 [required]、[number] 和 [min];
- 第 19 行:字段 [b] 亦同;
各种消息可在项目中的 [messages.properties] 文件中找到:
![]() |
[messages_fr.properties]
NotNull=Le champ est obligatoire
typeMismatch=Format invalide
actionModel01.a.min=Le nombre doit être supérieur ou égal à 0
DecimalMin.actionModel01.a=Le nombre doit être supérieur ou égal à 0
DecimalMax.actionModel01.b=Le nombre doit être supérieur ou égal à 0
actionModel01.b.min=Le nombre doit être supérieur ou égal à 0
valeur.a=valeur de A
valeur.b=valeur de B
actionModel01.a.min.value=0
actionModel01.b.min.value=0
labelHeureCalcul=Heure de calcul :
LabelErreur=Une erreur s''est produite : [{0}]
labelAplusB=A+B=
labelAmoinsB=A-B=
labelAfoisB=A*B=
labelAdivB=A/B=
titre.part1=Opérations arithmétiques sur deux nombres réels A et B positifs ou nuls
labelHeureGetCulture=Heure de chargement : [{0}], culture : [{1}]
action.calculer=Calculer
erreur.aleatoire=erreur aléatoire
resultats=Résultats
resultats.erreur=Une erreur s''est produite : [{0}]
resultats.titre=Résultats
message.zone=Nombre d'accès :
[messages_en.properties]
NotNull=Required field
typeMismatch=Invalid format
actionModel01.a.min=The number must be greater or equal to 0
DecimalMin.actionModel01.a=The number must be greater or equal to 0
DecimalMax.actionModel01.b=The number must be greater or equal to 0
actionModel01.b.min=The number must be greater or equal to 0
valeur.a=A value
valeur.b=B value
actionModel01.a.min.value=0
actionModel01.b.min.value=0
labelHeureCalcul=Computing hour:
LabelErreur=There was an error: [{0}]
labelAplusB=A+B=
labelAmoinsB=A-B=
labelAfoisB=A*B=
labelAdivB=A/B=
titre.part1=Arithmetic operations on two positive or equal to zero real numbers
labelHeureGetCulture=Loading hour: [{0}], culture: [{1}]
action.calculer=Calculate
erreur.aleatoire=randomly generated error
resultats=Results
resultats.erreur=Some error occurred : [{0}]
resultats.titre=Results
message.zone=Number of hits:
现在,让我们来研究 [form] 标签的属性:
<form id="formulaire" name="formulaire" method="post" data-ajax-update="#resultats" data-ajax-complete="afterComplete" data-ajax-begin="beforeSend" data-ajax-loading-duration="0" data-ajax-mode="replace" data-ajax="true" data-ajax-method="post" data-ajax-loading="#loading" action="/ajax-02.html">
我们可以识别出 [form] 标签的经典属性:
<form id="formulaire" name="formulaire" method="post" action="/ajax-02.html">
可以立即注意到,如果在显示该页面的浏览器中禁用了 JavaScript,则表单将提交至 URL [/ajax-02.html]。现在,让我们分析其他属性:
<form ... data-ajax-update="#resultats" data-ajax-complete="afterComplete" data-ajax-begin="beforeSend" data-ajax-loading-duration="0" data-ajax-mode="replace" data-ajax="true" data-ajax-method="post" data-ajax-loading="#loading">
[data-ajax-xxx] 属性由 jS [unobtrusive-ajax] 库管理,该库已被 [vue-01.xml] 视图导入:
<script type="text/javascript" src="/js/jquery/jquery.unobtrusive-ajax.js"></script>
当存在 [data-ajax-xxx] 属性时,表单中的 [submit] 将通过 [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"]:在执行 [submit] 之前需执行的 jS 函数;
- [data-ajax-complete="afterComplete"]:在收到响应时需执行的函数 jS;
- [data-ajax-update="#resultats"]:服务器发送的结果将被放置的字段标识符。视图 [vue-01.xml] 包含以下字段:
<div id="resultats" />
- [data-ajax-mode="replace"]:将结果插入前一个区域的模式。模式 [replace] 将导致结果“覆盖”ID 为 [resultats] 的区域中原本的内容;
需要注意的是,只有当验证器判定测试值有效时,[submit] JavaScript 才会执行。
jS [unobtrusive-ajax] 库有两个目的:
- 确保表单能正确适应两种情况:浏览器是否启用了 JavaScript;
- 避免编写 JavaScript 代码。不过我们稍后会看到,在此处无法完全避免。
7.2.5. [/ajax-02] 操作
我们已看到,提交的值会被发送至 [/ajax-02] 操作。该操作如下:
@RequestMapping(value = "/ajax-02", method = RequestMethod.POST, produces = "text/html; charset=UTF-8")
public String ajax02(ActionModel01 formulaire, Locale locale, Model modèle, HttpSession session) throws InterruptedException {
// 节奏?
Integer tempo = (Integer) session.getAttribute("tempo");
if (tempo != null && tempo > 0) {
Thread.sleep(tempo);
}
// 准备下一个视图的模板
Resultats résultats = new Resultats();
modèle.addAttribute("resultats", résultats);
// 设置本地化
setLocale(locale, modèle, résultats);
// 时间
résultats.setHeurePost(new SimpleDateFormat("hh:mm:ss").format(new Date()));
...
}
- 我们先进行简化:假设正在执行的 POST 确实是由视图 [vue-01.xml] 的 JavaScript 触发的。稍后我们将重新审视这一假设;
- 第2行:提交的[a,b]值被放入[ActionModel01]模板中;
- 第4-7行:如果用户在之前的GET中设置了延迟,该值将从会话中获取,并执行延迟操作(第6行)。 此操作旨在让用户在表单中看到 [data-ajax-loading="#loading"] 属性的效果;
- 第9-10行:在模板中添加[resultats]属性;
- 第12行:将区域设置[fr-FR]或[en-US]添加到模板中;
- 第14行:在模板中设置POST的时间;
回顾模板中设置的类型 [Resultats]:
public class Resultats {
// 数据
private String aplusb;
private String amoinsb;
private String amultiplieparb;
private String adiviseparb;
private String heureGet;
private String heurePost;
private String erreur;
private String vue;
private String culture;
// 获取器和设置器
...
}
操作代码 [/ajax-02] 如下所示:
@RequestMapping(value = "/ajax-02", method = RequestMethod.POST, produces = "text/html; charset=UTF-8")
public String ajax02(ActionModel01 formulaire, BindingResult result, Locale locale, Model modèle, HttpSession session) throws InterruptedException {
...
résultats.setHeurePost(new SimpleDateFormat("hh:mm:ss").format(new Date()));
// 每两次生成一次错误
int val = new Random().nextInt(2);
if (val == 0) {
// 返回一条错误消息
résultats.setErreur("erreur.aleatoire");
return "vue-03";
}
...
}
- 第6-11行:本示例演示如何向客户端返回错误页面 jS。每两次中,会返回以下视图 [vue-03.xml]:
![]() |
请注意第 9 行,这里放入模型的不是消息本身,而是消息键:
[messages_fr.properties]
erreur.aleatoire=erreur aléatoire
[messages_fr.properties]
erreur.aleatoire=randomly generated error
视图 [vue-03.xml] 的代码如下:
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<body>
<h4>Résultats</h4>
<p>
<strong>
<span th:text="#{labelHeureCalcul}">Heure de calcul :</span>
<span id="heureCalcul" th:text="${resultats.heurePost}"></span>
</strong>
</p>
<p style="color: red;">
<span th:text="#{LabelErreur(#{${resultats.erreur}})}">Une erreur s'est produite :</span>
<!-- <span id="erreur" th:text="${resultats.erreur}"></span> -->
</p>
</body>
</html>
操作代码 [/ajax-02] 继续如下:
@RequestMapping(value = "/ajax-02", method = RequestMethod.POST, produces = "text/html; charset=UTF-8")
public String ajax02(ActionModel01 formulaire, BindingResult result, Locale locale, Model modèle, HttpSession session) throws InterruptedException {
...
// 获取提交的值
double a = formulaire.getA();
double b = formulaire.getB();
// 构建模型
résultats.setAplusb(String.valueOf(a + b));
résultats.setAmoinsb(String.valueOf(a - b));
résultats.setAmultiplieparb(String.valueOf(a * b));
try {
résultats.setAdiviseparb(String.valueOf(a / b));
} catch (RuntimeException e) {
résultats.setAdiviseparb("NaN");
}
// 显示视图
return "vue-02";
}
- 第5-15行:对数字[a,b]进行四则运算,并将结果封装在模型的实例[Resultats]中;
- 第17行:返回以下视图 [vue-02.xml]:
![]() |
视图 [vue-02.xml] 如下所示:
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<body>
<h4>Résultats</h4>
<p>
<strong>
<span th:text="#{labelHeureCalcul}">Heure de calcul :</span>
<span id="heureCalcul" th:text="${resultats.heurePost}"></span>
</strong>
</p>
<p>
<span th:text="#{labelAplusB}">A+B=</span>
<span id="aplusb" th:text="${resultats.aplusb}"></span>
</p>
<p>
<span th:text="#{labelAmoinsB}">A-B=</span>
<span id="amoinsb" th:text="${resultats.amoinsb}"></span>
</p>
<p>
<span th:text="#{labelAfoisB}">A*B=</span>
<span id="amultiplieparb" th:text="${resultats.amultiplieparb}"></span>
</p>
<p>
<span th:text="#{labelAdivB}">A/B=</span>
<span id="adiviseparb" th:text="${resultats.adiviseparb}"></span>
</p>
</body>
</html>
无论结果是视图 [vue-02.xml] 还是视图 [vue-03.xml], 该结果 HTML 会被放置在视图 [vue-01.xml] 中由 [resultats] 标识的区域内,这是由于表单的 [data-ajax-update="#resultats"] 属性所致。
7.2.6. 已输入值的 POST
这里在提交值时遇到了一点困难。我们正在处理 [fr-FR] 和 [en-US] 这两种文化,它们对实数的写法不同。我们在第 191 页第 6.3 节中,需要将实数提交到两种不同的文化时,曾解决过这一难题。 我们将在此复用当时使用的工具。但现在面临一个额外难题:我们无法访问将输入值转换为 POST 格式的处理方法。因此,我们在表单标签中添加了以下属性:
- [data-ajax-begin="beforeSend"]:在执行 [submit] 之前需运行的 jS 函数;
- [data-ajax-complete="afterComplete"]:在收到响应时需执行的函数 jS;
我们无法访问负责提交输入值的 jS 函数,但我们可以编写两个 jS 函数:
- [beforeSend]:在 POST 之前执行的 jS 函数;
- [afterComplete]:在收到对 POST 的响应后执行的 jS 函数;
这两个函数被放置在文件 [local1.js] 中:
![]() |
文件 [local1.js] 以如下方式初始化视图 [vue-01.xml] 的环境 jS:
// 全局数据
var loading;
var formulaire;
var résultats;
var a, b;
// 在文档加载时
$(document).ready(function() {
// 获取页面中各个组件的引用
loading = $("#loading");
formulaire = $("#formulaire");
resultats = $('#结果');
a = $("#a");
b = $("#b");
// 隐藏某些元素
loading.hide();
// 解析表单验证规则
$.validator.unobtrusive.parse(formulaire);
// 管理两个区域设置 [fr_FR, en_US]
// 服务器将实际数据 [a,b] 以英语格式发送
// 如有必要,将其转换为法语格式
checkCulture(2);
});
- 第 22 行:函数 [checkCulture] 稍后将进行介绍;
函数 jS [beforeSend] 将如下所示:
function beforeSend(jqXHR, settings) {
// 在 POST 之前
// 数字必须以英式格式发布
var culture = Globalize.culture().name;
if (culture === 'fr-FR') {
checkCulture(1);
settings.data = formulaire.serialize();
}
}
function afterComplete(jqXHR, settings) {
...
}
function checkCulture(mode) {
if (mode == 1) {
// 将数字 [a,b] 转换为英式格式
var value1 = a.val().replace(",", ".");
a.val(value1);
var value2 = b.val().replace(",", ".");
b.val(value2);
}
if (mode == 2) {
...
}
}
- 第4-6行:检查视图的属性是否为[fr-FR]。 如果是,则需修改提交的值。因为如果用户输入的是 [1,6],则需提交值 [1.6];否则,值 [1,6] 将在服务器端被拒绝。 为此,只需将提交值中的逗号替换为小数点(第18-21行);
- 但仅此还不够。实际上,当调用函数 [beforeSend] 时,POST 值的字符串 [a=val1&b=valB] 已经构建完成。 因此我们需要对其进行修改。这可以通过该函数的第二个参数 [settings] 来实现;
- 第 7 行:[settings.data](settings 是该函数的参数)代表已提交的字符串。 我们使用表达式 [formulaire.serialize()] 重新构建该字符串。该表达式遍历表单以查找待提交的值,并构建 POST 的字符串。随后,它将采用 [a,b] 中带有小数点的新的值;
如果不再进行其他操作,服务器将发送响应,该响应将正确显示。但此时 [a,b] 的值已采用小数点格式,而当前仍处于 [fr-FR] 的文化设置中。 因此,如果用户没有察觉到这一点并再次点击 [Calculer],验证器会提示 [a,b] 的值无效。这是正确的。 此时,在收到结果时执行的 [afterComplete] 函数便发挥了作用:
function beforeSend(jqXHR, settings) {
// 在 POST 之前
...
}
function afterComplete(jqXHR, settings) {
// 在 POST 之后
// 如有必要,需将数字转换为法语格式
var culture = Globalize.culture().name;
if (culture === 'fr-FR') {
checkCulture(2);
}
}
function checkCulture(mode) {
if (mode == 1) {
...
}
if (mode == 2) {
// 将数字转换为法语格式
var value1 = a.val().replace(".", ",");
a.val(value1);
var value2 = b.val().replace(".", ",");
b.val(value2);
}
}
- 第9-12行:如果视图的格式为[fr-FR],则将数字[a,b]转换为法语格式。
7.2.7. 测试
以下是部分测试截图:
![]() |
- 在 [1] 中,服务器响应;
![]() |
- 在 [2] 中,服务器返回的包含错误信息的响应;
![]() |
- 在 [3] 中,设置了 5 秒的延迟。这意味着服务器将等待 5 秒后再发送响应。在 [form] 标签中,我们使用了 [data-ajax-loading='#loading'] 属性。 参数 [loading] 是一个区域的标识符,该区域:
- 在等待期间始终显示;
- 在收到服务器响应后隐藏;
此处 [loading] 是动画图片的标识符,该图片在 [4] 中可见。
7.2.8. 在 [en-US] 环境中禁用 JavaScript
如果禁用浏览器的 JavaScript 会发生什么?
输入值的处理将根据标签 [form] 进行,其中 [data-ajax-attr] 的属性将不会被使用。一切都仿佛我们拥有以下标签 [form] 一样:
<form id="formulaire" name="formulaire" method="post" action="/ajax-02.html">
因此,输入的值将被提交至操作 [/ajax-02]。这些值在客户端尚未经过验证。因此,将由服务器端的验证器进行处理。虽然服务器端验证器此前也已介入,但当时处理的是已在客户端验证过的(即正确的)值。现在情况已不同。
我们将操作 [/ajax-02] 修改如下:
@RequestMapping(value = "/ajax-02", method = RequestMethod.POST, produces = "text/html; charset=UTF-8")
public String ajax02(@Valid ActionModel01 formulaire, BindingResult result, Locale locale, Model modèle, HttpSession session, HttpServletRequest request) throws InterruptedException {
// Ajax请求?
boolean isAjax = "XMLHttpRequest".equals(request.getHeader("X-Requested-With"));
...
}
- 第 4 行:现在可以通过 POST Ajax 或传统的 POST 调用 [/ajax-02] 操作。我们需要区分这两种情况。 我们通过客户端浏览器发送的 HTTP 请求头来实现这一区分;
当在启用 JavaScript 的状态下查看 Chrome 开发者控制台(Ctrl+Shift+I)中的网络请求时,可以看到客户端在 POST 请求时发送了以下头部信息:
![]() |
上文可见:
- 已发送 [X-Requested-With] 请求头;
- 参数 [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 请求?
boolean isAjax = "XMLHttpRequest".equals(request.getHeader("X-Requested-With"));
// 时序?
Integer tempo = (Integer) session.getAttribute("tempo");
if (tempo != null && tempo > 0) {
Thread.sleep(tempo);
}
// 准备下一个视图的模板
Resultats résultats = new Resultats();
modèle.addAttribute("resultats", résultats);
// 设置区域设置
setLocale(locale, modèle, résultats);
// 时间
String heure = new SimpleDateFormat("hh:mm:ss").format(new Date());
résultats.setHeurePost(heure);
résultats.setHeureGet(heure);
// 请求有效?
if (!isAjax && result.hasErrors()) {
return "vue-01";
}
...
- 第 2 行:参数 [@Valid ActionModel01 formulaire] 触发服务器端验证器;
- 第 20-22 行:如果调用不是 Ajax 调用且验证失败,则返回包含错误消息的视图 [vue-01.xml]。
以下是一个示例:
![]() | ![]() |
接下来继续研究 [/ajax-02] 操作:
@RequestMapping(value = "/ajax-02", method = RequestMethod.POST, produces = "text/html; charset=UTF-8")
public String ajax02(@Valid ActionModel01 formulaire, BindingResult result, Locale locale, Model modèle, HttpSession session, HttpServletRequest request) throws InterruptedException {
// Ajax 请求?
boolean isAjax = "XMLHttpRequest".equals(request.getHeader("X-Requested-With"));
...
// 请求有效吗?
if (!isAjax && result.hasErrors()) {
return "vue-01";
}
// 每两次生成一次错误
int val = new Random().nextInt(2);
if (val == 0) {
// 返回错误信息
résultats.setErreur("erreur.aleatoire");
if (isAjax) {
return "vue-03";
} else {
résultats.setVue("vue-03");
return "vue-01";
}
}
...
- 第 14 行:生成一个随机错误;
- 第16行:如果是Ajax调用,则返回视图[vue-03.xml],该视图将被放置在由[resultats]标识的区域中;
- 第 18 行:若为非 Ajax 调用,将待显示的视图放入类型为 [Resultats] 的模型中;
- 第 19 行:再次返回视图 [vue-01.xml];
视图 [vue-01.xml] 修改如下:
<div id="resultats" />
<div th:if="${resultats.vue}=='vue-02'" th:include="vue-02" />
<div th:if="${resultats.vue}=='vue-03'" th:include="vue-03" />
- 第 3 行:视图 [vue-03.xml] 将插入到区域 [resultats] 下方;
示例如下:
![]() |
请注意,现在 [1] 和 [2] 这两个视图是相同的。
继续研究 [/ajax-02] 操作:
@RequestMapping(value = "/ajax-02", method = RequestMethod.POST, produces = "text/html; charset=UTF-8")
public String ajax02(@Valid ActionModel01 formulaire, BindingResult result, Locale locale, Model modèle, HttpSession session, HttpServletRequest request) throws InterruptedException {
// Ajax 请求?
boolean isAjax = "XMLHttpRequest".equals(request.getHeader("X-Requested-With"));
...
// 获取提交的值
double a = formulaire.getA();
double b = formulaire.getB();
// 构建模型
résultats.setAplusb(String.valueOf(a + b));
résultats.setAmoinsb(String.valueOf(a - b));
résultats.setAmultiplieparb(String.valueOf(a * b));
try {
résultats.setAdiviseparb(String.valueOf(a / b));
} catch (RuntimeException e) {
résultats.setAdiviseparb("NaN");
}
// 显示视图
if (isAjax) {
return "vue-02";
} else {
résultats.setVue("vue-02");
return "vue-01";
}
}
- 第7-17行:将四项算术运算的结果放入模型中;
- 第 22-23 行:通过插入视图 [vue-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>
<!-- 隐藏字段 -->
<input type="hidden" id="culture" name="culture" th:value="${resultats.culture}"></input>
</form>
- 第 5 行:将 [fr-FR] 或 [en-US] 的值放入属性字段 [name=culture] 中。 由于表单中包含 [input] 标签,其值将与 [a] 和 [b] 的值一同提交。因此,提交的字符串将呈现如下形式:
理解这一点非常重要。
接下来,我们在应用程序配置中添加一个过滤器:
![]() |
文件 [Config] 已按以下方式修改:
@Configuration
@ComponentScan({ "istia.st.springmvc.controllers", "istia.st.springmvc.models" })
@EnableAutoConfiguration
public class Config extends WebMvcConfigurerAdapter {
...
@Bean
public Filter cultureFilter() {
return new CultureFilter();
}
}
- 第 7 行:由于 Bean [cultureFilter] 返回类型为 [Filter],因此它被定义为过滤器。该 Bean 本身可以使用任意名称;
下一步是创建过滤器本身:
![]() |
package istia.st.springmvc.config;
import java.io.IOException;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.web.filter.OncePerRequestFilter;
public class CultureFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
// 下一个处理程序
filterChain.doFilter(new CultureRequestWrapper(request), response);
}
}
- 第 12 行:我们继承了 Spring 类 [OncePerRequestFilter],需要重写该类的 [doFilterInternal] 方法;
- 第15行:方法[doFilterInternal]接收三项信息:
- [HttpServletRequest request]:待过滤的请求。该参数不可修改,
- [HttpServletResponse response]:将发送给服务器的响应。过滤器可以决定自行生成该响应,
- [FilterChain filterChain]:过滤器链。一旦方法 [doFilterInternal] 完成工作,它必须将请求传递给过滤器链中的下一个过滤器;
- 第 18 行:基于接收到的请求 [new CultureRequestWrapper(request)] 创建一个新请求,并将其传递给下一个过滤器。由于无法修改原始请求 [HttpServletRequest request],因此创建一个新的请求;
类 [CultureRequestWrapper] 如下所示:
![]() |
package istia.st.springmvc.config;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
public class CultureRequestWrapper extends HttpServletRequestWrapper {
public CultureRequestWrapper(HttpServletRequest request) {
super(request);
}
@Override
public String[] getParameterValues(String name) {
// 提交的值 a 和 b
if (name != null && (name.equals("a") || name.equals("b"))) {
String[] values = super.getParameterValues(name);
String[] newValues = values.clone();
newValues[0] = newValues[0].replace(",", ".");
return newValues;
}
// 其他情况
return super.getParameterValues(name);
}
}
- 第 6 行:类 [CultureRequestWrapper] 继承自类 [HttpServletRequestWrapper],并将重写其中某些方法;
- 第 8-10 行:构造函数接收待过滤的请求,并将其传递给父类;
- 需注意,经过过滤的请求最终将作为参数传递给名为Servlet的类。 在 Spring MVC 中,该 Servlet 的类型为 [DispatcherServlet]。该类提供了多种方法用于获取请求参数:[getParameter, getParameterMap, getParameterNames, getParameterValues, ...]。 需要重写 Servlet 所使用的该方法。为此,应阅读 [DispatcherServlet] 类的代码。我并未这样做,而是直接重写了多个方法。最终重写的是 [getParameterValues] 方法;
- 第13行:方法[getParameterValues]接收参数,该参数是方法[getParameterNames]返回的某个参数的名称,并需返回其值的数组。因为我们知道,一个参数在请求中可能出现多个实例;
- 第 18 行:将逗号替换为小数点;
以下是一个执行示例:
![]() |
- 在 [1] 中,[a,b] 的值采用法语格式输入;
- 在 [2] 中,显示结果;
- 在 [3] 中,服务器返回了一页采用英式格式的数字。
最后一个问题可以通过 Thymeleaf 在视图 [vue-01.xml] 中按以下方式解决
<tr>
<td>
<input type="text" id="a" name="a" th:value="${resultats.culture}=='fr-FR' and ${actionModel01.a}!=null? ${#strings.replace(actionModel01.a,'.',',')} : ${actionModel01.a}" data-val="true" th:attr="data-val-required=#{NotNull},data-val-number=#{typeMismatch},data-val-min=#{actionModel01.a.min},data-val-min-value=#{actionModel01.a.min.value}" />
</td>
<td>
<input type="text" id="b" name="b" th:value="${resultats.culture}=='fr-FR' and ${actionModel01.b}!=null? ${#strings.replace(actionModel01.b,'.',',')} : ${actionModel01.b}" data-val="true" th:attr="data-val-required=#{NotNull},data-val-number=#{typeMismatch},data-val-min=#{actionModel01.b.min},data-val-min-value=#{actionModel01.b.min.value}" />
</td>
</tr>
第 3 行和第 6 行需要进行多项修改。我们先分析第 3 行:
- 我们之前写的是 [th:field="*{a}"]。参数 [th:field] 用于设置生成的 HTML 标签的 [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. 链接 [Calculer] 的处理
让我们来分析主页面 [vue-01.xml] 中的链接 [Calculer]:
![]() | ![]() |
视图 [vue-01.xml] 中链接 [Calculer] 的代码如下:
<a href="javascript:postForm()" th:text="#{action.calculer}">Calculer</a>
函数 jS [postForm] 在文件 [local1.js] 中定义如下:
// 总体数据
var loading;
var formulaire;
var résultats;
var a, b;
function postForm() {
// 表单有效?
if (!formulaire.validate().form()) {
// 表单无效 - 已完成
return;
}
// 管理两个本地化版本 [fr_FR, en_US]
// 实际数据 [a,b] 在任何情况下都必须以英式格式发布
// 将通过过滤器 [CultureFilter] 进行处理
// 手动发起一次 Ajax 请求
$.ajax({
url : '/ajax-02',
headers : {
'X-Requested-With' : 'XMLHttpRequest'
},
type : 'POST',
data : formulaire.serialize(),
dataType : 'html',
beforeSend : function() {
loading.show();
},
success : function(data) {
resultats.html(data);
},
complete : function() {
loading.hide();
},
error : function(jqXHR) {
résultats.html(jqXHR.responseText);
}
})
}
- 第 2-5 行:请注意,这些元素已由函数 [$(document).ready] 初始化;
- 第 9-12 行:执行表单的 jS 验证器。如果其中一个值无效,则表达式 [formulaire.validate().form()] 会将值设置为 false。 此时,表单中的 [submit] 将被取消;
- 第 18-38 行:手动发起 Ajax 调用;
- 第 19 行:Ajax 调用的目标 URL;
- 第20-22行:一个HTTP头字段数组,用于补充HTTP请求中默认存在的头字段。 此处添加了 HTTP 头部,用于告知服务器正在进行 Ajax 调用;
- 第23行:使用的HTTP方法;
- 第24行:提交的数据。[formulaire.serialize] 生成待提交的字符串 [culture=fr-FR&a=12,7&b=20,89],该字符串来自 ID 为 [formulaire] 的表单。 这里将再次遇到之前研究过的问题:[a,b] 的值必须以英式格式提交。我们知道,随着过滤器 [cultureFilter] 的创建,这个问题现已得到解决;
- 第 25 行:预期返回的数据类型。已知服务器将返回 HTML 数据流;
- 第26行:请求启动时需执行的方法。此处指定需显示ID为[loading]的组件,即加载时的动画图片;
- 第 29 行:Ajax 请求成功时需执行的方法。参数 [data] 是服务器的完整响应。已知其为数据流 HTML;
- 第30行:使用参数[data]中的HTML更新ID为[résultats]的组件。
- 第 33 行:隐藏等待信号;
- 第35行:当收到服务器响应时执行的函数,无论响应结果是成功还是错误;
- 第35-37行: 若发生错误(服务器返回了状态码为 HTTP 的响应,表明服务器端发生错误),则在 [resultats] 区域中显示服务器的 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) {
...
// 视图
return "vue-04";
}
7.3.2. 视图 [vue-04.xml]
![]() |
视图 [vue-04.xml] 沿用了视图 [vue-01.xml] 的主体内容,但存在以下差异:
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
...
<script type="text/javascript" src="/js/local4.js"></script>
<script th:inline="javascript">
/*<![CDATA[*/
var culture = [[${resultats.culture}]];
Globalize.culture(culture);
/*]]>*/
</script>
</head>
<body>
<h2>Ajax - 04</h2>
...
<form id="formulaire" name="formulaire" th:object="${actionModel01}">
...
<p>
<img id="loading" style="display: none" src="/images/loading.gif" />
<a href="javascript:postForm()" th:text="#{action.calculer}">Calculer</a>
</p>
<!-- 隐藏字段 -->
<input type="hidden" id="culture" name="culture" th:value="${resultats.culture}"></input>
</form>
<hr />
<div id="entete">
<h4 id="titre">Résultats</h4>
<p>
<strong>
<span id="labelHeureCalcul">Heure de calcul :</span>
<span id="heureCalcul">12:10:87</span>
</strong>
</p>
</div>
<div id="résultats">
<p>
A+B=
<span id="aplusb">16,7</span>
</p>
<p>
A-B=
<span id="amoinsb">16,7</span>
</p>
<p>
A*B=
<span id="afoisb">16,7</span>
</p>
<p>
A/B=
<span id="adivb">16,7</span>
</p>
</div>
<div id="erreur">
<p style="color: red;">
<span id="msgErreur">xx</span>
</p>
</div>
</body>
</html>
- 第 5 行:视图的 JavaScript 现已移至文件 [local4.js] 中;
- 第 16 行:[form] 标签不再包含 [Unobtrusive Ajax] 库中的 [data-ajax-attr] 参数。此处我们将不使用该标签。 [form] 标签也不包含 [method] 和 [action] 属性,这些属性用于指定表单中输入的值应如何及发送至何处。 这是因为该表单将由函数 jS(第 20 行)进行提交;
- 第 26-57 行:ID 字段 [resultats] 此前为空,现已包含用于显示结果的代码 HTML;
- 第26-34行:结果页眉,其中显示了计算时间;
- 第35-52行:四则运算的结果;
- 第 53-57 行:服务器可能发送的错误消息;
在加载视图 [vue-04.xm] 时执行的代码 jS 位于文件 [local4.js] 中。内容如下:
// 全局数据
var loading;
var formulaire;
var résultats;
var titre;
var labelHeureCalcul;
var heureCalcul;
var aplusb;
var amoinsb;
var afoisb;
var adivb;
var msgErreur;
// 在文档加载时
$(document).ready(function() {
// 获取页面中各个组件的引用
loading = $("#loading");
formulaire = $("#formulaire");
résultats = $('#结果');
titre=$("#titre");
labelHeureCalcul=$("#labelHeureCalcul");
heureCalcul=$("#heureCalcul");
aplusb=$("#aplusb");
amoinsb=$("#amoinsb");
afoisb=$("#afoisb");
adivb=$("#adivb");
msgErreur=$("#msgErreur");
// 隐藏某些元素
résultats.hide();
erreur.hide();
loading.hide();
});
- 第 17-27 行:获取页面中所有元素的 jQuery 引用;
- 第 29 行:隐藏结果区域;
- 第 30 行:同时隐藏错误区域;
- 第31行:同时隐藏加载动画;
- 第2-12行:将获取的引用设为全局变量,以便其他函数调用;
7.3.3. 函数 jS [postForm]
链接 [Calculer] 如下:
<p>
<img id="loading" style="display: none" src="/images/loading.gif" />
<a href="javascript:postForm()" th:text="#{action.calculer}">Calculer</a>
</p>
函数 jS [postForm] 在文件 [local.js] 中定义如下:
function postForm() {
// 表单有效吗?
if (!formulaire.validate().form()) {
// 表单无效 - 结束
return;
}
// 手动发起 Ajax 请求
$.ajax({
url : '/ajax-05',
headers : {
'Accept' : 'application/json'
},
type : 'POST',
data : formulaire.serialize(),
dataType : 'json',
beforeSend : onBegin,
success : onSuccess,
error : onError,
complete : onComplete
})
}
// Ajax 调用前
function onBegin() {
...
}
// 收到服务器响应时
// 成功时
function onSuccess(data) {
...
}
// 收到服务器响应时
// 失败时
function onError(jqXHR) {
...
}
// 在 [onSuccess, onError] 之后
function onComplete() {
...
}
- 第 3-6 行:在提交输入值之前,先对其进行验证。如果值不正确,则不执行表单的 POST 操作;
- 第 9 行:将输入的值发送至 [/ajax-05] 操作,我们稍后将对此进行详细说明;
- 第10-12行:添加HTTP请求头,告知服务器期待收到jSON格式的响应;
- 第 13 行:将提交输入的值;
- 第 14 行:将输入的值序列化为可直接发送的字符串 [a=1,6&b=2,4&culture=fr-FR];
- 第15行:服务器发送的响应类型。该类型为jSON;
- 第16行:在提交前需执行的函数 POST;
- 第 17 行:若服务器响应成功,则在收到响应后需执行的函数。HTTP 请求的“成功”与否,取决于服务器响应 HTTP 的状态。 [HTTP/1.1 200 OK ] 响应表示成功。[HTTP/1.1 500 Internal Server Error] 响应表示失败。 所谓 HTTP 响应的状态,即指代码 [200] 或 [500]。其中部分代码与“成功”相关,而其他代码则与“失败”相关;
- 第18行:当服务器响应的状态为HTTP(即失败状态)时,在收到该响应后需执行的函数;
- 第18行:在执行完前面的[onSuccess, onError]函数后,最后执行的函数;
函数 [onBegin] 如下:
// 在Ajax调用之前
function onBegin() {
console.log("onBegin");
// 显示动画图片
loading.show();
// 隐藏视图中的某些元素
entete.hide();
résultats.hide();
erreur.hide();
}
在研究Ajax调用中的其他jS函数之前,我们需要了解由[/ajax-05]操作发送的响应。
7.3.4. 操作 [/ajax-05]
[/ajax-05] 操作如下:
@RequestMapping(value = "/ajax-05", method = RequestMethod.POST)
@ResponseBody()
// 处理视图 POST
public JsonResults ajax05(@Valid ActionModel01 formulaire, BindingResult result, Locale locale, HttpServletRequest request, HttpSession session) throws InterruptedException {
if(result.hasErrors()){
// 异常情况 - 不返回任何内容
return null;
}
...
}
- 第 2 行:[ResponseBody] 属性表明,[/ajax-05] 操作会自行向客户端返回响应。 由于项目依赖中包含库 jSON,Spring Boot 会自动配置此类操作,使其返回 jSON。 因此,将发送给客户端的是类型为 [JsonResults] 的 jSON 链(第 4 行);
- 第 2 行:提交的 [a, b, culture] 值将被封装在 [ActionModel01] 类型中,并请求对其进行 [@Valid ActionModel01] 验证。这只是形式上的要求。 我们假设客户端浏览器已启用JavaScript,因此当数据到达时,提交的值已在客户端经过验证。不过,我们仍需考虑一种特殊情况:即存在未通过我们jS客户端验证的POST数据。 在此情况下,验证可能会失败;
- 第5-7行:若发生错误,则返回一个空的jSON数据流;
继续研究 [/ajax-05] 操作:
@RequestMapping(value = "/ajax-05", method = RequestMethod.POST)
@ResponseBody()
// 处理视图 [vue-04] 中的 POST
public JsonResults ajax05(@Valid ActionModel01 formulaire, BindingResult result, Locale locale,
HttpServletRequest request, HttpSession session) throws InterruptedException {
...
// Spring 应用上下文
WebApplicationContext ctx = WebApplicationContextUtils.getWebApplicationContext(request.getServletContext());
// Tempo?
Integer tempo = (Integer) session.getAttribute("tempo");
if (tempo != null && tempo > 0) {
Thread.sleep(tempo);
}
...
// 渲染结果
return résultats;
}
- 第 8 行:从 Spring 应用程序中获取上下文 [ctx]。我们需要它来根据消息键和区域设置从 [messages.properties] 文件中检索消息。操作语法如下:
ctx.getMessage(clé_message, tableau_de_paramètres, locale)
- [clé_message]:要查找的消息键;
- [locale]:所使用的语言环境。因此,如果该语言环境为 [en_US],则将使用文件 [messages_en.properties];
- [tableau_de_paramètres]:生成的消息可按 [clé=message {0} {1}] 中的方式进行配置。 该消息中包含两个 [{0} {1}] 参数。需为 [ctx.getMessage] 的第二个参数提供一个包含两个值的数组;
- 第10-13行:如果会话中存在延迟,则在延迟期间暂停当前线程;
操作 [/ajax-05] 按以下方式继续:
// 准备下一个视图的模型
JsonResults résultats = new JsonResults();
...
}
- 第2行:创建字符串模板jSON并发送给客户端;
[JsonResults]模板如下:
![]() |
package istia.st.springmvc.models;
public class JsonResults {
// 数据
private String titre;
private String labelHeureCalcul;
private String heureCalcul;
private String aplusb;
private String amoinsb;
private String afoisb;
private String adivb;
private String msgErreur;
// getter 和 setter
...
}
- 第6-13行:类[JsonResult]中的每个字段都对应视图[vue-04.xml]中[id]的相应字段:
操作 [/ajax-05] 按以下方式继续:
// 准备下一个视图的模型
JsonResults résultats = new JsonResults();
// 页眉
résultats.setTitre(ctx.getMessage("resultats.titre", null, locale));
résultats.setLabelHeureCalcul(ctx.getMessage("labelHeureCalcul", null, locale));
résultats.setHeureCalcul(new SimpleDateFormat("hh:mm:ss").format(new Date()));
// 每两次生成一次错误
int val = new Random().nextInt(2);
if (val == 0) {
// 返回错误消息
résultats.setMsgErreur(ctx.getMessage("resultats.erreur",
new Object[] { ctx.getMessage("erreur.aleatoire", null, locale) }, locale));
return résultats;
}
- 第 2 行:创建发送给客户端的字符串模板 jSON;
- 第 4-6 行:生成结果标头消息;
- 第8-14行:平均每两次会生成一条错误消息。此时,程序不再继续执行,并将字符串jSON返回给客户端(第13行);
- 第11行:此处是一个带参数的消息示例:
erreur.aleatoire=erreur aléatoire
resultats.erreur=Une erreur s''est produite : [{0}]
操作 [/ajax-05] 后续流程如下:
// 获取提交的值
double a = formulaire.getA();
double b = formulaire.getB();
// 构建模型
résultats.setAplusb(String.valueOf(a + b));
résultats.setAmoinsb(String.valueOf(a - b));
résultats.setAfoisb(String.valueOf(a * b));
try {
résultats.setAdivb(String.valueOf(a / b));
} catch (RuntimeException e) {
résultats.setAdivb("NaN");
}
// 返回结果
return résultats;
- 第2-3行:获取[a]和[b]的值;
- 第5-12行:生成四个结果;
- 第 14 行:将字符串 jSON [JsonResults] 发送给客户端;
让我们看看使用客户端 [Advanced Rest Client] 时会得到什么结果:
![]() |
- 在 [1-2] 中,向操作 [/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] 如下:
// 收到服务器响应后
// 若操作成功
function onSuccess(data) {
console.log("onSuccess");
// 填充结果区域
titre.text(data.titre);
labelHeureCalcul.text(data.labelHeureCalcul);
heureCalcul.text(data.heureCalcul);
entete.show();
// 无错误的结果
if (!data.msgErreur) {
aplusb.text(data.aplusb);
amoinsb.text(data.amoinsb);
afoisb.text(data.afoisb);
adivb.text(data.adivb);
résultats.show();
return;
}
// 结果有错误
msgErreur.text(data.msgErreur);
erreur.show();
}
- 第 3 行:参数 [data] 是服务器返回的对象 jSON:
![]() |
当响应状态 HTTP 为 [500] 时执行的方法 [onError] 如下:
// 收到服务器响应时
// 操作失败时
function onError(jqXHR) {
console.log("onError");
// 系统错误
msgErreur.text(jqXHR.responseText);
erreur.show();
}
- 第 3 行:对象 JQuery [jqXHR] 的属性包括以下内容:
- responseText:服务器响应的文本,
- status:服务器返回的错误代码,
- statusText:与该错误代码关联的文本;
- 第 6 行:对象 [jqXHR.responseText] 是以下对象 jSON:
![]() |
7.3.6. 测试
让我们来看一些该 Web 应用程序运行时的屏幕截图:
![]() |
![]() |
![]() |
7.4. 单页Web应用程序
7.4.1. 简介
Ajax 技术可用于构建单页应用程序:
- 首页通过浏览器的常规请求加载;
- 后续页面则通过 Ajax 调用获取。因此,最终浏览器不会切换 URL 页面,也不会加载新页面。 此类应用程序被称为单页应用程序(APU),英文称为 Single Page Application(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] 的 ID 区域 [content] 中;
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]:
// 全局数据
var content;
function gotoPage(num) {
// 手动发起 Ajax 请求
$.ajax({
url : '/ajax-07',
type : 'POST',
data : 'num=' + num,
dataType : 'html',
beforeSend : function() {
},
success : function(data) {
content.html(data)
},
complete : function() {
},
error : function(jqXHR) {
// 系统错误
content.html(jqXHR.responseText);
}
})
}
// 在加载文档时
$(document).ready(function() {
// 获取页面中各个组件的引用
content = $("#content");
});
- 第 28 行:在加载页面时,将 ID 为 [content] 的区域存储起来,并将其设为全局变量(第 2 行);
- 第 4 行:函数 [gotoPage] 接收作为参数的当前视图中要显示的页面编号(1 或 2);
- 第7行:URL是POST的目标;
- 第 8 行:通过 POST 调用第 7 行中的 URL;
- 第9行:提交的字符串。这是一个名为[num]的参数。其值为当前视图中要显示的页码(第4行);
- 第10行:服务器将返回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:页码
switch (num) {
case 1:
return "vue-07";
case 2:
return "vue-08";
default:
return "vue-07";
}
}
- 第 2 行:获取名为 [num] 的 POST 参数。需注意第 2 行参数名必须与 POST 参数名称一致,此处为 [num]。[num] 表示页面或视图编号;
- 第5-6行:若[num==1],则返回视图[vue-07.xml];
- 第7-8行:若为[num==2],则返回视图[vue-08.xml];
- 第9-10行:在其他情况下(通常不可能发生),返回视图 [vue-07.xml];
7.4.7. 视图 [vue-08.xml]
视图 [vue-08.xml] 构成应用程序的第 2 页:
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<body>
<h4>Page 2</h4>
<p>
<a href="javascript:gotoPage(1)">Page 1</a>
</p>
</body>
</html>
7.5. 将多个数据流 HTML 嵌入到响应 jSON 中
7.5.1. 简介
我们考虑以下应用程序:
![]() |
页面 [1] 包含四个区域:
- [Zone 1, Zone 3] 是点击按钮 [Rafraîchir] 时会显示/隐藏的区域。 统计这两个区域[2]各自的出现次数。区域[Zone 1]使用法语,而区域[Zone 3]使用英语;
- 区域 [Zone 2] 始终存在;
- 区域 [Saisies] 始终存在;
链接 [Valider] 显示以下页面 [3]:
![]() |
- 链接 [Retour à la page 1] 将第 1 页恢复到其之前的状态 [4];
该应用程序采用单页模式。浏览器向服务器请求第一页,后续页面则通过 Ajax 调用从服务器获取。
7.5.2. 股票代码 [/ajax-09]
![]() |
操作 [/ajax-09] 如下:
@RequestMapping(value = "/ajax-09", method = RequestMethod.GET, produces = "text/html; charset=UTF-8")
public String ajax09() {
return "vue-09";
}
它仅用于显示视图 [vue-09.xml]。
7.5.3. 视图 XML
![]() |
视图 [vue-09.xml] 是应用程序的主页面:
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta name="viewport" content="width=device-width" />
<title>Ajax-09</title>
<link rel="stylesheet" href="/css/ajax01.css" />
<script type="text/javascript" src="/js/jquery/jquery-2.1.1.min.js"></script>
<script type="text/javascript" src="/js/json3.js"></script>
<script type="text/javascript" src="/js/local9.js"></script>
</head>
<body>
<h3>Ajax - 09 - Navigation dans une Application à Page Unique</h3>
<h3>avec des flux HTML embarqués dans des chaînes jSON</h3>
<hr />
<div id="content" th:include="vue-09-page1" />
<img id="loading" src="/images/loading.gif" />
<div id="erreur" style="background-color:lightgrey"></div>
</body>
</html>
- 第 9 行:应用程序中使用的文件 JS;
- 第 15 行:主页的内容;
- 第 16 行:一个等待动画:
- 第 17 行:用于显示可能出现的错误的区域;
视图 [vue-09-page1.xml] 是应用程序的第 1 页:
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<body>
<h2>Page 1</h2>
<!-- 区域 1 -->
<fieldset id="zone1" style="background-color:pink">
<legend>Zone 1</legend>
<span id="zone1-content" th:text="xx">xx</span>
</fieldset>
<!-- 区域 2 -->
<fieldset id="zone2" style="background-color:lightgreen">
<legend>Zone 2</legend>
<span>Ce texte reste toujours présent</span>
</fieldset>
<!-- 区域 3 -->
<fieldset id="zone3" style="background-color:yellow">
<legend>Zone 3</legend>
<span id="zone3-content" th:text="zz">zz</span>
</fieldset>
<br />
<p>
<button onclick="javascript:postForm()">Rafraîchir</button>
</p>
<hr />
<div id="saisies" th:include="vue-09-saisies">
</div>
</body>
</html>
- 第6-9行:[Zone 1]区域。其内容放置在[id="zone1-content"]组件中;
- 第 11-14 行:字段 [Zone 2],其内容保持不变;
- 第 16-19 行:字段 [Zone 3]。其内容被放入组件 [id="zone3-content"] 中;
- 第 22 行:函数 JS,用于提交表单;
- 第 25 行:包含输入区域;
需注意第 1 页没有 [form] 标签。所有内容都将通过 JavaScript 处理。
视图 [vue-09-saisies.xml] 如下:
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<div id="saisies">
<h4>Saisies :</h4>
<p>
Chaîne de caractères :
<input type="text" id="text1" size="30" th:value="${value1}" />
</p>
<p>
Nombre entier :
<input type="text" id="text2" size="10" th:value="${value2}" />
</p>
<p>
<a href="javascript:valider()">Valider</a>
</p>
</div>
</html>
- 第5-8行:输入字符串;
- 第13-16行:输入一个整数;
- 第14行:函数JS用于提交输入的值;
再次需要指出的是,输入区域没有 [form] 标签。
总体而言,第 1 页具有两项功能:
- [Rafraîchir]:用于刷新区域1和区域3。此操作由服务器处理,服务器会随机返回:
- 区域1及其访问计数器,区域3则无内容,
- 区域3及其访问计数器,而区域1为空,
- 两个区域均显示访问计数器;
- [Valider]:显示包含输入值的第2页,若输入数据无效则显示错误信息;
我们将首先关注按钮 [Rafraîchir]。
7.5.4. 用于管理按钮 [Rafraîchir] 的代码 JS
![]() |
文件 [local9.js] 的代码如下:
// 全局变量
var content;
var loading;
var erreur;
// 在文档加载时
$(document).ready(function() {
// 获取页面中各个组件的引用
loading = $("#loading");
loading.hide();
erreur = $("#erreur");
erreur.hide();
content = $("#content");
});
- 第 9-13 行:当主页面加载时,将 [loading, erreur, content] 标识的三个组件的引用存储起来;
- 第2-4行:这三个组件的引用被存储在全局变量中。它们保持不变,因为无论何时,这三个相关区域始终存在于显示的页面中。 正因这些引用保持不变,故可在 [$(document).ready] 中进行计算,并共享给 JS 文件中的其他函数;
函数 [postForm] 负责处理对按钮 [Rafraîchir] 的点击:
function postForm() {
console.log("postForm");
// 手动发起 Ajax 请求
$.ajax({
url : '/ajax-10',
headers : {
'Accept' : 'application/json'
},
type : 'POST',
dataType : 'json',
beforeSend : onBegin,
success : onSuccess,
error : onError,
complete : onComplete
})
}
- 第 4-15 行:向服务器发起 Ajax 调用;
- 第 5 行:由 [ajax-10] 操作处理 POST;
- 第6-8行:响应将来自jSON。客户端JS表示接受jSON文档;
- 第9行:调用操作[ajax-10],并执行操作POST;
- 第10行:将接收来自jSON的数据;
- 第11行:Ajax调用前执行的函数;
- 第12行:在成功接收服务器响应时执行的函数 [200 OK];
- 第13行:当服务器响应失败时,在收到响应后执行的函数 [500 Internal server error, ...];
- 第 14 行:收到响应后执行的函数;
函数 [onBegin] 如下:
// Ajax 调用前
function onBegin() {
console.log("onBegin");
// 加载等待图
loading.show();
}
该函数仅负责启动显示服务器响应等待状态的动画图片。
7.5.5. 操作 [/ajax-10]
![]() |
操作 [/ajax-10] 如下:
// 会话
@Autowired
private SessionModel1 session;
// Thymeleaf / Spring 引擎
@Autowired
private SpringTemplateEngine engine;
@RequestMapping(value = "/ajax-10", method = RequestMethod.POST)
@ResponseBody()
public JsonResult10 ajax10(HttpServletRequest request, HttpServletResponse response) {
...
}
- 第 3 行:注入会话。该会话类型为 [SessionModel1],具体如下:
![]() |
package istia.st.springmvc.models;
import java.io.Serializable;
import org.springframework.context.annotation.Scope;
import org.springframework.context.annotation.ScopedProxyMode;
import org.springframework.stereotype.Component;
@Component
@Scope(value = "session", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class SessionModel1 implements Serializable {
private static final long serialVersionUID = 1L;
// 两个计数器
private int cpt1 = 0;
private int cpt3 = 0;
// 三个区域
private String zone1 = "xx";
private String zone3 = "zz";
private String saisies;
private boolean zone1Active = true;
private boolean zone3Active = true;
// getter 和 setter
...
}
会话 [SessionModel1] 存储以下内容:
- 第 15 行:显示字段 [Zone 1] 的次数(即 [cpt1] 的出现次数);
- 第 16 行:显示字段 [Zone 3] 的次数;
- 第18-20行:区域[Zone 1]、[Zone 3]和[Saisies]的流HTML。 在 [Page 1] --> [Page 2] --> [Page 1] 的序列中,此操作是必要的。 从 [Page 2] 切换到 [Page 1] 时,必须恢复 [Page 1] 及其三个字段;
- 第21-22行:两个布尔值,用于指示字段[Zone 1]和[Zone 3]是否显示(可见);
注入到控制器 [AjaxController] 中的另一个元素如下:
// Thymeleaf / Spring 引擎
@Autowired
private SpringTemplateEngine engine;
类型为 [SpringTemplateEngine] 的 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 {
// 数据
private String content;
private String zone1;
private String zone3;
private String erreur;
private String saisies;
private boolean zone1Active;
private boolean zone3Active;
public JsonResult10() {
}
// getter 和 setter
...
}
- 第6行:由[content]标识的字段中的内容HTML;
- 第 7 行:[Zone 1] 字段的内容 HTML;
- 第8行:[Zone 3]区域的内容HTML;
- 第 9 行:区域 [Erreur] 的内容 HTML;
- 第 10 行:区域 [Saisies] 的内容 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 上下文
WebContext thymeleafContext = new WebContext(request, response, request.getServletContext());
// 响应
JsonResult10 result = new JsonResult10();
// 会话
session.setZone1(null);
session.setZone3(null);
session.setZone1Active(false);
session.setZone3Active(false);
// 返回随机响应
int cas = new Random().nextInt(3);
switch (cas) {
case 0:
// 区域 1 激活
setZone1(thymeleafContext, result);
return result;
case 1:
// 区域 3 激活
setZone3(thymeleafContext, result);
return result;
case 2:
// 区域 1 和 3 处于活动状态
setZone1(thymeleafContext, result);
setZone3(thymeleafContext, result);
return result;
}
return null;
}
- 第 5 行:我们获取上下文 [Thymeleaf]。稍后我们将看到它将用于什么;
- 第 7 行:目前创建一个空响应;
- 第9-12行:我们将会话中包含的两个字段赋值给[null],并指定它们不应显示。这两个字段即将生成,但可能只有其中一个会被生成;
- 第14-29行:生成这两个字段;
- 第17-19行:仅生成字段[Zone 1];
- 第 21-23 行:仅生成字段 [Zone 3];
- 第25-28行:生成两个区域[Zone 1]和[Zone 3];
[Zone 1] 字段的流 HTML 通过以下方法生成:
private void setZone1(WebContext thymeleafContext, JsonResult10 result) {
// 区域 1 处于活动状态
// 数据流 HTML
int cpt1 = session.getCpt1() + 1;
thymeleafContext.setVariable("cpt1", cpt1);
thymeleafContext.setLocale(new Locale("fr", "FR"));
String zone1 = engine.process("vue-09-zone1", thymeleafContext);
result.setZone1(zone1);
result.setZone1Active(true);
// 会话
session.setCpt1(cpt1);
session.setZone1(zone1);
session.setZone1Active(true);
}
- 第 1 行:参数为:
- 类型为 [WebContext] 的上下文 [Thymeleaf],
- 正在构建的类型为 [JsonResult10] 的客户端响应;
- 第 3 行:递增该会话的计数器 [cpt1],该计数器记录字段 [Zone 1] 被显示的次数;
- 第4行:类型为[WebContext]的上下文[Thymeleaf]的行为与Spring [Model]的模板MVC有些相似。 要向模板添加元素,需使用 [WebContext.setVariable]。因此,此处将计数器 [cpt1] 放入模板 [Thymeleaf] 中。这将使 Thymeleaf 表达式 [${cpt1}] 得以求值
- 第5行:上下文[Thymeleaf]具有语言环境。这使其能够求解[#{clé_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 行:将通过 Thymeleaf 模板评估表达式 [${cpt1}];
键 [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) {
// 第3区已激活
// 数据流 HTML
int cpt3 = session.getCpt3() + 1;
thymeleafContext.setVariable("cpt3", cpt3);
thymeleafContext.setLocale(new Locale("en", "US"));
String zone3 = engine.process("vue-09-zone3", thymeleafContext);
result.setZone3(zone3);
result.setZone3Active(true);
// 会话
session.setCpt3(cpt3);
session.setZone3(zone3);
session.setZone3Active(true);
}
- 第 6 行:[Zone 3] 字段的区域设置为英语;
7.5.6. 处理 [/ajax-10] 操作的响应
让我们回到 [local9.js] 中的代码 JS,该代码将处理服务器的响应:
// 收到服务器响应时
// 成功时
function onSuccess(data) {
console.log("onSuccess");
// 内容
if (data.content) {
content.html(data.content);
}
// 区域 1
if (data.zone1Active) {
$("#zone1").show();
if (data.zone1) {
$("#zone1-content").html(data.zone1);
}
} else {
$("#zone1").hide();
}
// 区域 3 是否激活?
if (data.zone3Active) {
$("#zone3").show();
if (data.zone3) {
$("#zone3-content").html(data.zone3);
}
} else {
$("#zone3").hide();
}
// 输入?
if (data.saisies) {
$("#saisies").html(data.saisies);
}
// 错误?
if (data.erreur) {
erreur.text(data.erreur);
erreur.show();
} else {
erreur.hide();
}
}
回顾第 3 行中存储在变量 [data] 中的响应的 Java 结构:
public class JsonResult10 {
// 数据
private String content;
private String zone1;
private String zone3;
private String erreur;
private String saisies;
private boolean zone1Active;
private boolean zone3Active;
}
- 第6-8行:如果为[data.content!=null],则用其初始化[id=content]字段。该字段代表完整的[Page 1]或[Page 2]。 在本演示中,我们有 [data.content==null],因此区域 [id=content] 不会被修改,并将继续显示 [Page 1];
- 第10-17行:若[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行:对于区域[Erreur],推理过程类似,但有以下细微差别:
- 第 33 行:[data.erreur] 将是一个文本格式的错误消息;
- 第36行:如果[data.erreur==null]成立,则隐藏字段[Erreur]。因为该字段可能在前一次查询中已被显示;
如果发生服务器端错误(如 HTTP 状态码为 500 Internal server error),则执行以下函数:
// 收到服务器响应时
// 失败时
function onError(jqXHR) {
console.log("onError");
// 系统错误
erreur.text(jqXHR.responseText);
erreur.show();
}
为了查看此类错误,我们将函数 [postForm] 修改如下:
function postForm() {
console.log("postForm");
// 从当前页面获取引用
...
// 手动发起 Ajax 请求
$.ajax({
url : '/ajax-10x',
...
})
}
- 第 7 行:添加一个不存在的 URL;
点击 [Rafraîchir] 按钮后的结果如下:
![]() |
值得注意的是,该错误同样以字符串 jSON 的形式被发送。
收到服务器响应后执行的方法如下:
// 在 [onSuccess, onError] 之后
function onComplete() {
console.log("onComplete");
// 加载中图片
loading.hide();
}
我们仅需隐藏等待时的动画图片。
7.5.7. 显示页面 [Page 2]
链接 [Valider] 的代码 HTML 如下:
<a href="javascript:valider()">Valider</a>
JS [valider] 函数如下:
// 验证输入值
function valider() {
// 提交的值
var post = JSON3.stringify({
"value1" : $("#text1").val().trim(),
"value2" : $("#text2").val().trim()
});
// 手动发起 Ajax 请求
$.ajax({
url : '/ajax-11A',
headers : {
'Accept' : 'application/json',
'Content-Type' : 'application/json'
},
type : 'POST',
data : post,
dataType : 'json',
beforeSend : onBegin,
success : onSuccess,
error : onError,
complete : onComplete
})
}
- 第 4-7 行:我们需要发布两个值 v1 和 v2:它们分别来自由 [#text1] 和 [#text2] 标识的输入组件。 我们将尝试一种新方法。我们将以字符串形式 jSON {"value1":v1,"value2":v2} 提交这两个值;
- 第 10 行:提交的值将发送至 [ajax-11A] 操作;
- 第12行:因为我们知道将收到响应jSON,因此指定可以接收来自jSON的响应;
- 第13行:告知服务器将以字符串形式发送POST值,格式为jSON;
- 第15-16行:将待发送的值转换为POST格式;
- 第17行:将接收jSON;
7.5.8. 操作 [ajax-11A]
处理已发布的字符串 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 {
// 数据
@Size(min = 4, max = 6)
@NotNull
private String value1;
@Range(min = 10, max = 14)
@NotNull
private Integer value2;
// getter 和 setter
...
}
- 对象 [PostAjax11A] 的结构必须与已提交对象的结构一致 {"value1":v1,"value2":v2}。 因此需要字段 [value1](第 13 行)和 [value2](第 16 行);
- 我们已对这两个字段设置了完整性约束;
让我们回到操作 [ajax-11A] 的代码:
@RequestMapping(value = "/ajax-11A", method = RequestMethod.POST, consumes = "application/json")
@ResponseBody
public JsonResult10 ajax11A(@RequestBody @Valid PostAjax11A post, BindingResult bindingResult, Locale locale, HttpServletRequest request, HttpServletResponse response) {
// Thymeleaf 上下文
WebContext thymeleafContext = new WebContext(request, response, request.getServletContext());
// 响应
JsonResult10 result = new JsonResult10();
// 提交有效吗?
if (bindingResult.hasErrors()) {
// 返回第 1 页并显示错误
result.setZone1Active(session.isZone1Active());
result.setZone3Active(session.isZone3Active());
result.setErreur(getErreursForModel(bindingResult));
return result;
}
...
}
- 第3行:注解[@RequestBody]指代客户端发送的文档。这是客户端通过jSON提交的值。因此,该值将用于构建对象[PostAjax11A];
- 第3行:注释[@Valid]强制对提交的值进行验证;
- 第 9 行:如果验证失败:
- 第 13 行:返回一条错误消息,
- 第11-12行:将字段1和3恢复至原状态(显示或隐藏);
错误消息的计算方式如下:
private String getErreursForModel(BindingResult result) {
StringBuffer buffer = new StringBuffer();
for (FieldError error : result.getFieldErrors()) {
StringBuffer bufferCodes = new StringBuffer("(");
for (String code : error.getCodes()) {
bufferCodes.append(String.format("%s ", code));
}
bufferCodes.append(")");
buffer.append(String.format("[%s:%s:%s:%s]", error.getField(), error.getRejectedValue(), bufferCodes,
error.getDefaultMessage()));
}
return buffer.toString();
}
这是一个我们已经见过的函数。
操作 [ajax-11A] 后续流程如下:
@RequestMapping(value = "/ajax-11A", method = RequestMethod.POST, consumes = "application/json")
@ResponseBody
public JsonResult10 ajax11A(@RequestBody @Valid PostAjax11A post, BindingResult bindingResult, Locale locale, HttpServletRequest request, HttpServletResponse response) {
// Thymeleaf 上下文
WebContext thymeleafContext = new WebContext(request, response, request.getServletContext());
// 响应
JsonResult10 result = new JsonResult10();
// 提交有效?
if (bindingResult.hasErrors()) {
...
}
// 保存输入字段
thymeleafContext.setVariable("value1", post.getValue1());
thymeleafContext.setVariable("value2", post.getValue2());
session.setSaisies(engine.process("vue-09-saisies", thymeleafContext));
// 发送第2页
result.setContent(engine.process("vue-09-page2", thymeleafContext));
return result;
}
- 第 13-14 行:将提交的值放入 Thymeleaf 上下文中;
- 第15行:利用该上下文计算视图[vue-09-saisies],并将其放入会话中以便日后重新生成;
- 第 17 行:将第 2 页放入即将发送给客户端的结果中;
视图 [vue-09-page2.xml] 如下:
![]() |
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<body>
<h2>Page 2</h2>
<p>
<h4>Valeurs saisies :</h4>
<p>
Chaîne de caractères :
<span th:text="${value1}"></span>
</p>
<p>
Nombre entier :
<span th:text="${value2}"></span>
</p>
<a href="javascript:retourPage1()">Retour à la page 1</a>
</p>
</body>
</html>
- 第 9 行和第 13 行,显示了操作 [/ajax-11A] 放入 Thymeleaf 上下文中的 [value1, value2] 值;
7.5.9. 处理 [/ajax-11A] 操作的响应
在客户端,操作 [/ajax-10] 的响应由函数 [onSuccess] 处理:
function onSuccess(data) {
console.log("onSuccess");
// 内容
if (data.content) {
content.html(data.content);
}
// 区域 1
if (data.zone1Active) {
$("#zone1").show();
if (data.zone1) {
$("#zone1-content").html(data.zone1);
}
} else {
$("#zone1").hide();
}
// 区域 3 是否有效?
if (data.zone3Active) {
$("#zone3").show();
if (data.zone3) {
$("#zone3-content").html(data.zone3);
}
} else {
$("#zone3").hide();
}
// 输入?
if (data.saisies) {
$("#saisies").html(data.saisies);
}
// 错误?
if (data.erreur) {
erreur.text(data.erreur);
erreur.show();
} else {
erreur.hide();
}
}
我们已对该代码进行了注释。让我们分别考虑两种情况:带错误响应和不带错误响应:
出现错误
在此情况下,操作 [/ajax-11A] 发送了响应 jSON,其格式为 {"zone1":null, "zone3":null,"saisies":null,"erreur":erreur,"zone1Active":zone1Active,"zone3Active":zone3Active,"content":null}。如果按照上述代码,可以看到:
- [content] 区域未发生变化。该区域原本包含第 1 页;
- [Erreur] 区域被显示出来;
- 区域 [Zone 1]、[Zone 3]、[Saisies] 保持原状;
无错误
在此情况下,操作 [/ajax-11A] 发送了响应 jSON,其格式为 {"zone1":null, "zone3":null,"saisies":null,"erreur":null,"zone1Active":false,"zone3Active":false,"content":content}。如果按照上述代码,可以看到:
- 显示了区域 [content]。其中包含第 2 页;
以下是三个执行示例:
一个验证错误的案例:
![]() | ![]() |
一个包含错误 POST 的案例:
![]() | ![]() |
这种错误类型有所不同。由于 Spring 无法将字符串 jSON 转换为类型 [PostAjax11A],因此返回了包含 [status=400] 的响应 HTTP。 操作 [ajax-11A] 未被执行;
无错误的情况:
![]() | ![]() |
7.5.10. 返回第 1 页
第 2 页中的链接 [Retour vers la page 1] 如下:
<a href="javascript:retourPage1()">Retour à la page 1</a>
方法 JS [retourPage1] 如下:
// 返回第 1 页
function retourPage1() {
// 手动发起 Ajax 请求
$.ajax({
url : '/ajax-11B',
headers : {
'Accept' : 'application/json',
},
type : 'POST',
dataType : 'json',
beforeSend : onBegin,
success : onSuccess,
error : onError,
complete : onComplete
})
}
它会生成一个 POST(无提交值),并将其传递给操作 [/ajax-11B]。
7.5.11. 操作 [/ajax-11B]
操作 [/ajax-11B] 如下:
@RequestMapping(value = "/ajax-11B", method = RequestMethod.POST)
@ResponseBody
public JsonResult10 ajax11B(HttpServletRequest request, HttpServletResponse response) {
// Thymeleaf 上下文
WebContext thymeleafContext = new WebContext(request, response, request.getServletContext());
// 响应
JsonResult10 result = new JsonResult10();
// 将第1页恢复为原始状态
result.setContent(engine.process("vue-09-page1", thymeleafContext));
result.setSaisies(session.getSaisies());
result.setZone1(session.getZone1());
result.setZone3(session.getZone3());
result.setZone1Active(session.isZone1Active());
result.setZone3Active(session.isZone3Active());
return result;
}
该操作需重新生成第1页及其三个区域 [Zone1, Zone3, Erreur]:
- 第 9 行:将第 1 页放入结果中;
- 第10行:将输入区域放入结果中;
- 第 11 行:将区域 [Zone 1] 放入结果中;
- 第12行:将字段[Zone 3]放入结果中;
- 第13-14行:将字段[Zone 1]和[Zone 3]的状态写入结果;
7.5.12. 处理操作 [/ajax-11B] 的响应
操作 [/ajax-11B] 的响应由函数 [onSuccess] 处理:
function onSuccess(data) {
console.log("onSuccess");
// 内容
if (data.content) {
content.html(data.content);
}
// 区域 1
if (data.zone1Active) {
$("#zone1").show();
if (data.zone1) {
$("#zone1-content").html(data.zone1);
}
} else {
$("#zone1").hide();
}
// 区域 3 是否处于活动状态?
if (data.zone3Active) {
$("#zone3").show();
if (data.zone3) {
$("#zone3-content").html(data.zone3);
}
} else {
$("#zone3").hide();
}
// 输入?
if (data.saisies) {
$("#saisies").html(data.saisies);
}
// 错误?
if (data.erreur) {
erreur.text(data.erreur);
erreur.show();
} else {
erreur.hide();
}
}
操作 [/ajax-11B] 发送了格式为 {"zone1":zone1, "zone3":zone3,"saisies":saisies,"erreur":null,"zone1Active":zone1Active,"zone3Active":zone3Active,"content":content}。根据上述代码可知:
- [content] 区域已被修改。该区域原先包含第 2 页,现在将包含第 1 页;
- [Erreur] 区域被隐藏;
- 区域 [Zone 1]、[Zone 3]、[Saisies] 保持原样显示;
7.6. 客户端会话管理
7.6.1. 简介
在上文中,我们管理了一个具有以下结构的会话:
public class SessionModel1 implements Serializable {
// 两个计数器
private int cpt1 = 0;
private int cpt3 = 0;
// 三个区域
private String zone1 = "xx";
private String zone3 = "zz";
private String saisies;
private boolean zone1Active = true;
private boolean zone3Active = true;
...
}
当用户数量非常庞大时,所有用户会话占用的内存可能会造成问题。因此,通常的做法是尽量缩小会话大小。APU 模型(单页应用程序)允许在客户端管理会话,并实现无会话的 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. 用于管理按钮 [Rafraîchir] 的代码 JS
![]() |
文件 [local12.js] 的代码如下:
// 全局变量
var content;
var loading;
var erreur;
var page1;
var page2;
var value1;
var value2;
var session = {
"cpt1" : 0,
"cpt3" : 0
};
// 在加载文档时
$(document).ready(function() {
// 获取页面中各个组件的引用
loading = $("#loading");
loading.hide();
erreur = $("#erreur");
erreur.hide();
content = $("#content");
});
- 第17-21行:当主页面加载时,将由[loading, erreur, content]标识的三个组件的引用存储在第2-4行的全局变量中;
- 第 5-6 行:用于存储这两页;
- 第 7-8 行:用于存储由链接 [Valider] 提交的两个值;
- 第9行:会话。它在客户端存储计数器[cpt1, cpt3]的值;
函数 [postForm] 处理对按钮 [Rafraîchir] 的点击:
function postForm() {
console.log("postForm");
// 保存会话
var post = JSON3.stringify(session);
// 手动发起 Ajax 请求
$.ajax({
url : '/ajax-13',
headers : {
'Accept' : 'application/json',
'Content-Type' : 'application/json'
},
type : 'POST',
data : post,
dataType : 'json',
beforeSend : onBegin,
success : function(data) {
...
},
error : onError,
complete : onComplete
})
}
与上一版本的区别如下:
- 第 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;
// 两个计数器
private int cpt1 = 0;
private int cpt3 = 0;
// getter 和 setter
...
}
会话 [SessionModel2] 记录以下内容:
- 第 9 行:显示字段 [Zone 1] 的次数;
- 第 10 行:显示字段 [Zone 3] 的次数;
接下来继续研究操作代码 [/ajax-13]:
@RequestMapping(value = "/ajax-13", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
@ResponseBody()
public JsonResult13 ajax13(@RequestBody SessionModel2 session2, HttpServletRequest request, HttpServletResponse response) {
...
}
- 第3行,响应的类型[JsonResult13]如下:
![]() |
package istia.st.springmvc.models;
public class JsonResult13 {
// 数据
private String page2;
private String zone1;
private String zone3;
private String erreur;
private String value1;
private Integer value2;
// 会话
private SessionModel2 session;
// 获取器和设置器
...
}
- 第 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 上下文
WebContext thymeleafContext = new WebContext(request, response, request.getServletContext());
// 响应
JsonResult13 result = new JsonResult13();
result.setSession(session2);
// 返回随机响应
int cas = new Random().nextInt(3);
switch (cas) {
case 0:
// 区域 1 激活
setZone1B(thymeleafContext, result);
return result;
case 1:
// 区域 3 激活
setZone3B(thymeleafContext, result);
return result;
case 2:
// 区域 1 和 3 处于活动状态
setZone1B(thymeleafContext, result);
setZone3B(thymeleafContext, result);
return result;
}
return null;
}
- 第9行:会话被放入操作结果中;
激活区域 [Zone 1] 的方法 [setZone1B] 如下:
private void setZone1B(WebContext thymeleafContext, JsonResult13 result) {
// 正在恢复会话
SessionModel2 session = result.getSession();
// 区域 1 处于活动状态
// 数据流 HTML
int cpt1 = session.getCpt1() + 1;
thymeleafContext.setVariable("cpt1", cpt1);
thymeleafContext.setLocale(new Locale("fr", "FR"));
String zone1 = engine.process("vue-09-zone1", thymeleafContext);
result.setZone1(zone1);
// 会话
session.setCpt1(cpt1);
}
- 第 3 行:获取会话。该会话将在第 12 行通过新计数器 [cpt1] 进行修改。需注意,该会话将被返回给客户端;
- 第10行:新的字段[Zone 1];
激活字段 [Zone 3] 的方法 [setZone3B] 与此类似:
private void setZone3B(WebContext thymeleafContext, JsonResult13 result) {
// 正在检索会话
SessionModel2 session = result.getSession();
// 区域 3 激活
// 数据流 HTML
int cpt3 = session.getCpt3() + 1;
thymeleafContext.setVariable("cpt3", cpt3);
thymeleafContext.setLocale(new Locale("en", "US"));
String zone3 = engine.process("vue-09-zone3", thymeleafContext);
result.setZone3(zone3);
// 会话
session.setCpt3(cpt3);
}
7.6.5. 处理操作 [/ajax-13] 的响应
在客户端,操作 [/ajax-13] 的响应 jSON 由以下函数 [onSuccess] 处理:
function postForm() {
console.log("postForm");
// 提交会话
var post = JSON3.stringify(session);
// 手动发起 Ajax 请求
$.ajax({
...
success : function(data) {
// 保存会话
session = data.session;
// 更新两个字段
if (data.zone1) {
$("#zone1-content").html(data.zone1);
$("#zone1").show();
} else {
$("#zone1").hide();
}
if (data.zone3) {
$("#zone3").show();
$("#zone3-content").html(data.zone3);
} else {
$("#zone3").hide();
}
},
...
})
}
- 第12-17行:如果服务器在响应的[zone1]字段中填充了内容,则需重新生成[Zone 1]字段并显示它;否则应将其隐藏;
- 第18-23行:对区域[Zone 3]采用相同的处理逻辑;
7.6.6. 显示页面 [Page 2]
链接 [Valider] 的代码 HTML 如下:
<a href="javascript:valider()">Valider</a>
JS [valider] 函数如下:
// 验证输入值
function valider() {
// 保存第 1 页
page1 = content.html();
// 保存输入值
value1 = $("#text1").val().trim();
value2 = $("#text2").val().trim();
// 已提交的值
var post = JSON3.stringify({
"value1" : value1,
"value2" : value2,
"pageRequired" : page2 ? false : true
});
// 手动发起Ajax请求
$.ajax({
url : '/ajax-14',
headers : {
'Accept' : 'application/json',
'Content-Type' : 'application/json'
},
type : 'POST',
data : post,
dataType : 'json',
beforeSend : onBegin,
success : function(data) {
...
},
error : onError,
complete : onComplete
})
}
- 我们将执行 POST,这通常会使我们跳转到第 2 页;
- 第4行:保存第1页,以便日后返回;
- 第6-7行:前一步操作并未保存输入的值,仅保存了页面的代码HTML。因此,现在我们需要保存表单中输入的两个值;
- 第9-13行:将两个输入值放入字符串jSON中。该字符串将被提交;
- 第12行:一个参数用于告知服务器是否需要第2页。我们将按此方式操作。首先请求第2页,然后将其存储在变量JS [page2]中。之后,我们将不再重新请求该页面。 我们将使用缓存中的页面。第2行,如果变量[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 {
// 第 2 页
private boolean pageRequired;
// getter 和 setter
...
}
- 第 3 行:类 [PostAjax14] 继承了上一版本的类 [PostAjax11A]。因此,它具有 [value1, value2, pageRequired] 的结构;
操作 [/ajax-14] 按以下方式继续:
@RequestMapping(value = "/ajax-14", method = RequestMethod.POST)
@ResponseBody
public JsonResult13 ajax14(@RequestBody @Valid PostAjax14 post, BindingResult bindingResult, Locale locale, HttpServletRequest request, HttpServletResponse response) {
// Thymeleaf 上下文
WebContext thymeleafContext = new WebContext(request, response, request.getServletContext());
// 响应
JsonResult13 result = new JsonResult13();
// 提交有效吗?
if (bindingResult.hasErrors()) {
// 返回错误
result.setErreur(getErreursForModel(bindingResult));
return result;
}
// 发送第 2 页
result.setValue1(post.getValue1());
result.setValue2(post.getValue2());
// 请求的页面?
if (post.isPageRequired()) {
result.setPage2(engine.process("vue-12-page2", thymeleafContext));
}
return result;
}
- 第 9-13 行:如果提交的 [value1, value2] 值无效,则返回一条错误消息;
- 第15-16行:通常情况下,服务器应使用提交的值进行计算。此处仅将其原样返回,以表明已成功接收;
- 第18-20行:只有当客户端请求时,才会返回第2页。第19行,视图[vue-12-page2]是新的:
![]() |
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<body>
<h2>Page 2</h2>
<p>
<h4>Valeurs saisies :</h4>
<p>
Chaîne de caractères :
<span id="value1"></span>
</p>
<p>
Nombre entier :
<span id="value2"></span>
</p>
<a href="javascript:retourPage1()">Retour à la page 1</a>
</p>
</body>
</html>
- 代码 XML 不再包含由 Thymeleaf 计算的值(与之前不同);
- 我们已确定了放置服务器返回的 [value1, value2] 值的区域。第 9 行,[id='value1'] 指定了放置 [value1] 的位置。 第 13 行,[value2] 亦同;
7.6.8. 处理 [/ajax-14] 操作的响应
操作 [/ajax-14] 的响应由以下函数 [success] 处理:
// 验证输入值
function valider() {
...
// 手动发起 Ajax 请求
$.ajax({
...
success : function(data) {
// 错误?
if (data.erreur) {
// 显示错误
erreur.html(data.erreur);
erreur.show();
} else {
// 无错误
erreur.hide();
// 第 2 页
if (page2) {
// 使用缓存页面
content.html(page2);
} else {
// 正在缓存第2页
page2 = data.page2;
// 正在显示
content.html(data.page2);
}
// 使用服务器信息更新页面
$("#value1").text(data.value1);
$("#value2").text(data.value2);
}
},
...
})
}
- 第 9-13 行:如果服务器返回了错误,则显示该错误;
- 第14-29行:处理未发生错误的情况。此时需显示第2页;
- 第17行:检查第2页是否已存储在变量[page2]中;
- 第19行:若已存在,则使用变量[page2]显示第2页;
- 第24行:否则,使用服务器提供的字段[data.page2];
- 第22行:需将第2页保存起来,以便后续不再重新请求;
- 第27-28行:在第2页中,显示服务器发送的两条信息[value1, value2];
7.6.9. 返回第1页
第2页中的链接[Retour vers la page 1]如下:
<a href="javascript:retourPage1()">Retour à la page 1</a>
方法 JS [retourPage1] 如下:
// 返回第1页
function retourPage1() {
// 重新生成第1页
content.html(page1);
// 重新生成输入数据
$("#text1").val(value1);
$("#text2").val(value2);
}
- 这是一项不与服务器交互的 JS 操作,因为第 1 页已保存在本地变量 [page1] 中;
- 第4行:重新生成第1页;
- 第6-7行:仅第1页的HTML部分被存储,未包含用户输入。因此必须重新生成这些输入;
7.6.10. 结论
通过充分利用 APU 模型的功能,我们成功简化了 Web 服务器,使其现在成为无状态(无会话)服务器,且负载更低:
- 我们在函数 JS([retourPage1])中删除了与服务器的交互;
- 服务器仅生成一次第2页;
7.7. JavaScript 代码分层结构
7.7.1. 引言
先前应用程序的 JavaScript 代码开始变得复杂。现在是时候将其分层结构化了。应用程序将保持与之前相同。除了定义一个新的起始页面外,我们不会对服务器进行任何改动。我们将重构代码 JS。
新的架构如下:
![]() |
7.7.2. 启动页面
启动应用程序的操作是以下 [/ajax-16] 操作:
@RequestMapping(value = "/ajax-16", method = RequestMethod.GET, produces = "text/html; charset=UTF-8")
public String ajax16() {
return "vue-16";
}
它将显示以下视图 [vue-16.xml]:
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta name="viewport" content="width=device-width" />
<title>Ajax-12</title>
<link rel="stylesheet" href="/css/ajax01.css" />
<script type="text/javascript" src="/js/jquery/jquery-2.1.1.min.js"></script>
<script type="text/javascript" src="/js/json3.js"></script>
<script type="text/javascript" src="/js/local16-dao.js"></script>
<script type="text/javascript" src="/js/local16-ui.js"></script>
</head>
<body>
<h3>Ajax - 16 - Navigation dans une Application à Page Unique</h3>
<h3>Structuration du code JS</h3>
<hr />
<div id="content" th:include="vue-09-page1" />
<img id="loading" src="/images/loading.gif" />
<div id="erreur" style="background-color:lightgrey"></div>
</body>
</html>
- 第 9-10 行:代码 JS 被放置在两个不同的文件中:
- [local-ui] 实现了 [présentation] 层,
- [local-dao] 实现了 [DAO] 层;
![]() |
7.7.3. [DAO] 层的实现
![]() |
7.7.4. 接口
[DAO] 层在 [local-dao.js] 中将向 [présentation] 层提供以下接口:
| 通过按钮 [Rafraîchir] 更新第 1 页 |
| 通过按钮 [Valider] 显示第 2 页 |
JavaScript 没有“接口”的概念。我使用这个术语只是为了说明,[présentation] 层承诺仅通过前两个函数与 [DAO] 层进行交互。
7.7.5. 接口的实现
实现的骨架如下:
var session = {
"cpt1" : 0,
"cpt3" : 0
};
// 更新第1页
function updatePage1(deferred, sendMeBack) {
...
}
// 第2页
function getPage2(deferred, sendMeBack, value1, value2, pageRequired) {
...
}
[DAO] 层的目标是向 [présentation] 层隐藏针对 Web 服务器发出的 HTTP 请求的细节。 会话是这些细节的一部分。因此,会话现由 [DAO] 层管理。
7.7.5.1. 函数 [updatePage1]
函数 [updatePage1] 是 [présentation] 层调用以刷新第 1 页的函数。其代码如下:
// 更新第1页
function updatePage1(deferred, sendMeBack) {
// 请求 HTTP
executePost(deferred, sendMeBack, '/ajax-13', session);
}
- 第 1 行:函数 [updatePage1] 接收两个参数:
- 一个类型为 [jQuery.Deferred] 的对象。该对象类型存储一种状态,该状态可能有三个值:['pending', 'resolved', 'rejected']。 当该对象进入函数 [updatePage1] 时,其状态为 [pending];
- 需返回的对象为 JS,位于 [présentation] 层;
所有 HTTP 请求均由后续的 [executePost] 函数执行:
// 请求 HTTP
function executePost(deferred, sendMeBack, url, post) {
// 手动发起 Ajax 请求
$.ajax({
headers : {
'Accept' : 'application/json',
'Content-Type' : 'application/json'
},
url : url,
type : 'POST',
data : JSON3.stringify(post),
dataType : 'json',
success : function(data) {
// 保存会话
if (data.session) {
session = data.session;
}
// 返回结果
deferred.resolve({
"status" : 1,
"data" : data,
"sendMeBack" : sendMeBack
});
},
error : function(jqXHR) {
// 返回错误
deferred.resolve({
"status" : 2,
"data" : jqXHR.responseText,
"sendMeBack" : sendMeBack
});
}
});
}
- 第 1 行:函数 [executePost] 执行类型为 POST 的 Ajax 调用。它需要四个参数:
- 处于 [pending] 状态的 [jQuery.Deferred] 类型对象;
- 一个位于 [présentation] 层中待返回的 JS 对象;
- POST 中的 URL;
- 作为对象 JS 发布的值;
- 第5-8行:jSON(第7行)的发布函数接收来自jSON(第6行)的参数;
- 第 11 行:待提交的值被转换为 jSON;
- 第13-24行:Ajax调用成功时执行的函数;
- 第19-23行:如果服务器返回了会话,则将其保存;
- 第13-18行:将对象[deferred]传递至状态[resolved],同时传递包含以下字段的结果:
- [status]:成功时为1,失败时为2;
- [data]:服务器的响应 jSON,
- [sendMeBack]:函数的第二个参数,即调用方希望获取的对象;
- 第17-31行:Ajax调用失败时执行的函数。操作与之前相同,但有两点不同:
- [status] 值设为 2 以标记错误;
- [data] 仍是服务器返回的 jSON 响应,但获取方式不同;
7.7.5.2. 函数 [getPage2]
函数 [getPage2] 如下:
// 第 2 页
function getPage2(deferred, sendMeBack, value1, value2, pageRequired) {
// 请求 HTTP
executePost(deferred, sendMeBack, '/ajax-14', {
"value1" : value1,
"value2" : value2,
"pageRequired" : pageRequired,
});
}
- 该函数接收以下参数:
- [deferred]:处于 [pending] 状态的 [jQuery.Deferred] 类型对象,
- [sendMeBack]:一个 JS 对象,用于返回至 [présentation] 层,
- [value1]:第 1 页中的第一条记录,
- [value2]:第2页的第二条记录,
- [pageRequired]:一个布尔值,用于指示服务器是否应发送第2页的HTML数据流;
- 调用函数 [executePost] 以执行必要的请求 HTTP;
7.7.6. [présentation] 层
![]() |
[présentation] 层由文件 [local-ui.js] 实现。该文件采用了 [local12.js] 文件中的代码,并进行了重构以使用前面的 [DAO] 层。 仅有两个函数发生变更:[postForm] 和 [valider]。
7.7.6.1. 函数 [postForm]
函数 [postForm] 如下:
// 更新第 1 页
function postForm() {
// 更新第 1 页
var deferred = $.Deferred();
loading.show();
updatePage1(deferred, {
'发件人:"postForm",
'信息:10
});
// 显示结果
deferred.done(postFormDone);
}
- 第 4 行:创建一个 [jQuery.Deferred] 对象。默认情况下,它处于 [pending] 状态;
- 第 5 行:显示加载图片
- 第 6-9 行:执行函数 [updatePage1]。传入一个虚构的 [sendMeBack] 对象,仅用于演示其用途;
- 第 11 行:函数 [deferred.done] 的参数本身是一个函数。当对象 [deferred] 的状态切换到 [resolved] 时,将执行该函数。 我们刚才看到,函数 DAO [executePost] 在收到服务器响应后,将该对象的状态传递给了 [resolved]。 这意味着当函数 [postFormDone] 执行时,已收到服务器的响应;
函数 [postFormDone] 如下:
function postFormDone(result) {
// 等待结束
loading.hide();
// 正在检索数据
var data = result.data
// 用于演示
console.log(JSON3.stringify(result.sendMeBack));
// 分析状态
switch (result.status) {
case 1:
// 更新两个字段
if (data.zone1) {
$("#zone1-content").html(data.zone1);
$("#zone1").show();
} else {
$("#zone1").hide();
}
if (data.zone3) {
$("#zone3").show();
$("#zone3-content").html(data.zone3);
} else {
$("#zone3").hide();
}
break;
case 2:
// 显示错误
erreur.html(data);
break;
}
}
- 第 1 行:接收到的参数 [result] 是传递给函数 [executePost] 中方法 [deferred.resolve] 的参数,例如:
// 返回结果
deferred.resolve({
"status" : 1,
"data" : data,
"sendMeBack" : sendMeBack
});
- 第 5 行:获取服务器的响应;
- 第10-24行:此处的代码在上一版本中位于函数[postForm]的子函数[onSuccess]中;
- 第25-28行:此处代码在上一版本中位于函数[postForm]的子函数[onError]中;
7.7.6.2. 参数 [sendMeBack] 的作用
参数 [sendMeBack] 的用途是什么?让我们看看函数 [updatePage1] 的调用代码:
// 更新第 1 页
function postForm() {
// 更新第 1 页
var deferred = $.Deferred();
loading.show();
updatePage1(deferred, {
'发件人:"postForm",
'信息:10
});
// 显示结果
deferred.done(postFormDone);
}
以及函数 [validerDone] 的签名:
function postFormDone(result) {
}
函数 [postForm] 如何将信息传递给函数 [postFormDone]?后者仅有一个参数 [result]。 该参数由 [DAO] 层中的 [executePost] 函数创建。 要将信息传递给函数 [postFormDone],函数 [postForm] 必须先将其传递给函数 [updatePage1]。这就是参数 [sendMeBack] 的作用。 其使用方法如下:
function postFormDone(result) {
// 等待结束
loading.hide();
// 正在检索数据
var data = result.data
// 用于演示
console.log(JSON3.stringify(result.sendMeBack));
// 分析状态
switch (result.status) {
...
- 第 7 行, 函数 [postFormDone] 检索到了参数 [sendMeBack],该参数最初由函数 [postForm] 传递给函数 DAO [updatePage1];
7.7.7. 函数 [valider]
函数 [valider] 如下:
// 验证输入值
function valider() {
// 保存第1页
page1 = content.html();
// 保存输入值
value1 = $("#text1").val().trim();
value2 = $("#text2").val().trim();
// 无错误
erreur.hide();
// 请求第 2 页
var deferred = $.Deferred();
loading.show();
getPage2(deferred, {
'发送方:'确认',
'info' : 20
}, value1, value2, page2 ? false : true);
// 显示结果
deferred.done(validerDone);
}
以及函数 [validerDone](第 18 行)如下:
function validerDone(result) {
// 等待结束
loading.hide();
// 正在检索数据
var data = result.data
// 用于演示
console.log(JSON3.stringify(result.sendMeBack));
// 正在分析状态
switch (result.status) {
case 1:
// 错误?
if (data.erreur) {
// 显示错误
erreur.html(data.erreur);
erreur.show();
} else {
// 无错误
erreur.hide();
// 第 2 页
if (page2) {
// 使用缓存页面
content.html(page2);
} else {
// 正在缓存第2页
page2 = data.page2;
// 正在显示
content.html(data.page2);
}
// 使用服务器信息更新页面
$("#value1").text(data.value1);
$("#value2").text(data.value2);
}
break;
case 2:
// 显示错误
erreur.html(data);
erreur.show();
break;
}
}
- 第 5 行:获取服务器的响应;
- 第 10-32 行:此处的代码在上一版本中位于 [valider] 函数的 [onSuccess] 子函数中;
- 第34-38行:此处代码在上一版本中位于函数[valider]的子函数[onError]中;
7.7.8. 测试
应用程序仍按原样运行,在 Chrome 控制台中,可以看到 [postForm] 和 [valider] 函数的 [sendMeBack] 参数:
![]() |
7.8. Conclusion
让我们回到 Spring 应用程序的总体架构 MVC:
![]() |
借助页面中嵌入的、在浏览器中执行的 JavaScript(HTML),以及 APU 模型,我们可以将代码迁移到浏览器端,从而实现以下架构:
![]() |
- 我们构建了一个客户端 [2] / 服务器 [1] 的架构,其中客户端与服务器通过 jSON 进行通信;
- 在 [1] 中,Spring Web 层 MVC 通过 jSON 提供视图、视图片段和数据;
- 在 [2] 中:应用程序启动时加载的视图中嵌入的 JavaScript 代码可以分层结构化:
- [présentation] 层负责处理用户交互,
- [DAO] 层负责通过 Web 服务器 [1] 访问数据,
- [métier] 层可能不存在,或承接服务器 [métier] 层中某些非机密功能,以减轻服务器负担;
- 客户端 [2] 可将某些视图缓存,以此进一步减轻服务器负担。它负责管理会话;













































































