domingo, 27 de junio de 2010

Cómo hacer llamas AJAX a otros sitios, o qué es JSONP

Una de las limitaciones que tiene el objeto XMLHTTPRequest (el objeto que da vida a AJAX) es no poder hacer llamadas a URLs fuera de nuestro dominio, es decir, si desde mi sitio quisiera traer las fotos de Flikr con el tag “dog” llamando a la URL http://api.flickr.com/services/feeds/photos_public.gne?tags=dog con el siguiente código

<html>
<head>
  <script src="http://code.jquery.com/jquery-latest.min.js"></script>
  <script type="text/javascript">   
    $(function()
    {
        $("#boton").click(ajax);
    });

    function ajax()
    {
        var url = "http://api.flickr.com/services/feeds/photos_public.gne?tags=dog";
        $.get(url, null, function(data)
        {
            $("#contenedor").html(data);
        });
    }
    </script>
</head>
<body>
    <div id="contenedor" />
    <input type="button" id="boton" value="probar" />
</body>
</html>

Utilizando jQuery hacemos un simple llamada AJAX y no pasa nada, vemos qué nos dice Fiddler

image

Vemos que la llamada se hace y los datos vienen pero no pasa nada, vamos al Firebug y vemos algo interesante

image

Firebug no nos muestra datos, esto es porque no se permiten llamadas a otro dominio con XMLHTTPRequest, he aquí el problema.

JSONP la solución

Una cosa que si permiten los navegadores es cargar recursos desde otros dominios, es decir, nada impide que un elemento <img> tenga su src apuntando a un recurso externo, es más, un <script> puede tener su origen en un dominio externo como en el ejemplo anterior que cargamos el archivo de jQuery desde http://code.jquery.com/jquery-latest.min.js y funciona perfectamente, otra cosa que podemos hacer es crear dinámicamente elementos <script>, entonces imaginemos que el recurso externo que queremos cargar en lugar de traernos solamente un conjunto de datos nos trae una llamada a un función dentro de nuestro código? para ser más claros, imaginemos que queremos llamar una URL que nos trae el valor “hola mundo”, no podemos recuperarlo con AJAX por la limitación que comentamos, pero qué pasaría si en lugar de traer “hola mundo” no trae código Javascript que incluya una llamada a una función?, así: MiFuncion(“hola mundo) y generamos dinámicamente un tag <script> que apunta a la URL esa, es decir, hacemos esto:

function jsonp()
{
    var url = "http://hostRemoto/datos";
    $("<script>").attr("src",url).appendTo("head");
}

function MiFuncion(data)
{
    alert(data);
}

y http://hostRemot/datos retorna MiFuncion(“hola mundo”)

Entonces conseguimos utilizar datos generados en hostRemoto, a esto se le llama JSONP JSON+Padding, lo interesante de esta técnica es que muchas API públicas la implementan, la de Yahoo, Flikr, Twitter, etc. En general la forma de utilizarlas es indicando como parámetro el nombre de nuestra función, es decir

http://hostRemoto/datos?callback=MiFuncion

Con eso le indicamos a hostRemoto que agregue delante de los datos que retorna una llamada a MiFuncion, en el caso de Flickr es así

http://api.flickr.com/services/feeds/photos_public.gne?tags=dog&format=json&jsoncallback=MiFuncion

Entonces, un ejemplos completo de una implementación de JSONP llamando a Flickr sería así:

<html>
<head>
  <script src="http://code.jquery.com/jquery-latest.min.js"></script>
  <script type="text/javascript">   
    $(function()
    {
        $("#boton").click(jsonp);
    });
    function jsonp()
    {
        var url = "http://api.flickr.com/services/feeds/photos_public.gne?tags=dog&format=json&jsoncallback=MiFuncion";
        $("<script>").attr("src",url).appendTo("head");
    }
    function MiFuncion(data)
    {
        $.each(data.items, function(i,item){
            $("<img/>").attr("src", item.media.m).appendTo("#contenedor");
        });
    }
    </script>
</head>
<body>
    <input type="button" id="boton" value="probar" /><br />
    <div id="contenedor" />
</body>
</html>

Llamamos a la API, le decimos que el nombre de nuestra función es MiFuncion, cargamos dentro de un <script> generado dinámicamente y se ejecuta la llamada a MiFuncion, esta recibe los datos JSON y genera elementos <img> que se van agregando al DIV contenedor, mágico.

JSONP en jQuery

Los que acabamos de hacer funciona perfecto, pero es un poco trabajoso, para evitarnos semejante cantidad de trabajo jQuery ya trae la posibilidad de hacer llamadas JSONP y lo mejor es que lo hace de manera que es transparente para nosotros.

<html>
<head>
  <script src="http://code.jquery.com/jquery-latest.min.js"></script>
  <script type="text/javascript">   
    $(function()
    {
        $("#boton").click(jsonp);
    });
    function jsonp()
    {
        var url = "http://api.flickr.com/services/feeds/photos_public.gne?tags=dog&format=json&jsoncallback=?";
        $.getJSON(url, null, function(data)
        {
            $.each(data.items, function(i,item){
                $("<img/>").attr("src", item.media.m).appendTo("#contenedor");
            });
        });
    }
    </script>
</head>
<body>
    <input type="button" id="boton" value="probar" /><br />
    <div id="contenedor" />
</body>
</html>

Vemos la generación dinámica del tag <script>

image

Básicamente la funcion getJSON al detectar la cadena callback=? en la URL que recibe genera automáticamente el tag <script> y llama a la URL reemplazando el ? por un nombre generado al azar, recupera la llamada y hace que el funcionamiento de JSONP sea transparente para nosotros, el parámetro jsoncallback es propio de la API de Flickr. Enjoy.

Referencias:

FlickAPI

jQuery getJSON

JSONP

Leonardo.