Skip to content

S7: Peticiones AJAX

Juan Gonzalez-Gomez edited this page Mar 21, 2022 · 6 revisions

Sesión 7: Peticiones AJAX

  • Tiempo: 2h (50 + 50min)
  • Objetivos de la sesión:
    • Aprender a hacer peticiones ligeras con AJAX usando JSON

Contenido

Introducción

Para lograr que las páginas web sean más interactivas se pueden añadir programas en javascript que intercambian pequeños trozos de datos con el servidor, en segundo plano, mientras el usuario navega normalmente por la página. Un ejemplo de esto son las sugerencias que nos muestran los buscadores mientras escribimos nuestro elemento a buscar

En este ejemplo se está buscando a Chuck Norris en Google. Antes de terminar de escribirlo, el navegador nos ofrece sugerencias de posibles búsquedas. ¿De dónde han salido si todavía no le hemos dado al botón de buscar? El javascript que se ha ejecutado al entrar en la página del buscador es el que realiza estas peticiones al servidor, según vamos escribiendo

Peticiones AJAX

Estas peticiones reciben el nombre de peticiones AJAX (AJAX = Asynchronous JavaScript And XML). Son peticiones de datos al servidor, que originalmente se hacían en XML, pero que actualmente se implementan mayormente en JSON

Así, el servidor web deberá atender a dos tipos de peticiones:

  • Las peticiones del usuario, que serán páginas html, imágenes, etc.
  • Las peticiones de la aplicación: serán datos (JSON)

El protocolo usado en ambos casos es HTTP

Los datos se devuelven con estructura, y se usan formatos como XML o JSON. Originalmente AJAX estaba diseñado para trabajar con XML, sin embargo, actualmente se maneja con JSON. Es lo que haremos nosotros

La única diferencia entre las peticiones de usuario y de datos es que en las segundas se devuelve un fichero JSON, mientras que en las primeras será un HTML (que a su vez generarán peticiones de CSS, imágenes o javascript)

Para implementar páginas con AJAX hay que trabajar en ambos lados:

  • Servidor: Hay que implementar puntos de entrada al servidor que provean de los servicios necesarios
  • Cliente: Hay que implementar un cliente js que se ejecute en el navegador y que realice las peticiones a estos servicios

Servidor de datos JSON

En el lado del servidor debe haber unos recursos que permitan acceder a la lectura de los datos que requiera la aplicación. Para hacerlo más práctico vamos a trabajar con un ejemplo concreto: La lectura de los productos disponibles en mi tienda

El servidor de datos debe devolver en el cuerpo los datos en formato JSON, y asegurarse que la cabecera Content-Type es del tipo application/json

Ejemplo 1: Servidor JSON de productos

Este servidor tiene el punto de acceso de datos /productos para devolver un Array con los nombres de los productos disponibles en la tienda. Cualquier otro punto de acceso hace que se devuelva una página principal html, para humanos

El fichero con lo datos es muy básico, para que el ejemplo sea sencillo. Lo nombramos Ej-01.json:

[
    "FPGA-1", "RISC-V", "74LS00",
    "FPGA-2", "74ls01", "AVR",
    "Arduino-UNO"
]

Es un Array con los nombres de 7 productos de la tienda. En la aplicación real de la tiendas estos nombres estarán en la base de datos, que se leerá y se procesará para obtener el array con los nombres de los productos

El fichero HTML es Ej-01.html, y sólo está formado por un encabezado y dos párrafos. Contiene un enlace al recurso de datos (/productos)

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Ejemplo 1</title>
</head>
<body>
    <h2>Servidor JSON</h2>
    <p>Prueba de acceso a fichero JSON</p>
    <p>Accede al recurso <a href="/productos">/productos</a> para recibir 
    un objeto JSON con información sobre todos los productos disponibles
    en la tienda </p>
</body>
</html>

Este es el javascript del servidor

  • Fichero: Ej-01.js
//-- Servidor JSON

const http = require('http');
const fs = require('fs');
const PUERTO = 8080;

//-- Cargar pagina web principal
const MAIN = fs.readFileSync('Ej-01.html','utf-8');

//-- Leer fichero JSON con los productos
const PRODUCTOS_JSON = fs.readFileSync('Ej-01.json');

//-- SERVIDOR: Bucle principal de atención a clientes
const server = http.createServer((req, res) => {

    //-- Construir el objeto url con la url de la solicitud
    const myURL = new URL(req.url, 'http://' + req.headers['host']);  
  
    //-- Por defecto entregar página web principal
    let content_type = "text/html";
    let content = MAIN;
  
    if (myURL.pathname == '/productos') {
        content_type = "application/json";
        content = PRODUCTOS_JSON;
    }
  
    //-- Generar respuesta
    res.setHeader('Content-Type', content_type);
    res.write(content);
    res.end()
  
  });
  
  server.listen(PUERTO);
  console.log("Escuchando en puerto: " + PUERTO);

