JSONP: JSON with Padding

De wikijoan
Salta a la navegació Salta a la cerca

Introducció

JSONP (JSON with Padding) és una tècnica utilitzada pels desenvolupadors web per sobrepassar les restriccions de domini creuat (overcome the cross-domain restrictions), imposades pels navegadors que impedeixen, per motius de seguretat, agafar dades i informació d'altres servidors diferents que el servidor propi que serveix la pàgina.

Què és JSONP?

La cosa important a recordar de jsonp és que no és un protocol ni un tipus de dades. És tan sols una manera de carregar un script al vol i processar aquest script que s'ha introduït a la pàgina. Això significa introduir un nou objecte javascript des del servidor a l'aplicació client.

Quan és necessari JSONP?

És una manera de permetre que un domini pugui accedir i processar dades des d'un altre domini en la mateixa pàgina i de forma asíncrona. Primerament, s'utilitza per saltar-se les restriccions CORS (Cross Origin Resource Sharing) que passen amb XHR (ajax) request. La càrrega de scripts no està subjecta a les restriccions CORS.

Com funciona?

Introduir un nou objecte javascript object des del servidor es pot fer de moltes maneres, però la pràctica més comuna per al servidor és implementar l'execució d'una funció de callback, amb l'objecte que volem obtenir com a argument d'aquest script. The callback function is just a function you have already set up on the client which the script you load calls at the point the script loads to process the data passed in to it.

Teoria JSONP

Exemple: crida bàsica

prova_jsonp.php:

<?php
//header('content-type: application/json; charset=utf-8');
header('content-type: text/plain; charset=utf-8'); //millor
header("access-control-allow-origin: *");

$data = array(1, 2, 3, 4, 5, 6, 7, 8, 9);

echo $_GET['callback'] . '('.json_encode($data).')';
?>

