7. Ajaxificación de una aplicación Spring MVC
7.1. El lugar de AJAX en una aplicación web
Por el momento, los ejemplos de aprendizaje estudiados tenían la siguiente arquitectura:
![]() |
Para pasar de una vista [Vue1] a una vista [Vue2], el navegador:
- envía una solicitud a la aplicación web;
- recibe la vista [Vue2] y la muestra en lugar de la vista [Vue1].
Este es el esquema clásico:
- solicitud del navegador;
- elaboración de una vista en respuesta al cliente por parte del servidor web;
- visualización de esta nueva vista por parte del navegador.
Desde hace unos años existe otro modo de interacción entre el navegador y el servidor web: AJAX (Asynchronous Javascript And Xml). Se trata, de hecho, de interacciones entre la vista mostrada por el navegador y el servidor web. El navegador sigue haciendo lo que sabe hacer, mostrar una vista HTML, pero ahora es manipulado por Javascript integrado en la vista HTML mostrada. El esquema es el siguiente:
![]() |
- en [1], se produce un evento en la página mostrada en el navegador (clic en un botón, cambio de un texto, etc.). Este evento es interceptado por Javascript (jS) integrado en la página;
- en [2], el código Javascript realiza una solicitud HTTP tal y como lo habría hecho el navegador. La solicitud es asíncrona: el usuario puede seguir interactuando con la página sin verse bloqueado por la espera de la respuesta a la solicitud HTTP. La solicitud sigue el proceso clásico de procesamiento. Nada (o casi nada) la distingue de una solicitud clásica;
- en [3], se envía una respuesta al cliente jS. En lugar de una vista HTML completa, se envía más bien una vista HTML parcial, un flujo XML o jSON (JavaScript Object Notation);
- en [4], el Javascript recupera esta respuesta y la utiliza para actualizar una región de la página HTML mostrada.
Para el usuario, se produce un cambio de vista porque lo que ve ha cambiado. Sin embargo, no se recarga la página por completo, sino que simplemente se modifica parcialmente la página mostrada. Esto contribuye a dotar a la página de fluidez e interactividad: al no producirse una recarga total de la página, es posible gestionar eventos que antes no se podían gestionar. Por ejemplo, ofrecer al usuario una lista de opciones a medida que va introduciendo caracteres en un campo de entrada. Con cada nuevo carácter introducido, se envía una solicitud AJAX al servidor, que a continuación devuelve otras propuestas. Sin Ajax, este tipo de ayuda a la introducción de datos era antes imposible. No se podía recargar una nueva página con cada carácter introducido.
7.2. Actualización de una página con un flujo HTML
7.2.1. Las vistas
Nos proponemos estudiar la siguiente aplicación:
![]() |
- en [1], la hora de carga de la página;
- en [2], se realizan las cuatro operaciones aritméticas con dos números reales A y B;
- en [3], la respuesta del servidor se inscribe en una región de la página;
- en [4], la hora del cálculo. Esta es diferente de la hora de carga de la página [5]. Esta última es igual a [1], lo que indica que la región [6] no se ha recargado. Por otra parte, el URL [7] de la página no ha cambiado.
7.2.2. La acción [/ajax-01]
![]() |
El controlador [Ajax.java] define la siguiente acción [/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) {
// ¿tiempo válido?
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));
}
}
// se prepara la plantilla de la vista [vue-01]
...
}
- línea 2: la acción [/ajax-01] solo admite un único parámetro [tempo]. Se trata del tiempo, en milisegundos, que el servidor deberá esperar antes de enviar los resultados de las operaciones aritméticas;
- línea 4: el parámetro [tempo] es opcional;
- líneas 5-12: se comprueba que el valor del parámetro [tempo] sea válido;
- líneas 13-15: si es así, el valor del tiempo de espera se establece para la sesión. Esto significa que estará vigente mientras no se modifique;
El código de la acción [/ajax-01] continúa así:
@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) {
// ¿Tempo válido?
...
// se está preparando la plantilla de la vista [vue-01]
modèle.addAttribute("actionModel01", new ActionModel01());
...
// vista
return "vue-01";
}
La clase [ActionModel01] sirve principalmente para encapsular los valores enviados por la acción [/ajax-01]. Aquí no se envía nada. Se crea una clase vacía que se incluye en el modelo porque la vista [vue-01.xml] la utiliza. La clase [ActionModel01] es la siguiente:
package istia.st.springmvc.models;
import javax.validation.constraints.DecimalMin;
import javax.validation.constraints.NotNull;
public class ActionModel01 {
// datos enviados
@NotNull
@DecimalMin(value = "0.0")
private Double a;
@NotNull
@DecimalMin(value = "0.0")
private Double b;
// getters y setters
...
}
- líneas 11 y 15: dos valores reales [a,b] que se enviarán mediante un formulario;
Volvamos al código de la acción:
@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) {
...
// se prepara el modelo de la vista [vue-01]
modèle.addAttribute("actionModel01", new ActionModel01());
Resultats résultats = new Resultats();
modèle.addAttribute("resultats", résultats);
...
// vista
return "vue-01";
}
- líneas 6-7: se inserta una instancia de tipo [Resultats] en la plantilla;
El tipo [Resultats] incluido en el modelo es el siguiente:
![]() |
package istia.st.springmvc.models;
public class Resultats {
// datos
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 y setters
...
}
- líneas 6-9: el resultado de las cuatro operaciones aritméticas con los números [a,b];
- línea 10: la hora de la carga inicial de la página;
- línea 11: la hora de ejecución de las cuatro operaciones aritméticas;
- línea 12: un posible mensaje de error;
- línea 13: la vista que se debe mostrar, si la hay;
- línea 14: la cultura de la vista, [fr-FR] o [en-US];
El código de la acción [/ajax-01] continúa así:
@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) {
...
// configuración regional
setLocale(locale, modèle, résultats);
...
}
- línea 5: el método [setLocale] sirve para introducir en la plantilla de la vista la configuración regional que se va a utilizar, [fr-FR] o [en-US]. Esta configuración regional está destinada al Javascript integrado en la vista;
El método [setLocale] es el siguiente:
private void setLocale(Locale locale, Model modèle, Resultats résultats) {
// solo se gestionan las configuraciones regionales fr-FR y en-US
String language = locale.getLanguage();
String country = null;
switch (language) {
case "fr":
country = "FR";
break;
default:
language = "en";
country = "US";
break;
}
// cultura
résultats.setCulture(String.format("%s-%s", language, country));
}
En el modelo, la cadena [${resultats.culture}] será igual a «fr-FR» o «en-US».
Volvamos a la acción [/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) {
...
// configuración regional
setLocale(locale, modèle, résultats);
// hora
résultats.setHeureGet(new SimpleDateFormat("hh:mm:ss").format(new Date()));
// vista
return "vue-01";
}
- línea 7: se introduce la hora de GET en la plantilla;
- líneas 9: se muestra la vista [vue-01.xml]:
7.2.3. La vista [vue-01.xml]
![]() | ![]() |
La vista [vue-01.xml] es la siguiente:
<!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>
- líneas 7-12: las bibliotecas jQuery de validación e internacionalización (culturas);
- línea 15: la biblioteca [client-validation] creada en el apartado 6.3;
- línea 14: la biblioteca jSON utilizada por la biblioteca [client-validation]. Es opcional si se han desactivado los registros de validación;
- línea 13: la biblioteca [Unobtrusive Ajax] de Microsoft. Esta biblioteca permite, en ocasiones, evitar tener que escribir Javascript;
- línea 16: un archivo jS para nuestras propias necesidades;
- líneas 17-22: para gestionar en el lado del cliente las configuraciones de formato [fr-FR] y [en-US]. Ya hemos visto este código;
- línea 27: un mensaje configurado. Los hemos estudiado en el apartado 5.18;
- líneas 36-38: el formulario al que volveremos más adelante;
- línea 40: el área del documento en la que Javascript colocará la respuesta del servidor;
7.2.4. El formulario
![]() |
En la vista [vue-01.xml], el formulario es el siguiente:
<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>
que genera el siguiente 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>
- línea 16: al campo [a] se asocian los validadores [required], [number] y [min];
- línea 19: lo mismo ocurre con el campo [b];
Los distintos mensajes se encuentran en los archivos [messages.properties] del proyecto:
![]() |
[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:
Ahora, analicemos los atributos de la etiqueta [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">
Se reconocen los atributos clásicos de la etiqueta [form]:
<form id="formulaire" name="formulaire" method="post" action="/ajax-02.html">
Se puede observar de inmediato que, si en el navegador que muestra la página, el Javascript está desactivado, entonces el formulario se enviará al URL [/ajax-02.html]. Ahora, analicemos los demás 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">
Los atributos [data-ajax-xxx] son gestionados por la biblioteca jS [unobtrusive-ajax], que ha sido importada por la vista [vue-01.xml]:
<script type="text/javascript" src="/js/jquery/jquery.unobtrusive-ajax.js"></script>
Cuando los atributos [data-ajax-xxx] están presentes, el [submit] del formulario se ejecutará mediante una llamada Ajax de la biblioteca [unobtrusive-ajax]. El significado de los parámetros es el siguiente:
- [data-ajax="true"]: es la presencia de este atributo lo que hace que el [submit] del formulario se ejecute mediante Ajax;
- [data-ajax-method="post"]: el método del [submit]. El URL del post será el del atributo [action="/ajax-02.html"];
- [data-ajax-loading="#loading"]: el id de un campo que se mostrará mientras se espera la respuesta del servidor. El campo identificado por [loading] en la vista [vue-01.xml] es el siguiente:
<img id="loading" style="display: none" src="/images/loading.gif" />
Se trata de una imagen animada de espera que se mostrará mientras no se haya recibido la respuesta del servidor;
- [data-ajax-loading-duration="0"]: el tiempo de espera en milisegundos antes de que se muestre el área [data-ajax-loading="#loading"]. En este caso, se mostrará tan pronto como comience la espera;
- [data-ajax-begin="beforeSend"]: la función jS que se debe ejecutar antes de realizar la [submit];
- [data-ajax-complete="afterComplete"]: la función jS que se debe ejecutar cuando se haya recibido la respuesta;
- [data-ajax-update="#resultats"]: el identificador del campo donde se colocará el resultado enviado por el servidor. La vista [vue-01.xml] tiene el siguiente campo:
<div id="resultats" />
- [data-ajax-mode="replace"]: el modo de inserción del resultado en el campo anterior. El modo [replace] hará que el resultado «sobrescriba» lo que había anteriormente en el campo de id [resultats];
Cabe señalar que el [submit] Javascript solo se aplicará si los validadores han declarado válidos los valores probados.
La biblioteca jS [unobtrusive-ajax] tiene dos objetivos:
- asegurar que el formulario se adapte correctamente a las dos posibilidades: activación o no de Javascript en el navegador;
- evitar escribir Javascript. Veremos que, en este caso, no se ha podido evitar.
7.2.5. La acción [/ajax-02]
Hemos visto que los valores enviados se remitían a la acción [/ajax-02]. Esta es la siguiente:
@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);
}
// se prepara la plantilla de la siguiente vista
Resultats résultats = new Resultats();
modèle.addAttribute("resultats", résultats);
// se establece la hora local
setLocale(locale, modèle, résultats);
// hora
résultats.setHeurePost(new SimpleDateFormat("hh:mm:ss").format(new Date()));
...
}
- Vamos a simplificar en un primer momento: suponemos que la acción POST que tiene lugar ha sido realizada por la acción Javascript de la vista [vue-01.xml]. Volveremos sobre esta hipótesis un poco más adelante;
- línea 2: los valores [a,b] enviados se introducen en la plantilla [ActionModel01];
- líneas 4-7: si el usuario había establecido un tiempo de espera en un GET anterior, este se recupera en la sesión y se aplica el tiempo de espera (línea 6). El objetivo de esta es permitir al usuario ver el efecto del atributo [data-ajax-loading="#loading"] en el formulario;
- líneas 9-10: se añade un atributo [resultats] a la plantilla;
- línea 12: se introduce la cultura [fr-FR] o [en-US] en la plantilla;
- línea 14: se introduce la hora del POST en la plantilla;
Recordemos el tipo [Resultats] introducido en la plantilla:
public class Resultats {
// datos
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 y setters
...
}
El código de la acción [/ajax-02] continúa así:
@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()));
// se genera un error una de cada dos veces
int val = new Random().nextInt(2);
if (val == 0) {
// se devuelve un mensaje de error
résultats.setErreur("erreur.aleatoire");
return "vue-03";
}
...
}
- líneas 6-11: a modo de ejemplo, se muestra cómo devolver una página de error al cliente jS. Una de cada dos veces, se devuelve la siguiente vista [vue-03.xml]:
![]() |
Cabe destacar, en la línea 9, que no se trata de un mensaje que se incluye en la plantilla, sino de una clave de mensaje:
[messages_fr.properties]
erreur.aleatoire=erreur aléatoire
[messages_fr.properties]
erreur.aleatoire=randomly generated error
El código de la vista [vue-03.xml] es el siguiente:
<!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>
- En la línea 12, observamos un mensaje configurado mediante una clave de mensaje que, a su vez, se calcula. Hemos introducido este concepto en el apartado 5.18, página 171.
El código de la acción [/ajax-02] continúa así:
@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 {
...
// se recuperan los valores enviados
double a = formulaire.getA();
double b = formulaire.getB();
// se construye el modelo
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");
}
// se muestra la vista
return "vue-02";
}
- líneas 5-15: las cuatro operaciones aritméticas se realizan sobre los números [a,b] y se encapsulan en la instancia [Resultats] del modelo;
- línea 17: se devuelve la vista [vue-02.xml] siguiente:
![]() |
La vista [vue-02.xml] es la siguiente:
<!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>
Tanto si el resultado es la vista [vue-02.xml] como si es la vista [vue-03.xml], este resultado HTML se coloca en el campo identificado por [resultats] en la vista [vue-01.xml], debido al atributo [data-ajax-update="#resultats"] del formulario.
7.2.6. El POST de los valores introducidos
Aquí tenemos una dificultad con los valores publicados. Trabajamos con dos culturas, [fr-FR] y [en-US], que escriben los números reales de forma diferente. Ya abordamos esta dificultad cuando, en el apartado 6.3, página 191, tuvimos que publicar números reales en dos culturas diferentes. Vamos a retomar aquí las herramientas utilizadas entonces. Pero tenemos una dificultad adicional: no tenemos acceso al método que opera el POST de los valores introducidos. Por este motivo, hemos añadido los siguientes atributos a la etiqueta del formulario:
- [data-ajax-begin="beforeSend"]: la función jS que se debe ejecutar antes de realizar el [submit];
- [data-ajax-complete="afterComplete"]: la función jS que se debe ejecutar cuando se haya recibido la respuesta;
No tenemos acceso a la función jS, que enviará los valores introducidos, pero podemos escribir dos funciones jS:
- [beforeSend]: una función jS ejecutada antes de la POST;
- [afterComplete]: una función jS ejecutada al recibir la respuesta a POST;
Estas dos funciones se encuentran en un archivo [local1.js]:
![]() |
El archivo [local1.js] inicializa el entorno jS de la vista [vue-01.xml] de la siguiente manera:
// datos globales
var loading;
var formulaire;
var résultats;
var a, b;
// al cargar el documento
$(document).ready(function() {
// se recuperan las referencias de los diferentes componentes de la página
loading = $("#loading");
formulaire = $("#formulaire");
resultats = $('#resultados');
a = $("#a");
b = $("#b");
// se ocultan algunos elementos
loading.hide();
// se analizan los validadores del formulario
$.validator.unobtrusive.parse(formulaire);
// se gestionan dos locales [fr_FR, en_US]
// los datos reales [a,b] son enviados por el servidor en formato anglosajón
// se les aplica el formato francés si es necesario
checkCulture(2);
});
- línea 22: la función [checkCulture] se presenta un poco más adelante;
La función jS [beforeSend] será la siguiente:
function beforeSend(jqXHR, settings) {
// antes de POST
// los nombres deben publicarse en formato anglosajón
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) {
// se ponen los números [a,b] en formato anglosajón
var value1 = a.val().replace(",", ".");
a.val(value1);
var value2 = b.val().replace(",", ".");
b.val(value2);
}
if (mode == 2) {
...
}
}
- líneas 4-6: se comprueba si la cultura de la vista es [fr-FR]. En ese caso, hay que cambiar los valores enviados. De hecho, si el usuario ha introducido [1,6], hay que enviar el valor [1.6]; de lo contrario, el valor [1,6] será rechazado por el servidor. Para ello, basta con cambiar la coma de los valores enviados por un punto decimal (líneas 18-21);
- pero no podemos quedarnos ahí. De hecho, cuando se llama a la función [beforeSend], la cadena de valores enviados [a=val1&b=valB] ya se ha construido. Por lo tanto, debemos modificarla. Esto se hace mediante el segundo parámetro [settings] de la función;
- línea 7: [settings.data] (settings es un parámetro de la función) representa la cadena enviada. Recreamos esta cadena con la expresión [formulaire.serialize()]. Esta expresión recorre el formulario en busca de los valores que se van a enviar y construye la cadena de POST. A continuación, tomará los nuevos valores de [a,b] con puntos decimales;
Si no se hace nada más, el servidor enviará su respuesta, que se mostrará correctamente. Solo que ahora los valores de [a,b] tienen el punto decimal, mientras que seguimos en la configuración de [fr-FR]. Por lo tanto, si el usuario no se da cuenta y vuelve a hacer clic en [Calculer], los validadores le responden que los valores [a,b] no son válidos. Lo cual es correcto. Ahí es donde interviene la función [afterComplete], que se ejecuta al recibir el resultado:
function beforeSend(jqXHR, settings) {
// antes de POST
...
}
function afterComplete(jqXHR, settings) {
// después de POST
// los números deben volver al formato francés si es necesario
var culture = Globalize.culture().name;
if (culture === 'fr-FR') {
checkCulture(2);
}
}
function checkCulture(mode) {
if (mode == 1) {
...
}
if (mode == 2) {
// se ponen los números en formato francés
var value1 = a.val().replace(".", ",");
a.val(value1);
var value2 = b.val().replace(".", ",");
b.val(value2);
}
}
- líneas 9-12: si la configuración regional de la vista es [fr-FR], se vuelven a poner los números [a,b] en formato francés.
7.2.7. Pruebas
A continuación se muestran algunas capturas de pantalla de las pruebas:
![]() |
- en [1], la respuesta del servidor;
![]() |
- en [2], la respuesta del servidor con un mensaje de error;
![]() |
- en [3], se establece un tiempo de espera de 5 segundos. Esto significa que el servidor esperará 5 segundos antes de enviar su respuesta. En la etiqueta [form], hemos utilizado el atributo [data-ajax-loading='#loading']. El parámetro [loading] es el identificador de un área que se:
- se muestra durante todo el tiempo de espera;
- se oculta tras recibir la respuesta del servidor;
Aquí, [loading] es el identificador de una imagen animada que se ve en [4].
7.2.8. Desactivación de Javascript con la configuración [en-US]
¿Qué ocurre si se desactiva el Javascript del navegador?
La POST de los valores introducidos se realizará según la etiqueta [form], cuyos atributos [data-ajax-attr] no se utilizarán. Todo ocurre como si tuviéramos la siguiente etiqueta [form]:
<form id="formulaire" name="formulaire" method="post" action="/ajax-02.html">
Por lo tanto, los valores introducidos se enviarán a la acción [/ajax-02]. No se habrán verificado en el lado del cliente. Por lo tanto, serán los validadores del lado del servidor los que intervengan. Ya intervenían anteriormente, pero sobre valores ya validados en el lado del cliente, por lo que eran correctos. Ya no es así.
Modificamos la acción [/ajax-02] de la siguiente manera:
@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 {
// ¿Solicitud Ajax?
boolean isAjax = "XMLHttpRequest".equals(request.getHeader("X-Requested-With"));
...
}
- línea 4: la acción [/ajax-02] puede ahora invocarse mediante un POST Ajax o mediante un POST clásico. Debemos saber diferenciar estos dos casos. Lo hacemos con los encabezados HTTP enviados por el navegador del cliente;
Cuando se observan los intercambios de red en la consola de desarrollo de Chrome (Ctrl-Mayús-I) mientras Javascript está activado, se ve que el cliente envía los siguientes encabezados en el momento de POST:
![]() |
Se observa arriba que:
- se ha enviado un encabezado [X-Requested-With] [1];
- se ha añadido un parámetro [X-Requested-With] a los valores enviados [2];
Esto no ocurre en el caso de un POST clásico. Por lo tanto, tenemos dos posibilidades para recuperar la información: recuperarla en los encabezados HTTP o en los valores publicados. La línea 4 de la acción [/ajax-02] ha optado por la primera solución.
Continuemos con el código de esta acción:
@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 {
// ¿Solicitud Ajax?
boolean isAjax = "XMLHttpRequest".equals(request.getHeader("X-Requested-With"));
// ¿tiempo?
Integer tempo = (Integer) session.getAttribute("tempo");
if (tempo != null && tempo > 0) {
Thread.sleep(tempo);
}
// se prepara la plantilla de la siguiente vista
Resultats résultats = new Resultats();
modèle.addAttribute("resultats", résultats);
// se establece la configuración regional
setLocale(locale, modèle, résultats);
// hora
String heure = new SimpleDateFormat("hh:mm:ss").format(new Date());
résultats.setHeurePost(heure);
résultats.setHeureGet(heure);
// ¿solicitud válida?
if (!isAjax && result.hasErrors()) {
return "vue-01";
}
...
- línea 2: el parámetro [@Valid ActionModel01 formulaire] activa los validadores del lado del servidor;
- líneas 20-22: si la llamada no es una llamada Ajax y la validación ha fallado, se devuelve la vista [vue-01.xml] con los mensajes de error.
He aquí un ejemplo:
![]() | ![]() |
Continuemos con el análisis de la acción [/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 {
// ¿Solicitud Ajax?
boolean isAjax = "XMLHttpRequest".equals(request.getHeader("X-Requested-With"));
...
// ¿Solicitud válida?
if (!isAjax && result.hasErrors()) {
return "vue-01";
}
// se genera un error una de cada dos veces
int val = new Random().nextInt(2);
if (val == 0) {
// se devuelve un mensaje de error
résultats.setErreur("erreur.aleatoire");
if (isAjax) {
return "vue-03";
} else {
résultats.setVue("vue-03");
return "vue-01";
}
}
...
- línea 14: se genera un error aleatorio;
- línea 16: en caso de una llamada Ajax, se devuelve la vista [vue-03.xml], que se colocará en el área identificada por [resultats];
- línea 18: en caso de una llamada no Ajax, se coloca la vista que se va a mostrar en el modelo de tipo [Resultats];
- línea 19: se vuelve a mostrar la vista [vue-01.xml];
La vista [vue-01.xml] se modifica de la siguiente manera:
<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" />
- línea 3: la vista [vue-03.xml] se insertará debajo del área [resultats];
He aquí un ejemplo:
![]() |
Cabe señalar que, a partir de ahora, las horas [1] y [2] son idénticas.
Continuemos con el análisis de la acción [/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 {
// ¿Solicitud Ajax?
boolean isAjax = "XMLHttpRequest".equals(request.getHeader("X-Requested-With"));
...
// se recuperan los valores enviados
double a = formulaire.getA();
double b = formulaire.getB();
// se crea el modelo
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");
}
// se muestra la vista
if (isAjax) {
return "vue-02";
} else {
résultats.setVue("vue-02");
return "vue-01";
}
}
- líneas 7-17: los resultados de las cuatro operaciones aritméticas se introducen en el modelo;
- líneas 22-23: se genera la vista [vue-01.xml] (línea 22) insertando en ella la vista [vue-02.xml] (línea 22);
Esta inserción se realiza de la siguiente manera en [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" />
- línea 2: la vista [vue-02.xml] se insertará debajo del área [resultats];
A continuación se muestra un ejemplo de ejecución:
![]() |
7.2.9. Desactivación de Javascript con la configuración [fr-FR]
Con la cultura [fr-FR] se presenta el siguiente problema:
![]() | ![]() |
Los valores introducidos en formato francés se han declarado inválidos. De hecho, el servidor espera valores reales en formato anglosajón. La solución es bastante compleja. Vamos a crear un filtro que:
- intercepte la solicitud;
- cambiar las comas de los valores enviados [a] y [b] por el punto decimal;
- y, a continuación, pasar la nueva solicitud a la acción que debe procesarla;
En primer lugar, introducimos un campo oculto en la vista [vue-01.xml]:
<form ...>
...
</p>
<!-- campos ocultos -->
<input type="hidden" id="culture" name="culture" th:value="${resultats.culture}"></input>
</form>
- línea 5: el valor de [fr-FR] o [en-US] se introduce en el campo de atributo [name=culture]. Como la etiqueta [input] está en el formulario, su valor se enviará junto con los valores de [a] y [b]. Se obtendrá entonces una cadena enviada con el siguiente formato:
Es importante comprender este punto.
A continuación, incluimos un filtro en la configuración de la aplicación:
![]() |
El archivo [Config] se modifica de la siguiente manera:
@Configuration
@ComponentScan({ "istia.st.springmvc.controllers", "istia.st.springmvc.models" })
@EnableAutoConfiguration
public class Config extends WebMvcConfigurerAdapter {
...
@Bean
public Filter cultureFilter() {
return new CultureFilter();
}
}
- línea 7: el hecho de que el bean [cultureFilter] devuelva un tipo [Filter] lo convierte en un filtro. El bean, por su parte, puede tener cualquier nombre;
El siguiente paso es crear el filtro propiamente dicho:
![]() |
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 {
// siguiente controlador
filterChain.doFilter(new CultureRequestWrapper(request), response);
}
}
- línea 12: extendemos la clase [OncePerRequestFilter], que es una clase de Spring, y lo que debemos hacer es redefinir el método [doFilterInternal] de esta clase;
- línea 15: el método [doFilterInternal] recibe tres datos:
- [HttpServletRequest request]: la consulta que se va a filtrar. Esta no se puede modificar;
- [HttpServletResponse response]: la respuesta que se enviará al servidor. El filtro puede decidir enviarla él mismo,
- [FilterChain filterChain]: la cadena de filtros. Una vez que el método [doFilterInternal] ha terminado su trabajo, debe pasar la solicitud al siguiente filtro de la cadena de filtros;
- línea 18: se crea una nueva solicitud a partir de la recibida [new CultureRequestWrapper(request)] y se pasa al siguiente filtro. Como no se puede modificar la solicitud inicial [HttpServletRequest request], se crea una nueva;
La clase [CultureRequestWrapper] es la siguiente:
![]() |
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) {
// valores enviados a y 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;
}
// otros casos
return super.getParameterValues(name);
}
}
- línea 6: la clase [CultureRequestWrapper] extiende la clase [HttpServletRequestWrapper] y va a redefinir algunos de sus métodos;
- líneas 8-10: el constructor que recibe la solicitud que se va a filtrar y la pasa a la clase padre;
- hay que entender aquí que la solicitud filtrada acabará siendo un parámetro de entrada de una clase llamada servlet. Con Spring MVC, este servlet es de tipo [DispatcherServlet]. Esta clase dispone de varios métodos para recuperar los parámetros de la solicitud: [getParameter, getParameterMap, getParameterNames, getParameterValues, ...]. Hay que redefinir el método utilizado por el servlet. Para ello, habría que leer el código de la clase [DispatcherServlet]. Yo no lo hice y redefiní varios métodos. Finalmente, fue el método [getParameterValues] el que se redefinió;
- línea 13: el método [getParameterValues] recibe como parámetro el nombre de uno de los parámetros devueltos por el método [getParameterNames] y debe devolver la matriz de sus valores. De hecho, sabemos que un parámetro puede aparecer varias veces en una solicitud;
- línea 18: se sustituye la coma por un punto decimal;
He aquí un ejemplo de ejecución:
![]() |
- en [1], los valores [a,b] se introducen en formato francés;
- en [2], los resultados;
- en [3], el servidor ha devuelto una página con números en formato anglosajón.
Este último problema se puede resolver con Thymeleaf de la siguiente manera en la 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>
Hay que realizar varios cambios en las líneas 3 y 6. Analicemos la línea 3:
- Habíamos escrito [th:field="*{a}"]. El parámetro [th:field] establece los atributos [id, name, value] de la etiqueta HTML [input] generada. Aquí queremos gestionar el atributo [value] nosotros mismos. Por lo tanto, también establecemos los atributos [id, name] nosotros mismos;
- el atributo [th:value] evalúa una expresión que utiliza el operador ternario ?. Comprobamos la expresión [${resultats.culture}=='fr-FR' and ${actionModel01.b}!=null]. Si es verdadera, asignamos al atributo [value] el valor de [actionModel01.a], donde el punto decimal se sustituye por la coma. Si es falsa, se asigna al atributo [value] el valor de [actionModel01.a] sin modificaciones;
- línea 6: se repite el mismo proceso para el campo [b];
He aquí un ejemplo de ejecución:
![]() |
- en [1], los números [a,b] han conservado la notación francesa. No es el caso en [2];
Este nuevo problema se gestiona de la misma manera que el anterior. Se modifica la vista [vue-03.xml] de la siguiente manera:
<!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>
He aquí un ejemplo:
![]() | ![]() |
Ahora tenemos una aplicación que gestiona correctamente dos idiomas en un entorno que utilice o no Javascript. Para ello, ha sido necesario complicar considerablemente el código del lado del servidor. A partir de ahora, siempre daremos por hecho que el Javascript del navegador está activado. Esto permite hacer cosas que son imposibles en el modo solo servidor.
7.2.10. Gestión del enlace [Calculer]
Examinemos el enlace [Calculer] de la página principal [vue-01.xml]:
![]() | ![]() |
El código del enlace [Calculer] en la vista [vue-01.xml] es el siguiente:
<a href="javascript:postForm()" th:text="#{action.calculer}">Calculer</a>
La función jS [postForm] se define en el archivo [local1.js] de la siguiente manera:
// datos globales
var loading;
var formulaire;
var résultats;
var a, b;
function postForm() {
// ¿Formulario válido?
if (!formulaire.validate().form()) {
// formulario no válido - finalizado
return;
}
// se gestionan dos locales [fr_FR, en_US]
// los datos reales [a,b] deben enviarse en formato anglosajón en todos los casos
// se hará mediante el filtro [CultureFilter]
// se realiza una llamada Ajax manualmente
$.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);
}
})
}
- líneas 2-5: recordemos que estos elementos han sido inicializados por la función [$(document).ready];
- líneas 9-12: se ejecutan los validadores jS del formulario. Si alguno de los valores no es válido, la expresión [formulaire.validate().form()] devuelve el valor false. En este caso, se anula el [submit] del formulario;
- líneas 18-38: se realiza una llamada Ajax manualmente;
- línea 19: el URL es el destino de la llamada Ajax;
- líneas 20-22: una matriz de encabezados HTTP que se añade a los que aparecen por defecto en la solicitud HTTP. Aquí se añade el encabezado HTTP, que indicará al servidor que se está realizando una llamada Ajax;
- línea 23: el método HTTP utilizado;
- línea 24: los datos enviados. [formulaire.serialize] crea la cadena a enviar [culture=fr-FR&a=12,7&b=20,89] del formulario de id [formulaire]. Aquí nos encontramos con el problema estudiado anteriormente: los valores [a,b] deben enviarse en formato anglosajón. Sabemos que este problema ya se ha resuelto con la creación del filtro [cultureFilter];
- línea 25: el tipo de datos esperado en la respuesta. Sabemos que el servidor devolverá un flujo HTML;
- línea 26: el método que se debe ejecutar cuando se inicia la solicitud. Aquí se indica que hay que mostrar el componente de id [loading]. Se trata de la imagen animada de espera;
- línea 29: el método que se debe ejecutar si la solicitud Ajax tiene éxito. El parámetro [data] es la respuesta completa del servidor. Sabemos que se trata de un flujo HTML;
- línea 30: se actualiza el componente de id [résultats] con el HTML del parámetro [data].
- línea 33: se oculta la señal de espera;
- línea 35: función ejecutada cuando se ha recibido la respuesta del servidor, independientemente de si es un éxito o un error;
- líneas 35-37: en caso de error (el servidor ha devuelto una respuesta HTTP con un estado que indica que se ha producido un error en el lado del servidor), se muestra la respuesta HTML del servidor en el campo [resultats];
A continuación se muestra un ejemplo de ejecución:
![]() | ![]() |
7.3. Actualización de una página HTML con un flujo jSON
En el ejemplo anterior, el servidor web respondía a la solicitud Ajax HTTP con un flujo HTML. En este flujo, había datos acompañados de formato HTML. Proponemos retomar el ejemplo anterior, pero esta vez con respuestas jSON (JavaScript Object Notation) que solo contienen los datos. La ventaja es que así se transmiten menos bytes. Suponemos que Javascript está activado en el navegador.
7.3.1. La acción [/ajax-04]
La acción [/ajax-04] es idéntica a la acción [/ajax-01], salvo que se muestra la vista [vue-04.xml] en lugar de la 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) {
...
// vista
return "vue-04";
}
7.3.2. La vista [vue-04.xml]
![]() |
La vista [vue-04.xml] retoma el cuerpo de la vista [vue-01.xml] con las siguientes diferencias:
<!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>
<!-- campos ocultos -->
<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>
- línea 5: el Javascript de la vista se encuentra ahora en el archivo [local4.js];
- línea 16: la etiqueta [form] ya no tiene los parámetros [data-ajax-attr] de la biblioteca [Unobtrusive Ajax]. No la vamos a utilizar aquí. La etiqueta [form] tampoco tiene los atributos [method] y [action], que indican cómo y dónde enviar los valores introducidos en el formulario. Esto se debe a que este va a ser enviado por una función jS (línea 20);
- líneas 26-57: el campo de id [resultats], que antes estaba vacío, ahora contiene código HTML para mostrar los resultados;
- líneas 26-34: el encabezado de los resultados, donde se muestra la hora del cálculo;
- líneas 35-52: los resultados de las cuatro operaciones aritméticas;
- líneas 53-57: un posible mensaje de error enviado por el servidor;
El código jS, que se ejecuta al cargar la vista [vue-04.xm], se encuentra en el archivo [local4.js]. Es el siguiente:
// datos globales
var loading;
var formulaire;
var résultats;
var titre;
var labelHeureCalcul;
var heureCalcul;
var aplusb;
var amoinsb;
var afoisb;
var adivb;
var msgErreur;
// al cargar el documento
$(document).ready(function() {
// se recuperan las referencias de los diferentes componentes de la página
loading = $("#loading");
formulaire = $("#formulaire");
résultats = $('#resultados');
titre=$("#titre");
labelHeureCalcul=$("#labelHeureCalcul");
heureCalcul=$("#heureCalcul");
aplusb=$("#aplusb");
amoinsb=$("#amoinsb");
afoisb=$("#afoisb");
adivb=$("#adivb");
msgErreur=$("#msgErreur");
// se ocultan algunos elementos
résultats.hide();
erreur.hide();
loading.hide();
});
- líneas 17-27: se recuperan las referencias jQuery de todos los elementos de la página;
- línea 29: se oculta el área de resultados;
- línea 30: así como el área de error;
- línea 31: así como la imagen animada de espera;
- líneas 2-12: las referencias recuperadas se convierten en globales para que las demás funciones puedan disponer de ellas;
7.3.3. La función jS [postForm]
El enlace [Calculer] es el siguiente:
<p>
<img id="loading" style="display: none" src="/images/loading.gif" />
<a href="javascript:postForm()" th:text="#{action.calculer}">Calculer</a>
</p>
La función jS [postForm] se define en el archivo [local.js] de la siguiente manera:
function postForm() {
// ¿Formulario válido?
if (!formulaire.validate().form()) {
// ¿Formulario inválido? - Finalizado
return;
}
// se realiza una llamada Ajax manualmente
$.ajax({
url : '/ajax-05',
headers : {
'Accept' : 'application/json'
},
type : 'POST',
data : formulaire.serialize(),
dataType : 'json',
beforeSend : onBegin,
success : onSuccess,
error : onError,
complete : onComplete
})
}
// antes de la llamada Ajax
function onBegin() {
...
}
// al recibir la respuesta del servidor
// en caso de éxito
function onSuccess(data) {
...
}
// al recibir la respuesta del servidor
// en caso de fallo
function onError(jqXHR) {
...
}
// después de [onSuccess, onError]
function onComplete() {
...
}
- líneas 3-6: antes de enviar los valores introducidos, se comprueban. Si son incorrectos, no se ejecuta la función POST del formulario;
- línea 9: los valores introducidos se envían a la acción [/ajax-05], que detallamos más adelante;
- líneas 10-12: un encabezado HTTP para indicar al servidor que se espera una respuesta en formato jSON;
- línea 13: los valores introducidos se van a enviar;
- línea 14: serialización de los valores introducidos en una cadena lista para ser enviada [a=1,6&b=2,4&culture=fr-FR];
- línea 15: el tipo de respuesta enviada por el servidor. Será jSON;
- línea 16: la función que se debe ejecutar antes de POST;
- línea 17: la función que se debe ejecutar al recibir la respuesta del servidor si esta es satisfactoria. El «éxito» de una solicitud HTTP se mide en función del estado de la respuesta HTTP del servidor. Una respuesta [HTTP/1.1 200 OK ] es una respuesta de éxito. Una respuesta [HTTP/1.1 500 Internal Server Error] es una respuesta de error. Lo que se denomina estado de una respuesta HTTP es el código [200] o [500]. Algunos de estos códigos están relacionados con el «éxito», mientras que otros están relacionados con el «fracaso»;
- línea 18: la función que se debe ejecutar al recibir la respuesta del servidor cuando el estado HTTP de dicha respuesta es un estado de fallo;
- línea 18: la función que se debe ejecutar en último lugar, después de las funciones [onSuccess, onError] anteriores;
La función [onBegin] es la siguiente:
// antes de la llamada Ajax
function onBegin() {
console.log("onBegin");
// se muestra la imagen animada
loading.show();
// se ocultan algunos elementos de la vista
entete.hide();
résultats.hide();
erreur.hide();
}
Antes de estudiar las demás funciones jS de la llamada Ajax, necesitamos conocer la respuesta enviada por la acción [/ajax-05].
7.3.4. La acción [/ajax-05]
La acción [/ajax-05] es la siguiente:
@RequestMapping(value = "/ajax-05", method = RequestMethod.POST)
@ResponseBody()
// procesa el POST de la vista [vue-04]
public JsonResults ajax05(@Valid ActionModel01 formulaire, BindingResult result, Locale locale, HttpServletRequest request, HttpSession session) throws InterruptedException {
if(result.hasErrors()){
// caso anómalo: no se muestra nada
return null;
}
...
}
- línea 2: el atributo [ResponseBody] indica que la acción [/ajax-05] devuelve ella misma la respuesta al cliente. Dado que una biblioteca jSON se encuentra entre las dependencias del proyecto, Spring Boot autoconfigura este tipo de acciones para que devuelvan jSON. Por lo tanto, es la cadena jSON de un tipo [JsonResults] (línea 4) la que se enviará al cliente;
- línea 2: los valores enviados [a, b, culture] se encapsularán en un tipo [ActionModel01], cuya validación se solicita como [@Valid ActionModel01]. Es solo por formalidad. Partimos de la hipótesis de que JavaScript estaba activado en el navegador del cliente y, por lo tanto, cuando llegan, los valores enviados ya han sido verificados por parte del cliente. No obstante, podemos prever el caso de un POST no autorizado que no utilizaría nuestro cliente jS. En ese caso, la validación puede fallar;
- líneas 5-7: en caso de error, se devuelve un flujo jSON vacío;
Continuemos con el análisis de la acción [/ajax-05]:
@RequestMapping(value = "/ajax-05", method = RequestMethod.POST)
@ResponseBody()
// procesa el POST de la vista [vue-04]
public JsonResults ajax05(@Valid ActionModel01 formulaire, BindingResult result, Locale locale,
HttpServletRequest request, HttpSession session) throws InterruptedException {
...
// el contexto de la aplicación Spring
WebApplicationContext ctx = WebApplicationContextUtils.getWebApplicationContext(request.getServletContext());
// ¿tempo?
Integer tempo = (Integer) session.getAttribute("tempo");
if (tempo != null && tempo > 0) {
Thread.sleep(tempo);
}
...
// se devuelve el resultado
return résultats;
}
- línea 8: se recupera el contexto [ctx] de la aplicación Spring. Lo necesitamos para recuperar los mensajes de los archivos [messages.properties] a partir de una clave de mensaje y una configuración regional. Esto se hace con la siguiente sintaxis:
ctx.getMessage(clé_message, tableau_de_paramètres, locale)
- [clé_message]: la clave del mensaje buscado;
- [locale]: la configuración regional utilizada. Así, si esta configuración regional es [en_US], se utilizará el archivo [messages_en.properties];
- [tableau_de_paramètres]: el mensaje obtenido se puede configurar tal y como en [clé=message {0} {1}]. En este mensaje hay dos parámetros [{0} {1}]. Como segundo parámetro de [ctx.getMessage] habrá que proporcionar una matriz de dos valores;
- líneas 10-13: si hay una pausa en la sesión, se detiene el hilo actual mientras dura dicha pausa;
La acción [/ajax-05] continúa de la siguiente manera:
// se prepara el modelo de la siguiente vista
JsonResults résultats = new JsonResults();
...
}
- línea 2: creación de la plantilla de la cadena jSON enviada al cliente;
La plantilla [JsonResults] es la siguiente:
![]() |
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 y setters
...
}
- líneas 6-13: cada uno de los campos de la clase [JsonResult] corresponde a un campo del mismo [id] en la vista [vue-04.xml]:
La acción [/ajax-05] continúa de la siguiente manera:
// se prepara la plantilla de la siguiente vista
JsonResults résultats = new JsonResults();
// encabezado
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()));
// se genera un error una de cada dos veces
int val = new Random().nextInt(2);
if (val == 0) {
// se devuelve un mensaje de error
résultats.setMsgErreur(ctx.getMessage("resultats.erreur",
new Object[] { ctx.getMessage("erreur.aleatoire", null, locale) }, locale));
return résultats;
}
- línea 2: creación de la plantilla de la cadena jSON enviada al cliente;
- líneas 4-6: se crean los mensajes del encabezado de los resultados;
- líneas 8-14: aproximadamente una de cada dos veces, se genera un mensaje de error. En ese caso, no se continúa y se devuelve la cadena jSON al cliente (línea 13);
- línea 11: aquí tenemos un ejemplo de mensaje configurado:
erreur.aleatoire=erreur aléatoire
resultats.erreur=Une erreur s''est produite : [{0}]
La acción [/ajax-05] continúa de la siguiente manera:
// se recuperan los valores enviados
double a = formulaire.getA();
double b = formulaire.getB();
// se construye el modelo
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");
}
// se devuelve el resultado
return résultats;
- líneas 2-3: se recuperan los valores de [a] y [b];
- líneas 5-12: se construyen los cuatro resultados;
- línea 14: se envía al cliente la cadena jSON [JsonResults];
Veamos qué pasa con el cliente [Advanced Rest Client]:
![]() |
- en [1-2], se realiza una consulta POST a la acción [/ajax-05];
- en [3], se envían valores incorrectos;
- en [4], el servidor devolvió un flujo vacío;
![]() |
- en [1], se envían valores correctos;
- en [2], el objeto jSON devuelto por el servidor, con un mensaje de error;
![]() |
- en [1], se envían valores correctos;
- en [2], el objeto jSON devuelto por el servidor, con los cuatro resultados;
![]() |
- en [1], se envían valores correctos;
- en [2], se ha provocado una excepción en el lado del servidor. Se observa que el servidor envía de nuevo un objeto jSON. En este mensaje, se ve que el estado HTTP de la respuesta es [500], lo que indica que se ha producido un error en el servidor;
7.3.5. La función jS [postForm] - 2
Ahora que conocemos el objeto jSON devuelto por el servidor, podemos utilizarlo en el javascript. El método [onSuccess] que se ejecuta cuando el servidor envía una respuesta con el estado HTTP [200] es el siguiente:
// al recibir la respuesta del servidor
// en caso de éxito
function onSuccess(data) {
console.log("onSuccess");
// se rellena el área de resultados
titre.text(data.titre);
labelHeureCalcul.text(data.labelHeureCalcul);
heureCalcul.text(data.heureCalcul);
entete.show();
// resultados sin error
if (!data.msgErreur) {
aplusb.text(data.aplusb);
amoinsb.text(data.amoinsb);
afoisb.text(data.afoisb);
adivb.text(data.adivb);
résultats.show();
return;
}
// resultados con error
msgErreur.text(data.msgErreur);
erreur.show();
}
- línea 3: el parámetro [data] es el objeto jSON devuelto por el servidor:
![]() |
El método [onError] ejecutado cuando el estado de la respuesta HTTP es [500] es el siguiente:
// al recibir la respuesta del servidor
// en caso de fallo
function onError(jqXHR) {
console.log("onError");
// error del sistema
msgErreur.text(jqXHR.responseText);
erreur.show();
}
- línea 3: el objeto JQuery [jqXHR] tiene entre sus propiedades las siguientes:
- responseText: el texto de la respuesta del servidor,
- status: el código de error devuelto por el servidor,
- statusText: el texto asociado a este código de error;
- línea 6: el objeto [jqXHR.responseText] es el siguiente objeto jSON:
![]() |
7.3.6. Pruebas
Veamos algunas capturas de pantalla de la ejecución de la aplicación web:
![]() |
![]() |
![]() |
7.4. Aplicación web de una sola página
7.4.1. Introducción
La tecnología Ajax permite crear aplicaciones de página única:
- la primera página se obtiene mediante una solicitud clásica del navegador;
- las páginas siguientes se obtienen mediante llamadas Ajax. Por lo tanto, al final, el navegador nunca cambia de URL y nunca carga una nueva página. A este tipo de aplicación se le denomina «aplicación de página única» (APU) o, en inglés, «Single Page Application» (SPA).
A continuación se muestra un ejemplo básico de este tipo de aplicación. La nueva aplicación tendrá dos vistas:
![]() |
![]() |
- en [1], la acción [/ajax-06] nos permite acceder a la primera página, la página 1;
- en [2], un enlace nos permite pasar a la página 2 mediante una llamada Ajax;
- en [3], el URL no ha cambiado. La página mostrada es la página 2;
- en [4], un enlace nos permite volver a la página 1 mediante una llamada Ajax;
- en [5], el URL no ha cambiado. La página que se muestra es la página 1.
7.4.2. La acción [/ajax-06]
El código de la acción [/ajax-06] es el siguiente:
@RequestMapping(value = "/ajax-06", method = RequestMethod.GET, produces = "text/html; charset=UTF-8")
public String ajax06() {
return "vue-06";
}
- líneas 1-4: la acción [/ajax-06] se limita a renderizar la vista [vue-06.xml];
7.4.3. La vista [vue-06.xml]
La vista [vue-06.xml] es la siguiente:
<!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>
- línea 8: la vista utiliza un script [local6.js];
- línea 12: se incluye la vista [vue-07.xml] en el área de id [content] de la vista [vue-06.xml];
7.4.4. La vista [vue-07.xml]
La vista [vue-07.xml] es la siguiente:
<!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. La función jS [gotoPage]
El enlace [Page 2] de la vista [vue-07.xml] utiliza la función jS [gotoPage] definida en el siguiente archivo [local6.js]:
// datos globales
var content;
function gotoPage(num) {
// se realiza una llamada Ajax manualmente
$.ajax({
url : '/ajax-07',
type : 'POST',
data : 'num=' + num,
dataType : 'html',
beforeSend : function() {
},
success : function(data) {
content.html(data)
},
complete : function() {
},
error : function(jqXHR) {
// error del sistema
content.html(jqXHR.responseText);
}
})
}
// al cargar el documento
$(document).ready(function() {
// se recuperan las referencias de los diferentes componentes de la página
content = $("#content");
});
- línea 28: al cargar la página, se almacena el área de id [content] y se convierte en una variable global (línea 2);
- línea 4: la función [gotoPage] recibe como parámetro el número de la página (1 o 2) que se va a mostrar en la vista actual;
- línea 7: el URL es el destino del POST;
- línea 8: la función URL de la línea 7 se solicita mediante una POST;
- línea 9: la cadena enviada. Se envía un parámetro denominado [num]. Su valor es el número de página (línea 4) que se debe mostrar en la vista actual;
- línea 10: el servidor devolverá HTML, el de la página que se va a mostrar;
- líneas 13-15: en caso de éxito (estado HTTP igual a 200), el HTML enviado por el servidor se coloca en el campo id [content];
- líneas 18-20: en caso de fallo (estado HTTP igual a 500), el HTML enviado por el servidor se coloca en la zona de id [content];
7.4.6. La acción [/ajax-07]
El código de la acción [/ajax-07] es el siguiente:
@RequestMapping(value = "/ajax-07", method = RequestMethod.POST, produces = "text/html; charset=UTF-8")
public String ajax07(int num) {
// num: número de página
switch (num) {
case 1:
return "vue-07";
case 2:
return "vue-08";
default:
return "vue-07";
}
}
- línea 2: se recupera el parámetro enviado que se llama [num]. Recordamos que el parámetro de la línea 2 debe llevar el nombre del parámetro enviado, en este caso [num]. [num] es un número de página o de vista;
- líneas 5-6: en el caso de que [num==1], se devuelve la vista [vue-07.xml];
- líneas 7-8: en el caso de que sea [num==2], se devuelve la vista [vue-08.xml];
- líneas 9-10: en los demás casos (lo cual es imposible normalmente), se devuelve la vista [vue-07.xml];
7.4.7. La vista [vue-08.xml]
La vista [vue-08.xml] forma la página n.º 2 de la aplicación:
<!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 varios flujos HTML en una respuesta jSON
7.5.1. Introducción
Consideramos la siguiente aplicación:
![]() |
La página [1] tiene cuatro áreas:
- [Zone 1, Zone 3] son zonas que aparecen o desaparecen al hacer clic en el botón [Rafraîchir]. Se cuenta el número de apariciones de cada una de estas dos zonas [2]. La zona [Zone 1] utiliza el idioma francés, mientras que la zona [Zone 3] utiliza el idioma inglés;
- la zona [Zone 2] está presente de forma permanente;
- la zona [Saisies] está presente de forma permanente;
El enlace [Valider] muestra la página siguiente [3]:
![]() |
- el enlace [Retour à la page 1] devuelve la página n.º 1 al estado en el que se encontraba [4];
La aplicación es de una sola página. El navegador solicita la primera página al servidor. Las siguientes se obtienen del servidor mediante llamadas Ajax.
7.5.2. La acción [/ajax-09]
![]() |
La acción [/ajax-09] es la siguiente:
@RequestMapping(value = "/ajax-09", method = RequestMethod.GET, produces = "text/html; charset=UTF-8")
public String ajax09() {
return "vue-09";
}
Se limita a mostrar la vista [vue-09.xml].
7.5.3. Las vistas XML
![]() |
La vista [vue-09.xml] es la página maestra de la aplicación:
<!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>
- línea 9: el archivo JS utilizado en la aplicación;
- línea 15: el contenido de la página maestra;
- línea 16: una imagen animada de espera:
- línea 17: área para mostrar un posible error;
La vista [vue-09-page1.xml] es la página 1 de la aplicación:
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<body>
<h2>Page 1</h2>
<!-- zona 1 -->
<fieldset id="zone1" style="background-color:pink">
<legend>Zone 1</legend>
<span id="zone1-content" th:text="xx">xx</span>
</fieldset>
<!-- zona 2 -->
<fieldset id="zone2" style="background-color:lightgreen">
<legend>Zone 2</legend>
<span>Ce texte reste toujours présent</span>
</fieldset>
<!-- zona 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>
- líneas 6-9: el área [Zone 1]. Su contenido se coloca en el componente [id="zone1-content"];
- líneas 11-14: el campo [Zone 2], que no cambia;
- líneas 16-19: el campo [Zone 3]. Su contenido se coloca en el componente [id="zone3-content"];
- línea 22: la función JS que envía el formulario;
- línea 25: inclusión del área de entrada de datos;
Cabe señalar que la página 1 no tiene la etiqueta [form]. Todo se procesará en javascript.
La vista [vue-09-saisies.xml] es la siguiente:
<!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>
- líneas 5-8: introducción de una cadena de caracteres;
- líneas 13-16: introducción de un número entero;
- línea 14: la función JS que envía los valores introducidos;
Una vez más, cabe señalar que el campo de entrada no tiene la etiqueta [form].
En total, la página n.º 1 presenta dos funcionalidades:
- [Rafraîchir]: que actualiza los campos 1 y 3. Esta acción es procesada por el servidor, que devuelve aleatoriamente:
- el área 1 con su contador de visitas y nada para el área 3,
- la zona 3 con su contador de accesos y nada para la zona 1,
- las dos zonas con sus contadores de acceso;
- [Valider]: que muestra la página 2 con los valores introducidos o un mensaje de error si los datos introducidos no son válidos;
Nos centraremos primero en el botón [Rafraîchir].
7.5.4. El código JS de gestión del botón [Rafraîchir]
![]() |
El código del archivo [local9.js] es el siguiente:
// variables globales
var content;
var loading;
var erreur;
// al cargar el documento
$(document).ready(function() {
// se recuperan las referencias de los diferentes componentes de la página
loading = $("#loading");
loading.hide();
erreur = $("#erreur");
erreur.hide();
content = $("#content");
});
- líneas 9-13: cuando se carga la página maestra, se memorizan las referencias de los tres componentes identificados por [loading, erreur, content];
- líneas 2-4: las referencias de estos tres componentes se almacenan en variables globales. Se mantienen fijas porque los tres campos en cuestión siempre están presentes en la página mostrada, independientemente del momento. Como permanecen fijas, pueden calcularse en [$(document).ready] y compartirse con las demás funciones del archivo JS;
La función [postForm] gestiona el clic en el botón [Rafraîchir]:
function postForm() {
console.log("postForm");
// se realiza una llamada Ajax manualmente
$.ajax({
url : '/ajax-10',
headers : {
'Accept' : 'application/json'
},
type : 'POST',
dataType : 'json',
beforeSend : onBegin,
success : onSuccess,
error : onError,
complete : onComplete
})
}
- líneas 4-15: la llamada Ajax al servidor;
- línea 5: es la acción [ajax-10] la que procesará el POST;
- líneas 6-8: la respuesta será jSON. El cliente JS indica que acepta los documentos jSON;
- línea 9: se invoca la acción [ajax-10] con una operación POST;
- línea 10: se va a recibir de jSON;
- línea 11: la función ejecutada antes de la llamada Ajax;
- línea 12: la función ejecutada al recibir la respuesta del servidor, cuando esta es correcta [200 OK];
- línea 13: la función ejecutada al recibir la respuesta del servidor, cuando esta falla [500 Internal server error, ...];
- línea 14: la función ejecutada tras recibir la respuesta;
La función [onBegin] es la siguiente:
// antes de la llamada Ajax
function onBegin() {
console.log("onBegin");
// imagen de espera
loading.show();
}
Se limita a poner en marcha la imagen animada de espera del resultado del servidor.
7.5.5. La acción [/ajax-10]
![]() |
La acción [/ajax-10] es la siguiente:
// la sesión
@Autowired
private SessionModel1 session;
// el motor Thymeleaf / Spring
@Autowired
private SpringTemplateEngine engine;
@RequestMapping(value = "/ajax-10", method = RequestMethod.POST)
@ResponseBody()
public JsonResult10 ajax10(HttpServletRequest request, HttpServletResponse response) {
...
}
- línea 3: se inyecta la sesión. Esta tiene el siguiente 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;
// dos contadores
private int cpt1 = 0;
private int cpt3 = 0;
// las tres zonas
private String zone1 = "xx";
private String zone3 = "zz";
private String saisies;
private boolean zone1Active = true;
private boolean zone3Active = true;
// getters y setters
...
}
La sesión [SessionModel1] almacena los siguientes elementos:
- línea 15: el número de veces [cpt1] en que se muestra el campo [Zone 1];
- línea 16: el número de veces [cpt3] en que se muestra el campo [Zone 3];
- líneas 18-20: los flujos HTML de las zonas [Zone 1], [Zone 3] y [Saisies]. Esto es necesario en la secuencia [Page 1] --> [Page 2] --> [Page 1]. Al pasar de [Page 2] a [Page 1], hay que restaurar [Page 1] y, por lo tanto, sus tres campos;
- líneas 21-22: dos valores booleanos que indican si los campos [Zone 1] y [Zone 3] se muestran (son visibles);
El otro elemento inyectado en el controlador [AjaxController] es el siguiente:
// el motor Thymeleaf / Spring
@Autowired
private SpringTemplateEngine engine;
El bean de tipo [SpringTemplateEngine] se define en el archivo de configuración [Config]:
![]() |
Se define de la siguiente manera:
@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;
}
- líneas 2-10: conocemos el bean de tipo [SpringResourceTemplateResolver], que nos permite definir ciertas características de las vistas;
- líneas 13-17: el bean de tipo [SpringTemplateEngine] nos permite definir el «motor» de vistas, la clase encargada de generar las respuestas [Thymeleaf] a las clients. [Thymeleaf] tiene un «motor» por defecto y otro cuando se utiliza en un entorno [Spring]. Es este último el que utilizamos aquí;
La firma de la acción [/ajax-10] es la siguiente:
@RequestMapping(value = "/ajax-10", method = RequestMethod.POST)
@ResponseBody()
public JsonResult10 ajax10(HttpServletRequest request, HttpServletResponse response) {
...
}
- línea 1: la acción [/ajax-10] solo acepta un POST;
- línea 2: la acción [/ajax-10] devuelve ella misma la respuesta al cliente. Esta se transformará automáticamente en jSON;
- línea 3: la respuesta es del tipo [JsonResult10], a saber:
![]() |
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 y setters
...
}
- línea 6: el contenido HTML del campo identificado por [content];
- línea 7: el contenido HTML del campo [Zone 1];
- línea 8: el contenido HTML de la zona [Zone 3];
- línea 9: el contenido HTML del campo [Erreur];
- línea 10: el contenido HTML del campo [Saisies];
- línea 11: valor booleano que indica si debe mostrarse el campo [Zone 1];
- línea 12: valor booleano que indica si debe mostrarse el campo [Zone 3];
El código de la acción [/ajax-10] es el siguiente:
@RequestMapping(value = "/ajax-10", method = RequestMethod.POST)
@ResponseBody()
public JsonResult10 ajax10(HttpServletRequest request, HttpServletResponse response) {
// contexto de Thymeleaf
WebContext thymeleafContext = new WebContext(request, response, request.getServletContext());
// respuesta
JsonResult10 result = new JsonResult10();
// sesión
session.setZone1(null);
session.setZone3(null);
session.setZone1Active(false);
session.setZone3Active(false);
// se devuelve una respuesta aleatoria
int cas = new Random().nextInt(3);
switch (cas) {
case 0:
// zona 1 activa
setZone1(thymeleafContext, result);
return result;
case 1:
// zona 3 activa
setZone3(thymeleafContext, result);
return result;
case 2:
// zonas 1 y 3 activas
setZone1(thymeleafContext, result);
setZone3(thymeleafContext, result);
return result;
}
return null;
}
- línea 5: recuperamos el contexto [Thymeleaf]. Más adelante veremos para qué nos servirá;
- línea 7: creamos una respuesta vacía por el momento;
- líneas 9-12: asignamos a [null] los dos campos contenidos en la sesión e indicamos que no deben mostrarse. Estos dos campos se generarán en breve, pero es posible que solo se genere uno de ellos;
- líneas 14-29: se generan los dos campos;
- líneas 17-19: solo se genera el campo [Zone 1];
- líneas 21-23: solo se genera el campo [Zone 3];
- líneas 25-28: se generan las dos zonas [Zone 1] y [Zone 3];
El flujo HTML del campo [Zone 1] se genera mediante el siguiente método:
private void setZone1(WebContext thymeleafContext, JsonResult10 result) {
// zona 1 activa
// flujo 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);
// sesión
session.setCpt1(cpt1);
session.setZone1(zone1);
session.setZone1Active(true);
}
- línea 1: los parámetros son:
- el contexto [Thymeleaf] de tipo [WebContext],
- la respuesta al cliente en proceso de construcción de tipo [JsonResult10];
- línea 3: se incrementa el contador [cpt1] de la sesión que cuenta el número de veces que se muestra el campo [Zone 1];
- línea 4: el contexto [Thymeleaf], de tipo [WebContext], se comporta de forma similar al modelo [Model] de Spring MVC. Para añadir un elemento al modelo, se utiliza [WebContext.setVariable]. Aquí, por lo tanto, se coloca el contador [cpt1] en el modelo [Thymeleaf]. Esto permitirá evaluar la expresión Thymeleaf [${cpt1}]
- línea 5: el contexto [Thymeleaf] tiene una configuración regional. Esto le permite evaluar expresiones del tipo [#{clé_msg}]. Aquí, se asocia el contexto Thymeleaf a una configuración regional francesa;
- línea 6: esta es la instrucción más interesante. El motor Thymeleaf procesará la vista [vue-09-zone1.xml] con la plantilla y la configuración regional que acabamos de calcular y, en lugar de enviar el flujo HTML resultante al cliente, lo devuelve como una cadena de caracteres;
- líneas 7-9: el flujo HTML de la zona [Zone 1] que se acaba de calcular se almacena en la sesión y en el resultado que se enviará al cliente. Además, se indica que debe mostrarse el campo [Zone 1];
- líneas 11-13: se almacena en la sesión la información relativa al área [Zone 1] para poder regenerarla;
La línea 7 procesa la siguiente 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>
- línea 3: la expresión [#{message.zone}] se evaluará mediante la variable local;
- línea 4: la expresión [${cpt1}] se evaluará mediante la plantilla Thymeleaf;
El mensaje de clave [message.zone] se define en los archivos de mensajes [messages_fr.properties] y [messages_en.properties]:
![]() |
[messages_fr.properties]
message.zone=Nombre d'accès :
[messages_en.properties]
message.zone=Number of hits:
El flujo HTML de la zona [Zone 3] se genera mediante un método similar:
private void setZone3(WebContext thymeleafContext, JsonResult10 result) {
// zona 3 activa
// flujo 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);
// sesión
session.setCpt3(cpt3);
session.setZone3(zone3);
session.setZone3Active(true);
}
- línea 6: la configuración regional del campo [Zone 3] es la configuración regional inglesa;
7.5.6. Procesamiento de la respuesta de la acción [/ajax-10]
Volvamos al código JS de [local9.js], que procesará la respuesta del servidor:
// al recibir la respuesta del servidor
// en caso de éxito
function onSuccess(data) {
console.log("onSuccess");
// contenido
if (data.content) {
content.html(data.content);
}
// zona 1
if (data.zone1Active) {
$("#zone1").show();
if (data.zone1) {
$("#zone1-content").html(data.zone1);
}
} else {
$("#zone1").hide();
}
// ¿zona 3 activa?
if (data.zone3Active) {
$("#zone3").show();
if (data.zone3) {
$("#zone3-content").html(data.zone3);
}
} else {
$("#zone3").hide();
}
// ¿entradas?
if (data.saisies) {
$("#saisies").html(data.saisies);
}
// ¿error?
if (data.erreur) {
erreur.text(data.erreur);
erreur.show();
} else {
erreur.hide();
}
}
Recordemos la estructura Java de la respuesta recibida en la línea 3 en la variable [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;
}
- líneas 6-8: si [data.content!=null], entonces se inicializa el campo [id=content] con él. Este campo representa [Page 1] o [Page 2] en su totalidad. En la demostración actual, tenemos [data.content==null] y, por lo tanto, el campo [id=content] no se modificará y seguirá mostrando [Page 1];
- líneas 10-17: se muestra [Zone 1] si [data.zone1Active==true]. Si además [data.zone1!=null], entonces se modifica el contenido de [Zone 1]; de lo contrario, permanece igual;
- líneas 19-26: lo mismo para [Zone 3];
- líneas 28-30: si tenemos [data.saisies!=null], entonces se regenera la zona [Saisies]. En la demostración actual, tenemos [data.saisies==null] y, por lo tanto, la zona [Saisies] permanece igual;
- líneas 32-37: razonamiento análogo para la zona [Erreur] con las siguientes matizaciones:
- línea 33: [data.erreur] será un mensaje de error en formato de texto;
- línea 36: si [data.erreur==null], entonces el campo [Erreur] se oculta. De hecho, es posible que se haya mostrado en la solicitud anterior;
En caso de error del lado del servidor (HTTP, estado del tipo 500 Internal server error), se ejecuta la siguiente función:
// al recibir la respuesta del servidor
// en caso de fallo
function onError(jqXHR) {
console.log("onError");
// error del sistema
erreur.text(jqXHR.responseText);
erreur.show();
}
Para ver un error de este tipo, modifiquemos la función [postForm] de la siguiente manera:
function postForm() {
console.log("postForm");
// se recuperan referencias de la página actual
...
// se realiza una llamada Ajax manualmente
$.ajax({
url : '/ajax-10x',
...
})
}
- línea 7: introducimos un URL que no existe;
Estos son los resultados al hacer clic en el botón [Rafraîchir]:
![]() |
Es interesante observar que el error también se ha enviado en forma de cadena jSON.
El método ejecutado tras recibir la respuesta del servidor es el siguiente:
// después de [onSuccess, onError]
function onComplete() {
console.log("onComplete");
// imagen de espera
loading.hide();
}
Simplemente se oculta la imagen animada de espera.
7.5.7. Visualización de la página [Page 2]
El código HTML del enlace [Valider] es el siguiente:
<a href="javascript:valider()">Valider</a>
La función JS [valider] es la siguiente:
// validación de los valores introducidos
function valider() {
// valor enviado
var post = JSON3.stringify({
"value1" : $("#text1").val().trim(),
"value2" : $("#text2").val().trim()
});
// se realiza una llamada Ajax manualmente
$.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
})
}
- líneas 4-7: tenemos dos valores v1 y v2 que enviar: los de los componentes de entrada identificados por [#text1] y [#text2]. Vamos a hacer algo nuevo. Vamos a enviar estos dos valores en forma de cadena jSON {"value1":v1,"value2":v2};
- línea 10: los valores enviados se remitirán a la acción [ajax-11A];
- línea 12: como sabemos que vamos a recibir una respuesta jSON, indicamos que podemos recibirla de jSON;
- línea 13: se indica al servidor que se le va a enviar el valor enviado en forma de cadena jSON;
- líneas 15-16: se crea un POST a partir del valor que se va a enviar;
- línea 17: vamos a recibir un jSON;
7.5.8. La acción [ajax-11A]
La acción [ajax-11A] que procesa la cadena jSON enviada es la siguiente:
@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) {
...
}
- línea 1: se indica con ["application/json"] que la acción espera un documento en formato jSON. Este documento es el valor enviado por el cliente;
- línea 3: el valor enviado se recuperará en el siguiente 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 y setters
...
}
- la estructura del objeto [PostAjax11A] debe reproducir la estructura del objeto enviado {"value1":v1,"value2":v2}. Por lo tanto, se necesitan los campos [value1] (línea 13) y [value2] (línea 16);
- Se han aplicado restricciones de integridad a ambos campos;
Volvamos al código de la acción [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) {
// contexto Thymeleaf
WebContext thymeleafContext = new WebContext(request, response, request.getServletContext());
// respuesta
JsonResult10 result = new JsonResult10();
// ¿post es válido?
if (bindingResult.hasErrors()) {
// se devuelve la página 1 con un error
result.setZone1Active(session.isZone1Active());
result.setZone3Active(session.isZone3Active());
result.setErreur(getErreursForModel(bindingResult));
return result;
}
...
}
- línea 3: la anotación [@RequestBody] designa el documento enviado por el cliente. Se trata del valor enviado por este en jSON. Por lo tanto, este se utilizará para construir el objeto [PostAjax11A];
- línea 3: la anotación [@Valid] fuerza la validación del valor enviado;
- línea 9: si la validación falla:
- línea 13: se devuelve un mensaje de error;
- líneas 11-12: los campos 1 y 3 se devuelven al estado en el que se encontraban (visibles o no);
El cálculo del mensaje de error se realiza de la siguiente manera:
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();
}
Es una función que ya hemos visto.
La acción [ajax-11A] continúa de la siguiente manera:
@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) {
// contexto Thymeleaf
WebContext thymeleafContext = new WebContext(request, response, request.getServletContext());
// respuesta
JsonResult10 result = new JsonResult10();
// ¿post es válido?
if (bindingResult.hasErrors()) {
...
}
// se guarda el campo de entrada
thymeleafContext.setVariable("value1", post.getValue1());
thymeleafContext.setVariable("value2", post.getValue2());
session.setSaisies(engine.process("vue-09-saisies", thymeleafContext));
// se envía la página 2
result.setContent(engine.process("vue-09-page2", thymeleafContext));
return result;
}
- líneas 13-14: los valores enviados se colocan en el contexto Thymeleaf;
- línea 15: con este contexto, se calcula la vista [vue-09-saisies] y se coloca en la sesión para poder regenerarla posteriormente;
- línea 17: la página 2 se incluye en el resultado que se enviará al cliente;
La vista [vue-09-page2.xml] es la siguiente:
![]() |
<!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>
- líneas 9 y 13: se muestran los valores [value1, value2] que la acción [/ajax-11A] ha colocado en el contexto Thymeleaf;
7.5.9. Procesamiento de la respuesta de la acción [/ajax-11A]
En el lado del cliente, la respuesta de la acción [/ajax-10] es procesada por la función [onSuccess]:
function onSuccess(data) {
console.log("onSuccess");
// contenido
if (data.content) {
content.html(data.content);
}
// zona 1
if (data.zone1Active) {
$("#zone1").show();
if (data.zone1) {
$("#zone1-content").html(data.zone1);
}
} else {
$("#zone1").hide();
}
// ¿zona 3 activa?
if (data.zone3Active) {
$("#zone3").show();
if (data.zone3) {
$("#zone3-content").html(data.zone3);
}
} else {
$("#zone3").hide();
}
// ¿Datos introducidos?
if (data.saisies) {
$("#saisies").html(data.saisies);
}
// ¿error?
if (data.erreur) {
erreur.text(data.erreur);
erreur.show();
} else {
erreur.hide();
}
}
Ya hemos comentado este código. Consideremos los dos casos, respuesta con o sin error:
Con error
En este caso, la acción [/ajax-11A] ha enviado una respuesta jSON con el formato {"zona1":null, "zona3":null,"entradas":null,"error":error,"zone1Active":zone1Active,"zone3Active":zone3Active,"content":null}. Si seguimos el código anterior, vemos que:
- el campo [content] no cambia. Contenía la página n.º 1;
- se muestra la zona [Erreur];
- las zonas [Zone 1], [Zone 3] y [Saisies] se mantienen tal y como estaban;
Sin error
En este caso, la acción [/ajax-11A] envió una respuesta jSON con el formato {"zona1":null, "zona3":null,"entradas":null,"error":null,"zone1Active":false,"zone3Active":false,"content":content}. Si seguimos el código anterior, vemos que:
- se muestra la zona [content]. Contiene la página n.º 2;
He aquí tres ejemplos de ejecución:
Un caso con error de validación:
![]() | ![]() |
Un caso con error de POST:
![]() | ![]() |
Este tipo de error es diferente. Como Spring no ha podido convertir la cadena jSON al tipo [PostAjax11A], ha devuelto una respuesta HTTP con [status=400]. La acción [ajax-11A] no se ha ejecutado;
Un caso sin error:
![]() | ![]() |
7.5.10. Volver a la página n.º 1
El enlace [Retour vers la page 1] de la página n.º 2 es el siguiente:
<a href="javascript:retourPage1()">Retour à la page 1</a>
El método JS [retourPage1] es el siguiente:
// volver a la página 1
function retourPage1() {
// se realiza una llamada Ajax manualmente
$.ajax({
url : '/ajax-11B',
headers : {
'Accept' : 'application/json',
},
type : 'POST',
dataType : 'json',
beforeSend : onBegin,
success : onSuccess,
error : onError,
complete : onComplete
})
}
Realiza un POST, sin valor contabilizado, hacia la acción [/ajax-11B].
7.5.11. La acción [/ajax-11B]
La acción [/ajax-11B] es la siguiente:
@RequestMapping(value = "/ajax-11B", method = RequestMethod.POST)
@ResponseBody
public JsonResult10 ajax11B(HttpServletRequest request, HttpServletResponse response) {
// contexto Thymeleaf
WebContext thymeleafContext = new WebContext(request, response, request.getServletContext());
// respuesta
JsonResult10 result = new JsonResult10();
// restablecemos la página 1 a su estado original
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;
}
La acción debe regenerar la página n.º 1 con sus tres zonas [Zone1, Zone3, Erreur]:
- línea 9: la página n.º 1 se incluye en el resultado;
- línea 10: el área de entradas se incluye en el resultado;
- línea 11: el campo [Zone 1] se incluye en el resultado;
- línea 12: el campo [Zone 3] se incluye en el resultado;
- líneas 13-14: se incluyen los estados de los campos [Zone 1] y [Zone 3] en el resultado;
7.5.12. Procesamiento de la respuesta de la acción [/ajax-11B]
La respuesta de la acción [/ajax-11B] se procesa mediante la función [onSuccess]:
function onSuccess(data) {
console.log("onSuccess");
// contenido
if (data.content) {
content.html(data.content);
}
// zona 1
if (data.zone1Active) {
$("#zone1").show();
if (data.zone1) {
$("#zone1-content").html(data.zone1);
}
} else {
$("#zone1").hide();
}
// ¿zona 3 activa?
if (data.zone3Active) {
$("#zone3").show();
if (data.zone3) {
$("#zone3-content").html(data.zone3);
}
} else {
$("#zone3").hide();
}
// ¿entradas?
if (data.saisies) {
$("#saisies").html(data.saisies);
}
// ¿error?
if (data.erreur) {
erreur.text(data.erreur);
erreur.show();
} else {
erreur.hide();
}
}
La acción [/ajax-11B] ha enviado una respuesta jSON con el formato {"zona1":zona1, "zona3":zona3,"entradas":entradas,"error":null,"zone1Active":zone1Active,"zone3Active":zone3Active,"content":content}. Si seguimos el código anterior, vemos que:
- el campo [content] se modifica. Contenía la página n.º 2. A partir de ahora contendrá la página n.º 1;
- la zona [Erreur] queda oculta;
- las zonas [Zone 1], [Zone 3] y [Saisies] se muestran tal y como estaban;
7.6. Gestionar la sesión del lado del cliente
7.6.1. Introducción
En el apartado anterior, hemos gestionado una sesión cuya estructura era la siguiente:
public class SessionModel1 implements Serializable {
// dos contadores
private int cpt1 = 0;
private int cpt3 = 0;
// las tres zonas
private String zone1 = "xx";
private String zone3 = "zz";
private String saisies;
private boolean zone1Active = true;
private boolean zone3Active = true;
...
}
Cuando hay un gran número de usuarios, la memoria que ocupan las sesiones de todos ellos puede suponer un problema. Por lo tanto, la norma es reducir al mínimo su tamaño. El modelo APU (aplicación de página única) permite gestionar la sesión del lado del cliente y disponer de un servidor web sin sesiones. De hecho, la página única se carga inicialmente en el navegador. Junto con ella, llega el archivo Javascript que la acompaña. Como no hay recarga de página, este archivo JS permanecerá permanentemente en el navegador tal y como se cargó inicialmente. Así, podemos utilizar sus variables globales para almacenar información sobre las diferentes acciones del usuario. Esto es lo que vamos a ver ahora. No solo gestionaremos la sesión del lado del cliente, sino que rediseñaremos la aplicación JS para solicitar el menor esfuerzo posible al servidor.
7.6.2. La acción [/ajax-12]
![]() |
La acción [/ajax-12] es la siguiente:
@RequestMapping(value = "/ajax-12", method = RequestMethod.GET, produces = "text/html; charset=UTF-8")
public String ajax12() {
return "vue-12";
}
La vista [vue-12.xml] es la siguiente:
![]() |
<!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 es idéntica a la vista [vue-09], con la única diferencia del script JS utilizado en la línea 9;
La vista mostrada es la siguiente:
![]() |
7.6.3. El código JS para la gestión del botón [Rafraîchir]
![]() |
El código del archivo [local12.js] es el siguiente:
// variables globales
var content;
var loading;
var erreur;
var page1;
var page2;
var value1;
var value2;
var session = {
"cpt1" : 0,
"cpt3" : 0
};
// al cargar el documento
$(document).ready(function() {
// se recuperan las referencias de los diferentes componentes de la página
loading = $("#loading");
loading.hide();
erreur = $("#erreur");
erreur.hide();
content = $("#content");
});
- líneas 17-21: cuando se carga la página maestra, se almacenan las referencias de los tres componentes identificados por [loading, erreur, content] en las variables globales de las líneas 2-4;
- líneas 5-6: para almacenar las dos páginas;
- líneas 7-8: para almacenar los dos valores enviados por el enlace [Valider];
- línea 9: la sesión. Almacena en el lado del cliente los valores de los contadores [cpt1, cpt3];
La función [postForm] gestiona el clic en el botón [Rafraîchir]:
function postForm() {
console.log("postForm");
// se envía la sesión
var post = JSON3.stringify(session);
// se realiza una llamada Ajax manualmente
$.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
})
}
Las diferencias con respecto a la anterior version son las siguientes:
- la función URL de la línea 7 es diferente;
- línea 4: se envía un valor, mientras que antes no se enviaba ninguno. Este valor es la cadena jSON de la sesión. El principio es el siguiente:
- el cliente envía la sesión al servidor,
- este la modifica y se la devuelve,
- el cliente memoriza la nueva sesión;
- línea 10: se envía un documento en formato jSON (valor enviado);
- línea 13: hay algo que enviar;
- líneas 15-20: las funciones [beforeSend, error, complete] son las mismas que las de la anterior version. Solo cambia la función [success] (líneas 16-18);
7.6.4. La acción [/ajax-13]
![]() |
La acción [/ajax-13] es la siguiente:
@RequestMapping(value = "/ajax-13", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
@ResponseBody()
public JsonResult13 ajax13(@RequestBody SessionModel2 session2, HttpServletRequest request, HttpServletResponse response) {
...
}
- línea 3: el parámetro [@RequestBody SessionModel2 session2] recupera la sesión enviada por el cliente. Esta tiene el siguiente tipo [SessionModel2]:
![]() |
package istia.st.springmvc.models;
import java.io.Serializable;
public class SessionModel2 implements Serializable {
private static final long serialVersionUID = 1L;
// dos contadores
private int cpt1 = 0;
private int cpt3 = 0;
// getters y setters
...
}
La sesión [SessionModel2] almacena los siguientes elementos:
- línea 9: el número de veces [cpt1] en que se muestra el campo [Zone 1];
- línea 10: el número de veces [cpt3] en que se muestra el campo [Zone 3];
Continuemos con el análisis del código de la acción [/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) {
...
}
- línea 3, el tipo [JsonResult13] de la respuesta es el siguiente:
![]() |
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;
// sesión
private SessionModel2 session;
// getters y setters
...
}
- línea 14: la sesión. El servidor la devuelve al cliente para su almacenamiento;
- línea 6: el contenido HTML de la página n.º 2;
- línea 7: el contenido HTML del área [Zone 1];
- línea 8: el contenido HTML de la zona [Zone 3];
- línea 9: el posible mensaje de error;
- líneas 10-11: dos datos calculados por el servidor y mostrados en la página n.º 2;
Continuemos con el análisis del código de la acción [/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) {
// contexto Thymeleaf
WebContext thymeleafContext = new WebContext(request, response, request.getServletContext());
// respuesta
JsonResult13 result = new JsonResult13();
result.setSession(session2);
// se devuelve una respuesta aleatoria
int cas = new Random().nextInt(3);
switch (cas) {
case 0:
// zona 1 activa
setZone1B(thymeleafContext, result);
return result;
case 1:
// zona 3 activa
setZone3B(thymeleafContext, result);
return result;
case 2:
// zonas 1 y 3 activas
setZone1B(thymeleafContext, result);
setZone3B(thymeleafContext, result);
return result;
}
return null;
}
- línea 9: la sesión se incluye en el resultado de la acción;
El método [setZone1B] que activa el campo [Zone 1] es el siguiente:
private void setZone1B(WebContext thymeleafContext, JsonResult13 result) {
// se recupera la sesión
SessionModel2 session = result.getSession();
// zona 1 activa
// flujo 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);
// sesión
session.setCpt1(cpt1);
}
- línea 3: se recupera la sesión. Se modificará en la línea 12 con el nuevo contador [cpt1]. Recordemos que esta sesión se devolverá al cliente;
- línea 10: el nuevo campo [Zone 1];
El método [setZone3B] que activa el campo [Zone 3] es similar:
private void setZone3B(WebContext thymeleafContext, JsonResult13 result) {
// se recupera la sesión
SessionModel2 session = result.getSession();
// zona 3 activa
// flujo 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);
// sesión
session.setCpt3(cpt3);
}
7.6.5. Procesamiento de la respuesta de la acción [/ajax-13]
En el lado del cliente, la respuesta jSON de la acción [/ajax-13] se procesa mediante la siguiente función [onSuccess]:
function postForm() {
console.log("postForm");
// se envía la sesión
var post = JSON3.stringify(session);
// se realiza una llamada Ajax manualmente
$.ajax({
...
success : function(data) {
// se guarda la sesión
session = data.session;
// se actualizan los dos campos
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();
}
},
...
})
}
- líneas 12-17: si el servidor ha introducido algo en el campo [zone1] de la respuesta, entonces hay que regenerar el campo [Zone 1] y mostrarlo; de lo contrario, debe ocultarse;
- líneas 18-23: mismo razonamiento para el área [Zone 3];
7.6.6. Visualización de la página [Page 2]
El código HTML del enlace [Valider] es el siguiente:
<a href="javascript:valider()">Valider</a>
La función JS [valider] es la siguiente:
// validación de los valores introducidos
function valider() {
// se guarda la página 1
page1 = content.html();
// se guardan los valores introducidos
value1 = $("#text1").val().trim();
value2 = $("#text2").val().trim();
// valor enviado
var post = JSON3.stringify({
"value1" : value1,
"value2" : value2,
"pageRequired" : page2 ? false : true
});
// se realiza una llamada Ajax manualmente
$.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 a hacer un POST que normalmente nos llevará a la página n.º 2;
- línea 4: memorizamos la página n.º 1 para poder volver a ella más adelante;
- líneas 6-7: la operación anterior no memoriza los valores introducidos, solo el código HTML de la página. Por lo tanto, ahora memorizamos los dos valores introducidos en el formulario;
- líneas 9-13: los dos valores introducidos se colocan en una cadena jSON. Es esta la que se enviará;
- línea 12: un parámetro para indicar al servidor si necesitamos la página n.º 2. Procederemos de la siguiente manera. Solicitaremos la página n.º 2 una primera vez y, a continuación, la memorizaremos en la variable JS [page2]. Después, ya no la volveremos a solicitar. Utilizaremos la página almacenada en caché. Línea 2, [pageRequired] es igual a [true] si la variable [page2] no contiene nada, y a [false] en caso contrario;
- cabe señalar que la sesión no se envía. De hecho, esta memoriza contadores que la acción [/ajax-14] de la línea 20 no modifica;
7.6.7. La acción [/ajax-14]
La acción [/ajax-14] es la siguiente:
@RequestMapping(value = "/ajax-14", method = RequestMethod.POST)
@ResponseBody
public JsonResult13 ajax14(@RequestBody @Valid PostAjax14 post, BindingResult bindingResult, Locale locale, HttpServletRequest request, HttpServletResponse response) {
...
}
- línea 3: la respuesta es siempre de tipo [JsonResult13];
- línea 3: el valor enviado se encapsula en el siguiente tipo [PostAjax14]:
package istia.st.springmvc.models;
public class PostAjax14 extends PostAjax11A {
// página 2
private boolean pageRequired;
// getters y setters
...
}
- línea 3: la clase [PostAjax14] extiende la clase [PostAjax11A] de la anterior version. Por lo tanto, tiene una estructura [value1, value2, pageRequired];
La acción [/ajax-14] continúa de la siguiente manera:
@RequestMapping(value = "/ajax-14", method = RequestMethod.POST)
@ResponseBody
public JsonResult13 ajax14(@RequestBody @Valid PostAjax14 post, BindingResult bindingResult, Locale locale, HttpServletRequest request, HttpServletResponse response) {
// contexto Thymeleaf
WebContext thymeleafContext = new WebContext(request, response, request.getServletContext());
// respuesta
JsonResult13 result = new JsonResult13();
// ¿post es válido?
if (bindingResult.hasErrors()) {
// se devuelve un error
result.setErreur(getErreursForModel(bindingResult));
return result;
}
// se envía la página 2
result.setValue1(post.getValue1());
result.setValue2(post.getValue2());
// ¿página requerida?
if (post.isPageRequired()) {
result.setPage2(engine.process("vue-12-page2", thymeleafContext));
}
return result;
}
- líneas 9-13: si los valores enviados a [value1, value2] no son válidos, se devuelve un mensaje de error;
- líneas 15-16: normalmente, el servidor debería realizar un cálculo con los valores enviados. En este caso, se limita a devolverlos para indicar que los ha recibido correctamente;
- líneas 18-20: la página n.º 2 solo se devuelve si ha sido solicitada por el cliente. Línea 19, la vista [vue-12-page2] es nueva:
![]() |
<!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>
- el código XML ya no contiene valores evaluados por Thymeleaf como ocurría anteriormente;
- se han identificado las zonas donde colocar los valores devueltos por el servidor [value1, value2]. Línea 9, [id='value1'] indica dónde colocar [value1]. Línea 13, lo mismo para [value2];
7.6.8. Procesamiento de la respuesta de la acción [/ajax-14]
La respuesta de la acción [/ajax-14] se procesa mediante la siguiente función [success]:
// validación de los valores introducidos
function valider() {
...
// se realiza una llamada Ajax manualmente
$.ajax({
...
success : function(data) {
// ¿Error?
if (data.erreur) {
// visualización del error
erreur.html(data.erreur);
erreur.show();
} else {
// sin error
erreur.hide();
// página 2
if (page2) {
// se utiliza la página en caché
content.html(page2);
} else {
// se guarda la página 2
page2 = data.page2;
// se muestra
content.html(data.page2);
}
// se actualiza con la información del servidor
$("#value1").text(data.value1);
$("#value2").text(data.value2);
}
},
...
})
}
- líneas 9-13: si el servidor ha devuelto un error, se muestra;
- líneas 14-29: el caso en el que no se ha producido ningún error. En ese caso, se debe mostrar la página n.º 2;
- línea 17: se comprueba si la página n.º 2 ya está almacenada en la variable [page2];
- línea 19: en ese caso, se utiliza la variable [page2] para mostrar la página n.º 2;
- línea 24: de lo contrario, se utiliza el campo [data.page2] proporcionado por el servidor;
- línea 22: nos aseguramos de memorizar la página n.º 2 para no volver a solicitarla posteriormente;
- líneas 27-28: en la página n.º 2, se muestran los dos datos [value1, value2] enviados por el servidor;
7.6.9. Volver a la página n.º 1
El enlace [Retour vers la page 1] de la página n.º 2 es el siguiente:
<a href="javascript:retourPage1()">Retour à la page 1</a>
El método JS [retourPage1] es el siguiente:
// volver a la página 1
function retourPage1() {
// se regenera la página 1
content.html(page1);
// se regeneran las entradas
$("#text1").val(value1);
$("#text2").val(value2);
}
- se trata de una acción JS sin interacción con el servidor, ya que la página n.º 1 se ha almacenado localmente en la variable [page1];
- línea 4: se regenera la página n.º 1;
- líneas 6-7: solo se había almacenado la parte HTML de la página n.º 1. No las entradas. Por lo tanto, hay que regenerarlas;
7.6.10. Conclusión
Aprovechando las posibilidades del modelo APU, hemos logrado simplificar el servidor web, que ahora es sin estado (sin sesión) y está menos solicitado:
- hemos eliminado la interacción con el servidor en la función JS ([retourPage1]);
- el servidor solo genera la página n.º 2 una vez;
7.7. Estructuración del código Javascript en capas
7.7.1. Introducción
El código Javascript de la aplicación anterior empieza a volverse complejo. Es hora de estructurarlo en capas. La aplicación seguirá siendo la misma que antes. No vamos a tocar el servidor, salvo para definir una nueva página de inicio. Vamos a remodelar el código JS.
La nueva arquitectura será la siguiente:
![]() |
7.7.2. La página de inicio
La acción que inicia la aplicación es la siguiente acción [/ajax-16]:
@RequestMapping(value = "/ajax-16", method = RequestMethod.GET, produces = "text/html; charset=UTF-8")
public String ajax16() {
return "vue-16";
}
Muestra la vista [vue-16.xml] siguiente:
<!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>
- líneas 9-10: el código JS se ha colocado en dos archivos diferentes:
- [local-ui] implementa la capa [présentation],
- [local-dao] implementa la capa [DAO];
![]() |
7.7.3. Implementación de la capa [DAO]
![]() |
7.7.4. Interfaz
La capa [DAO] en [local-dao.js] presentará la siguiente interfaz a la capa [présentation]:
| para actualizar la página 1 con el botón [Rafraîchir] |
| para mostrar la página 2 con el botón [Valider] |
El Javascript no tiene el concepto de interfaz. He utilizado este término simplemente para indicar que la capa [présentation] se comprometía a interactuar con la capa [DAO] únicamente a través de las dos funciones anteriores.
7.7.5. Implementación de la interfaz
El esqueleto de la implementación es el siguiente:
var session = {
"cpt1" : 0,
"cpt3" : 0
};
// actualizar página 1
function updatePage1(deferred, sendMeBack) {
...
}
// página 2
function getPage2(deferred, sendMeBack, value1, value2, pageRequired) {
...
}
El objetivo de la capa [DAO] es ocultar a la capa [présentation] los detalles de las solicitudes HTTP realizadas al servidor web. La sesión forma parte de esos detalles. Por lo tanto, ahora es gestionada por la capa [DAO].
7.7.5.1. La función [updatePage1]
La función [updatePage1] es la función llamada por la capa [présentation] para actualizar la página 1. Su código es el siguiente:
// actualizar página 1
function updatePage1(deferred, sendMeBack) {
// solicitud HTTP
executePost(deferred, sendMeBack, '/ajax-13', session);
}
- línea 1: la función [updatePage1] recibe dos parámetros:
- un objeto de tipo [jQuery.Deferred]. Este tipo de objeto almacena un estado que puede tener tres valores ['pending', 'resolved', 'rejected']. Cuando llega a la función [updatePage1], se encuentra en el estado [pending];
- un objeto JS que se debe devolver a la capa [présentation];
Todas las consultas HTTP se realizan mediante la siguiente función [executePost]:
// solicitud HTTP
function executePost(deferred, sendMeBack, url, post) {
// se realiza una llamada Ajax manualmente
$.ajax({
headers : {
'Accept: 'application/json',
'Content-Type' : 'application/json'
},
url : url,
type : 'POST',
data : JSON3.stringify(post),
dataType : 'json',
success : function(data) {
// se guarda la sesión
if (data.session) {
session = data.session;
}
// se muestra el resultado
deferred.resolve({
"status" : 1,
"data" : data,
"sendMeBack" : sendMeBack
});
},
error : function(jqXHR) {
// se muestra el error
deferred.resolve({
"status" : 2,
"data" : jqXHR.responseText,
"sendMeBack" : sendMeBack
});
}
});
}
- línea 1: la función [executePost] ejecuta una llamada Ajax de tipo POST. Espera cuatro parámetros:
- un objeto de tipo [jQuery.Deferred] en el estado [pending];
- un objeto JS que se devolverá en la capa [présentation];
- el URL del POST;
- el valor que se va a publicar como objeto JS;
- líneas 5-8: la función de contabilización del jSON (línea 7) y recibe del jSON (línea 6);
- línea 11: el valor que se va a enviar se transforma en jSON;
- líneas 13-24: la función ejecutada si la llamada Ajax tiene éxito;
- líneas 19-23: si el servidor ha devuelto una sesión, se almacena;
- líneas 13-18: pasan el objeto [deferred] al estado [resolved], pasando además un resultado con los siguientes campos:
- [status]: 1 en caso de éxito, 2 en caso de fallo;
- [data]: la respuesta jSON del servidor,
- [sendMeBack]: el segundo parámetro de la función, que es un objeto que el llamante desea recuperar;
- líneas 17-31: la función que se ejecuta en caso de fallo de la llamada Ajax. Se hace lo mismo que antes con dos diferencias:
- [status] pasa a 2 para indicar un error;
- [data] es, de nuevo, la respuesta jSON del servidor, pero obtenida de una forma diferente;
7.7.5.2. La función [getPage2]
La función [getPage2] es la siguiente:
// página 2
function getPage2(deferred, sendMeBack, value1, value2, pageRequired) {
// solicitud HTTP
executePost(deferred, sendMeBack, '/ajax-14', {
"value1" : value1,
"value2" : value2,
"pageRequired" : pageRequired,
});
}
- La función recibe los siguientes parámetros:
- [deferred]: un objeto de tipo [jQuery.Deferred] en el estado [pending],
- [sendMeBack]: un objeto JS que se debe devolver en la capa [présentation],
- [value1]: la primera entrada en la página 1,
- [value2]: la segunda entrada de la página 2,
- [pageRequired]: un valor booleano que indica al servidor si debe o no enviar el flujo HTML de la página n.º 2;
- se llama a la función [executePost] para ejecutar la consulta HTTP necesaria;
7.7.6. La capa [présentation]
![]() |
La capa [présentation] se implementa mediante el archivo [local-ui.js]. Este último retoma el código del archivo [local12.js] rediseñado para utilizar la capa [DAO] anterior. Solo cambian dos funciones: [postForm] y [valider].
7.7.6.1. La función [postForm]
La función [postForm] es la siguiente:
// actualización de la página 1
function postForm() {
// se actualiza la página 1
var deferred = $.Deferred();
loading.show();
updatePage1(deferred, {
'remitente: «postForm»,
'info: 10
});
// visualización de resultados
deferred.done(postFormDone);
}
- línea 4: se crea un objeto [jQuery.Deferred]. Por defecto, se encuentra en el estado [pending];
- línea 5: se muestra la imagen de espera
- líneas 6-9: se ejecuta la función [updatePage1]. Se pasa un objeto ficticio [sendMeBack], solo para mostrar para qué puede servir;
- línea 11: el parámetro de la función [deferred.done] es a su vez una función. Es la función que se debe ejecutar cuando el estado del objeto [deferred] pasa al estado [resolved]. Acabamos de ver que la función DAO [executePost] pasaba el estado de este objeto a [resolved] al recibir la respuesta del servidor. Esto significa que, cuando se ejecuta la función [postFormDone], ya se ha recibido la respuesta del servidor;
La función [postFormDone] es la siguiente:
function postFormDone(result) {
// fin de espera
loading.hide();
// se recuperan los datos
var data = result.data
// para demostración
console.log(JSON3.stringify(result.sendMeBack));
// se analiza el estado
switch (result.status) {
case 1:
// se actualizan las dos zonas
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:
// visualización de error
erreur.html(data);
break;
}
}
- línea 1: el parámetro [result] recibido es el parámetro pasado al método [deferred.resolve] en la función [executePost], por ejemplo:
// se muestra el resultado
deferred.resolve({
"status" : 1,
"data" : data,
"sendMeBack" : sendMeBack
});
- línea 5: se recupera la respuesta del servidor;
- líneas 10-24: tenemos el código que, en el version anterior, se encontraba en la función [onSuccess] de la función [postForm];
- líneas 25-28: tenemos el código que en la version anterior estaba en la función [onError] de la función [postForm];
7.7.6.2. La función del parámetro [sendMeBack]
¿Para qué sirve el parámetro [sendMeBack]? Veamos el código de llamada de la función [updatePage1]:
// actualización de la página 1
function postForm() {
// se actualiza la página 1
var deferred = $.Deferred();
loading.show();
updatePage1(deferred, {
'remitente: «postForm»,
'info: 10
});
// visualización de resultados
deferred.done(postFormDone);
}
y la firma de la función [validerDone]:
function postFormDone(result) {
}
¿Cómo puede la función [postForm] pasar información a la función [postFormDone]? Esta última solo tiene un parámetro: [result]. Este lo crea la función [executePost] de la capa [DAO]. Para transmitir información a la función [postFormDone], la función [postForm] debe transmitirla primero a la función [updatePage1]. Esta es la función del parámetro [sendMeBack]. Se utiliza de la siguiente manera:
function postFormDone(result) {
// fin de espera
loading.hide();
// se recuperan los datos
var data = result.data
// para demostración
console.log(JSON3.stringify(result.sendMeBack));
// se analiza el estado
switch (result.status) {
...
- línea 7, la función [postFormDone] ha recuperado el parámetro [sendMeBack] transmitido inicialmente a la función DAO [updatePage1] por la función [postForm];
7.7.7. La función [valider]
La función [valider] es la siguiente:
// validación de los valores introducidos
function valider() {
// se guarda la página 1
page1 = content.html();
// se guardan los valores introducidos
value1 = $("#text1").val().trim();
value2 = $("#text2").val().trim();
// sin errores
erreur.hide();
// se solicita la página 2
var deferred = $.Deferred();
loading.show();
getPage2(deferred, {
'«sender»: «validar»,
'info: 20
}, value1, value2, page2 ? false : true);
// visualización de resultados
deferred.done(validerDone);
}
y la función [validerDone] (línea 18) es la siguiente:
function validerDone(result) {
// fin de la espera
loading.hide();
// se recuperan los datos
var data = result.data
// para demostración
console.log(JSON3.stringify(result.sendMeBack));
// se analiza el estado
switch (result.status) {
case 1:
// ¿error?
if (data.erreur) {
// visualización de error
erreur.html(data.erreur);
erreur.show();
} else {
// sin error
erreur.hide();
// página 2
if (page2) {
// se utiliza la página en caché
content.html(page2);
} else {
// se guarda la página 2
page2 = data.page2;
// se muestra
content.html(data.page2);
}
// se actualiza con la información del servidor
$("#value1").text(data.value1);
$("#value2").text(data.value2);
}
break;
case 2:
// se muestra un error
erreur.html(data);
erreur.show();
break;
}
}
- línea 5: se recupera la respuesta del servidor;
- líneas 10-32: tenemos el código que en la anterior version estaba en la función [onSuccess] de la función [valider];
- líneas 34-38: tenemos el código que en la version anterior estaba en la función [onError] de la función [valider];
7.7.8. Pruebas
La aplicación sigue funcionando como antes y en la consola de Chrome se pueden ver los parámetros [sendMeBack] de las funciones [postForm] y [valider]:
![]() |
7.8. Conclusión
Volvamos al esquema general de una aplicación Spring MVC:
![]() |
Gracias al Javascript integrado en las páginas HTML y ejecutado en el navegador, y gracias al modelo APU, se puede trasladar código al navegador y obtener la siguiente arquitectura:
![]() |
- tenemos una arquitectura cliente [2] / servidor [1] en la que el cliente y el servidor se comunican en jSON;
- en [1], la capa web Spring MVC entrega vistas, fragmentos de vista y datos en jSON;
- en [2]: el código Javascript integrado en la vista cargada al inicio de la aplicación puede estructurarse en capas:
- la capa [présentation] se encarga de las interacciones con el usuario,
- la capa [DAO] se encarga del acceso a los datos a través del servidor web [1],
- la capa [métier] puede no existir o asumir algunas de las funcionalidades no confidenciales de la capa [métier] del servidor para aliviar la carga de este;
- el cliente [2] puede almacenar en caché determinadas vistas para, una vez más, aliviar la carga del servidor. Gestiona la sesión;













































