Lo probamos primero desde el navegador, conectándonos a su página principal localhost:8080

Ahora pinchamos en el enlace y vemos los datos JSON:

¡Ya tenemos la parte del servidor lista! Desde cualquier cliente ya tenemos acceso a los productos de la tienda

También lo podemos probar desde curl:

Ejemplo 2: Servidor JSON con carga de ficheros JS

En este siguiente ejemplos vamos a asegurarnos que el servidor es capaz de entregar el cliente javascript pedido: cliente-1.js y que se ejecuta correctamente en el cliente

Este es el código del servidor:

//-- Servidor JSON

const http = require('http');
const fs = require('fs');
const PUERTO = 8080;

//-- Cargar pagina web principal
const MAIN = fs.readFileSync('Ej-02.html','utf-8');

//-- Leer fichero JSON con los productos
const PRODUCTOS_JSON = fs.readFileSync('Ej-01.json');

//-- SERVIDOR: Bucle principal de atención a clientes
const server = http.createServer((req, res) => {

    //-- Construir el objeto url con la url de la solicitud
    const myURL = new URL(req.url, 'http://' + req.headers['host']);  
  
    //-- Por defecto entregar página web principal
    let content_type = "text/html";
    let content = MAIN;
  
    //-- Leer recurso y eliminar la / inicial
    let recurso = myURL.pathname;
    recurso = recurso.slice(1); 

    switch (recurso) {
        case 'productos':
            content_type = "application/json";
            content = PRODUCTOS_JSON;
            break;

        case 'cliente-1.js':
            //-- Leer fichero javascript
            console.log("recurso: " + recurso);
            fs.readFile(recurso, 'utf-8', (err,data) => {
                if (err) {
                    console.log("Error: " + err)
                    return;
                } else {
                  res.setHeader('Content-Type', 'application/javascript');
                  res.write(data);
                  res.end();
                }
            });
            
            return;
            break;
    }
  
    //-- Generar respuesta
    res.setHeader('Content-Type', content_type);
    res.write(content);
    res.end()
  
  });
  
  server.listen(PUERTO);
  console.log("Escuchando en puerto: " + PUERTO);

En el fichero html Ej-02.html añadimos un botón para usarlo desde Javascript, y que se añada el texto "Hola desde JS!" en el párrafo identificado como display. Se carga el fichero cliente-1.js

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <script src="cliente-1.js" defer></script>
    <title>Ejemplo 2</title>
</head>
<body>
    <h2>Servidor JSON</h2>
    <p>Prueba de acceso a fichero JSON</p>
    <p>Accede al recurso <a href="/productos">/productos</a> para recibir 
    un objeto JSON con información sobre todos los productos disponibles
    en la tienda </p>
    <p>Botón de prueba: <button id="boton_test">Test JS</button></p>
    <h2>Mensaje desde Javascript:</h2>
    <p id="display"></p>
</body>
</html>

Este es el fichero cliente-1.js que se ejecuta en el navegador cliente:

console.log("Ejecutando Javascript...");

const display = document.getElementById("display");
const boton_test = document.getElementById("boton_test");

boton_test.onclick = ()=> {
    display.innerHTML+="<p>Hola desde JS!</p>";
}

En esta animación se muestra su funcionamiento:

Repasemos todo lo que ha ocurrido cuando desde el navegador accedemos a nuestro servidor:

  1. En el servidor se ejecuta el fichero Ej-2.js
  2. El navegador hace una petición GET para obtener la página principal: Ej-2.html
  3. El navegador hace otra petición get para obtener el fichero javascript cliente-1.js
  4. El navegador ejecuta el fichero Javascript. Si el usuario pulsa el botón, el Javascript toma el control e imprime en el párrafo el mensaje "Hola desde JS!"
  5. Si el usuario pulsa en /productos se hace una petición GET y se obtiene el fichero JSON con la lista de productos

Cliente AJAX

Nuestros programas javascript que se ejecutan en el navegador del cliente realizan peticiones de datos al servidor utilizando la API XMLHttpRequest

XMLHttpRequest

