Diferència entre revisions de la pàgina «JSONP: JSON with Padding»

De wikijoan
Salta a la navegació Salta a la cerca
Línia 318: Línia 318:
 
*http://opendata.amb.cat/equipaments/search?subambit=parcs&callback=foo
 
*http://opendata.amb.cat/equipaments/search?subambit=parcs&callback=foo
  
==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:
 
<pre>
 
$ 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)
 
</pre>
 
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:
 
*[[Arduino#Sensor_de_temperatura_digital_DS18B20]]
 
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:
 
*http://79.151.101.86
 
que ens ha de tornar la següent informació:
 
<pre>
 
JSONPHandler([22.6])
 
</pre>
 
É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:
 
*[[Arduino#Sensor_de_temperatura_digital_DS18B20]]
 
Per codificar la part de l'arduino tenim un enllaç molt clarificador:
 
*https://gist.github.com/sfentress/2126328
 
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):
 
<pre>
 
#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");
 
  }
 
}
 
</pre>
 
===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''':
 
<pre>
 
<!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>
 
</pre>
 
I podem accedir a la temperatura des de qualsevol domini, fins i tot localment o des d'un domini extern:
 
*file:///home/joan/test_temperatura.html
 
*http://joanqc.no-ip.biz/iesbalmes/wec/test_temperatura.html
 
 
==RecipeXML i JSONP==
 
==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.
 
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.

Revisió del 15:19, 1 feb 2022

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://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": [{ "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\":\"Onychogomphus\" , \"especie\":\"forcipatus\" }]}";
echo $callback.'('.$data.')';
?> 

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

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.

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:

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