logoCoderhouse.png
By Nicolas Alliaume • febrero 26, 2016

Introducción a Ajax: mejorando tu web trivia

Hola! Soy Nico Alliaume, profesor de Coderhouse en Uruguay. En esta oportunidad te voy a comentar sobre Ajax, y explicarte por qué es tan importante éste concepto, además de estar super copado y divertido.

Voy a retomar la web que desarrollamos en mi post previo, que podés ver aquí, para poner en práctica lo que aprenderás en esta entrada.

Primero lo primero. ¿Qué es Ajax?

Ajax es una sigla para Asynchronous Javascript and XML. ¿Te dice algo?

En esencia, y citando a los amigos de W3School, ajax "es sobre actualizar partes de una página sin recargarla por completo". Es un concepto muy simple, pero algunas veces confuso. Asi que, para no entrar en confusiones, vayamos a las básicas de una página web.

Back to the basics

Cuando escribimos una página web, escribimos muchos (o uno sólo, pero espero que no!) archivos con HTML, CSS, Javascript, y quizás algunas otras cosas. Estos archivos los colocamos en un servidor web, y allí quedan disponibles para que nuestros usuarios y nosotros mismos podamos acceder.

Ahora supongamos que una persona quiere ver nuestro sitio. Abre su navegador preferido (el browser), y escribe "http://misitioweb.com/home.html". Nuestro usuario quiere entrar a home.html.

A través de una lógica de rutas, el navegador llegará a tu sitio web. Esta no es la parte que nos interesa en este momento. Pero lo que sí nos importa es qué pasa cuando el navegador encuentra tu HTML (en este caso, home.html).

Cuando el navegador recibe el contenido de la página HTML, está recibiendo, en esencia, texto con un formato específico (en este caso HTML). Pero el navegador entiende HTML, y sabe que es una página web, que probablemente tenga mucho contenido y otros tags que hacer referencia a otros archivos (como hojas de estilo y archivos Javascript). Irá a buscar estos archivos y uno a uno llegarán al navegador (por ahora mágicamente) e serán procesados por el browser. Si es un CSS, se lo aplicará a la página para que el sitio se vea bien. Si es un Javascript, ejecutará el código que tenga dentro.

Vamos a darle nombre a algunas de estas cosas.

Conceptos importantes

Cuando el navegador va a buscar uno de los archivos de tu sitio (el HTML, un archivo CSS, un archivo Javascript) diremos que está haciendo una Request. Una request, en castellano simple, es el proceso de ir a un servidor a buscar algo. Si el castellano simple no es lo tuyo, podés mirar ésta descripción bieeeeen técnica.

El resultado de una request puede ser, en esencia, uno de dos: salió bien o salió mal. Dentro de salió bien hay varias opciones, pero aún más hay dentro de salió mal. Por ejemplo, puede haber un error en el server, puede no existir el archivo, puede requerir permisos especiales, entre otros. Por ahora nos limitaremos a decir salió bien o salió mal.

De vuelta a Ajax

Volviendo a lo que nos compete en este post, y ahora que vimos otros conceptos nuevos, Ajax es ejecutar requests sin recargar la página. Cuando alguien visita home.html de tu sitio, el navegador traerá todos los archivos relacionados a esa página, y listo. Una vez que se muestra, no hay más interacción con el server hasta que hacen clic en un link o algo similar, y se mueven a otra página. ¿Y para qué queremos ajax si la página ya está cargada?

¿Por qué Ajax?

Ajax es muy útil cuando nuestra página tiene lógica que debe interactuar con el servidor, pero no queremos sacar de contexto al usuario (y evitar volverlo loco). ¿Te imaginás si en Gmail cada vez que tocas un correo en tu bandeja se recargara toda la página (se pone en blanco y vuelve a aparecer las cosas como la primera vez que entraste)? ¿O si cada vez que ponés un like en Facebook se recargara todo? ¿O si tuvieran que agregarle un paginador al final (Siguiente, Anterior) para ver más cosas del Feed? ¿Te imaginás Google Docs con un botón de guardar, que si no lo apretas perdés todo lo que venías escribiendo? Muchas de las cosas que damos por sentado en la web necesitan de Ajax para funcionar tan bien como lo hacen.

Si, pensamos lo mismo. Ajax hace la vida mejor.

La "A" de Ajax

Quizás lo más importante de la sigla Ajax es la "A" y su significado.

Asíncrono quiere decir que no tiene una sucesión inmediata; que una cosa no ocurre inmediatamente luego de otra. Cuando hacemos una llamada Ajax, la respuesta de la llamada no será inmediata. Esto tiene consecuencias muy importantes, que pueden producir mucha confusión y bugs que no logramos entender al principio.