Esta API está disponible en los navegadores a través del método XMLHttpRequest del objeto window. Alguno de los métodos de interés son:

  • Métodos:
    • open(método, url, asíncrono): Configurar la petición
    • send(): Enviar la petición
  • Propiedades:
    • onreadystatechange: Evento que se emite cuando hay algún cambio en el estado de la petición, una vez lanzada
    • readyState: Estado de la petición:
      • 0: Sin inicializar. Objeto creado pero no inicializado (no se ha llamado a open())
      • 1: Loading: Objeto creado e inicializado, pero no se ha enviado la petición todavía (no se ha llamado a send())
      • 2: Loaded: Se ha llamado al método send, pero todavía no están disponibles los datos
      • 3: Interactive: Se ha recibido parte de los datos, pero no todos todavía
      • 4: Completed: Todos los datos recibidos. La información está disponible en la propiedad ResponseText
    • responseText: El cuerpo del mensaje HTTP con la respuesta
    • status: Código de respuesta del mensaje HTTP
    • statusText: Mensaje textual asociado al código de respuesta del mensaje HTTP

Pasos para realizar la petición AJAX

  1. Crear un objeto XMLHttpRequest
const m = new XMLHttpRequest();
  1. Establecer la función de retrollamada asociada al evento de cambio de estado. Tendrá esta forma:
m.onreadystatechange = () => {

  //-- Hay un cambio de estado en la peticion
  //-- Típicamente sólo estremos interesados en lo que ha ocurrido
  //-- cuando se complete
  if (m.readyState==4) {
    //-- Peticion completada
    //...procesar la respuesta
  }
}
  1. Enviar la petición al servidor. Se hace en dos paso. Primero llamando a open() y luego a send()
//-- Configurar la petición
m.open("GET", "/recurso", true);

//-- Enviar la peticion
m.send();

El último parámetro de open() indica si la petición es asíncrona (true) o síncrona (false). Típicamente se usará la asíncrona. Si se usa la síncrona el navegador se queda bloqueado hasta que se reciba la respuesta del servidor

  1. Procesar la respuesta, si es correcta. Dentro de la función de retrollamada se hace el procesamiento. Antes de analizar la respuesta hay que verificar si el servidor ha respondido con un OK:
//-- Funcion de retrollamada de la peticion
m.onreadystatechange = () => {

  //-- Peticion completa
  if (m.readyState==4) {

    //-- El mensaje recibido es ok
    if (m.status == 200) {
      //-- Procesar la respuesta
      //-- que se encuentra en m.responseText
      //-- ....
     }
  }
}

Ejemplo 3: Mi primera petición AJAX

El objetivo ahora es conseguir recibir ese array de productos en un programa javascript que se ejecuta en el navegador. Esto lo logramos mediante nuestra primera petición AJAX. El array que está en el servidor, queremos que se transfiera a un array en el cliente.

El javascript cliente se llama desde la página HTML principal, y nos lo envía el servidor. Por ello, lo primero es tener un servidor capaz de atender la petición de 3 objetos al menos: la página principal HTML (al acceder al recurso /), el fichero javascript (que lo pide automáticamente el navegador al leer el HTML) y finalmente los datos JSON (recurso /productos), que los pide la aplicación cliente

El servidor: Ej-03.js

Este es el servidor de prueba. Es muy básico y limitado, sólo lo usaremos para hacer esta prueba y comprobar las peticiones AJAX. Sólo tiene implementado el acceso a estos tres recursos:

  • recurso /: Página principal. Se lee inicialmente del fichero Ej-03.html
  • /cliente-2.js: Programa cliente en javascript que se ejecuta en el navegador. Es el que implementa la petición AJAX al servidor
  • /productos : devuelve un fichero JSON con el listado de todos los productos de mi base de datos (Se lee del fichero Ej-01.json)

Si se le solicita un recurso que no es ninguno de los anteriores, devuelve una página de error, que se carga inicialmente desde el fichero error_page.html

  • Fichero Ej-03.js:
//-- Servidor JSON

const http = require('http');
const fs = require('fs');
const PUERTO = 8080;

//-- Cargar la Página de error
const ERROR = fs.readFileSync('error_page.html');

//-- Cargar pagina web principal
const MAIN = fs.readFileSync('Ej-03.html','utf-8');

//-- Leer fichero JSON con los productos
const PRODUCTOS_JSON = fs.readFileSync('Ej-01.json');

