JSON
AJAX
Børre Stenseth
Moduler>AJAX>Generelt>Andre domener

AJAX fra andre domener

Hva
Data fra andre domener

Vi skal bruke vevstedet GeoNames [1] som eksempel. geonames tilby en rekke tjenseter som har med geografisk informasjon å gjøre. Vi kan laste ned filer og databaser og vi kan bruke webservices. Begrepet webservice brukes i romslig betydning, og innebærer ikke SOAP- eller WSDL-formater. Det vi kan gjøre er rett og slett å bruke en url med parametere for å hente ferdigpakkede data. Pakkene er enten JSON eller XML.

Du kan eksperimentere med de interaktive tjenestene på geonames, eller du kan forsøke å kopiere følgende URL'er i nettleserens addressfelt:

ws.geonames.org/postalCodeSearch?postalcode=1850&country=NO
ws.geonames.org/postalCodeSearchJSON?postalcode=1880&country=NO
ws.geonames.org/findNearbyPlaceName?lat=59.5166667&lng=11.2666667&radius=20
ws.geonames.org/findNearbyPlaceNameJSON?lat=59.5166667&lng=11.2666667&radius=20
		

Dette er bare noen av de tjenestene som er tigjengelige. Parameterne i eksemplene ovenfor er vel rimelig selvforklarende. I en rekke av de andre tjenestene må vi sette oss litt inn i hvordan spørsmål kan parametriseres og hvordan svarene skal tolkes.

Hvis jeg nå ønsker å bruke noen av tjenestene på mine vevsider, er det selvsagt fristende å bruke AJAX. De fleste tjenestene er slik at de gir svaret enten i JSON- eller XML-format. Problemet er at tjenesten ligger i et annet domene enn min vevside, og jeg kan ikke uten videre sende en XMLHttpRequest. Vi skal se på to måter å løse dette på.

Indirekte

indirekte

Vi skriver et program på tjeneren, geoaccess, som henter data fra geoNames. Vi kommuniserer fra vevsiden til dette programmet med AJAX.

Dette innbærer at AJAX-kommunikasjonen foregår innen samme domene.

Direkte

direkte

Vi bruker en litt tvilsom strategi som gjør det mulig å hente materialet direkt via AJAX.

Dette innbærer at AJAX-kommunikasjonen foregår på tvers av domener.

Som eksempel bruker vi for enkelhets skyld tjenesten findNearbyPlaceNameJSON. Vi vil altså finne vilke steder som ligger innenfor en bestemt radius fra de koordinatene som er registrert for et beestemt postnummer. Vi holder oss til norske postnummere.

Indirekte

Vi begynner med et skript som skal gå på tjeneren. Skriptet er laget slik at det videreformidler en forespørsel, tar mot svaret og sender det direkte tilbake til vevsiden. Skriptet er laget så generelt som mulig og er ikke begrenset til den oppgaven vi har satt oss.

#! /usr/bin/python2
import urllib
import cgi
import cgitb; cgitb.enable()
# Attempt to convey an AJAX-request to
# geonames as general and smooth as possible
form=cgi.FieldStorage()
print 'Content-type: text/html charset=iso-8859-1\n'
#expect to find: function and any set of parameters
function='postalCodeSearch'
if form.has_key('function'):
    function=form['function'].value
lookUp='http://ws.geonames.org/'+function
ks=form.keys()
params=''
for k in ks:
    if k=='function':
        continue
    params+=k+'='+form[k].value+'&'
if len(ks) > 1:
    params=params[:-1]
lookUp+='?'+params
try:
    f=urllib.urlopen(lookUp)
    S=f.read()
    f.close()
    S=S.decode('utf')
    S=S.encode('iso-8859-1')
    print S
except:
    print 'cannot load'

Fra vevsiden bruker vi skriptet slik:

// relies on jquey.js
function findNearbyPostalCodesJSON(postnummer,radius)
{
    var params='function=findNearbyPostalCodesJSON&country=NO&postalcode='
                    +postnummer+'&radius='+radius;
   $.ajax({
    url:'http://www.it.hiof.no/~borres/cgi-bin/ajax/geonames/geoaccess.py',
    data:params,
    success:function(data)
    {
        processNearbyPlaceNameJSON(data);
    },
    error:function(data)
    {
        $('#result').html("Could not access content");
    }
    });
}
 function processNearbyPlaceNameJSON(jData)
 {
    try{
        p=eval('('+jData+')');
        var pcodes= p.postalCodes;
        T=''
        for(ix=0;ix<pcodes.length;ix++)
        {
            var navn=pcodes[ix].placeName+'<br/>';
            if(T.indexOf(navn)==-1)
                T+=navn;
        }
        document.getElementById('result').innerHTML=T;
    }
    catch(e)
    {
        document.getElementById('result').innerHTML=
                            'Finner ikke noe';
    }
}
Du kan teste og inspisere kildekoden til HTML-fila herhttp://www.it.hiof.no/~borres/dw/crossdomain/indirect.html

