JSONP: JSON with Padding

De Wikijoan
Dreceres ràpides: navegació, cerca

Contingut

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.

Desenvolupament

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://arthropoda.joanillo.org/prova_jsonp.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. file:///home/joan/M06_WEC_1516/UF4/jsonp/prova_jsonp.html (has de ficar la teva ruta)

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):

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": [{ "genere":"Acherontia" , "especie":"atropos" },{ "genere":"Acmaeodera" , "especie":"rubromaculata" }]})
foo({"insectes": [{ "genere":"Acherontia" , "especie":"atropos" },{ "genere":"Acmaeodera" , "especie":"rubromaculata" }]})

Pots provar també la versió llarga:

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.

insectes_jsonp_curt.php:

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

Podem recuperar la informació remota des d'un recurs local o d'un altre domini:

llista_insectes.html

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

  <p>
  <select name="insectes" onchange="startFetch(this.value)">
  <option value="">Escull una de les llistes</option>
  <option value="insectes_jsonp_curt.php">Llista curta</option>
  <option value="insectes_jsonp.php">Llista llarga</option>
  </select>
  <div id="info"></div>

  <script type="text/javascript">
    
    var textbox = document.getElementById("textbox");
    var button = document.getElementById("button");
    var tempscript;
    
    function startFetch(url) {
      if (url!="") {
        tempscript = document.createElement("script");
        tempscript.type = "text/javascript";
        tempscript.id = "tempscript";
        tempscript.src = "http://arthropoda.joanillo.org/"+url+"?callback=JSONPHandler";
        document.body.appendChild(tempscript);
      } else {
        document.getElementById("info").innerHTML = "";
      } 
    }
    
    function JSONPHandler(data) {
      //alert(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 arthropoda.joanillo.org. Això és el que intentarem fer en la següent pràctica: un servei web per accedir a la temperatura de la ciutat de Barcelona.

Exercici: conversió euro-dòlar

Tenim una url que ens dóna la conversió actual entre euro i dòlar, en format JSON:

Amb tot el que hem vist fins ara et serà fàcil fer el següent exercici: mostrar en un pàgina web local, i amb tamany extra-gran, la conversió actual entre l'euro i el dòlar.

Solució::

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
</head>
<body>
<h1>Conversió euro-dolar. API-JSONP</h1>

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

  <script type="text/javascript">
    String.prototype.replaceAll = function(searchStr, replaceStr) {
        var str = this;
        
        // escape regexp special characters in search string
        searchStr = searchStr.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
        
        return str.replace(new RegExp(searchStr, 'gi'), replaceStr);
    };


    function euro_dolar() {
      tempscript = document.createElement("script");
      tempscript.type = "text/javascript";
      tempscript.id = "tempscript";
      tempscript.src = "http://api.fixer.io/latest?callback=JSONPHandler";
      document.body.appendChild(tempscript);
    }
    
    function JSONPHandler(data) {
      console.log(data);
      var JSONdata = JSON.stringify(data);
      console.log(JSONdata);
      var JSONdataObj=JSON.parse(JSONdata);
      console.log(JSONdataObj);
      var USD=JSONdataObj.rates.USD;
      console.log(USD);
      info = document.getElementById("info");
      info.style.color = "blue";
      info.style.fontSize = "200px";
      info.innerHTML = USD;
    }
    
  </script>
    
<body onload="euro_dolar()">
<p id="info"></p>
</html>

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:

Servei Web Temperatura de Barcelona (projecte d'electrònica)

NOTA: l'Arduino (i per tant la lectura real de la temperatura) estaran disponibles entre les dates xx/xx/2015 i xx/xx/2015.

Volem fer un petit projecte d'electrònica. Connectem un sensor de temperatura digital a un arduino equipat d'una Ethernet Shield, en una ADSL domèstica. Configurem l'Arduino per tenir una IP fixa en la xarxa local: 192.168.1.40. Necessitem saber la IP pública del router:

$ wget http://checkip.dyndns.org/ -O - -o /dev/null | cut -d: -f 2 | cut -d\< -f 1
79.151.101.86 (aquest valor pot canviar)

i en aquest router obrim el port 80 i les peticions que arriben a aquest port les redirigim a l'Arduino (192.168.1.40).

En l'Arduino hem muntat el circuit per llegir la temperatura. El projecte bàsic és poder llegir la temperatura en l'Arduino, i fer un servidor web per llegir la temperatura remotament:

En aquests moments ja funcionava una pàgina web, on es veia el valor de la temperatura, com es veu a la foto. El nostre arduino fa de servidor web que serveix una pàgina web quan fem un request des del client. Però això no és el que volem. El que volem és que l'arduino serveixi una pàgina web en format callback i que contingui la temperatura en format JSON.

Farem una aplicació per a l'Arduino que consistirà en servir una pàgina web amb format JSON amb el valor de la temperatura. Aquest script estarà disponible a la següent url:

que ens ha de tornar la següent informació:

JSONPHandler([22.6])

És a dir, el valor de la temperatura incrustat en una funció de callback, on l'argument té format JSON.

Hem fet un servei web que proporciona la temperatura de Barcelona a temps real. Un cop tenim aquest recurs disponible i funcionant, a l'alumne li serà fàcil fer una petita aplicació web on mostri en una cada div el valor real de la temperatura.

L'explicació de com es fa el muntatge del sensor de temperatura digital DS18B20:

Per codificar la part de l'arduino tenim un enllaç molt clarificador:

En l'exemple de l'enllaç es llegeix un sensor analògic. El nostre sensor és digital, per tal adaptem el codi per llegir el valor del sensor DS18B20. El resultat és el fitxer arduino_jsonp.ino (script de l'Arduino):

#include <SPI.h>
#include <Ethernet.h>
#include <OneWire.h>
#include <DallasTemperature.h>

byte mac[] = { 0x9A, 0xA2, 0xDA, 0x00, 0x0A, 0xDC };
IPAddress ip(192, 168, 1, 40);

EthernetServer server(80);

// Data wire is plugged into pin 2 on the Arduino
#define ONE_WIRE_BUS 2
 
// Setup a oneWire instance to communicate with any OneWire devices 
// (not just Maxim/Dallas temperature ICs)
OneWire oneWire(ONE_WIRE_BUS);
 
// Pass our oneWire reference to Dallas Temperature.
DallasTemperature sensors(&oneWire);

float temp;

void setup() {
  // Open serial communications and wait for port to open:
  Serial.begin(9600);
  while (!Serial) {
    ; // wait for serial port to connect. Needed for native USB port only
  }

  sensors.begin();

  // start the Ethernet connection and the server:
  Ethernet.begin(mac, ip);
  server.begin();
  Serial.print("server is at ");
  Serial.println(Ethernet.localIP());
  
}


void loop() {
  // listen for incoming clients
  EthernetClient client = server.available();
  if (client) {
    Serial.println("new client");
    // an http request ends with a blank line
    boolean currentLineIsBlank = true;
    while (client.connected()) {
      if (client.available()) {
        char c = client.read();

        // send a standard http response header
        client.println("HTTP/1.1 200 OK");
        //client.println("Content-Type: application/json");
        client.println("Content-Type: text/plain"); //millor. necessari per mozilla?
        client.println(); //aquesta linia es important!
          
        Serial.print(" Requesting temperatures...");
        sensors.requestTemperatures(); // Send the command to get temperatures
        Serial.println("DONE");
        temp = sensors.getTempCByIndex(0);
        Serial.print("Temperature for Device 1 is: ");
        Serial.println(temp);
        client.print("JSONPHandler([");
        client.print(temp);
        client.print("])");    

        break;
    }
    }
    // give the web browser time to receive the data
    delay(1);
    // close the connection:
    client.stop();
    Serial.println("client disconnected");
  }
}

Aplicació web client

Ara que ja disposem del servei web, la única cosa que hem de fer és una petita pàgina web. En un div carregarem la informació de la temperatura, fent una crida asíncrona i remota mitjançant JSONP. L'exemple més senzill és:

test_temperatura.html:

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<style>
body {
	background-color: black;
	color: yellow;
}

#info {
	margin-left: 100px;
	font-size: 80px;
}
</style>
</head>
<body onload="startFetch()">
  <h1>Temperatura de Barcelona</h1>
  <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://79.151.101.86/temperaturabcn.php?callback=JSONPHandler";
      document.body.appendChild(tempscript);
    }
    
    function JSONPHandler(data) {
      //alert(data);
      document.getElementById("info").innerHTML = data;
    }
    
  </script>
    
</body>
</html>

I podem accedir a la temperatura des de qualsevol domini, fins i tot localment o des d'un domini extern:

RecipeXML i JSONP

Al començament de la UF, quan volíem accedir als fitxers XML que estan en un repositori comú, vam tenir problemes degut a la política de domini únic (http://localhost/M06b/receptes/index2.html). Ara que ja coneixem JSONP, estem en condicions de poder subsanar aquest problema.

La idea que s'ha comentat a classe és tenir un repositori amb tots els fitxers XML de receptes que van generant els alumnes. Aquest repositori estarà a:

En el servidor s'executa un script PHP que llegeix el fitxer xml. Per exemple:

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

$xmlrecipe = file_get_contents('recipes/'.$url);

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

I en el domini local (localhost o qualsevol altre domini diferent de joanqc.no-ip.biz), podem atacar aquests fitxers XML amb la tècnica JSONP, i parsejar-los.

script index_jsonp.html:

<html>
<head>
<meta http-equiv="content-type" content="text/html charset=utf-8" />
<title>Receptes AJAX</title>
<script>

function loadXMLDoc_jsonp(url)
{
      tempscript = document.createElement("script");
      tempscript.type = "text/javascript";
      tempscript.id = "tempscript";
      tempscript.src = "http://joanqc.no-ip.biz/iesbalmes/wec/receptes/read_xml_recipe.php?callback=JSONPHandler&url="+url;
      document.body.appendChild(tempscript);
}

function JSONPHandler(data) {
	//alert(data);
	//document.getElementById("info").innerHTML = data;

	txt="<h2>Ingredients</h2>";
	txt = txt + "<ul>";
	txt_ingredient = "";
	txt_quantity = "";
	txt_unit = "";
	txt_fooditem = "";

	//hem de parsejar el document XML que tenim a data
	//http://www.w3schools.com/xml/xml_parser.asp
	parser = new DOMParser();
	xmlDoc = parser.parseFromString(data,"text/xml");
	ingredients=xmlDoc.getElementsByTagName("ingredient");

	for (i=0;i<ingredients.length;i++) {
		//alert(ingredients[i].innerHTML);			
		for (k=0;k<ingredients[i].childNodes.length;k++) {			
			//alert(ingredients[i].childNodes[k].nodeName);
			if (ingredients[i].childNodes[k].nodeName == "quantity") txt_quantity = ingredients[i].childNodes[k].childNodes[0].nodeValue;
			if (ingredients[i].childNodes[k].nodeName == "unit") txt_unit = ingredients[i].childNodes[k].childNodes[0].nodeValue;
			if (ingredients[i].childNodes[k].nodeName == "fooditem") txt_fooditem = ingredients[i].childNodes[k].childNodes[0].nodeValue;
		}
		txt_ingredient = "<li>" + txt_fooditem + " (" + txt_quantity + " " + txt_unit + ")</li>"
		txt = txt + txt_ingredient;
	}
	txt = txt + "</ul>";

	document.getElementById('info').innerHTML=txt;
}

</script>
</head>
<body>
<h1>Receptes (JSONP)</h1>
Les receptes estan en un repositori remot:
<ul><li><a href="http://joanqc.no-ip.biz/iesbalmes/wec/receptes/recipes/">http://joanqc.no-ip.biz/iesbalmes/wec/receptes/recipes/</a></li></ul>

<div id="formulari">
<form name="frm_receptes" id="formu" action="#">
	<select name="recipes" onchange="loadXMLDoc_jsonp(this.value)">
	<option value="">Escull una recepta</option>
	<option value="corn-chowder.xml">corn chowder abs</option>
	<option value="bean-and-ham.xml">Bean and Ham abs</option>
	</select><br />
</form>
</div>
<div id="info">
</div>
</body>
</html>



creat per Joan Quintana Compte, gener 2016

Eines de l'usuari
Espais de noms
Variants
Accions
Navegació
Institut Jaume Balmes
Màquines recreatives
CNC
Informàtica musical
joanillo.org Planet
Eines