//-- SERVIDOR: Bucle principal de atención a clientes
const server = http.createServer((req, res) => {

    //-- Construir el objeto url con la url de la solicitud
    const myURL = new URL(req.url, 'http://' + req.headers['host']);  
  
    //-- Variables para el mensaje de respuesta
    let content_type = "text/html";
    let content = "";
  
    //-- Leer recurso y eliminar la / inicial
    let recurso = myURL.pathname;
    recurso = recurso.slice(1); 

    switch (recurso) {
        case '':
            console.log("Main page");
            content = MAIN;
            break;

        case 'productos':
            console.log("Peticion de Productos!")
            content_type = "application/json";
            content = PRODUCTOS_JSON;
            break;

        case 'cliente-2.js':
            //-- Leer fichero javascript
            console.log("recurso: " + recurso);
            fs.readFile(recurso, 'utf-8', (err,data) => {
                if (err) {
                    console.log("Error: " + err)
                    return;
                } else {
                  res.setHeader('Content-Type', 'application/javascript');
                  res.write(data);
                  res.end();
                }
            });
            
            return;
            break;

            //-- Si no es ninguna de las anteriores devolver mensaje de error
        default:
            res.setHeader('Content-Type','text/html');
            res.statusCode = 404;
            res.write(ERROR);
            res.end();
            return;
    }
  
    //-- Generar respuesta
    res.setHeader('Content-Type', content_type);
    res.write(content);
    res.end()
  
  });
  
  server.listen(PUERTO);
  console.log("Escuchando en puerto: " + PUERTO);

Páginas estáticas: Principal y de error

  • Fichero: error_page.html

Aquí está implementada la página de error, que se carga al arrancar el servidor

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Error</title>
</head>
<body>
    <h1>ERROR 404: Página no encontrada</h1>
</body>
</html>
  • Fichero: Ej-03.html

Esta es la página principal. En ella se especifica que se cargue el fichero javascript en el cliente (cliente-2.js). Al finalizar la carga de la página se comienza a ejecutar el fichero cliente-2.js

Además del botón Test-JS del ejemplo anterior, se ha añadido uno nuevo: Ver productos que es el que lanza la petición AJAX e imprime los resultados en un contenedor <div>

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <script src="cliente-2.js" defer></script>
    <title>Ejemplo 3</title>
</head>
<body>
    <h2>Ej 3: Servidor JSON</h2>
    <p>Prueba de acceso a ficheros JSON</p>
    <p>Accede al recurso manualmente <a href="/productos">/productos</a> para recibir 
    un objeto JSON con información sobre todos los productos disponibles
    en la tienda </p>
    <p>Botón de prueba: <button id="boton_test">Test JS</button></p>
    <h2>Mensaje desde Javascript:</h2>
    <p id="display1"> -> </p>
    <p>Botón para realizar peticion Ajax: <button id="boton_ajax">Ver productos</button></p>
    <div id="display2"></div>
</body>
</html>

Cliente javascript: client-2.js

Este es el cliente que corre en el navegador. Atiende a dos botones. Uno que simplemente es para hacer una prueba (igual que el ejemplo 2) y otro que es el que realiza la petición AJAX para obtener el listado de productos del servidor

Si la petición AJAX falla, se saca un mensaje de error. El error en la petición puede ocurrir porque el servidor esté caido o porque tengamos una errata en el nombre del recurso a pedir en la petición

Si todo ha ido bien, se lee el contenido en JSON y se crea el objeto productos que es un array con todos los productos. Lo recorremos utilizando un bucle for para imprmir sus nombres en el display html

console.log("Ejecutando Javascript...");

//-- Elementos HTML para mostrar informacion
const display1 = document.getElementById("display1");
const display2 = document.getElementById("display2");

//-- Botones
const boton_test = document.getElementById("boton_test");
const boton_ajax = document.getElementById("boton_ajax");

//-- Retrollamada del boton de Test-JS
boton_test.onclick = ()=> {
    display1.innerHTML+="<p>Hola desde JS!</p>";
}

//-- Retrollamda del boton de Ver productos
boton_ajax.onclick = () => {
    
    display2.innerHTML+="<p>Haciendo petición...</p>\n";

    //-- Crear objeto para hacer peticiones AJAX
    const m = new XMLHttpRequest();

    //-- Función de callback que se invoca cuando
    //-- hay cambios de estado en la petición
    m.onreadystatechange = () => {

        //-- Petición enviada y recibida. Todo OK!
        if (m.readyState==4) {

            console.log("Peticion completada");
            console.log("status: " + m.status);

            //-- Solo la procesamos si la respuesta es correcta
            if (m.status==200) {

                //-- La respuesta es un objeto JSON
                let productos = JSON.parse(m.responseText)

                //-- Meter el resultado en un párrafo html
                display2.innerHTML += "<p>";

                //--Recorrer los productos del objeto JSON
                for (let i=0; i < productos.length; i++) {

                    //-- Añadir cada producto al párrafo de visualización
                    display2.innerHTML += productos[i];

                    //-- Separamos los productos por ',''
                    if (i < productos.length-1) {
                    display2.innerHTML += ', ';
                    }
                }

                //-- Cerrar el párrafo
                display2.innerHTML += "</p>"

            } else {
                //-- Hay un error en la petición
                //-- Lo notificamos en la consola y en la propia web
                console.log("Error en la petición: " + m.status + " " + m.statusText);
                display2.innerHTML += '<p>ERROR</p>'
            }
        }
    }

    //-- Configurar la petición
    m.open("GET","/productos", true);

    //-- Enviar la petición!
    m.send();
}