Aquest és el script bàsic que retorna una crida JSON. Aquest és el script remot (en aquest cas joanillo.org, al qual l'alumne no hi té accés). Per veure com funciona, podem fer:

([1,2,3,4,5,6,7,8,9])
foo([1,2,3,4,5,6,7,8,9])

Ara volem accedir localment a aquest recurs.

prova_jsonp.html:

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
</head>
<body>
<h1>Crida bàsica JSONP</h1>

  <p><button type="button" id="button" onclick="startFetch()">Fetch data</button></p>
  <div id="info"></div>

  <script type="text/javascript">
    
    var textbox = document.getElementById("textbox");
    var button = document.getElementById("button");
    var tempscript;
    
    function startFetch() {
      tempscript = document.createElement("script");
      tempscript.type = "text/javascript";
      tempscript.id = "tempscript";
      tempscript.src = "http://joanqc.no-ip.biz/iesbalmes/wec/prova_json.php?callback=JSONPHandler";
      document.body.appendChild(tempscript);
    }
    
    function JSONPHandler(data) {
      //alert(data);
      document.getElementById("info").innerHTML = data;
    }
    
  </script>
    
</body>
</html>

Aquest script cerca les dades JSON en el servidor, que són retornades en forma de funció de callback, que és una funció que es processa en el cantó del client. És d'aquesta manera que aconseguim rebre dades d'un servidor remot.

Exemple: agafar informació aleatòria de la wikipèdia

Executarem el següent script que es presenta a continuació. Fixem-nos que el script el podem executar localment (i agafarà informació remota de la wikipèdia, per tant estem sobre-passant la restrició del cross domain):

  • file:///home/joan/M06_WEC_1516/UF4/jsonp/wikipedia.html (has de ficar la teva ruta)

wikipedia.html:

<meta name="description" content="Demo to fetch a random extract of text from Wikipedia" />
<!DOCTYPE html>
<html>
<body>

  <p><textarea id="textbox" style="width:350px; height:150px"></textarea></p>
  <p><button type="button" id="button" onclick="startFetch(100, 500)">
    Fetch random Wikipedia extract</button></p>
  
  <script type="text/javascript">
    
    var textbox = document.getElementById("textbox");
    var button = document.getElementById("button");
    var tempscript = null, minchars, maxchars, attempts;
    
    function startFetch(minimumCharacters, maximumCharacters, isRetry) {
      if (tempscript) return; // a fetch is already in progress
      if (!isRetry) {
        attempts = 0;
        minchars = minimumCharacters; // save params in case retry needed
        maxchars = maximumCharacters;
        button.disabled = true;
        button.style.cursor = "wait";
      }
      tempscript = document.createElement("script");
      tempscript.type = "text/javascript";
      tempscript.id = "tempscript";
      tempscript.src = "http://en.wikipedia.org/w/api.php"
        + "?action=query&generator=random&prop=extracts"
        + "&exchars="+maxchars+"&format=json&callback=onFetchComplete&requestid="
        + Math.floor(Math.random()*999999).toString();
      document.body.appendChild(tempscript);
      // onFetchComplete invoked when finished
    }
    
    function onFetchComplete(data) {
      document.body.removeChild(tempscript);
      tempscript = null
      var s = getFirstProp(data.query.pages).extract;
      s = htmlDecode(stripTags(s));
      if (s.length > minchars || attempts++ > 5) {
        textbox.value = s;
        button.disabled = false;
        button.style.cursor = "auto";
      } else {
        startFetch(0, 0, true); // retry
      }
    }
    
    function getFirstProp(obj) {
      for (var i in obj) return obj[i];
    }
    
    // This next bit borrowed from Prototype / hacked together
    // You may want to replace with something more robust
    function stripTags(s) {
      return s.replace(/<\w+(\s+("[^"]*"|'[^']*'|[^>])+)?>|<\/\w+>/gi, "");
    }
    function htmlDecode(input){
      var e = document.createElement("div");
      e.innerHTML = input;
      return e.childNodes.length === 0 ? "" : e.childNodes[0].nodeValue;
    }
    
  </script>
    
</body>
</html>

Si ens fixem bé, des del nostre script local fem una crida remota a la següent url:

Aquesta crida retorna un string JSON, i ens hem de fixar en la part extract, que és la que ens interessa.

/**/onFetchComplete({"batchcomplete":"","continue":{"grncontinue":"0.208827411977|0.208827468372|31983552|0","continue":"grncontinue||"},"query":{"pages":{"16770230":{"pageid":16770230,"ns":10,"title":"Template:Ghotki","extract":"<p><b>How to manage this template's initial visibility</b><br />\nTo manage this template's visibility when it first appears, add the parameter:</p>\n<dl>\n<dd><code class=\"nowrap\">|state=collapsed</code> to show the template in its collapsed state, i.e. hidden apart from its titlebar \u2013 e.g. <code>{{Ghotki |state=collapsed}}</code></dd>\n<dd><code class=\"nowrap\">|state=expanded</code> to show the template in its expanded state, i.e. fully visible \u2013 e.g. <code>{{Ghotki |state=expanded}}</code></dd>\n</dl>..."}}}})

Cada crida apareixerà una informació diferent, doncs és aleatori.

Per tant, fixem-nos com, des d'un fitxer local, estem accedint a un recurs remot (em salto la cross-domain restriction).

És important notar la funció de callback:

...callback=onFetchComplete...

és la funció que s'executarà quan la crida s'hagi completat. En aquesta funció es defineix què fem quan s'ha acabat d'executar el script, que bàsicament és posar la informació que ens ha retornat la wikipedia en el nostre Textarea.

De l'estudi pormenoritzat d'aquest codi en podem aprendre moltes coses. Fixa't com es crea un script al vuelo i s'executa en l'event de clicar el botó. I com s'executa la funció de callback que al final ens omple en el texarea la informació que ens interessa.

Exemple: Recuperar informació JSON en un servidor (llista d'insectes)

Disposem en un servidor remot d'una llista dels insectes en format JSON. El script PHP retorna la informació com a funció de callback.

insectes_jsonp.php:

<?php
$callback = $_GET['callback'];
$data = "{\"insectes\": [{ \"genere\":\"Acherontia\" , \"especie\":\"atropos\" },{ \"genere\":\"Onychogomphus\" , \"especie\":\"forcipatus\" }]}";
echo $callback.'('.$data.')';
?> 

Podem accedir al script php, que veiem com retorna un objece json:

En aquest cas, el script PHP conté la informació dels insectes com a cadena, però podria ser ben bé una consulta a una base de dades.

Podem recuperar la informació remota des d'un recurs local o d'un altre domini: (aquí has de posar la teva ruta en el teu servidor web)

llista_insectes.html

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
</head>
<body>
<h1>Crida bàsica JSONP. Llista d'insectes</h1>

  <div id="info"></div>

  <script type="text/javascript">
    
    var textbox = document.getElementById("textbox");
    var button = document.getElementById("button");
    var tempscript;
    startFetch();
    
    function startFetch() {
      tempscript = document.createElement("script");
      tempscript.type = "text/javascript";
      tempscript.id = "tempscript";
      tempscript.src = "http://joanqc.no-ip.biz/iesbalmes/wec/insectes_jsonp.php?callback=JSONPHandler";
      document.body.appendChild(tempscript); 
    }
    
    function JSONPHandler(data) {
      console.log(data);
      var jsondata=eval( data );
      var vinsectes=jsondata.insectes;
      var output = '<ul>';
      for (var i = 0; i < vinsectes.length; i++){
        output += '<li>';
        output += vinsectes[i].genere + ' ' + vinsectes[i].especie;
        output += '</li>';
      }
      document.getElementById("info").innerHTML = output;
    }
    
  </script>
    
</body>
</html>

De fet, estem oferint un servei web públic per tal de què gent externa pugui utilitzar la informació del domini http://joanqc.no-ip.biz. Això és el que intentarem fer en la següent pràctica: accedir a les receptes de cuina que estaran allotjades en un repositori públic.

NOTA: la funció eval() avalua la cadena de text (que té format json) i com que té format JSON ho tracta com a objecte JSON. En aquest cas, si no posem eval() l'avaluació també es fa de forma correcta.

En aquest cas concret no tenim cap objecte JSON. Rebem una cadena de text i obtenim un objecte javascript. Per tant, no hi ha objecte JSON. (si tinguéssim objecte JSON el que s'ha de fer és JSON.parse (ja ho veurem).

Exercici: open data. Dades obertes de l'Àrea Metropolitana de Barcelona

L'AMB proporciona informació en format JSON (i també XML i CSV) de diversos aspectes de l'Àrea Metropolitana.

Per exemple, si ens fixem en Parcs, tenim la següent url amb informació JSON:

Se't proposa que, com a mínim, facis una llista de tots els parcs de l'Àrea Metropolitana i una foto de cadascun. I amb la mateixa url, també podem passar una funció de callback:

RecipeXML i JSONP

Al començament de la UF, quan volíem accedir als fitxers XML que estan en un repositori comú i públic, vam tenir problemes degut a la política de domini únic. Ara que ja coneixem JSONP, estem en condicions de poder subsanar aquest problema.

Això és el que farem en la pràctica corresponent.



creat per Joan Quintana Compte, febrer 2022