Algo tan simple como:

1 var resultado = llamadaAjax();
2 alert(resultado);

no funcionará. ¿Por qué? Porque una llamada ajax nunca retorna un resultado inmediatamente. El programa continuará su ejecución linea tras linea, mientras por detrás se está ejecutando la llamada Ajax al servidor. En algún momento del tiempo ese llamada terminará, y tendremos un resultado que podemos usar. Pero esto no será en la siguiente línea de código ejecutable. Es asíncrono. Lo veremos nuevamente con un ejemplo más adelante.

La revelación

He aquí una revelación que a veces toma años llegar a ella. ¿Preparado/a? Ajax no es magia. Lo único que hacemos es enviar un request al server, como si fuéramos a cargar una página desde cero, pero sin hacerlo. Digamos que lo hacemos "a las espaldas del navegador".

Y ahora, sin más preámbulos... Ajax. Y aún mejor; con jQuery.

Manos en el Ajax

jQuery tiene varias funciones para hacer Ajax, pero nos centraremos en la principal y más genérica de ellas. Se llama… bueno, ajax. Y se utiliza así:

$.ajax ( config );

Veamos un ejemplo básico.
home.html

<html>
<head>
<title>Prueba ajax</title>
<script type="text/javascript" src="https://code.jquery.com/jquery-2.2.1.min.js"></script>
<script>

// 1
$(document).ready(function() {

// 2
$.ajax({
url : 'lorem.html',
success : onAjaxSuccess,
error : onAjaxError
});

});

// 3
function onAjaxError(jqXHR, textStatus, errorThrown) {
alert('Ha ocurrido un error: ' + textStatus);
}

// 4
function onAjaxSuccess(data, textStatus, jqXHR) {
$('#content').html(data);
}

</script>
</head>
<body>
<div>Probando Ajax</div>
<div id="content"></div>
</body>
</html>

lorem.html

<h1>Funciona!</h1>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur auctor venenatis ipsum eu placerat. Quisque eu ex ac elit fermentum porttitor. Phasellus sagittis enim non sodales malesuada. Vivamus laoreet maximus lobortis. Cras tincidunt, sapien eget scelerisque rutrum, tortor lectus maximus purus, in rhoncus urna elit non metus. Donec aliquet mollis sapien, eget convallis leo bibendum vitae. Vestibulum posuere est placerat gravida commodo. Proin ac sapien scelerisque, varius augue feugiat, egestas purus.

[Es importante que ambos archivos se encuentren en la misma carpeta]

Si abres home.php con tu navegador, deberías ver lo siguiente:

Captura de Ajax funcionando

¿¡Pero cómo!? Mi página solo decía "Probando Ajax"!

Ajax ha funcionado, y ha hecho de las suyas. Veamos el código Javascript dentro de home.html.

//1: Le decimos a jQuery que queremos ejecutar el código dentro de la función cuando la página termine de cargarse.

//2: Ejecutamos la llamada ajax. Esto crea un request a nuestro servidor (en este caso nuestra computadora) y busque el archivo lorem.html. Cuando la request termine, si salió bien, queremos que jQuery llame a la función onAjaxSuccess, y si salió mal, a onAjaxError. Nótese que no usé paréntesis para onAjaxSuccess y onAjaxError. Eso es porque estamos pasando una función como parámetro. jQuery se encargará de llamar a la función y pasarle algunos parámetros, que están definidos en la documentación.

//3: La función que se llamará si la request salió mal. Muestra una alerta con el error. Las alertas son horribles!, pero bueno, es un ejemplo básico.

//4: La función que se llamará si la request sale bien. Agrega el contenido retornado a la página que estamos viendo, dentro del elemento con id content.

//3 y //4 son funciones que se ejecutan cuando la llamada Ajax ha terminado. Volviendo al concepto de asincronicidad, son funciones que llamaremos callbacks, porque implica una llamada de vuelta a algo que estábamos haciendo y queremos retomar luego de que una tarea termina de hacerse. En este caso, luego que la llamada ajax termina. Las funciones //3 y //4 no son llamadas desde nuestro código. Son llamadas por jQuery cuando la request termina.

Como este es un ejemplo que probablemente lo pruebes en tu propia computadora, funcionará muy rápido y no verás que hay un tiempo entre que se carga home.html y que aparece el contenido de lorem.html en la página. Así que cambiaremos un poquito el código para que puedas ver la llamada Ajax en vivo y directo.
Reemplazá lo que pusimos en //2 por esto:

