JSONP, o la inserción dinámica de scripts que podría sustituir a Ajax

Parece una tecnología nueva, pero no lo es ni mucho menos. Sugerida en diciembre del 2005 por Bob Ippolito (ver este post en su blog) como una solución para cargar código de manera asíncrona desde diferentes dominios, se hizo popular cuando Google la utilizó para implementar Google Instant Search. Sin embargo, tras este nombre peculiar, se esconde algo muy sencillo y extremadamente útil que, para mi gusto, es más potente y sencillo que Ajax. En este post voy a intentar explicar cómo funciona Ajax por dentro junto con una alternativa mucho más sencilla, que es la que usa Jsonp.

Ajax por dentro

Hace ya tiempo que todos conocemos Ajax y lo usamos a diario en nuestras aplicaciones web, pero tenemos que recordar que el objeto XMLHttpRequest fue una invención de Microsoft en el 1999 que no estandarizó hasta el 2006 y que, hasta hoy en día, sigue siendo un hack que funcione en cualquier navegador. De hecho, nadie usa Ajax directamente, sino que siempre se hace uso de alguna librería (como jQuery, Prototype, Mootols o cualquier otra), y ninguna de ellas se libra de tener algo parecido a esto:

var httpRequest;
if (window.XMLHttpRequest) {
    httpRequest = new XMLHttpRequest();
} else if (window.ActiveXObject) {
    try { httpRequest = new ActiveXObject("MSXML2.XMLHTTP");
    } catch (e) {
        try { httpRequest = new ActiveXObject("Microsoft.XMLHTTP");
        } catch (e) {}
    }
}

Lo cual es bastante aberrante, pero funciona.

Cómo solemos usar Ajax

Aunque inicialmente Ajax se ideó para hacer peticiones Xml (la “x” final de Ajax viene precisamente de Xml), el coste que supone parsear este tipo de formato en Javascript hizo que se sustituyera por algo mucho más sencillo de parsear y que, además, viene implícito de manera nativa en el lenguaje Javascript: JSON.

JSON es un formato extremadamente sencillo para guardar colecciones, basicamente listas de elementos y mapas de parejas clave/valor (también conocidos en Javascript como Objetos y en otros lenguajes como tablas hash), que se pueden combinar entre sí para hacer estructuras más complejas:

["Primer elemento","Segundo", 800, true, false, null]

