7. Implementação de Ajax numa aplicação Spring MVC
7.1. O papel do AJAX numa aplicação Web
Até agora, os exemplos de aprendizagem que estudámos tinham a seguinte arquitetura:
![]() |
Para mudar de uma vista [View1] para uma vista [View2], o navegador:
- envia um pedido à aplicação web;
- recebe a vista [View2] e exibe-a no lugar da vista [View1].
Este é o padrão clássico:
- pedido do navegador;
- o servidor web gera uma vista em resposta ao cliente;
- exibição desta nova vista pelo navegador.
Há já vários anos que existe outro modo de interação entre o navegador e o servidor web: o AJAX (Asynchronous JavaScript and XML). Isto envolve interações entre a visualização apresentada pelo navegador e o servidor web. O navegador continua a fazer aquilo que faz melhor — apresentar uma visualização HTML — mas agora é controlado por JavaScript incorporado na visualização HTML apresentada. O diagrama é o seguinte:
![]() |
- Em [1], ocorre um evento na página exibida no navegador (um clique num botão, uma alteração de texto, etc.). Este evento é interceptado pelo JavaScript (JS) incorporado na página;
- Em [2], o código JavaScript efetua um pedido HTTP tal como o navegador teria feito. O pedido é assíncrono: o utilizador pode continuar a interagir com a página sem ficar bloqueado enquanto aguarda a resposta HTTP. O pedido segue o fluxo de processamento padrão. Nada (ou muito pouco) o distingue de um pedido padrão;
- Em [3], é enviada uma resposta ao cliente JS. Em vez de uma visualização HTML completa, é normalmente enviada uma visualização HTML parcial, um feed XML ou JSON (JavaScript Object Notation);
- Em [4], o JavaScript recupera esta resposta e utiliza-a para atualizar uma região da página HTML apresentada.
Para o utilizador, há uma alteração na visualização porque o que vê mudou. No entanto, não há um recarregamento completo da página; em vez disso, ocorre apenas uma modificação parcial da página exibida. Isto ajuda a tornar a página mais fluida e interativa: como não há recarregamento completo da página, podemos lidar com eventos que anteriormente não podiam ser geridos. Por exemplo, oferecer ao utilizador uma lista de opções à medida que este digita caracteres num campo de entrada. Com cada novo caractere digitado, é enviada uma solicitação AJAX ao servidor, que, por sua vez, devolve sugestões adicionais. Sem o AJAX, este tipo de assistência à entrada era anteriormente impossível. Não podíamos recarregar uma nova página com cada caractere digitado.
7.2. Atualizar uma página com um feed HTML
7.2.1. As Visualizações
Propomos estudar a seguinte aplicação:
![]() |
- em [1], o tempo de carregamento da página;
- em [2], as quatro operações aritméticas são realizadas em dois números reais A e B;
- em [3], a resposta do servidor é apresentada numa região da página;
- em [4], o tempo do cálculo. Este é diferente do tempo de carregamento da página [5]. Este último é igual a [1], o que mostra que a região [6] não foi recarregada. Além disso, o URL da página [7] não se alterou.
7.2.2. A ação [/ajax-01]
![]() |
O controlador [Ajax.java] define a seguinte ação [/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]
...
}
- linha 2: a ação [/ajax-01] aceita apenas um parâmetro [tempo]. Trata-se da duração, em milissegundos, que o servidor deve esperar antes de enviar os resultados das operações aritméticas;
- linha 4: o parâmetro [tempo] é opcional;
- linhas 5–12: verificamos se o valor do parâmetro [tempo] é válido;
- linhas 13–15: se for o caso, o valor do tempo limite é armazenado na sessão. Isto significa que permanecerá em vigor até ser alterado;
O código para a ação [/ajax-01] continua da seguinte forma:
@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";
}
A classe [ActionModel01] é usada principalmente para encapsular os valores enviados pela ação [/ajax-01]. Aqui, nada é enviado. Criamos uma classe vazia e colocamo-la no modelo porque a vista [vue-01.xml] a utiliza. A classe [ActionModel01] é a seguinte:
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
...
}
- linhas 11 e 15: dois números reais [a,b] que serão enviados através de um formulário;
Voltemos ao código da ação:
@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";
}
- linhas 6-7: adicionamos uma instância do tipo [Results] ao modelo;
O tipo [Results] colocado no modelo é o seguinte:
![]() |
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
...
}
- linhas 6–9: os resultados das quatro operações aritméticas nos números [a, b];
- linha 10: o tempo em que a página foi carregada inicialmente;
- linha 11: o tempo em que as quatro operações aritméticas foram executadas;
- linha 12: quaisquer mensagens de erro;
- linha 13: a vista a ser exibida, se houver;
- linha 14: a localização da visualização, [fr-FR] ou [en-US];
O código da ação [/ajax-01] continua da seguinte forma:
@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);
...
}
- linha 5: o método [setLocale] é utilizado para definir a localização a utilizar no modelo de visualização, [fr-FR] ou [en-US]. Esta localização destina-se ao JavaScript incorporado na visualização;
O método [setLocale] é o seguinte:
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));
}
No modelo, a sequência [${results.culture}] será igual a 'fr-FR' ou 'en-US'.
Voltemos à ação [/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";
}
- linha 7: define a hora a partir da solicitação GET no modelo;
- Linha 9: Exibimos a vista [vue-01.xml]:
7.2.3. A vista [view-01.xml]
![]() | ![]() |
A visualização [view-01.xml] é a seguinte:
<!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>
- linhas 7–12: as bibliotecas jQuery de validação e internacionalização (cultures);
- linha 15: a biblioteca [client-validation] criada na secção 6.3;
- linha 14: a biblioteca JSON utilizada pela biblioteca [client-validation]. É opcional se os registos de validação tiverem sido desativados;
- linha 13: a biblioteca [Unobtrusive Ajax] da Microsoft. Esta biblioteca permite, por vezes, evitar a escrita de JavaScript;
- linha 16: um ficheiro JavaScript para as nossas próprias necessidades;
- linhas 17–22: para lidar com as configurações regionais [fr-FR] e [en-US] no lado do cliente. Já nos deparámos com este código;
- linha 27: uma mensagem configurada. Estudámos isto na secção 5.18;
- linhas 36–38: o formulário ao qual voltaremos mais tarde;
- Linha 40: a área do documento onde o JavaScript colocará a resposta do servidor;
7.2.4. O formulário
![]() |
Na vista [vue-01.xml], o formulário é o seguinte:
<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>
o que produz o seguinte 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>
- linha 16: o campo [a] está associado aos validadores [required], [number] e [min];
- linha 19: o mesmo se aplica ao campo [b];
As várias mensagens encontram-se nos ficheiros [messages.properties] do projeto:
![]() |
[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:
Agora, vamos examinar os atributos da tag [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">
Podemos reconhecer os atributos padrão da tag [form]:
<form id="formulaire" name="formulaire" method="post" action="/ajax-02.html">
É imediatamente evidente que, se o JavaScript estiver desativado no navegador que exibe a página, o formulário será enviado para o URL [/ajax-02.html]. Agora, vamos analisar os outros atributos:
<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">
Os atributos [data-ajax-xxx] são geridos pela biblioteca JavaScript [unobtrusive-ajax], que foi importada pela vista [vue-01.xml]:
<script type="text/javascript" src="/js/jquery/jquery.unobtrusive-ajax.js"></script>
Quando os atributos [data-ajax-xxx] estão presentes, o botão [submit] do formulário será executado através de uma chamada Ajax da biblioteca [unobtrusive-ajax]. Os parâmetros têm os seguintes significados:
- [data-ajax="true"]: a presença deste atributo faz com que o [submit] do formulário seja executado via Ajax;
- [data-ajax-method="post"]: o método do [submit]. A URL POST será a do atributo [action="/ajax-02.html"];
- [data-ajax-loading="#loading"]: o ID de uma área a ser exibida enquanto se aguarda a resposta do servidor. A área identificada por [loading] na vista [vue-01.xml] é a seguinte:
<img id="loading" style="display: none" src="/images/loading.gif" />
Esta é uma imagem animada de carregamento que será exibida até que a resposta do servidor seja recebida;
- [data-ajax-loading-duration="0"]: o tempo de espera, em milissegundos, antes de a área [data-ajax-loading="#loading"] ser exibida. Neste caso, será exibida assim que a espera começar;
- [data-ajax-begin="beforeSend"]: a função JavaScript a executar antes do envio;
- [data-ajax-complete="afterComplete"] : a função JavaScript a executar quando a resposta for recebida;
- [data-ajax-update="#resultats"]: o ID da área onde o resultado enviado pelo servidor será colocado. A vista [vue-01.xml] contém a seguinte área:
<div id="resultats" />
- [data-ajax-mode="replace"]: o modo de inserção do resultado na área anterior. O modo [replace] fará com que o resultado "substitua" o que quer que estivesse anteriormente na área com o ID [resultats];
Note que o JavaScript [submit] só será executado se os validadores tiverem declarado os valores testados como válidos.
A biblioteca JavaScript [unobtrusive-ajax] tem dois objetivos:
- garantir que o formulário se adapta corretamente a ambas as possibilidades: quer o JavaScript esteja ativado ou desativado no navegador;
- evitar escrever JavaScript. Veremos que, neste caso, isso não foi possível evitar.
7.2.5. A ação [/ajax-02]
Vimos que os valores enviados foram direcionados para a ação [/ajax-02]. Ela é a seguinte:
@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()));
...
}
- Vamos simplificar as coisas por agora: vamos assumir que o pedido POST foi efetivamente enviado pelo JavaScript na vista [vue-01.xml]. Voltaremos a esta suposição um pouco mais tarde;
- linha 2: os valores enviados [a,b] são colocados no modelo [ActionModel01];
- linhas 4–7: se o utilizador definiu um tempo limite durante uma solicitação GET anterior, este é recuperado da sessão e o tempo limite é aplicado (linha 6). O objetivo disso é permitir que o utilizador veja o efeito do atributo [data-ajax-loading="#loading"] no formulário;
- linhas 9-10: um atributo [results] é adicionado ao modelo;
- linha 12: a localização [fr-FR] ou [en-US] é adicionada ao modelo;
- linha 14: definimos o tempo de POST no modelo;
Lembre-se do tipo [Resultats] adicionado ao modelo:
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
...
}
O código para a ação [/ajax-02] continua da seguinte forma:
@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";
}
...
}
- Linhas 6–11: Neste exemplo, mostramos como devolver uma página de erro ao cliente JavaScript. Metade das vezes, devolvemos a seguinte vista [view-03.xml]:
![]() |
Observe a linha 9: o que colocamos no modelo não é uma mensagem, mas uma chave de mensagem:
[messages_fr.properties]
erreur.aleatoire=erreur aléatoire
[messages_fr.properties]
erreur.aleatoire=randomly generated error
O código da visualização [vue-03.xml] é o seguinte:
<!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>
- linha 12, observe uma mensagem configurada por uma chave de mensagem que é, ela própria, calculada. Introduzimos este conceito na secção 5.18, página 170.
O código para a ação [/ajax-02] continua da seguinte forma:
@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";
}
- linhas 5–15: as quatro operações aritméticas são realizadas nos números [a, b] e encapsuladas na instância [Resultats] do modelo;
- linha 17: é devolvida a seguinte vista [view-02.xml]:
![]() |
A visualização [view-02.xml] é a seguinte:
<!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>
Quer o resultado seja a vista [vue-02.xml] ou a vista [vue-03.xml], este resultado HTML é colocado na área identificada por [resultats] na vista [vue-01.xml], devido ao atributo [data-ajax-update="#resultats"] do formulário.
7.2.6. Envio dos valores introduzidos
Encontramos aqui um desafio com os valores enviados. Estamos a trabalhar com duas localizações [fr-FR] e [en-US] que representam números reais de forma diferente. Abordámos esta questão na Secção 6.3, página 190, quando precisámos de enviar números reais em duas localizações diferentes. Iremos reutilizar as ferramentas utilizadas nessa altura. No entanto, enfrentamos um desafio adicional: não temos acesso ao método que trata do envio dos valores introduzidos. É por isso que adicionámos os seguintes atributos à tag do formulário:
- [data-ajax-begin="beforeSend"]: a função JavaScript a executar antes de enviar o formulário;
- [data-ajax-complete="afterComplete"]: a função JavaScript a executar quando a resposta for recebida;
Não temos acesso à função JavaScript que irá enviar os valores introduzidos via POST, mas podemos escrever duas funções JavaScript:
- [beforeSend]: uma função JavaScript executada antes do POST;
- [afterComplete]: uma função JavaScript executada após a receção da resposta POST;
Estas duas funções são colocadas num ficheiro chamado [local1.js]:
![]() |
O ficheiro [local1.js] inicializa o ambiente JavaScript da vista [vue-01.xml] da seguinte forma:
// 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);
});
- linha 22: a função [checkCulture] é descrita um pouco mais adiante;
A função JavaScript [beforeSend] será a seguinte:
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) {
...
}
}
- linhas 4-6: verificamos se a localização da vista é [fr-FR]. Neste caso, os valores enviados devem ser alterados. De facto, se o utilizador introduziu [1,6], o valor [1.6] deve ser enviado; caso contrário, o valor [1,6] será rejeitado no lado do servidor. Para tal, basta alterar a vírgula nos valores enviados para um ponto decimal (linhas 18–21);
- mas não podemos ficar por aqui. Quando a função [beforeSend] é chamada, a cadeia de valores enviados [a=val1&b=valB] já foi construída. Por isso, precisamos de a modificar. Isto é feito utilizando o segundo parâmetro da função [settings];
- Linha 7: [settings.data] (settings é um parâmetro da função) representa a string enviada. Recriamos esta string utilizando a expressão [form.serialize()]. Esta expressão percorre o formulário para encontrar os valores a enviar e constrói a string POST. Em seguida, irá utilizar os novos valores de [a,b] com pontos decimais;
Se não fizermos mais nada, o servidor enviará a sua resposta, que será apresentada corretamente. No entanto, os valores de [a,b] têm agora pontos decimais, apesar de ainda estarmos na localização [fr-FR]. Assim, se o utilizador não reparar nisto e clicar novamente em [Calcular], os validadores indicar-lhe-ão que os valores [a,b] são inválidos. O que está correto. É aqui que entra a função [afterComplete], executada ao receber o resultado:
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);
}
}
- linhas 9-12: se a localização da vista for [fr-FR], converte os números [a,b] para o formato francês.
7.2.7. Testes
Aqui estão algumas capturas de ecrã de teste:
![]() |
- em [1], a resposta do servidor;
![]() |
- em [2], a resposta do servidor com uma mensagem de erro;
![]() |
- em [3], é definido um tempo limite de 5 segundos. Isto significa que o servidor irá esperar 5 segundos antes de enviar a sua resposta. Na tag [form], utilizámos o atributo [data-ajax-loading='#loading']. O parâmetro [loading] é o identificador de uma área que é:
- exibida durante todo o tempo de espera;
- oculta após a resposta do servidor ser recebida;
Aqui, [loading] é o identificador de uma imagem animada que pode ser vista em [4].
7.2.8. Desativar o JavaScript com a localização [en-US]
O que acontece se desativarmos o JavaScript no navegador?
O envio (POST) dos valores introduzidos ocorrerá de acordo com a tag [form], cujos atributos [data-ajax-attr] não serão utilizados. Tudo acontece como se tivéssemos a seguinte tag [form]:
<form id="formulaire" name="formulaire" method="post" action="/ajax-02.html">
Os valores introduzidos serão, portanto, enviados para a ação [/ajax-02]. Não terão sido validados no lado do cliente. Por conseguinte, os validadores do lado do servidor assumirão o controlo. Estes já estavam envolvidos anteriormente, mas em valores que já tinham sido validados no lado do cliente e que, por isso, estavam corretos. Este já não é o caso.
Modificamos a ação [/ajax-02] da seguinte forma:
@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"));
...
}
- Linha 4: A ação [/ajax-02] pode agora ser chamada através de um POST Ajax ou de um POST padrão. Precisamos de ser capazes de distinguir entre estes dois casos. Fazemos isto utilizando os cabeçalhos HTTP enviados pelo navegador do cliente;
Quando analisamos o tráfego de rede no Chrome DevTools (Ctrl-Shift-I) com o JavaScript ativado, vemos que o cliente envia os seguintes cabeçalhos durante a solicitação POST:
![]() |
Conforme mostrado acima:
- foi enviado um cabeçalho [X-Requested-With] [1];
- foi adicionado um parâmetro [X-Requested-With] aos valores enviados [2];
Isto não é feito no caso de um POST padrão. Temos, portanto, duas opções para recuperar a informação: recuperá-la dos cabeçalhos HTTP ou dos valores enviados. A linha 4 da ação [/ajax-02] escolheu a primeira solução.
Vamos continuar com o código para esta ação:
@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";
}
...
- linha 2: o parâmetro [@Valid ActionModel01 form] aciona os validadores do lado do servidor;
- linhas 20–22: se o pedido não for um pedido Ajax e a validação falhar, então a vista [vue-01.xml] é devolvida com mensagens de erro.
Eis um exemplo:
![]() | ![]() |
Vamos continuar a nossa análise da ação [/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";
}
}
...
- linha 14: é gerado um erro aleatório;
- linha 16: no caso de uma chamada Ajax, a vista [vue-03.xml] é devolvida e colocada na área identificada por [resultats];
- linha 18: no caso de uma chamada não Ajax, a vista a ser exibida é colocada no modelo [Resultats];
- linha 19: a vista [vue-01.xml] é renderizada novamente;
A vista [vue-01.xml] é modificada da seguinte forma:
<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" />
- Linha 3: A vista [view-03.xml] será inserida na área [results];
Eis um exemplo:
![]() |
Note que os tempos [1] e [2] são agora idênticos.
Vamos continuar o nosso estudo da ação [/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";
}
}
- linhas 7–17: os resultados das quatro operações aritméticas são colocados no modelo;
- linhas 22-23: a vista [vue-01.xml] (linha 22) é renderizada através da inserção da vista [vue-02.xml] (linha 22);
Esta inserção é feita da seguinte forma em [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" />
- Linha 2: A vista [vue-02.xml] será inserida na área [resultats];
Aqui está um exemplo do resultado:
![]() |
7.2.9. Desativar o JavaScript com a configuração regional [fr-FR]
Com a localização [fr-FR], deparamo-nos com o seguinte problema:
![]() | ![]() |
Os valores introduzidos no formato francês foram declarados inválidos. Isto deve-se ao facto de o servidor esperar números reais no formato anglo-saxónico. A solução é bastante complexa. Vamos criar um filtro que irá:
- intercepte o pedido;
- substituir as vírgulas nos valores enviados [a] e [b] por pontos decimais;
- e, em seguida, encaminhará a nova solicitação para a ação que precisa processá-la;
Primeiro, adicionamos um campo oculto à vista [vue-01.xml]:
<form ...>
...
</p>
<!-- hidden fields -->
<input type="hidden" id="culture" name="culture" th:value="${resultats.culture}"></input>
</form>
- linha 5: a cultura [fr-FR] ou [en-US] é colocada no campo do atributo [name=culture]. Como a tag [input] está no formulário, o seu valor será enviado juntamente com os valores de [a] e [b]. Teremos então uma string enviada no formulário:
É importante compreender este ponto.
Em seguida, incluímos um filtro na configuração da aplicação:
![]() |
O ficheiro [Config] é modificado da seguinte forma:
@Configuration
@ComponentScan({ "istia.st.springmvc.controllers", "istia.st.springmvc.models" })
@EnableAutoConfiguration
public class Config extends WebMvcConfigurerAdapter {
...
@Bean
public Filter cultureFilter() {
return new CultureFilter();
}
}
- Linha 7: O facto de o bean [cultureFilter] devolver um tipo [Filter] torna-o um filtro. O próprio bean pode ter qualquer nome;
O próximo passo é criar o próprio filtro:
![]() |
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);
}
}
- linha 12: estendemos a classe [OncePerRequestFilter], que é uma classe Spring, e o que precisamos de fazer é sobrescrever o método [doFilterInternal] desta classe;
- linha 15: o método [doFilterInternal] recebe três informações:
- [HttpServletRequest request]: o pedido a ser filtrado. Este não pode ser modificado,
- [HttpServletResponse response]: a resposta a ser enviada ao servidor. O filtro pode optar por gerá-la ele próprio,
- [FilterChain filterChain]: a cadeia de filtros. Assim que o método [doFilterInternal] terminar o seu trabalho, deve passar a solicitação para o próximo filtro na cadeia de filtros;
- linha 18: criamos uma nova solicitação a partir da que recebemos [new CultureRequestWrapper(request)] e a passamos para o próximo filtro. Como não podemos modificar a solicitação inicial [HttpServletRequest request], criamos uma nova;
A classe [CultureRequestWrapper] é a seguinte:
![]() |
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);
}
}
- linha 6: a classe [CultureRequestWrapper] estende a classe [HttpServletRequestWrapper] e irá substituir alguns dos seus métodos;
- linhas 8–10: o construtor que recebe a solicitação a ser filtrada e a passa para a classe pai;
- É importante compreender aqui que a solicitação filtrada acabará por se tornar um parâmetro de entrada para uma classe chamada servlet. Com o Spring MVC, este servlet é do tipo [DispatcherServlet]. Esta classe possui vários métodos para recuperar parâmetros de solicitação: [getParameter, getParameterMap, getParameterNames, getParameterValues, ...]. O método utilizado pelo servlet deve ser redefinido. Para tal, seria necessário ler o código da classe [DispatcherServlet]. Não o fiz e redefini vários métodos. Por fim, foi o método [getParameterValues] que foi redefinido;
- linha 13: o método [getParameterValues] recebe como parâmetro o nome de um dos parâmetros devolvidos pelo método [getParameterNames] e deve devolver uma matriz com os seus valores. De facto, sabemos que um parâmetro pode aparecer várias vezes numa solicitação;
- linha 18: a vírgula é substituída por um ponto decimal;
Eis um exemplo de execução:
![]() |
- em [1], os valores [a,b] são introduzidos no formato francês;
- em [2], os resultados;
- em [3], o servidor devolveu uma página com números no formato anglo-saxónico.
Este problema pode ser resolvido com o Thymeleaf da seguinte forma na vista [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>
Há várias alterações a fazer nas linhas 3 e 6. Vamos concentrar-nos na linha 3:
- tínhamos escrito [th:field="*{a}"]. O parâmetro [th:field] define os atributos [id, name, value] da tag HTML [input] gerada. Aqui, queremos gerir o atributo [value] nós próprios. Por isso, também definimos os atributos [id, name] nós próprios;
- o atributo [th:value] avalia uma expressão utilizando o operador ternário ?. Testamos a expressão [${resultats.culture}=='fr-FR' and ${actionModel01.b}!=null]. Se for verdadeira, definimos o atributo [value] com o valor de [actionModel01.a], onde o ponto decimal é substituído por uma vírgula. Se for falsa, definimos o atributo [value] com o valor de [actionModel01.a] sem alterações;
- Linha 6: Fazemos o mesmo para o campo [b];
Eis um exemplo de execução:
![]() |
- Em [1], os números [a,b] mantêm a notação francesa. Este não é o caso em [2];
Esta nova questão é tratada da mesma forma que a anterior. Modificamos a vista [vue-03.xml] da seguinte forma:
<!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>
Eis um exemplo:
![]() | ![]() |
Agora temos uma aplicação que lida corretamente com duas configurações regionais num ambiente que pode ou não utilizar JavaScript. Para conseguir isto, tivemos de aumentar significativamente a complexidade do código do lado do servidor. Daqui em diante, assumiremos sempre que o JavaScript está ativado no navegador. Isto permite funcionalidades que são impossíveis no modo apenas de servidor.
7.2.10. Tratamento do link [Calcular]
Vamos examinar o link [Calcular] na página principal [vue-01.xml]:
![]() | ![]() |
O código para o link [Calcular] na vista [vue-01.xml] é o seguinte:
<a href="javascript:postForm()" th:text="#{action.calculer}">Calculer</a>
A função JavaScript [postForm] está definida no ficheiro [local1.js] da seguinte forma:
// 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);
}
})
}
- linhas 2–5: Recorde-se que estes elementos foram inicializados pela função [$(document).ready];
- linhas 9-12: Executamos os validadores JavaScript do formulário. Se algum dos valores for inválido, a expressão [form.validate().form()] retorna false. Neste caso, o [submit] do formulário é cancelado;
- linhas 18-38: fazemos uma chamada Ajax manual;
- linha 19: o URL de destino da chamada Ajax;
- linhas 20–22: um conjunto de cabeçalhos HTTP a adicionar aos incluídos por predefinição no pedido HTTP. Aqui, adicionamos o cabeçalho HTTP que indicará ao servidor que estamos a efetuar uma chamada Ajax;
- linha 23: o método HTTP utilizado;
- linha 24: os dados a serem enviados. [formulaire.serialize] cria a string a ser enviada [culture=fr-FR&a=12,7&b=20,89] a partir do formulário com ID [formulaire]. Aqui deparamo-nos com o problema discutido anteriormente: os valores [a,b] devem ser enviados no formato anglo-saxónico. Sabemos que este problema já foi resolvido com a criação do filtro [cultureFilter];
- linha 25: o tipo de dados de retorno esperado. Sabemos que o servidor irá devolver um fluxo HTML;
- linha 26: o método a executar quando a solicitação é iniciada. Aqui, especificamos que o componente com o id [loading] deve ser exibido. Trata-se da imagem animada de carregamento;
- linha 29: o método a executar se a solicitação Ajax for bem-sucedida. O parâmetro [data] é a resposta completa do servidor. Sabemos que se trata de um fluxo HTML;
- linha 30: atualizamos o componente com o ID [results] com o HTML do parâmetro [data].
- linha 33: ocultamos o indicador de carregamento;
- linha 35: função executada quando a resposta do servidor é recebida, independentemente de ser um sucesso ou um erro;
- Linhas 35–37: Se ocorrer um erro (o servidor devolveu uma resposta HTTP com um código de estado que indica um erro do lado do servidor), a resposta HTML do servidor é apresentada na área [results];
Aqui está um exemplo de execução:
![]() | ![]() |
7.3. Atualização de uma página HTML com um feed JSON
No exemplo anterior, o servidor web respondeu ao pedido HTTP Ajax com um fluxo HTML. Este fluxo continha dados acompanhados de formatação HTML. Vamos revisitar o exemplo anterior, desta vez utilizando respostas JSON (JavaScript Object Notation) que contêm apenas os dados. A vantagem é que são transmitidos menos bytes. Partimos do princípio de que o JavaScript está ativado no navegador.
7.3.1. A ação [/ajax-04]
A ação [/ajax-04] é idêntica à ação [/ajax-01], exceto que apresenta a vista [vue-04.xml] em vez da vista [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. A visualização [view-04.xml]
![]() |
A vista [view-04.xml] utiliza o corpo da vista [view-01.xml] com as seguintes diferenças:
<!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>
- linha 5: o JavaScript da vista encontra-se agora no ficheiro [local4.js];
- linha 16: a tag [form] já não tem os parâmetros [data-ajax-attr] da biblioteca [Unobtrusive Ajax]. Não iremos utilizá-los aqui. A tag [form] também já não tem os atributos [method] e [action], que especificam como e para onde enviar os valores introduzidos no formulário. Isto porque o formulário será enviado por uma função JavaScript (linha 20);
- linhas 26–57: a área com o ID [resultats], que anteriormente estava vazia, contém agora código HTML para apresentar os resultados;
- linhas 26–34: o cabeçalho dos resultados, onde é exibido o tempo de cálculo;
- linhas 35–52: os resultados das quatro operações aritméticas;
- linhas 53–57: quaisquer mensagens de erro enviadas pelo servidor;
O código JavaScript executado quando a vista [vue-04.xm] é carregada encontra-se no ficheiro [local4.js]. É o seguinte:
// 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();
});
- linhas 17–27: recuperam as referências jQuery para todos os elementos da página;
- linha 29: a área de resultados é ocultada;
- linha 30: assim como a área de erros;
- linha 31: assim como a imagem de carregamento animada;
- linhas 2–12: as referências recuperadas são tornadas globais para que outras funções possam aceder-lhes;
7.3.3. A função jS [postForm]
O link [Calculate] é o seguinte:
<p>
<img id="loading" style="display: none" src="/images/loading.gif" />
<a href="javascript:postForm()" th:text="#{action.calculer}">Calculer</a>
</p>
A função JavaScript [postForm] está definida no ficheiro [local.js] da seguinte forma:
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() {
...
}
- linhas 3–6: Antes de enviar os valores introduzidos, validamo-los. Se estiverem incorretos, não enviamos o formulário;
- linha 9: os valores introduzidos são enviados para a ação [/ajax-05], que explicaremos com mais detalhe mais tarde;
- linhas 10–12: um cabeçalho HTTP para indicar ao servidor que esperamos uma resposta no formato JSON;
- Linha 13: Os valores introduzidos serão enviados;
- linha 14: serialização dos valores introduzidos numa string pronta para ser enviada [a=1,6&b=2,4&culture=fr-FR];
- linha 15: o tipo de resposta enviada pelo servidor. Será JSON;
- linha 16: a função a executar antes do POST;
- linha 17: a função a executar ao receber a resposta do servidor, caso seja bem-sucedida. O «sucesso» de um pedido HTTP é determinado pelo estado da resposta HTTP do servidor. Uma resposta [HTTP/1.1 200 OK] é uma resposta bem-sucedida. Uma resposta [HTTP/1.1 500 Internal Server Error] é uma resposta de falha. O que se designa por estado de uma resposta HTTP é o código [200] ou [500]. Alguns destes códigos estão associados a «sucesso», enquanto outros estão associados a «falha»;
- linha 18: a função a executar ao receber a resposta do servidor quando o estado HTTP dessa resposta for um estado de falha;
- linha 18: a função a ser executada por último, após as funções [onSuccess, onError] anteriores;
A função [onBegin] é a seguinte:
// 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();
}
Antes de explorar as outras funções JavaScript da chamada Ajax, precisamos de conhecer a resposta enviada pela ação [/ajax-05].
7.3.4. A ação [/ajax-05]
A ação [/ajax-05] é a seguinte:
@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;
}
...
}
- linha 2: o atributo [ResponseBody] indica que a própria ação [/ajax-05] devolve a resposta ao cliente. Como uma biblioteca JSON está incluída nas dependências do projeto, o Spring Boot configura automaticamente este tipo de ação para devolver JSON. Portanto, a cadeia JSON do tipo [JsonResults] (linha 4) será enviada ao cliente;
- Linha 2: Os valores enviados [a, b, culture] serão encapsulados num tipo [ActionModel01], que validamos [@Valid ActionModel01]. Isto é apenas por uma questão de formalidade. Partimos do princípio de que o JavaScript está ativado no navegador do cliente, pelo que, quando chegam, os valores enviados já foram verificados no lado do cliente. No entanto, podemos antecipar o caso de um pedido POST não autorizado que não utilize o nosso cliente JavaScript. Neste caso, a validação pode falhar;
- linhas 5–7: em caso de erro, devolvemos um objeto JSON vazio;
Vamos continuar a nossa análise da ação [/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;
}
- Linha 8: Recuperamos o contexto [ctx] da aplicação Spring. Precisamos disso para recuperar mensagens dos ficheiros [messages.properties] com base numa chave de mensagem e numa localização. Isto é feito utilizando a seguinte sintaxe:
ctx.getMessage(clé_message, tableau_de_paramètres, locale)
- [chave_da_mensagem]: a chave da mensagem que está a ser procurada;
- [locale]: a localização utilizada. Assim, se esta localização for [en_US], será utilizado o ficheiro [messages_en.properties];
- [matriz_de_parâmetros]: a mensagem recuperada pode ser parametrizada como em [chave=mensagem {0} {1}]. Esta mensagem contém dois parâmetros [{0} {1}]. Deve fornecer uma matriz de dois valores como segundo parâmetro de [ctx.getMessage];
- linhas 10-13: se ocorrer um tempo limite na sessão, o segmento de execução atual é suspenso durante o período de tempo limite;
A ação [/ajax-05] continua da seguinte forma:
// on prépare le modèle de la prochaine vue
JsonResults résultats = new JsonResults();
...
}
- linha 2: criação do modelo de cadeia JSON enviado ao cliente;
O modelo [JsonResults] é o seguinte:
![]() |
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
...
}
- linhas 6–13: cada campo na classe [JsonResult] corresponde a um campo com o mesmo [id] na vista [vue-04.xml]:
A ação [/ajax-05] continua da seguinte forma:
// 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;
}
- linha 2: cria o modelo de string JSON enviado ao cliente;
- linhas 4–6: criar as mensagens para o cabeçalho de resultados;
- linhas 8–14: em média, é gerada uma mensagem de erro a cada duas tentativas. Neste caso, o processo pára aí e a string JSON é devolvida ao cliente (linha 13);
- linha 11: eis um exemplo de uma mensagem parametrizada:
erreur.aleatoire=erreur aléatoire
resultats.erreur=Une erreur s''est produite : [{0}]
A ação [/ajax-05] continua da seguinte forma:
// 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;
- linhas 2-3: recuperamos os valores de [a] e [b];
- linhas 5-12: construímos os quatro resultados;
- linha 14: a cadeia JSON [JsonResults] é enviada ao cliente;
Vamos ver como isto funciona com o [Advanced Rest Client]:
![]() |
- em [1-2], fazemos uma solicitação POST para a ação [/ajax-05];
- em [3], enviamos valores incorretos;
- em [4], o servidor devolveu uma resposta vazia;
![]() |
- Em [1], enviamos valores corretos;
- em [2], o objeto JSON devolvido pelo servidor, com uma mensagem de erro aqui;
![]() |
- Em [1], enviamos valores corretos;
- em [2], o objeto JSON devolvido pelo servidor, mostrando os quatro resultados;
![]() |
- em [1], enviamos valores corretos;
- em [2], provocámos uma exceção do lado do servidor. Vemos que o servidor continua a enviar um objeto JSON. Nesta mensagem, vemos que o estado HTTP da resposta é [500], indicando que ocorreu um erro do lado do servidor;
7.3.5. A função jS [postForm] - 2
Agora que conhecemos o objeto JSON devolvido pelo servidor, podemos utilizá-lo em JavaScript. O método [onSuccess], que é executado quando o servidor envia uma resposta com o estado HTTP [200], é o seguinte:
// 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();
}
- linha 3: o parâmetro [data] é o objeto JSON devolvido pelo servidor:
![]() |
O método [onError] executado quando o estado da resposta HTTP é [500] é o seguinte:
// on receipt of the server response
// in case of failure
function onError(jqXHR) {
console.log("onError");
// system error
msgErreur.text(jqXHR.responseText);
erreur.show();
}
- Linha 3: O objeto jQuery [jqXHR] tem as seguintes propriedades:
- responseText: o texto da resposta do servidor,
- status: o código de erro devolvido pelo servidor,
- statusText: o texto associado a este código de erro;
- linha 6: o objeto [jqXHR.responseText] é o seguinte objeto JSON:
![]() |
7.3.6. Testes
Vejamos algumas capturas de ecrã da aplicação web em funcionamento:
![]() |
![]() |
![]() |
7.4. Aplicação web de página única
7.4.1. Introdução
A tecnologia Ajax permite-lhe criar aplicações de página única:
- a primeira página é carregada através de um pedido normal do navegador;
- as páginas subsequentes são carregadas através de chamadas Ajax. Como resultado, o navegador nunca altera o seu URL e nunca carrega uma nova página. Este tipo de aplicação é designado por Aplicação de Página Única (SPA).
Aqui está um exemplo básico de uma aplicação deste tipo. A nova aplicação terá duas vistas:
![]() |
![]() |
- em [1], a ação [/ajax-06] abre a primeira página, a página 1;
- em [2], um link permite-nos navegar para a página 2 através de uma chamada Ajax;
- em [3], o URL não mudou. A página apresentada é a página 2;
- em [4], um link permite-nos regressar à página 1 através de uma chamada Ajax;
- em [5], o URL não mudou. A página apresentada é a página 1.
7.4.2. A ação [/ajax-06]
O código para a ação [/ajax-06] é o seguinte:
@RequestMapping(value = "/ajax-06", method = RequestMethod.GET, produces = "text/html; charset=UTF-8")
public String ajax06() {
return "vue-06";
}
- Linhas 1–4: A ação [/ajax-06] simplesmente renderiza a vista [vue-06.xml];
7.4.3. A vista [vue-06.xml]
A vista [vue-06.xml] é a seguinte:
<!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>
- linha 8: a vista utiliza um script [local6.js];
- linha 12: a vista [vue-07.xml] está incluída na área de ID [content] da vista [vue-06.xml];
7.4.4. A vista [vue-07.xml]
A vista [vue-07.xml] é a seguinte:
<!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. A função jS [gotoPage]
O link [Página 2] na vista [vue-07.xml] utiliza a função jS [gotoPage] definida no seguinte ficheiro [local6.js]:
// 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");
});
- linha 28: quando a página carrega, guardamos o elemento com o ID [content] e tornamo-lo uma variável global (linha 2);
- linha 4: a função [gotoPage] recebe como parâmetro o número da página (1 ou 2) a ser exibida na visualização atual;
- linha 7: o URL de destino para o pedido POST;
- linha 8: o URL da linha 7 é solicitado através de um POST;
- linha 9: a string enviada. É enviado um parâmetro chamado [num]. O seu valor é o número da página (linha 4) a ser exibida na visualização atual;
- linha 10: o servidor irá devolver HTML, especificamente o HTML da página a ser apresentada;
- linhas 13–15: se for bem-sucedido (estado HTTP 200), o HTML enviado pelo servidor é colocado no elemento com id [content];
- linhas 18-20: se a solicitação falhar (status HTTP 500), o HTML enviado pelo servidor é colocado no campo com id [content];
7.4.6. A ação [/ajax-07]
O código para a ação [/ajax-07] é o seguinte:
@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";
}
}
- linha 2: recuperamos o parâmetro enviado chamado [num]. Note que o parâmetro na linha 2 deve ter o mesmo nome que o parâmetro enviado, neste caso [num]. [num] é um número de página ou de visualização;
- linhas 5-6: se [num==1], devolvemos a vista [vue-07.xml];
- linhas 7-8: se [num==2], devolvemos a vista [vue-08.xml];
- linhas 9-10: em todos os outros casos (o que normalmente é impossível), a vista [vue-07.xml] é devolvida;
7.4.7. A vista [view-08.xml]
A vista [view-08.xml] constitui a página 2 da aplicação:
<!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. Incorporar vários fluxos HTML numa resposta JSON
7.5.1. Introdução
Considere a seguinte aplicação:
![]() |
A página [1] tem quatro áreas:
- [Zona 1] e [Zona 3] são zonas que aparecem ou desaparecem quando se clica no botão [Atualizar]. Contamos o número de vezes que cada uma destas duas zonas aparece [2]. A zona [Zona 1] utiliza o francês, enquanto a zona [Zona 3] utiliza o inglês;
- [Zona 2] está sempre presente;
- a secção [Entradas] está sempre visível;
O link [Submit] exibe a página seguinte [3]:
![]() |
- o link [Voltar à Página 1] restaura a Página 1 ao seu estado anterior [4];
A aplicação é uma aplicação de página única. A primeira página é solicitada ao servidor pelo navegador. As páginas subsequentes são recuperadas do servidor através de chamadas Ajax.
7.5.2. A ação [/ajax-09]
![]() |
A ação [/ajax-09] é a seguinte:
@RequestMapping(value = "/ajax-09", method = RequestMethod.GET, produces = "text/html; charset=UTF-8")
public String ajax09() {
return "vue-09";
}
Apenas apresenta a vista [vue-09.xml].
7.5.3. Visualizações XML
![]() |
A visualização [vue-09.xml] é a página principal da aplicação:
<!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>
- linha 9: o ficheiro JS utilizado na aplicação;
- linha 15: o conteúdo da página mestre;
- linha 16: uma imagem animada de carregamento:
- linha 17: área para exibir eventuais erros;
A vista [vue-09-page1.xml] é a página 1 da aplicação:
<!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>
- linhas 6–9: a área [Zone 1]. O seu conteúdo é colocado no componente [id="zone1-content"];
- linhas 11-14: a área [Zone 2], que não se altera;
- linhas 16-19: a área [Zone 3]. O seu conteúdo é colocado no componente [id="zone3-content"];
- linha 22: a função JS que envia o formulário;
- linha 25: inclusão da área de entrada;
Note que a página 1 não tem uma tag [form]. Tudo será tratado em JavaScript.
A visualização [vue-09-saisies.xml] é a seguinte:
<!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>
- linhas 5-8: introduza uma cadeia de caracteres;
- linhas 13-16: introduza um número inteiro;
- linha 14: a função JS que envia os valores introduzidos;
Mais uma vez, note que o campo de entrada não tem uma tag [form].
No total, a página 1 tem duas funcionalidades:
- [Atualizar]: que atualiza as zonas 1 e 3. Esta ação é tratada pelo servidor, que devolve aleatoriamente:
- o campo 1 com o seu contador de acessos e nada para o campo 3,
- a zona 3 com o seu contador de acessos e nada para a zona 1,
- ambas as zonas com os seus contadores de acesso;
- [Enviar]: que apresenta a página 2 com os valores introduzidos ou uma mensagem de erro se os dados introduzidos forem inválidos;
Vamos primeiro concentrar-nos no botão [Atualizar].
7.5.4. O código JS para o botão [Atualizar]
![]() |
O código no ficheiro [local9.js] é o seguinte:
// 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");
});
- linhas 9-13: quando a página mestre é carregada, as referências aos três componentes identificados por [loading, error, content] são armazenadas;
- linhas 2-4: as referências a estes três componentes são armazenadas em variáveis globais. Permanecem constantes porque as três áreas em questão estão sempre presentes na página apresentada, independentemente do momento. Como permanecem constantes, podem ser calculadas em [$(document).ready] e partilhadas com as outras funções no ficheiro JS;
A função [postForm] trata do clique no botão [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
})
}
- linhas 4–15: a chamada Ajax para o servidor;
- linha 5: a ação [ajax-10] irá tratar do POST;
- linhas 6-8: a resposta será JSON. O cliente JS indica que aceita documentos JSON;
- linha 9: a ação [ajax-10] é chamada com uma operação POST;
- linha 10: receberemos JSON;
- linha 11: a função executada antes da chamada Ajax;
- linha 12: a função executada ao receber a resposta do servidor, quando esta é bem-sucedida [200 OK];
- linha 13: a função executada ao receber a resposta do servidor, quando esta falha [500 Internal Server Error, ...];
- linha 14: a função executada após receber a resposta;
A função [onBegin] é a seguinte:
// before the Ajax call
function onBegin() {
console.log("onBegin");
// waiting image
loading.show();
}
Simplesmente exibe a imagem animada de carregamento enquanto aguarda a resposta do servidor.
7.5.5. A ação [/ajax-10]
![]() |
A ação [/ajax-10] é a seguinte:
// 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) {
...
}
- Linha 3: A sessão é injetada. Tem o seguinte tipo [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
...
}
A sessão [SessionModel1] armazena o seguinte:
- linha 15: o número de vezes [cpt1] que a área [Zona 1] é exibida;
- linha 16: o número de vezes [cpt3] que a área [Zone 3] é exibida;
- linhas 18–20: os fluxos HTML para as zonas [Zone 1], [Zone 3] e [Inputs]. Isto é necessário na sequência [Page 1] --> [Page 2] --> [Page 1]. Ao passar da [Page 2] para a [Page 1], a [Page 1] e as suas três zonas devem ser restauradas;
- linhas 21-22: dois booleanos que indicam se as zonas [Zona 1] e [Zona 3] são exibidas (visíveis);
O outro elemento injetado no [AjaxController] é o seguinte:
// the Thymeleaf / Spring engine
@Autowired
private SpringTemplateEngine engine;
O bean [SpringTemplateEngine] é definido no ficheiro de configuração [Config]:
![]() |
É definido da seguinte forma:
@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;
}
- linhas 2–10: estamos familiarizados com o bean [SpringResourceTemplateResolver], que nos permite definir certas características das visualizações;
- linhas 13–17: O bean [SpringTemplateEngine] permite-nos definir o «motor» da vista, a classe responsável por gerar respostas [Thymeleaf] para os clientes. O [Thymeleaf] tem um «motor» padrão e outro quando utilizado num ambiente [Spring]. É este último que estamos a utilizar aqui;
A assinatura da ação [/ajax-10] é a seguinte:
@RequestMapping(value = "/ajax-10", method = RequestMethod.POST)
@ResponseBody()
public JsonResult10 ajax10(HttpServletRequest request, HttpServletResponse response) {
...
}
- Linha 1: A ação [/ajax-10] aceita apenas uma solicitação POST;
- linha 2: a ação [/ajax-10] devolve a resposta ao próprio cliente. Esta será automaticamente convertida para JSON;
- linha 3: a resposta é do tipo [JsonResult10], como se segue:
![]() |
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
...
}
- linha 6: o conteúdo HTML da área identificada por [content];
- linha 7: o conteúdo HTML da área [Zona 1];
- linha 8: o conteúdo HTML da área [Zona 3];
- linha 9: o conteúdo HTML da área [Erro];
- linha 10: o conteúdo HTML da área [Inputs];
- linha 11: um valor booleano que indica se a área [Zone 1] deve ser exibida;
- linha 12: um valor booleano que indica se a área [Zona 3] deve ser exibida;
O código para a ação [/ajax-10] é o seguinte:
@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;
}
- Linha 5: Recuperamos o contexto [Thymeleaf]. Veremos mais tarde para que serve;
- linha 7: criamos uma resposta vazia por enquanto;
- linhas 9–12: definimos os dois campos na sessão como [null] e especificamos que não devem ser exibidos. Estes dois campos serão gerados em breve, mas é possível que apenas um deles venha a ser;
- linhas 14–29: ambos os campos são gerados;
- linhas 17–19: apenas a zona [Zone 1] é gerada;
- linhas 21–23: apenas a zona [Zone 3] é gerada;
- linhas 25–28: tanto a [Zona 1] como a [Zona 3] são geradas;
O fluxo HTML para a zona [Zona 1] é gerado pelo seguinte método:
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);
}
- linha 1: os parâmetros são:
- o contexto [Thymeleaf] do tipo [WebContext],
- a resposta para o cliente atualmente a ser construída do tipo [JsonResult10];
- linha 3: incrementamos o contador de sessão [cpt1], que conta o número de vezes que a zona [Zone 1] é exibida;
- linha 4: o contexto [Thymeleaf] do tipo [WebContext] comporta-se de forma semelhante ao [Model] no Spring MVC. Para adicionar um elemento ao modelo, utilizamos [WebContext.setVariable]. Aqui, colocamos o contador [cpt1] no modelo [Thymeleaf]. Isto permitirá que a expressão Thymeleaf [${cpt1}] seja avaliada
- linha 5: o contexto [Thymeleaf] tem uma localização. Isto permite-lhe avaliar expressões do tipo [#{key_msg}]. Aqui, associamos o contexto Thymeleaf a uma localização francesa;
- linha 6: esta é a instrução mais interessante. O motor Thymeleaf irá processar a vista [vue-09-zone1.xml] utilizando o modelo e a localização que acabámos de calcular e, em vez de enviar a saída HTML resultante para o cliente, devolve-a como uma cadeia de caracteres;
- linhas 7–9: a saída HTML para a área [Zone 1] que acabou de ser calculada é armazenada na sessão e no resultado a ser enviado ao cliente. Além disso, especificamos que a área [Zone 1] deve ser exibida;
- linhas 11–13: as informações relativas à área [Zone 1] são armazenadas na sessão para que possam ser regeneradas;
A linha 7 processa a seguinte vista [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>
- linha 3: a expressão [#{message.zone}] será avaliada utilizando a localização;
- linha 4: a expressão [${cpt1}] será avaliada utilizando o modelo Thymeleaf;
A mensagem-chave [message.zone] está definida nos ficheiros de mensagens [messages_fr.properties] e [messages_en.properties]:
![]() |
[messages_fr.properties]
message.zone=Nombre d'accès :
[messages_en.properties]
message.zone=Number of hits:
O fluxo HTML para a área [Zona 3] é gerado por um método semelhante:
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);
}
- linha 6: a localização para a zona [Zone 3] é o inglês;
7.5.6. Processamento da resposta da ação [/ajax-10]
Voltemos ao código JS em [local9.js] que irá processar a resposta do servidor:
// 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();
}
}
Vamos rever a estrutura Java da resposta recebida na linha 3 na variável [data]:
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;
}
- Linhas 6–8: Se [data.content] não for nulo, então o campo [id=content] é inicializado com esse valor. Este campo representa [Página 1] ou [Página 2] na sua totalidade. Neste exemplo, [data.content == null], pelo que a zona [id=content] não será modificada e continuará a exibir [Página 1];
- linhas 10-17: exibe [Zona 1] se [data.zone1Active==true]. Se, além disso, [data.zone1!=null], então o conteúdo de [Zona 1] é modificado; caso contrário, permanece como estava;
- linhas 19–26: o mesmo se aplica à [Zona 3];
- linhas 28–30: se [data.saisies!=null], então a zona [Saisies] é atualizada. Nesta demonstração, [data.saisies==null], pelo que a zona [Saisies] permanece inalterada;
- linhas 32–37: o mesmo raciocínio aplica-se ao campo [Error], com as seguintes nuances:
- linha 33: [data.error] será uma mensagem de erro em formato de texto;
- linha 36: se [data.error] for nulo, então o campo [Error] é ocultado. Isto porque pode ter sido exibido durante o pedido anterior;
No caso de um erro do lado do servidor (estado HTTP como 500 Internal Server Error), é executada a seguinte função:
// on receipt of the server response
// in case of failure
function onError(jqXHR) {
console.log("onError");
// system error
erreur.text(jqXHR.responseText);
erreur.show();
}
Para ver esse erro, vamos modificar a função [postForm] da seguinte forma:
function postForm() {
console.log("postForm");
// retrieve references to the current page
...
// make a manual Ajax call
$.ajax({
url : '/ajax-10x',
...
})
}
- linha 7: introduzimos um URL que não existe;
Eis os resultados quando clica no botão [Atualizar]:
![]() |
É interessante notar que o erro também foi enviado na forma de uma cadeia JSON.
O método executado após receber a resposta do servidor é o seguinte:
// after [onSuccess, onError]
function onComplete() {
console.log("onComplete");
// waiting image
loading.hide();
}
Simplesmente ocultamos a imagem animada de carregamento.
7.5.7. Exibir a página [Página 2]
O código HTML para o link [Submit] é o seguinte:
<a href="javascript:valider()">Valider</a>
A função JS [validate] é a seguinte:
// 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
})
}
- linhas 4-7: temos dois valores, v1 e v2, para enviar: os provenientes dos componentes de entrada identificados por [#text1] e [#text2]. Vamos fazer algo novo. Vamos enviar estes dois valores como uma string JSON {"value1":v1,"value2":v2};
- linha 10: os valores enviados serão encaminhados para a ação [ajax-11A];
- linha 12: como sabemos que vamos receber uma resposta JSON, especificamos que podemos receber JSON;
- linha 13: informamos ao servidor que vamos enviar-lhe o valor enviado como uma string JSON;
- linhas 15-16: enviamos o valor a ser enviado via POST;
- linha 17: iremos receber JSON;
7.5.8. A ação [ajax-11A]
A ação [ajax-11A] que processa a cadeia JSON enviada é a seguinte:
@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) {
...
}
- Linha 1: Especificamos com ["application/json"] que a ação espera um documento no formato JSON. Este documento é o valor enviado pelo cliente;
- linha 3: o valor enviado será recuperado no seguinte objeto [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
...
}
- A estrutura do objeto [PostAjax11A] deve corresponder à estrutura do objeto enviado {"value1":v1,"value2":v2}. Portanto, os campos [value1] (linha 13) e [value2] (linha 16) são obrigatórios;
- Colocámos restrições de integridade em ambos os campos;
Voltemos ao código da ação [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;
}
...
}
- linha 3: a anotação [@RequestBody] refere-se ao documento enviado pelo cliente. Este é o valor enviado pelo cliente no formato JSON. Será, portanto, utilizado para construir o objeto [PostAjax11A];
- linha 3: a anotação [@Valid] impõe a validação do valor enviado;
- linha 9: se a validação falhar:
- linha 13: é devolvida uma mensagem de erro,
- linhas 11–12: os campos 1 e 3 são restaurados ao seu estado anterior (exibidos ou não);
A mensagem de erro é calculada da seguinte forma:
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();
}
Esta é uma função que já vimos anteriormente.
A ação [ajax-11A] continua da seguinte forma:
@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;
}
- linhas 13-14: os valores enviados são colocados no contexto Thymeleaf;
- linha 15: utilizando este contexto, calculamos a vista [vue-09-saisies] e guardamo-la na sessão para que possamos regenerá-la mais tarde;
- linha 17: a página 2 é colocada no resultado que será enviado ao cliente;
A vista [view-09-page2.xml] é a seguinte:
![]() |
<!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>
- As linhas 9 e 13 exibem os valores [valor1, valor2] que a ação [/ajax-11A] colocou no contexto do Thymeleaf;
7.5.9. Processamento da resposta da ação [/ajax-11A]
No lado do cliente, a resposta da ação [/ajax-10] é processada pela função [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();
}
}
Já comentámos este código. Vamos considerar os dois casos: uma resposta com ou sem erro:
Com erro
Neste caso, a ação [/ajax-11A] enviou uma resposta JSON na forma {"zone1":null, "zone3":null,"saisies":null,"erreur":erreur,"zone1Active":zone1Active,"zone3Active":zone3Active,"content":null}. Se seguirmos o código acima, vemos que:
- o campo [content] não se altera. Este continha a página n.º 1;
- o campo [Error] é exibido;
- os campos [Zone 1], [Zone 3] e [Entries] permanecem inalterados;
Sem erro
Neste caso, a ação [/ajax-11A] enviou uma resposta JSON na forma {"zone1":null, "zone3":null,"saisies":null,"erreur":null,"zone1Active":false,"zone3Active":false,"content":content}. Se seguirmos o código acima, vemos que:
- o campo [content] é apresentado. Contém a página n.º 2;
Aqui estão três exemplos de execução:
Um caso com um erro de validação:
![]() | ![]() |
Um caso com um erro POST:
![]() | ![]() |
Este tipo de erro é diferente. Como o Spring não conseguiu converter a string JSON para o tipo [PostAjax11A], devolveu uma resposta HTTP com [status=400]. A ação [ajax-11A] não foi executada;
Um caso sem erro:
![]() | ![]() |
7.5.10. Voltar à página 1
O link [Voltar à página 1] na página 2 é o seguinte:
<a href="javascript:retourPage1()">Retour à la page 1</a>
O método JS [returnPage1] é o seguinte:
// 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
})
}
Envia um pedido POST, sem quaisquer dados enviados, para a ação [/ajax-11B].
7.5.11. A ação [/ajax-11B]
A ação [/ajax-11B] é a seguinte:
@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;
}
A ação deve regenerar a página n.º 1 com as suas três zonas [Zona1, Zona3, Erro]:
- linha 9: a página 1 é adicionada ao resultado;
- linha 10: a zona de entrada é adicionada ao resultado;
- linha 11: o campo [Zona 1] é incluído no resultado;
- linha 12: a zona [Zona 3] é adicionada ao resultado;
- linhas 13-14: o estado das zonas [Zona 1] e [Zona 3] é adicionado ao resultado;
7.5.12. Processamento da resposta da ação [/ajax-11B]
A resposta da ação [/ajax-11B] é processada pela função [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();
}
}
A ação [/ajax-11B] enviou uma resposta JSON no formato {"zone1":zone1, "zone3":zone3,"saisies":saisies,"erreur":null,"zone1Active":zone1Active,"zone3Active":zone3Active,"content":content}. Se seguirmos o código acima, vemos que:
- o campo [content] foi modificado. Anteriormente, continha a página n.º 2. Agora, conterá a página n.º 1;
- o campo [Erro] está oculto;
- as zonas [Zona 1], [Zona 3] e [Entradas] são apresentadas tal como estavam;
7.6. Gestão da sessão no lado do cliente
7.6.1. Introdução
Na secção anterior, gerimos uma sessão com a seguinte estrutura:
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;
...
}
Quando há um grande número de utilizadores, a memória ocupada por todas as sessões desses utilizadores pode tornar-se um problema. A regra é, portanto, minimizar o tamanho dessa memória. O modelo SPV (Single-Page Application) permite gerir a sessão no lado do cliente e ter um servidor web sem sessões. Na verdade, a página única é inicialmente carregada pelo navegador. Juntamente com ela vem o ficheiro JavaScript correspondente. Uma vez que não há recarregamento da página, este ficheiro JS permanecerá permanentemente no navegador tal como foi inicialmente carregado. Podemos então utilizar as suas variáveis globais para armazenar informações sobre as várias ações do utilizador. É isso que vamos ver agora. Não só iremos gerir a sessão no lado do cliente, como também redesenhar a aplicação JS para minimizar os pedidos ao servidor.
7.6.2. A ação [/ajax-12]
![]() |
A ação [/ajax-12] é a seguinte:
@RequestMapping(value = "/ajax-12", method = RequestMethod.GET, produces = "text/html; charset=UTF-8")
public String ajax12() {
return "vue-12";
}
A visualização [vue-12.xml] é a seguinte:
![]() |
<!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>
- Esta vista é idêntica à vista [vue-09], exceto pelo script JS utilizado na linha 9;
A vista apresentada é a seguinte:
![]() |
7.6.3. O código JS para o botão [Atualizar]
![]() |
O código no ficheiro [local12.js] é o seguinte:
// 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");
});
- linhas 17–21: quando a página mestre é carregada, as referências aos três componentes identificados por [loading, error, content] são armazenadas nas variáveis globais nas linhas 2–4;
- linhas 5-6: para armazenar as duas páginas;
- linhas 7-8: para armazenar os dois valores enviados através do link [Validate];
- linha 9: a sessão. Armazena os valores dos contadores [cpt1, cpt3] no lado do cliente;
A função [postForm] trata do clique no botão [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
})
}
As diferenças em relação à versão anterior são as seguintes:
- a URL na linha 7 é diferente;
- linha 4: é enviado um valor, enquanto anteriormente não era enviado nenhum. Este valor é a cadeia JSON da sessão. O princípio é o seguinte:
- o cliente envia a sessão para o servidor,
- o servidor modifica-a e reenvia-a,
- o cliente armazena a nova sessão;
- linha 10: enviamos um documento em formato JSON (valor enviado);
- linha 13: temos algo para enviar;
- linhas 15–20: as funções [beforeSend, error, complete] são as mesmas da versão anterior. Apenas a função [success] muda (linhas 16–18);
7.6.4. A ação [/ajax-13]
![]() |
A ação [/ajax-13] é a seguinte:
@RequestMapping(value = "/ajax-13", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
@ResponseBody()
public JsonResult13 ajax13(@RequestBody SessionModel2 session2, HttpServletRequest request, HttpServletResponse response) {
...
}
- Linha 3: O parâmetro [@RequestBody SessionModel2 session2] recupera a sessão enviada pelo cliente. Esta tem o seguinte tipo [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
...
}
A sessão [SessionModel2] armazena o seguinte:
- linha 9: o número de vezes [cpt1] que a área [Zone 1] é exibida;
- linha 10: o número de vezes [cpt3] que a área [Zone 3] é exibida;
Vamos continuar a examinar o código da ação [/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) {
...
}
- Linha 3: O tipo [JsonResult13] da resposta é o seguinte:
![]() |
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
...
}
- linha 14: a sessão. O servidor reenvia-a ao cliente para armazenamento;
- linha 6: o conteúdo HTML da página 2;
- linha 7: o conteúdo HTML da área [Zona 1];
- linha 8: o conteúdo HTML da área [Zona 3];
- linha 9: qualquer mensagem de erro;
- linhas 10–11: duas informações calculadas pelo servidor e apresentadas na página n.º 2;
Vamos continuar a examinar o código da ação [/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;
}
- Linha 9: A sessão é colocada no resultado da ação;
O método [setZone1B] que ativa a zona [Zone 1] é o seguinte:
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);
}
- Linha 3: Recuperamos a sessão. Ela será modificada na linha 12 com o novo contador [cpt1]. Note que esta sessão será enviada de volta ao cliente;
- linha 10: a nova zona [Zona 1];
O método [setZone3B] que ativa a zona [Zone 3] é semelhante:
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. Processamento da resposta da ação [/ajax-13]
No lado do cliente, a resposta JSON da ação [/ajax-13] é processada pela seguinte função [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();
}
},
...
})
}
- linhas 12–17: se o servidor tiver colocado algo no campo [zone1] da resposta, então a área [Zone 1] deve ser regenerada e exibida; caso contrário, deve ser ocultada;
- linhas 18-23: a mesma lógica aplica-se à área [Zone 3];
7.6.6. Exibição da página [Página 2]
O código HTML para o link [Submit] é o seguinte:
<a href="javascript:valider()">Valider</a>
A função JS [validate] é a seguinte:
// 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
})
}
- Vamos enviar uma solicitação POST, que normalmente nos levará à página 2;
- linha 4: guardamos a página 1 para podermos voltar a ela mais tarde;
- Linhas 6–7: A operação anterior não armazena os valores introduzidos, apenas o código HTML da página. Por isso, agora armazenamos os dois valores introduzidos no formulário;
- linhas 9–13: Os dois valores introduzidos são colocados numa string JSON. É isto que será enviado;
- linha 12: um parâmetro para indicar ao servidor se precisamos da página n.º 2. Procederemos da seguinte forma. Solicitaremos a página n.º 2 uma vez e, em seguida, guardá-la-emos na variável JS `[page2]`. Depois disso, não a solicitaremos novamente. Utilizaremos a página em cache. Linha 2: `[pageRequired]` é `true` se a variável `[page2]` estiver vazia, `false` caso contrário;
- note que a sessão não é enviada. Na verdade, ela armazena contadores que a ação [/ajax-14] na linha 20 não modifica;
7.6.7. A ação [/ajax-14]
A ação [/ajax-14] é a seguinte:
@RequestMapping(value = "/ajax-14", method = RequestMethod.POST)
@ResponseBody
public JsonResult13 ajax14(@RequestBody @Valid PostAjax14 post, BindingResult bindingResult, Locale locale, HttpServletRequest request, HttpServletResponse response) {
...
}
- linha 3: a resposta é sempre do tipo [JsonResult13];
- linha 3: o valor enviado é encapsulado no seguinte tipo [PostAjax14]:
package istia.st.springmvc.models;
public class PostAjax14 extends PostAjax11A {
// page 2
private boolean pageRequired;
// getters and setters
...
}
- Linha 3: A classe [PostAjax14] estende a classe [PostAjax11A] da versão anterior. Por isso, tem uma estrutura de [value1, value2, pageRequired];
A ação [/ajax-14] continua da seguinte forma:
@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;
}
- linhas 9–13: se os valores enviados [valor1, valor2] forem inválidos, é devolvida uma mensagem de erro;
- linhas 15-16: normalmente, o servidor deve realizar um cálculo utilizando os valores enviados. Aqui, limita-se a devolvê-los para indicar que os recebeu;
- linhas 18-20: a página n.º 2 só é devolvida se tiver sido solicitada pelo cliente. Na linha 19, a vista [view-12-page2] é nova:
![]() |
<!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>
- O código XML já não contém valores avaliados pelo Thymeleaf, como acontecia anteriormente;
- identificámos as áreas onde colocar os valores [value1, value2] devolvidos pelo servidor. Na linha 9, [id='value1'] indica onde colocar [value1]. Na linha 13, o mesmo se aplica a [value2];
7.6.8. Processamento da resposta da ação [/ajax-14]
A resposta da ação [/ajax-14] é processada pela seguinte função [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);
}
},
...
})
}
- linhas 9–13: se o servidor devolveu um erro, exibi-lo;
- linhas 14–29: o caso em que não houve erro. Devemos então exibir a página 2;
- linha 17: verificamos se a página 2 já está armazenada na variável [page2];
- linha 19: neste caso, use a variável [page2] para exibir a página 2;
- linha 24: caso contrário, usamos o campo [data.page2] fornecido pelo servidor;
- linha 22: certificamo-nos de armazenar a página n.º 2 para não termos de a solicitar novamente mais tarde;
- linhas 27–28: na página 2, exibimos as duas informações [value1, value2] enviadas pelo servidor;
7.6.9. Voltar à página 1
O link [Voltar à página 1] na página 2 é o seguinte:
<a href="javascript:retourPage1()">Retour à la page 1</a>
O método JS [returnPage1] é o seguinte:
// back to page 1
function retourPage1() {
// regenerate page 1
content.html(page1);
// regenerate foreclosures
$("#text1").val(value1);
$("#text2").val(value2);
}
- Esta é uma ação JavaScript que não interage com o servidor, porque a página 1 foi armazenada localmente na variável [page1];
- Linha 4: Recarregamos a página 1;
- linhas 6-7: apenas a parte HTML da página n.º 1 foi armazenada em cache. Não a entrada do utilizador. Temos, portanto, de recarregar a entrada do utilizador;
7.6.10. Conclusão
Ao tirar partido das capacidades do modelo APU, conseguimos simplificar o servidor web, que agora é stateless (sem sessões) e está menos sobrecarregado:
- removemos a interação com o servidor na função JS [returnPage1]);
- o servidor gera a página 2 apenas uma vez;
7.7. Estruturação do código JavaScript em camadas
7.7.1. Introdução
O código JavaScript da aplicação anterior está a começar a tornar-se complexo. É hora de o estruturar em camadas. A aplicação permanecerá a mesma de antes. Não faremos quaisquer alterações no servidor, exceto definir uma nova página de destino. Vamos refatorar o código JS.
A nova arquitetura será a seguinte:
![]() |
7.7.2. A página inicial
A ação que inicia a aplicação é a seguinte ação [/ajax-16]:
@RequestMapping(value = "/ajax-16", method = RequestMethod.GET, produces = "text/html; charset=UTF-8")
public String ajax16() {
return "vue-16";
}
Exibe a seguinte visualização [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>
- Linhas 9–10: O código JS foi colocado em dois ficheiros diferentes:
- [local-ui] implementa a camada [presentation],
- [local-dao] implementa a camada [DAO];
![]() |
7.7.3. Implementação da camada [DAO]
![]() |
7.7.4. Interface
A camada [DAO] em [local-dao.js] apresentará a seguinte interface à camada [presentação]:
| para atualizar a página 1 utilizando o botão [Refresh] |
| para exibir a página 2 com o botão [Submit] |
O JavaScript não possui o conceito de interface. Utilizei este termo simplesmente para indicar que a camada de [apresentação] concordou em comunicar com a camada [DAO] exclusivamente através das duas funções acima.
7.7.5. Implementação da interface
O esqueleto da implementação é o seguinte:
var session = {
"cpt1" : 0,
"cpt3" : 0
};
// update Page 1
function updatePage1(deferred, sendMeBack) {
...
}
// page 2
function getPage2(deferred, sendMeBack, value1, value2, pageRequired) {
...
}
O objetivo da camada [DAO] é ocultar os detalhes das solicitações HTTP feitas ao servidor web da camada [presentação]. A sessão faz parte desses detalhes. Por isso, agora é gerida pela camada [DAO].
7.7.5.1. A função [updatePage1]
A função [updatePage1] é a função chamada pela camada [presentation] para atualizar a página 1. O seu código é o seguinte:
// update Page 1
function updatePage1(deferred, sendMeBack) {
// requête HTTP
executePost(deferred, sendMeBack, '/ajax-13', session);
}
- Linha 1: A função [updatePage1] recebe dois parâmetros:
- um objeto do tipo [jQuery.Deferred]. Este tipo de objeto armazena um estado que pode ter três valores ['pending', 'resolved', 'rejected']. Quando chega à função [updatePage1], encontra-se no estado [pending];
- um objeto JS a ser devolvido à camada [presentation];
Todas as solicitações HTTP são feitas pela seguinte função [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
});
}
});
}
- linha 1: a função [executePost] executa uma chamada Ajax do tipo POST. Ela espera quatro parâmetros:
- um objeto [jQuery.Deferred] no estado [pending];
- um objeto JS a ser devolvido à camada [presentation];
- a URL POST;
- o valor a ser enviado como um objeto JS;
- Linhas 5–8: A função envia JSON (linha 7) e recebe JSON (linha 6);
- linha 11: o valor a ser enviado é convertido para JSON;
- linhas 13–24: a função executada se a chamada Ajax for bem-sucedida;
- linhas 19–23: se o servidor devolveu uma sessão, esta é armazenada;
- linhas 13–18: define o objeto [deferred] para o estado [resolved] e passa um resultado com os seguintes campos:
- [status]: 1 para sucesso, 2 para falha,
- [data]: a resposta JSON do servidor,
- [sendMeBack]: o segundo parâmetro da função, que é um objeto que o chamador deseja recuperar;
- linhas 17–31: a função executada se a chamada Ajax falhar. Fazemos o mesmo que antes, com duas diferenças:
- [status] é definido como 2 para indicar um erro;
- [data] é novamente a resposta JSON do servidor, mas obtida de uma forma diferente;
7.7.5.2. A função [getPage2]
A função [getPage2] é a seguinte:
// page 2
function getPage2(deferred, sendMeBack, value1, value2, pageRequired) {
// requête HTTP
executePost(deferred, sendMeBack, '/ajax-14', {
"value1" : value1,
"value2" : value2,
"pageRequired" : pageRequired,
});
}
- A função recebe os seguintes parâmetros:
- [deferred]: um objeto do tipo [jQuery.Deferred] no estado [pending],
- [sendMeBack]: um objeto JS a ser devolvido à camada [presentation],
- [value1]: a primeira entrada na página 1,
- [value2]: a segunda entrada na página 2,
- [pageRequired]: um booleano que indica ao servidor se deve ou não enviar o fluxo HTML da página 2;
- a função [executePost] é chamada para executar o pedido HTTP necessário;
7.7.6. A camada [presentation]
![]() |
A camada [presentation] é implementada pelo ficheiro [local-ui.js]. Este ficheiro reutiliza o código do ficheiro [local12.js], adaptado para utilizar a camada [DAO] anterior. Apenas duas funções foram alteradas: [postForm] e [valider].
7.7.6.1. A função [postForm]
A função [postForm] é a seguinte:
// 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);
}
- linha 4: criamos um objeto [jQuery.Deferred]. Por predefinição, este encontra-se no estado [pending];
- linha 5: a imagem de carregamento é exibida
- linhas 6–9: a função [updatePage1] é executada. Passamos um objeto fictício [sendMeBack], apenas para mostrar para que pode ser usado;
- linha 11: o parâmetro da função [deferred.done] é, ele próprio, uma função. Esta é a função a ser executada quando o estado do objeto [deferred] muda para [resolved]. Acabámos de ver que a função DAO [executePost] definiu o estado deste objeto como [resolved] ao receber a resposta do servidor. Isto significa que, quando a função [postFormDone] é executada, a resposta do servidor já foi recebida;
A função [postFormDone] é a seguinte:
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;
}
}
- Linha 1: O parâmetro [result] recebido é o parâmetro passado para o método [deferred.resolve] na função [executePost], por exemplo:
// on rend le résultat
deferred.resolve({
"status" : 1,
"data" : data,
"sendMeBack" : sendMeBack
});
- linha 5: recuperamos a resposta do servidor;
- linhas 10–24: este é o código que, na versão anterior, estava na função [onSuccess] da função [postForm];
- linhas 25–28: este é o código que anteriormente se encontrava na função [onError] da função [postForm];
7.7.6.2. A função do parâmetro [sendMeBack]
Para que serve o parâmetro [sendMeBack]? Vejamos o código que chama a função [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);
}
e a assinatura da função [validerDone]:
function postFormDone(result) {
}
Como é que a função [postForm] pode passar informações para a função [postFormDone]? Esta última tem apenas um parâmetro, [result]. Este é criado pela função [executePost] na camada [DAO]. Para passar informações para a função [postFormDone], a função [postForm] deve primeiro passá-las para a função [updatePage1]. Esta é a função do parâmetro [sendMeBack]. É utilizado da seguinte forma:
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) {
...
- na linha 7, a função [postFormDone] recuperou o parâmetro [sendMeBack] inicialmente passado para a função DAO [updatePage1] pela função [postForm];
7.7.7. A função [valider]
A função [valider] é a seguinte:
// 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);
}
e a função [validerDone] (linha 18) da seguinte forma:
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;
}
}
- linha 5: recuperamos a resposta do servidor;
- linhas 10–32: este é o código que, na versão anterior, estava na função [onSuccess] da função [validate];
- linhas 34–38: este é o código que anteriormente se encontrava na função [onError] da função [validate];
7.7.8. Testes
A aplicação continua a funcionar como antes e, na consola do Chrome, é possível ver os parâmetros [sendMeBack] das funções [postForm] e [validate]:
![]() |
7.8. Conclusão
Voltemos à arquitetura geral de uma aplicação Spring MVC:
![]() |
Graças ao JavaScript incorporado nas páginas HTML e executado no navegador, e graças ao modelo APU, podemos transferir código para o navegador e obter a seguinte arquitetura:
![]() |
- temos uma arquitetura cliente [2] / servidor [1] em que o cliente e o servidor comunicam através de JSON;
- em [1], a camada web Spring MVC fornece vistas, fragmentos de vista e dados em JSON;
- em [2]: o código JavaScript incorporado na vista carregada quando a aplicação é iniciada pode ser estruturado em camadas:
- a camada [de apresentação] lida com as interações do utilizador,
- a camada [DAO] lida com o acesso aos dados através do servidor web [1],
- a camada [negócio] pode não existir ou pode assumir certas funcionalidades não confidenciais da camada [negócio] do servidor para descarregar o servidor;
- o cliente [2] pode armazenar em cache determinadas vistas para descarregar ainda mais o servidor. Ele gere a sessão;













































