// 2
setTimeout(function() {
$.ajax({
url : 'lorem.html',
success : onAjaxSuccess,
error : onAjaxError
});
}, 1000);

Ahora verás que demora un segundo en aparecer el contenido. Eso es porque agregamos un timer de un segundo entre que carga la primer página y vamos al server a buscar nuevo contenido vía Ajax.

Inspeccionando las requests

Algo super útil cuando estamos desarrollando con Ajax es ver las requests que nuestro navegador hace, y los resultados.
Si estás en Chrome, dale clic derecho en cualquier lugar de la página, y luego Inspeccionar elemento. Esto abrirá la consola de desarrolladores. Una de las pestañas se llama Red, y ahí verás todas las requests que se hacen. Una de ellas será a lorem.html. Esa es la llamada Ajax.

Llamadas al server

Hacé clic sobre lorem.html y podrás ver los detalles de la request, y lo que retorna.

Headers de la llamada Ajax

Respuesta de la llamada Ajax

Mejorando la web trivia

Retomando la web trivia que desarrollamos aquí, vamos a agregarle capacidades de Ajax para hacer la un poco más pro.
Lo primero que personalmente me gustaría evitar es que los jugadores puedan inspeccionar la página y ver las respuestas. Eso no está muy bueno.

Inspección del quizzer sin Ajax

Otra cosa que podemos mejorar es que las preguntas vayan variando en contenido y orden, incluso en el orden de las respuestas. Para esto usaremos nuestros recientes conocimientos de ajax.

Los cambios que haremos

Utilizaremos Ajax para que cuando la página necesite obtener una pregunta para mostrar, le pidamos a nuestro server una tarjeta de pregunta, que viene como un fragmento de HTML sin la respuesta (para no poder verla inspeccionando el código), y luego validaremos la respuesta del usuario también en el servidor. Esto quiere decir que haremos 2 llamadas ajax, que llamaremos getQuestion y checkAnswer.

¡Código, please!

Vayamos directo al código, y luego vemos en detalle los cambios.

HTML

 

<!DOCTYPE html>
<html>
<head>
<title>Quizzer</title>
<meta charset="utf-8">
<link href="quizzer.css" type="text/css" rel="stylesheet">
<script src="jquery-2.2.0.min.js"></script>
<script src="quizzer.js"></script>
</head>
<body>
<header>
<div class="title">
<h1>Quizzer Ajax</h1>
<h2>¿Sabes tanto como creías?</h2>
</div>
<div class="countdown"><span class="time_left">12s</span></div>
<div class="points"><span class="points">0 points</span></div>
</header>
<div class="start">
<div class="card">
<p>¿Listo para comenzar?</p>
<button class="start">Comenzar</button>
</div>
</div>
<div class="play">
<div class="questions"></div>
</div>
<div class="finish card">
<p class="times_up">¡Se acabó el tiempo!</p>
<p>Has obtenido</p>
<p class="final_points">8 puntos</p>
<div class="play_again">
<button>Volver a jugar</button>
</div>
</div>
</body>
</html>

CSS

body {
background-color: #7CBB7A;
margin: 0;
font-family: 'Open sans', sans-serif;
}

* {
box-sizing: border-box;
}

button {
border: none;
border-radius: 6px;
padding: 10px 20px;
font-size: 20px;
color: #fff;
background-color: #ccc;
}

header .title {
background-color: #767D76;
color: #fff;
text-align: center;
padding: 15px;
}

header h1, header h2 {
margin: 0;
}

header div.countdown {
float: right;
}

header div.points {
float: left;
-webkit-transition: color 0.5s;
transition: color 0.5s;
}

header div.points.animate.right {
color: #767D76;
}

header div.points.animate.wrong {
color: #bf0000;
}

header div.countdown, header div.points {
font-weight: bold;
margin: 10px;
color: #fff;
padding: 0px;
font-size: 26px;
}

.card {
background-color: red;
width: 500px;
background-color: #fff;
margin: 100px auto 30px auto;
border-radius: 6px;
text-align: center;
padding: 20px;
}

div.start .card {
display: block;
}

div.start .card p, .question.card span.question {
font-size: 25px;
}

/*.card.question {
display: none;
}*/

.card.question.active {
display: block;
}

.card.question .options {
padding: 0;
list-style: none;
margin-top: 35px;
}

.card.question .options > li {
margin: 10px 80px;
}

.card.question .options > li label {
background-color: #7ED2CF;
display: block;
padding: 10px;
border-radius: 20px;
}

.card.question .options > li label:hover {
background-color: #8EE8E5;
}