Direkte

Nøkkelen er følgende script av Jason(!) Levitt [2] . Sikkerhetsadvarselen er lagt til i den versjonen som anbefales brukes av geonames.

// JSONscriptRequest -- a simple class for accessing Yahoo! Web Services
// using dynamically generated script tags and JSON
//
// Author: Jason Levitt
// Date: December 7th, 2005
//
// A SECURITY WARNING FROM DOUGLAS CROCKFORD:
// "The dynamic <script> tag hack suffers from a problem. It allows a page 
// to access data from any server in the web, which is really useful. 
// Unfortunately, the data is returned in the form of a script. That script 
// can deliver the data, but it runs with the same authority as scripts on 
// the base page, so it is able to steal cookies or misuse the authorization 
// of the user with the server. A rogue script can do destructive things to 
// the relationship between the user and the base server."
//
// So, be extremely cautious in your use of this script.
//
// Constructor -- pass a REST request URL to the constructor
//
function JSONscriptRequest(fullUrl) {
    // REST request path
    this.fullUrl = fullUrl; 
    // Keep IE from caching requests
    this.noCacheIE = '&noCacheIE=' + (new Date()).getTime();
    // Get the DOM location to put the script tag
    this.headLoc = document.getElementsByTagName("head").item(0);
    // Generate a unique script tag id
    this.scriptId = 'YJscriptId' + JSONscriptRequest.scriptCounter++;
}
// Static script ID counter
JSONscriptRequest.scriptCounter = 1;
// buildScriptTag method
//
JSONscriptRequest.prototype.buildScriptTag = function () {
    // Create the script tag
    this.scriptObj = document.createElement("script");
    
    // Add script object attributes
    this.scriptObj.setAttribute("type", "text/javascript");
    this.scriptObj.setAttribute("src", this.fullUrl + this.noCacheIE);
    this.scriptObj.setAttribute("id", this.scriptId);
}
 
// removeScriptTag method
// 
JSONscriptRequest.prototype.removeScriptTag = function () {
    // Destroy the script tag
    this.headLoc.removeChild(this.scriptObj);  
}
// addScriptTag method
//
JSONscriptRequest.prototype.addScriptTag = function () {
    // Create the script tag
    this.headLoc.appendChild(this.scriptObj);
}

Vi bruker dette biblioteket fra vår egen javascriptkode slik:

// this function will be called by our JSON callback
// the parameter jData will contain an array with postalcode objects
function processNearbyPlaceNameJSON(jData) 
{
  if (jData == null) {
    document.getElementById('result').innerHTML='ikke noe svar';
    return;
  }
  try{
      var pcodes = jData.postalCodes;        
      if (pcodes.length > 0) 
      {
            T=''
            for(ix=0;ix<pcodes.length;ix++)
            {
                var navn=pcodes[ix].placeName+'<br/>';
                if(T.indexOf(navn)==-1)
                    T+=navn;
            }
            document.getElementById('result').innerHTML=T;
      }
      else
          document.getElementById('result').innerHTML='fant ikke noe';
          
    }
    catch(E)
    {
          document.getElementById('result').innerHTML='feil spørsmål';
    }
}
function findNearbyPostalCodesJSON(pcode,radius) 
{
  request = 'http://ws.geonames.org/findNearbyPostalCodesJSON?postalcode=' 
            + pcode  + '&country=NO&radius='+radius 
            + '&callback=processNearbyPlaceNameJSON';
  // Create a new script object
  aObj = new JSONscriptRequest(request);
  // Build the script tag and execute it
  aObj.buildScriptTag();
  aObj.addScriptTag();
}
Du kan teste og inspisere kildekoden til HTML-fila herhttp://www.it.hiof.no/~borres/dw/crossdomain/direct.html

Forklaring