{key:"value", anotherKey:293

{id:201,
 nombre:"Alberto",
 profesion:"Programador",
 lenguajes: ["Java", "Groovy", "Flex", "Javascript"],
 webs: [{id:21, nombre:"Coderfacts", url:"http://coderfacts.com"},
        {id:178, nombre:"Blog", url:"http://albertovilches.com"}, 
        {id:44, nombre:"Greach", url:"http://greach.es"}
       ],
 contacto: {twitter:"@albertovilches",
            email:"vilches ARROBA gmail.com",
            skype:"alberto_vilches"
           }
}

Y parsearlo es algo tan sencillo como evaluarlo como expresión. Es decir, el formato JSON es exactamente el mismo que usa Javascript para guardar colecciones:

def texto = "['hola', {quien:'Mundo'}]"
def json = eval(texto)
alert(json[0])          // Muestra hola
alert(json[1].quien)    // Muestra mundo

Por esta razón, la mayoría de frameworks Ajax nos permiten decidir que tipo de respuesta esperamos: texto, html, xml o json. Son muchas las veces que usamos Ajax para obtener código html que inyectamos en el Dom de nuestra página web, pero poco a poco hemos ido cambiando el hábito y preferimos que las peticiones devuelvan Json en vez de html. Las ventajas son muchas: en el servidor, nos ahorramos renderizar el resultado para convertirlo en html, y nos limitamos a enviar la información transformada en Json, que ocupa mucho menos. Y en el cliente, podemos utilizar el Json recibido para cambiar el Dom y renderizar el resultado de la petición, pero también podemos hacer transformaciones y cálculos antes de convertirlo en algo visible en nuestra web. Sin duda, Json ha permitido aligerar la carga en los servidores, y delegar que la decisión sobre como renderizar los resultados se haga donde se tiene que mostrar, en la propia página.

Veamos donde estamos: tenemos una aplicación web que necesita ser modificada. Hacemos una petición Ajax al servidor solicitando una información determinada. El servidor recibe la petición, lee los parámetros, busca en la base de datos (o donde sea) y genera un json con la información. El cliente, nuestra página web, recibe el Json, lo parsea con Javascript y renderiza el resultado, modificando la página actual. Podemos continuar.

Javascript dinámico frente a Ajax

Intentemos responder a esta pregunta: si solo usamos Ajax para cargar Json, el cual solo puede ser parseado y utilizado desde Javascript, ¿porqué no cargar el Javascript con el Json y ejecutarlo directamente desde el servidor?

Supongamos este ejemplo básico y casi minimalista:

Nombre <span id="nombre"></span><br/>
Profesión <span id="profesion"></span>

<script type="text/javascript">
function pintaPersona(data) {
    document.getElementById("nombre").innerHTML = data.nombre;
    document.getElementById("profesion").innerHTML = data.profesion;
}
</script>

Solo tiene un par de etiquetas <span> donde mostrar un nombre y profesión, y una función cargaPersona() que se encargará de rellenar estas etiquetas con el objeto json recibido como parámetro. Así que podemos llamar en cualquier momento a la función cargaPersona() para rellenar los campos simplemente con:

pintaPersona({nombre:"Alberto", profesion:"Programador"});

Pero también podemos generar este Javascript dinámicamente en servidor a partir de una url, usando Java, Php, Ruby, etc. Por ejemplo:

<script src="cargaPersona.php?id=25" type="text/javascript"></script>

Esta url podría buscar un usuario (usando el parámetro id con valor 25) y generar directamente la función pintaPersona() con sus datos:

GET /cargaPersona.php?id=25
pintaPersona({nombre:"Alberto", profesion:"Programador"});

No es más que un ejemplo muy sencillo, ya que la función podría hacer algo mucho más completo, pero la idea es la misma: generar dinámicamente Javascript en servidor que llame a una función que ya tenemos previamente definida en nuestra página, con unos datos determinados.

Pero para que este Javascript dinámico pueda competir con Ajax, necesitamos que sea asíncrono. Es decir, poder cargarlo cuantas veces queramos y en cualquier momento, por ejemplo cuando se hace click en un listado de usuarios. Para ello, lo que haremos es crear una etiqueta <script> y añadirla al dom (por ejemplo, al final del body). El navegador cargará dicho Javascript (que puede estar en cualquier dominio) y lo ejecutará en ese mismo momento. Para crear y añadir scripts al Dom no hace falta ningún framework, basta con esto:

var script = document.createElement("script")
script.src = "cargaPersona.php?id=25"
script.type = 'text/javascript';
document.getElementsByTagName("body")[0].appendChild(script)

Aunque es mejorable, ya que la etiqueta script se puede reutilizar una y otra vez, desde mi punto de vista, no hay color con Ajax y la complejidad interna que supone crear y procesar una petición con XMLHttpRequest.

Un ejemplo con todo funcionando

Veamos un ejemplo de los dos sistemas juntos:

Nombre <span id="nombre"> /span><br/>
Profesión <span id="profesion"></span>
<script type="text/javascript">
    function pintaPersona(data) {
        document.getElementById("nombre").innerHTML = data.nombre;
        document.getElementById("profesion").innerHTML = data.profesion;
    }
</script>

<!-- Ajax jQuery flavor -->
<script type="text/javascript" src="http://code.jquery.com/jquery-1.6.4.js"></script>
<script type="text/javascript">
    function cargaPersonaAjax(id) {
        $.ajax({url: "http://localhost/cargaPersonaJson.php", data: {id:id},
                dataType: "json", success: function(json) { pintaPersona(json) }});
    }
</script>

<!-- No framework, script dynamic flavor -->
<script type="text/javascript">
    function cargaPersonaScript(id) {
        var script = document.createElement("script")
        script.src = "http://localhost/cargaPersonaScript.php?id=" + id
        document.getElementsByTagName("body")[0].appendChild(script)
    }
</script>

<ul>
    <li>Usuario 1 <a href="javascript:void(cargaPersonaAjax(1))">Mostrar</a></li>
    <li>Usuario 2 <a href="javascript:void(cargaPersonaScript(2))">Mostrar</a></li>
</ul>

Siendo el resultado de cargaPersonaScript.php un Javascript que llama a la función pintaPersona:

GET /cargaPersonaScript.php?id=25
pintaPersona({"nombre":"Alberto", "profesion":"Programador"});

Y el de cargaPersonaJson.php solamente Json puro, por lo que será responsabilidad del handler “success” de jQuery el llamar a pintaPersona() de nuevo con el Json recibido.

GET /cargaPersonaJson.php?id=25
{"nombre":"Alberto", "profesion":"Programador"}

JSONP

Muy bien, ahora que ya sabemos cargar scripts dinámicamente y que incluso podemos sustituir a Ajax con él (aunque también tiene sus limitaciones), ¿qué es JSONP? Si nos fijamos en el resultado de las dos peticiones anteriores, vemos que la diferencia es que una es Json puro, y la otra es el mismo Json, pero envuelto en una función.

JSON               {"nombre":"Alberto", "profesion":"Programador"}
JSONP pintaPersona({"nombre":"Alberto", "profesion":"Programador"});

Jsonp establece que el nombre de la función que envolverá el Json de respuesta lo debe decidir el usuario, enviándoselo en un parámetro llamado “callback”. Así que JsonP no es más que la carga dinámica de scripts definiendo en uno de los parámetros el nombre de la función que se va a llamar cuando se ejecute. De esta manera, podemos reutilizar la misma llamada para ejecutar diferentes funciones usando los mismos datos.

Siguiendo con nuestro ejemplo anterior, la función cargaPersona, responsable de hacer la llamada Jsonp, deberá enviarle al servidor en uno de los parámetros el nombre de la función de vuelta (callback) a ejecutar, en nuestro caso “pintaPersona”:

function cargaPersonaScript(id) {
    var script = document.createElement("script")
    script.src = "http://localhost/cargaPersonaJsonP.js?id=" + id;
    // Añadimos el nombre de la función callback
    script.src = script.src + "&callback=pintaPersona";
    script.type = 'text/javascript';
    document.getElementsByTagName("body")[0].appendChild(script)
}

En resumen, una llamada JSONP devuelve un JSON envuelto en una llamada a una función Javascript cuyo nombre se especifica en la propia petición como parámetro.

GET /cargaPersonaJsonP.php?id=25&callback=pintaPersona
pintaPersona({nombre:"Alberto", profesion:"Programador"});

GET /cargaPersonaJsonP.php?id=25&callback=otraFuncionDistinta
otraFuncionDistinta({nombre:"Alberto", profesion:"Programador"});

Incluso la versión jQuery del equivalente al ejemplo anterior es más reducida y simple:

function cargaPersonaAjax(id) {
    $.ajax({url: "http://localhost/b.js", data: {id:id},
            dataType: "jsonp", jsonpCallback: "pintaPersona"});
}

Internamente, el hecho de especificar Jsonp como método de envío hace que la petición se ejecute usando la inserción de una etiqueta <script> en vez Ajax, aunque el resultado es el mismo.

Conclusión

¿Debería Jsonp o la carga dinámica de etiquetas <script> sustituir a Ajax? No, ni mucho menos. Las dos formas son equivalentes en resultado y tienen sus ventajas e inconvenientes: con la inserción dinámica de scripts no se pueden hacer POST (aunque ¿quién los necesita realmente?) pero permite cargar contenidos en diferentes dominios (cross domain), cosa que no es posible con Ajax. De hecho, todas las soluciones llamadas Ajax cross-domain usan esta técnica, como ésta o esta otra. Eso sin contar lo obvio: que con Ajax podemos cargar cualquier tipo de contenido (texto,html,script,json o binario), pero tenemos que parsearlo, y que con JsonP nos limitamos a cargar Javascript, pero se ejecuta automáticamente por el navegador, al formar parte de una etiqueta <script>. Aunque esto también tiene sus riesgos: alguien puede interceptar la carga del javascript desde otro servidor y modificar el código que luego se ejecutará en nuestra página, así que solo deberemos usar esta técnica con sitios en los que confiemos, o hayamos creado nosotros. Y, por supuesto, asegurarnos de que no se usa el parámetro callback para inyectar código de vuelta, y para eso tenemos algunos trucos como sanitizar los parámetros.

Personalmente, si voy a trabajar con Json, usaré Jsonp, y dejaré Ajax para trabajar con html y binarios. Si estás dudando qué usar, siempre puedes hacer unos tests de rendimiento en tu propio navegador. Parece ser que JsonP es bastante más rápido y en algunos sitios importantes ya lo están usando, como Delicious, Flickr, Twitpic o Youtube. De todas formas, aprender a usar las dos y saber, además, como funcionan por dentro, son la clave para tomar la decisión correcta en el momento en el que lo necesitemos.

Bola Extra: limitaciones de las peticiones Get

La inserción de <script> solo permite el uso de urls que llegan al servidor como una petición GET. Por un lado, esto hace que sea imposible hacer upload de ficheros, que requieren una petición POST con un enctype “multipart/form-data”. Y por otro lado, la especificación de Htpp no define un límite para el tamaño de las urls, así que cada navegador tiene el suyo propio que debemos tener en cuenta:

  • Internet Explorer: 2083, según nota oficial de Microsoft, pero gente comenta en Stackoverflow que ha llegado a probar longitudes mucho mayores, como 4.000 caracteres en IE8.
  • Firefox: 65.536 de límite en teoría, pero acepta 100.000 en la práctica
  • Safari: 80.000
  • Opera: 190.000

Puedes ver las pruebas prácticas que han hecho algunas personas en este post.