.card.question .options > li input:checked + label {
background-color: #4D8E8C !important;
color: #fff;
}

.card.question .options > li input {
opacity: 0;
display: none;
}

.finish {
display: none;
}

.card.finish p {
font-size: 22px;
}

.card.finish .final_points {
font-size: 40px;
font-weight: bolder;
}

.play_again,
button.start {
width: 200px;
margin: 0 auto 20px auto;
color: #fff;
}

Javascript

var points,
pointsPerQuestion,
currentQuestion,
questionTimer,
timeForQuestion = 8, // seconds
timeLeftForQuestion,
questions = 5; // 1

$(function() {

$('button.start').click(start);
$('.play_again button').click(restart);

function restart() {
points = 0;
pointsPerQuestion = 10;
currentQuestion = 0;
timeLeftForQuestion = timeForQuestion;

$('.finish.card').hide();
$('div.start').show();
$('.times_up').hide();
$('.questions').html('');

updateTime();
updatePoints();
}

function start() {
$('div.start').fadeOut(200, function() {
moveToNextQuestion();
});
}

// 2
function moveToNextQuestion() {
currentQuestion += 1;
getQuestion();
}

// 3
function getQuestion() {
$.ajax({
url : 'backend.php',
data : { // 4
action : 'get_question',
number : currentQuestion
},
success : function(data) { // 5
showQuestionCard(data);
setupQuestionTimer();
},
error : function(jqXHR, textStatus, errorThrown) {
alert(textStatus);
}
})
}

// 6
function showQuestionCard(html) {
$('.questions').html(html);
$('.question.card input').change(optionSelected);
}

function setupQuestionTimer() {
if (currentQuestion > 1) {
clearTimeout(questionTimer);
}
timeLeftForQuestion = timeForQuestion;
questionTimer = setTimeout(countdownTick, 1000);
}

function countdownTick() {
timeLeftForQuestion -= 1;
updateTime();
if (timeLeftForQuestion == 0) {
return finish();
}
questionTimer = setTimeout(countdownTick, 1000);
}

function updateTime() {
$('.countdown .time_left').html(timeLeftForQuestion + 's');
}

function updatePoints() {
$('.points span.points').html(points + ' puntos');
}

// 7
function optionSelected() {
var selected = parseInt(this.value);
checkAnswer(selected);
}

// 8
function checkAnswer(selected) {
$.ajax({
url : 'backend.php',
data : {
action : 'check_answer',
number : currentQuestion,
answer : selected
},
success : function(data) {
// 9
if (data) {
points += pointsPerQuestion;
updatePoints();
correctAnimation();
} else {
wrongAnimation();
}
if (currentQuestion == questions.length) {
clearTimeout(questionTimer);
return finish();
}
moveToNextQuestion();
},
error : function(jqXHR, textStatus, errorThrown) {
alert(textStatus);
}
})
}

function correctAnimation() {
animatePoints('right');
}

function wrongAnimation() {
animatePoints('wrong');
}

function animatePoints(cls) {
$('header .points').addClass('animate ' + cls);
setTimeout(function() {
$('header .points').removeClass('animate ' + cls);
}, 500);
}

function finish() {
if (timeLeftForQuestion == 0) {
$('.times_up').show();
}
$('p.final_points').html(points + ' puntos');
$('.question.card:visible').hide();
$('.finish.card').show();
}

restart();

});

Detalle de los cambios

No hace falta mirar muy en detenimiento para ver que el código es muy similar a la web trivia anterior. ¡Eso es bueno! Con algunos pocos cambios mejoramos mucho nuestro código.

CSS

En el CSS sólo he hecho un cambio. Verás que las líneas 76, 77 y 78 fueron comentadas. Esto es porque no tenemos que ocultar ninguna tarjeta de pregunta; simplemente las tarjetas se crean y muestran cuando hacemos nuestra llamada ajax.

Javascript

El comienzo es muy parecido, excepto que no existen preguntas en un array. Luego hay algunos cambios que veremos en detalle. Allá vamos.

//1: Agregamos una variable para guardar la cantidad de preguntas que haremos en nuestro trivia

//2: Al movernos a la siguiente pregunta, necesitamos hacer una llamada ajax a nuestro server para obtener la tarjeta de pregunta.

//3: getQuestion() es la función que hace la llamada ajax, y obtiene una nueva pregunta.

//4: Vemos un parámetro nuevo en la llamada ajax llamado data. En este parámetro podemos enviar parámetros a nuestro server, como cuando llamamos a una función. En este caso enviamos 2 parámetros: uno llamado action y otro llamado number. El valor de number será el número de pregunta en que estamos. El server sabrá que hacer con esos parámetros.