Probándolo todo

Primero arrancamos el servidor:

$ node Ej-03.js
Escuchando en puerto: 8080

Ahora desde el navegador nos conectamos a la página principal

En la consola vemos la petición de la página principal (main page) y luego la del cliente-2.js. Este javascript se ha ejecutado, y en la consola vemos el mensaje: Ejecutando Javascript. El mensaje de error rojo que se ve en la consola es debido a la solicitud del favicon.ico. Lo ignoramos

Al apretar el botón Test JS vemos el mensaje Hola Desde JS!

Ahora pinchamos en el botón de Ver productos. En la consola vemos que el servidor ha recibido la petición. Y en la consola del navegador vemos el cliente que ha recibido todo correctamente. En la página web aparece la lista de los productos

Si volvemos a apretar en el botón de Ver productos se realiza otra petición

En esta animación se muestra el funcionamiento. Al final se cierra el servidor y la petición falla

Ejemplo 4: Petición AJAX con paso de parámetros

También podemos pasar parámetros al realizar la petición AJAX. La forma más sencilla es pasarla a través del recurso:

  //-- Configurar la petición
  m.open("GET","/productos?param1=hola&param2=wei", true);

En este ejemplo se pasan los parámetros param1 y param2

En el lado del servidor los parámetros los leemos como ya conocemos:

  //-- Leer los parámetros
  let param1 = myURL.searchParams.get('param1');
  let param2 = myURL.searchParams.get('param2');

En este servidor NO hacemos nada con los parámetros, simplemente los mostramos en la consola para comprobar que se reciben correctamente

Búsqueda de productos con autocompletado

Ya tenemos todo listo para implementar la última parte de la práctica 2: Búsquedas con autocompletado. Igual que hemos hecho con las otras partes, la idea es aprender los mecanismos usando ejemplos muy simplificados (a.k.a cutres)

En esta animación se muestra la implementación de un buscador con autocompletado. En la casilla de búsqueda se introduce el producto a buscar. Cada vez que se introduce una letra se realiza una petición AJAX para solicitar los productos que comienzan por esa cadena

En el terminal vemos lo que el servidor recibe y el array con los productos que cumplen esa búsqueda. Este es el array que se envía de vuelta al cliente para que lo muestre debajo de la caja de búsqueda

Los ficheros que se corresponden con este ejemplo son: (Están todos en el repositorio)

Se deja como ejercicio que estudies el código para que veas cómo funciona y lo puedas implementar en tu tienda de la práctica 2. Aquí se dejan las ideas principales:

  • La cadena de búsqueda se introduce en el cliente en un elemento HTML de tipo entrada de texto (input text)
  • El servidor tiene el recurso /productos al que se le pasa como parámetro la cadena de búsqueda (param1). Ej. /productos?param1=ard
  • Cuando el servidor recibe una petición construye un nuevo array con los resultados de la búsqueda, y es el que se devuelve al cliente. Para construirlo se recorren todos los productos de la base de datos y los que cuadren con la búsqueda se añaden al array
  • Estudia los métodos utilizados para esto. Y realiza búsquedas de información sobre ellos para entender cómo funcionan (push, startWith, toUpperCase...
  • El cliente tiene una función de retrollamada asociada al evento oninput que se genera cada vez que hay un cambio en la entrada de texto. De esta manera, cada vez que hay un cambio en el texto se realiza una petición AJAX al servidor

Con esto ya tienes todos los elementos disponibles para terminar tu tienda de la práctica 2

Autor

Créditos

Licencia

Enlaces

TEORIA

Soluciones

LABORATORIO

Prácticas y sesiones de laboratorio

Práctica 0: Herramientas

Práctica 1: Node.js: Tienda Básica

Práctica 2: Interacción cliente-servidor. Tienda mejorada

Práctica 3: Websockets: Chat

Practica 4: Electron: Home Chat

  • L11: Home chat (26-Abril-2022)
  • L12: Laboratorio puro. NO hay contenido nuevo (9-Mayo-2022)
  • L13: Laboratorio/Tutorias. No hay contenido nuevo (10-Mayo-2022)

EXAMENES

Curso 2020-2021

Clone this wiki locally