Dersom vi skal oppsummere kort hvordan denne koden virker så må det bli omtrent slik:

  1. Vi etablerer et scriptelement som dersom vi skulle skrive det i klartekst blir slik( jeg har lagt inn linjeskift av praktiske årsaker):
    <script
      type="text/javascript"
      src="http://ws.geonames.org/findNearbyPostalCodesJSON?
           postalcode=1850&
           country=NO&
           radius=2&
           callback=processNearbyPlaceNameJSON&
           noCacheIE=1193327617484"
      id="YJscriptId1">
    </script>
    	
    Dette bygges opp ved hjelp av funksjonene: JSONscriptRequest(request) og buildScriptTag(). Merk at navnet på den funksjonen som skal ta mot resultatet inngår i parameter lista: callback=processNearbyPlaceNameJSON.
  2. I det øyeblikk vi setter inn dette script-elementet, addScriptTag(), som barn til head-elementet blir det evaluert. Denne evalueringen innebærer at nettleseren forsøker å hente skriptet. Nøkkelen til å forstå hva som skjer er en analyse av hva som kommer tilbake. Med parametrene ovenfor vil vi få følgende tilbake (jeg har lagt inn linjeskift av praktiske årsaker):
    processNearbyPlaceNameJSON(
    {"postalCodes":[
            {"adminName2":"EIDSBERG","adminCode2":"0125",
             "adminCode1":"01","postalCode":"1850",
             "countryCode":"NO","lng":11.3333333,"placeName":"Mysen","lat":59.55,
             "adminName1":"Oestfold"},{"adminName2":"EIDSBERG","adminCode2":"0125",
             "adminCode1":"01","postalCode":"1851",
             "countryCode":"NO","lng":11.3333333,"placeName":"Mysen","lat":59.55,
             "adminName1":"Oestfold"},
             {"adminName2":"EIDSBERG","adminCode2":"0125",
              "adminCode1":"01","postalCode":"1859",
              "countryCode":"NO","lng":11.3333333,"placeName":"Slitu","lat":59.55,
              "adminName1":"Oestfold"}
               ]});
    	 
    Det vil si at vi får tilbake et funksjonskall med et JSON-objekt som parameter. GeoNames-serveren produserer dette formatet, og kan gjøre det fordi vi har sendt over navnet på vår mottagerfunksjon som parameter.
  3. Funksjonskallet effektueres siden det oppfattes om en JavaSciptlinje, som skal utføres på linje med alle JavaScript-setninger som parses i scriptelementet. Og vår funksjon, processNearbyPlaceNameJSON, utføres. Merk at parameteren som er et JSON-objekt som også evalueres.

Vi ser at det ikke er noe XMLHttpRequest involvert. Alt er basert på at vi "sniker" oss utenfor kontrollmekanismene ved å manipulere dom-treet ved å sette in et scriptelemment som evalueres.

Eksperiment (lokalt)

Vi forsøker å anvende denne teknikken mot en serverfunksjon vi skriver selv. Vi gjør dette på samme domene som web-siden, altså ikke fremmed domene.

Vi skriver følgende script på tjeneren:

#! /usr/bin/python
import cgi
print 'Content-type: text/plain\n'
form=cgi.FieldStorage()
callback='error'
if form.has_key('callback'):
	callback=form['callback'].value
print callback+'({"melding":"Halleluja"});'

Vi bruker biblioteket fra javascript koden over, og skriver funksjonene:

function takeIt(melding)
{
		alert('Im back, '+melding.melding);
}
function doRequest(theURL)
{
  request = theURL+'?callback=takeIt';
  // Create a new script object
  aObj = new JSONscriptRequest(request);
  // Build the script tag and execute it
  aObj.buildScriptTag();
  aObj.addScriptTag();
}
Du kan teste herhttp://www.it.hiof.no/~borres/cgi-bin/ajax/callback/index2.html

En enda enklere løsning

#! /usr/bin/python
import cgi
print 'Content-type: text/plain\n'
form=cgi.FieldStorage()
callback='error'
if form.has_key('callback'):
	callback=form['callback'].value
print callback+'("Halleluja");'
function doRequest(theURL)
{
  request = theURL+'?callback=alert';
  // Create a new script object
  aObj = new JSONscriptRequest(request);
  // Build the script tag and execute it
  aObj.buildScriptTag();
  aObj.addScriptTag();
}
Du kan teste herhttp://www.it.hiof.no/~borres/cgi-bin/ajax/callback/index3.html

Tja

Hvilken av teknikkene skal vi bruke ? Det er fristende å bruke den direkte metoden. Den krever lite kode og er åpenbart rask.

Det er imidlertid noen negative momenter/bergrensninger som det er verdt å merke seg:

  • teknikken omgår de sikkerhetselementene som er innebygd i domenegrensene for AJAX.
  • Kan ikke motta responsekoder, fungerer bare når forespørselene returnerer kode: 200.
  • Fungerer bare med get, og bare asynkront.
  • vi ser bort fra de mulighetene vi har for datakontroll og formatering som vi lett kan realisere i et tjenerskript.
Referanser
  1. GeonamesGeoNameswww.geonames.org/14-03-2010
  1. JSON and the Dynamic Script TagJason LevittXML.comwww.xml.com/pub/a/2005/12/21/json-dynamic-script-tag.html14-03-2010
  1. Cross Domain AjaxSNOOK.casnook.ca/archives/javascript/cross_domain_aj/14-03-2010
Vedlikehold

B. Stenseth, august 2007

(Velkommen) Moduler>AJAX>Generelt>Andre domener (AJAX i .NET)