Acceso a servicios web de terceros ================================== Los *servicios web* proporcionan una forma estándar de interoperabilidad entre diferentes aplicaciones, posiblemente heterogéneas. En este tema, veremos cómo acceder a servicios desde nuestras aplicaciones web, así como su relación con el estándar HTTP. En este tema nos centraremos en el acceso a servicios web ofrecidos por terceros; posteriormente, veremos cómo definir nuestros propios servicios web. .. Important:: Las habilidades que deberías adquirir con este tema incluyen las siguientes: - Saber usar Ajax, especialmente a través de la API Fetch, como una tecnología para crear aplicaciones de una única página. - Saber crear y encadenar promesas en un programa de JavaScript. - Comprender los fundamentos del protocolo HTTP y la configuración cliente-servidor. - Diferenciar los distintos componentes de un navegador. - Saber implementar aplicaciones *mashups* que accedan e integren servicios web de terceros. .. _label-servicios-http: El protocolo HTTP ----------------- HTTP (por *hypertext transfer protocol*) es el protocolo de comunicación (a nivel de capa de aplicación) diseñado para permitir el envío de información en la *world wide web* entre distintos ordenadores. Sus primeras versiones fueron desarrolladas en la década de los noventa a partir de los trabajos de Tim Berners Lee y su equipo (los creadores de la web). Las diferentes versiones del estándar son coordinadas por el `HTTP Working Group`_ de la Internet Engineering Task Force. HTTP/1.1 se publicó en 1997, HTTP/2 en 2015 y HTTP/3 en 2018, aunque tanto navegadores como servidores siguen soportando las versiones antiguas y son capaces de adaptarse para usar el mismo protocolo: por ejemplo, un navegador reciente que soporte HTTP/3 usará HTTP/2 o incluso HTTP/1.1 al comunicarse con un servidor que no haya sido actualizado en mucho tiempo. HTTP/3, a diferencia de anteriores versiones, ya no se apoya en el protocolo de transporte TCP (*transmission control protocol*), sino que usa QUIC (que no son siglas, sino un término que pretende evocar a la palabra inglesa *quick*). QUIC mejora la capacidad de multiplexación de TCP y suele mejorar la latencia y velocidad de las conexiones. .. _`HTTP Working Group`: https://httpwg.org/ .. admonition:: Hazlo tú ahora :class: hazlotu Estudia todos los vídeos de la serie "El protocolo HTTP" en los que se realiza un repaso a los principales elementos del protocolo: `parte 1-1`_, `parte 1-2`_ y `parte 1-3`_. .. _`parte 1-1`: https://drive.google.com/file/d/1Wy4iV-oi7MEypO77wL4AYGPDLfIAWoNy/view?usp=sharing .. _`parte 1-2`: https://drive.google.com/file/d/1scQJsHbPgrznE3qqx4BTqDXWgpEdcJsE/view?usp=sharing .. _`parte 1-3`: https://drive.google.com/file/d/1eAiocuglVBR0WCC_j9L77OMJ7zlsLRq4/view?usp=sharing Los conceptos que tienes que comprender del protocolo HTTP se encuentran recogidos en `estas diapositivas`_. .. _`estas diapositivas`: _static/slides/050-http-slides.html Herramientas para desarrolladores --------------------------------- Las herramientas para desarrolladores que incorporan los navegadores permiten estudiar los detalles de todas las conexiones establecidas por el navegador al ejecutar una aplicación web. .. Note: Las peticiones lanzadas desde la barra de direcciones del navegador son siempre de tipo GET. Para estudiar los otros tipos de peticiones de HTTP, necesitamos crear un formulario para peticiones de tipo POST, o, si queremos poder usar cualquier verbo, escribir código en JavaScript (usando la API Fetch que estudiáremos después) u otro lenguaje de programación. También hay herramientas como `Postman`_, una aplicación que permite crear cómodamente colecciones de peticiones a APIs (*application programming interfaces*). La herramienta `curl`_ es similar pero funciona desde la línea de órdenes. .. _`Postman`: https://www.getpostman.com/ .. _curl: https://curl.haxx.se/ .. admonition:: Hazlo tú ahora :class: hazlotu Familiarízate con las opciones de inspección de la actividad en red de la pestaña :guilabel:`Network` del entorno de las Chrome DevTools siguiendo la página de su documentación `Inspect Network Activity`_ . Practica las distintas posibilidades en DevTools con peticiones GET por ahora, pero recuerda volver a esta actividad cuando estudies cómo realizar desde JavaScript peticiones con otros verbos. .. _`Inspect Network Activity`: https://developers.google.com/web/tools/chrome-devtools/network .. _label-servicios-promesas: Promesas de JavaScript ---------------------- Los objetos de tipo ``Promise`` (una clase definida en la API de JavaScript de los navegadores modernos) se usan para representar la finalización con éxito de una tarea (normalmente asíncrona, es decir, que no bloquea el bucle de eventos, sino que define una función de *callback* que será ejecutada más adelante) o su fracaso. En términos informales, diremos que una promesa se cumple (en caso de éxito), se incumple (en caso de fracaso) o que está pendiente (cuando aún no se ha resuelto); en inglés se usan los términos *fullfilled*, *rejected* o *pending*, respectivamente. Los objetos promesa nos serán muy útiles en tareas como la comunicación con un servidor, que veremos más adelante en este tema. .. promesas: https://stackoverflow.com/a/42005046/1485627 Veamos un primer ejemplo. El siguiente código crea un objeto de tipo promesa ``p`` invocando al constructor de ``Promise``. Este constuctor recibe como parámetro una función llamada *función ejecutora* con el código que nosotros definamos para intentar cumplir la promesa. El constructor de ``Promise`` creará dos funciones: una que nuestro código tendrá que invocar si la tarea prometida se ha podido llevar a cabo; otra que nuestro código invocará en caso contrario. Para poder llamar a estas funciones nuestra función ejecutora tiene dos parámetros por los que recibe referencias a ellas (recuerda que las dunciones son objetos en JavaScript). Por tanto, estas dos funciones (parámetros ``resolve`` y ``reject`` en el código de abajo) serán invocadas en los lugares de nuestro código en los que determinemos que la tarea se ha desarrollado con éxito (en el caso del primer parámetro) o ha fracasado (para la función pasada como segundo parámetro). En este caso, vamos a suponer que nuestra promesa ejecuta una tarea asíncrona muy sencilla: generar tras un segundo un número aleatorio entre 0 y 1; la tarea se considera un éxito si el número es menor de 0,5. En lenguaje de la calle, sería como decir "te prometo que voy a generar (asíncronamente) un número aleatorio y que será menor que 0,5"; como cualquier promesa, en cualquier caso, esta puede cumpirse o no. Observa que la tarea es asíncrona (concretamente, una llamada a ``setTimeout``), lo que significa que el hilo de ejecución de nuestro programa no se queda bloqueado esperando a que pase dicho segundo, sino que la cuenta del tiempo se lleva a cabo por el navegador en otro hilo y al cumplirse el tiempo se añade a la cola de eventos la llamada al *callback* correspondiente. .. code-block:: :linenos: :force: let p= new Promise(function(resolve,reject) { // función ejecutora console.log('Entrando en el constructor de la promesa'); setTimeout( function() { const r= Math.random(); if (r<0.5) { resolve('la cosa fue bien'); } else { reject('algo salió mal'); } }, 1000); }); A las funciones ``resolve`` y ``reject`` les podemos pasar un único argumento que usaremos para dar información adicional relacionada con el éxito o fracaso de la tarea asíncrona; en este caso, es una simple cadena con un mensaje, pero puede ser cualquier objeto de JavaScript. Nada más invocar al contructor de ``Promise``, este establece el estado de la promesa a *pendiente* y llama a nuestra función ejecutora que el constructor ha recibido como parámetro. El código de la función ejecutora, como en nuestro ejemplo, normalmente definirá una operación asíncrona (como llamar a ``setTimeout`` o realizar una petición a un servidor) que terminará llamando a ``resolve`` y a ``reject`` en distintos puntos; estas dos funciones (recordemos que son creadas por el sistema y no pertenecen a nuestro código) cambian el estado de la promesa a *cumplida* o *incumplida* y llaman a continuación a las funciones que el programador haya definido para manejar ambas situaciones como veremos a continuación. El vínculo entre las funciones del sistema ``resolve`` y ``reject`` y nuestro código se establece llamando al método ``then`` sobre el objeto de tipo promesa. Al método ``then`` se le pasan dos *funciones manejadoras de promesas* que el objeto de tipo promesa registra internamente: la primera se vincula a ``resolve`` y la segunda a ``reject``. Estas funciones manejadoras pueden tener un parámetro que será el mismo que el usado como argumento en las llamadas a ``resolve`` y ``reject`` de la función ejecutora. Las funciones ``resolve``y ``reject`` definidas por el sistema invocarán con su parámetro a la función correspondiente de nuestro código registrada con ``then()``. .. code-block:: :linenos: :force: p.then(function(mensaje) { console.log(`Promesa cumplida: ${mensaje}`); }, function(mensaje) { console.log(`Promesa incumplida: ${mensaje}`); } ); .. Note: Como las llamadas a ``resolve`` y ``reject`` se realizarán más adelante cuando el bucle de eventos atienda nuestro *callback*, para entonces el intérprete ya habrá ejecutado el método ``then`` correspondiente que aparecerá en nuestro código después de crear la promesa y, por tanto, el vínculo entre las funciones ``resolve``y ``reject`` ya se habrá establecido. Observa cómo todo el proceso puede verse como un *partido de tenis* en el que la pelota pasa del campo del código de la librería de promesas de JavaScript a nuestro código varias veces. Nuestro código *saca* creando la promesa. El constructor de la promesa crea entonces las funciones ``resolve`` y ``reject`` y llama la función ejecutora. Nuestra función ejecutora crea una tarea asíncrona y le asocia una función de *callback*. En otro hilo el navegador lleva a cabo la tarea asíncrona y cuando esta se complete encolará la llamada a nuestra función de *callback*. Mientras tanto, nuestro código principal se sigue ejecutando y registra los dos manejadores llamando al método ``then``. Posteriormente, cuando sea desencolada por el bucle de eventos, nuestra función de *callback* comprobará si la tarea asíncrona se ha realizado con éxito o no. En base a una u otra cosa, nuestra función de *callback* llamará a ``resolve`` o ``reject``. Cuando una de estas funciones se ejecute invocará una de nuestras funciones manejadoras. Nuestra función manejadora se ejecutará y realizará las acciones que correspondan según la promesa se haya cumplido o no, terminando aquí el *punto de juego*. Además, observa en el código anterior cómo hemos usado *cadenas con plantillas* (*template strings*) para imprimir los mensajes por consola. Las cadenas con plantillas de JavaScript usan comillas invertidas en lugar de rectas, pueden contener variables embebidas como en el ejemplo, y pueden también ocupar más de una línea. .. figure:: https://csharpcorner-mindcrackerinc.netdna-ssl.com/UploadFile/BlogImages/01262017214716PM/Screen%20Shot%202017-01-26%20at%208.28.19%20pm.png :target: https://www.c-sharpcorner.com/blogs/overview-of-promises-in-javascript :alt: diagrama de los elementos implicados en una promesa Diagrama de los elementos de una promesa por Sumant Mishra El código anterior es equivalente al siguiente en el que en lugar de pasar dos funciones a ``then`` se define la función asociada al incumplimiento de la promesa en un método ``catch``: .. code-block:: :linenos: :force: let p= new Promise(function(resolve,reject) { setTimeout( function() { const r= Math.random(); if (r<0.5) {a introducción resolve('la cosa fue bien'); } else { reject('algo salió mal'); } }, 1000); }); p.then(function(mensaje) { console.log(`Promesa cumplida: ${mensaje}`); }) .catch(function(mensaje) { console.log(`Promesa incumplida: ${mensaje}`); }); Además, si encapsulamos el código de creación de la promesa en una función, usamos funciones flecha y gestionamos el error mediante una excepción (clase ``Error``), el código anterior se convierte en: .. code-block:: :linenos: :force: function aleatorio() { return new Promise( (resolve,reject) => { setTimeout( () => { const r= Math.random(); if (r<0.5) { resolve('la cosa fue bien'); } else { reject(new Error('algo salió mal')); } }, 1000); }); } aleatorio() .then( (mensaje) => {console.log(`Promesa cumplida: ${mensaje}`);}) .catch( (error) => {console.log(`Promesa incumplida: ${error.message}`);}); Las promesas pueden encadenarse simplemente haciendo que la función manejadora asociada al cumplimiento de la promesa (la indicada en la llamada al método ``then``) devuelva a su vez una promesa: .. code-block:: :linenos: :force: aleatorio().then( (mensaje) => { console.log(`Primera promesa cumplida: ${mensaje}`); return aleatorio2(0.8); }) .then( (mensaje) => { console.log(`Segunda promesa cumplida: ${mensaje}`); }) .catch( (error) => { console.log(`Una de las promesas fue incumplida: ${error.message}`); } ); function aleatorio() { return new Promise( (resolve,reject) => { setTimeout( () => { const r= Math.random(); if (r<0.5) { resolve('la cosa fue bien'); } else { reject(new Error('algo salió mal')); } }, 1000); }); } function aleatorio2(delta) { return new Promise( (resolve,reject) => { setTimeout( () => { const r= Math.random(); if (r { setTimeout( () => { const r= Math.random(); if (r<0.5) { resolve('la cosa fue bien'); } else { reject(new Error('algo salió mal')); } }, 1000); }); } function aleatorio2(delta) { return new Promise( (resolve,reject) => { setTimeout( () => { const r= Math.random(); if (r { const r= Math.random(); if (r<0.5) { let mensaje= 'la cosa fue bien'; console.log(`Primera promesa cumplida: ${mensaje}`); let delta= 0.8; setTimeout( () => { const r= Math.random(); if (r { throw new Error("algo salió mal"); }).catch(error => console.log(error.message)); es equivalente a este: .. code-block:: :linenos: :force: new Promise((resolve, reject) => { reject(new Error("algo salió mal")); }).catch(error => console.log(error.message)); Lo mismo pasa en los manejadores de promesas. Si lanzamos una excepción dentro del manejador definido en el ``then`` se considera como una promesa incumplida y el control se transfiere al manejador de error: .. code-block:: :linenos: :force: new Promise((resolve, reject) => { resolve("la cosa fue bien"); }).then((result) => { throw new Error(`${result}, pero luego algo salió mal`); }).catch(error => console.log(error.message)); .. _label-servicios-xhr: Los objetos de tipo XMLHttpRequest ---------------------------------- En los primeros años de la web, la mayoría de las aplicaciones web seguían el siguiente patrón de comportamiento al realizar, por ejemplo, una búsqueda de un determinado elemento en una base de datos: el usuario rellenaba los valores correspondientes para buscar el elemento en un formulario y pulsaba el botón de enviar; en ese momento, el navegador realizaba una petición al servidor y borraba la página web actual; el servidor realizaba la búsqueda en la base de datos y devolvía entonces una página web completa que el navegador mostraba en sustitución de la anterior. Además de generar una experiencia incómoda al usuario si se compara con una aplicación tradicional de escritorio (el usuario ha de esperar a que se cargue de nuevo toda la página para seguir interactuando con la aplicación), este procedimiento es muy ineficiente cuando la página web tiene mucho contenido que apenas cambia, y que, sin embargo, es enviado continuamente por el servidor. A partir de finales de los noventa y especialmente en los primeros años del siglo XXI, los desarrolladores comienzan a explotar el uso de funcionalidades de los navegadores que permiten realizar peticiones a un servidor sin tener que recargar la página completa. A estas técnicas se les denomina Ajax por razones históricas: el término lo acuñó un desarrollador en 2005 como acrónimo de *Asynchronous JavaScript and XML*. Con Ajax, los datos devueltos por el servidor se usan para generar dinámicamente HTML que es insertado convenientemente en el árbol DOM, conformando lo que se conoce como *aplicaciones de una solo página* (*single-page applications*). La más usada de las técnicas Ajax se basaba en los objetos de clase ``XMLHttpRequest`` (para abreviar se suele llamar *XHR*) que permite, como se ha comentado, solicitar (normalmente de forma asícrona) una serie de datos al servidor desde JavaScript en lugar de una página completa que reemplazará a la actual. Estos datos serán, entonces, procesados por una función definida por el programador. .. admonition:: Hazlo tú ahora :class: hazlotu Estudia el vídeo "`Los objetos de tipo XMLHttpRequest`_" en el que se realiza una traza del código que aparece a continuación para interactuar con un servicio web mediante un objeto de la clase ``XMLHttpRequest``. .. _`Los objetos de tipo XMLHttpRequest`: https://drive.google.com/file/d/1G6eoew4ZyPd3rnpkXOWanjKY8ACL9nIB/view?usp=sharing El siguiente es un ejemplo típico de uso de un objeto de tipo ``XMLHttpRequest``: .. code-block:: javascript :linenos: :force: // Los datos del servidor se insertarán en el elemento con id 'results': var resultado= document.querySelector("#results"); // Borra el contenido previo del elemento: resultado.textContent= ""; // Crea el objeto XHR: var xhr = new XMLHttpRequest(); console.log(xhr.readyState); var url= "https://ghibliapi.herokuapp.com/films/"; // Identifica el verbo, la URL y que la petición será asíncrona: xhr.open("GET", url, true); console.log(xhr.readyState); // La función asignada a onreadystatechange es invocada varias veces por // el navegador durante el transcurso de las diferentes fases de comunicación // con el servidor (conexión establecida, petición recibida, petición en proceso, // petición finalizada); la fase de la llamada actual se almacena en readyState; // normalmente, nos interesa la llamada en la que readyState tiene el valor 4, // que se hace en el momento en el que el servidor ha devuelto todos los // datos: xhr.onreadystatechange = function () { console.log(xhr.readyState); if (xhr.readyState == 4) { // Código de estado de HTTP: if (xhr.status != 200) { console.error("Hubo un error (" + xhr.status + ")!"); } else { // JSON.parse analiza la cadena pasada como argumento y la convierte // a un objeto de JavaScript; la respuesta de esta petición es un array // que se almacena en r: var r= JSON.parse(xhr.responseText); // Los datos devueltos están en formato JSON y son de la forma // [ {"id": "0440483e", "title": "Princess Mononoke", // "description": "Ashitaka, a prince of the disappearing Ainu tribe...", // "director": ..., "producer": ...}, {"id": dc2e6bd1", "title": "Spirited Away", // "description": "Spirited Away is an Oscar winning Japanese animated film about // a ten year old girl who wanders away from her parents...", "director": "Hayao // Miyazaki", ...}, {...}, {...} ] for (var i=0; i0) { for (var i=0; i