//5: Definimos las funciones para success y error de la llamada ajax inline. En vez de crear funciones aparte como hicimos anteriormente en el ejemplo básico de ajax, aquí las creamos ahí mismo. Si la llamada sale bien, mostraremos la pregunta y seteamos el timer.

//6: Mostrar una pregunta es mucho más sencillo que antes. Simplemente agregamos al DOM el HTML devuelvo por la request ajax, y agregamos los eventos de seleccionar una opción de respuesta igual que antes.

//7: Cuando seleccionamos una opción, debemos hacer la otra llamada ajax para validar la respuesta y agregar los puntos obtenidos.

//8: checkAnswer verifica la respuesta en el server. Ejecutamos una llamada ajax, y pasamos algunos parámetros que el server sabrá manejar. Uno de ellos es la respuesta seleccionada por el usuario.

//9: Si la llamada ajax resultó satisfactoria, verificamos si la respuesta del usuario fue correcta o no. El server responderá con el string vacío si la respuesta es incorrecta, o con un "1" si es correcta. No es la mejor elección en respuestas desde el server, pero es sencillo y suficiente para nuestro ejemplo.

¡Y listo! El resto del código es exactamente igual al trivia anterior. ¿Fácil, no?

El server

El server lo escribí en PHP. A pesar de que escapa de este post, te dejo aquí el código por si te interesa :)

Para poder ejecutarlo es necesario tener un servidor web corriendo en tu computadora (como el WAMP por ejemplo).

<?php

$questions = array(
array(
"Q" => "¿Que se utiliza para estilizar un sitio web?",
"A1" => "Javascript",
"A2" => "CSS",
"A3" => "PHP",
"A4" => "AngularJS",
"A" => 1
),
array(
"Q" => "¿Qué tipo de lenguaje es PHP?",
"A1" => "Interpretado",
"A2" => "Compilado",
"A3" => "Los 2 anteriores",
"A4" => "Ninguno de los anteriores",
"A" => 0
),
array(
"Q" => "¿jQuery es una biblioteca para qué lenguaje?",
"A1" => "Python",
"A2" => "PHP",
"A3" => "Java",
"A4" => "Ninguno de los anteriores",
"A" => 3
),
array(
"Q" => "¿Cómo se marca el inicio de código PHP?",
"A1" => "&lt;?php",
"A2" => "&lt;?",
"A3" => "Los 2 anteriores",
"A4" => "Ninguno de los anteriores",
"A" => 2
),
array(
"Q" => "¿Quién diseño Javascript?",
"A1" => "Mark Zuckerberg",
"A2" => "Bill Gates",
"A3" => "Brendan Eich",
"A4" => "Rasmus Lerdorf",
"A" => 2
)
);

function error($msg) {
http_response_code (501);
die ('Error: no action');
}

function get_question($number) {
global $questions;
$question = $questions[$number];
extract($question);
printf ("
<div class='card question'><span class='question'>%s</span>
<ul class='options'>
<li>
<input type='radio' value='0' id='do1'>
<label for='do1'>%s</label>
</li>
<li>
<input type='radio' value='1' id='do2'>
<label for='do2'>%s</label>
</li>
<li>
<input type='radio' value='2' id='do3'>
<label for='do3'>%s</label>
</li>
<li>
<input type='radio' value='3' id='do4'>
<label for='do4'>%s</label>
</li>
</ul>
</div>", $Q, $A1, $A2, $A3, $A4);
die ();
}

function check_answer($number, $answer) {
global $questions;
$question = $questions[$number];
$result = $question["A"] == (int)$answer;
die ($result);
}

if ($_GET['action'] == 'get_question') {
if (!isset($_GET['number']))
return error('Arguments missing');

$number = ((int) $_GET['number']) - 1;
get_question($number);
}

if ($_GET['action'] == 'check_answer') {
if (!isset($_GET['number']) || !isset($_GET['answer']))
return error('Arguments missing');

$number = ((int) $_GET['number']) - 1;
$answer = $_GET['answer'];
check_answer($number, $answer);
}

?>

Código completo

Podés descargar el código completo desde aquí.

Comentarios finales

Estos cambios han mejorado mucho nuestra trivia, y dan lugar a posibilidades más divertidas como retornar distintas preguntas cada vez que jugamos o mezclar el orden de las respuestas.

Ajax es un concepto muy importante y muy usado, por lo que es bueno que lo tengas en tu cinturón de herramientas.

¡Me encantaría escuchar qué cosas desarrollas luego de este tutorial! ¡Mucha suerte!