Wednesday, September 22, 2010

JSONP and cross-domain Ajax calls

I was looking at this demo and trying to figure out how the cross-domain Ajax works:

http://www.prettyklicks.com/demo/fbjson.php

Some background info is here:
http://www.ibm.com/developerworks/library/wa-aj-jsonp1/?ca=dgr-jw64JSONP jQuery&S_TACT=105AGY46&S_CMP=grsitejw64

Here's the nitty/gritty - it boils down to avoid using the XMLHttpRequest object, injecting a <script src> tag that retrieves a JSON-encoded response but wrapped in a callback function, and then creating the callback function in the native browser that then gets invoked.

Both client and server need to be equipped to handle this technique. On the client side, JQuery does all of this magic under the covers with its JSONP implementation. On the server side, the server needs to wrap a JSON data if a callback= parameter is specified.

The JavaScript code has this line:
var url = "http://graph.facebook.com/prettyklicks/feed?limit=5&callback=?";
But the actual URL call translates to:
http://graph.facebook.com/prettyklicks/feed?limit=5&callback=jsonp1285193963567
It turns out there's a bit of jQuery and server-side magic going on. On the server side, It seems that when this callback= function will wrap a function that can then be executed by the DOM tree, similar to what's being done here:

http://code.google.com/p/django-rest-interface/issues/detail?id=39

So the server will return a result such as the following (note the jsonp1285197073066() function):
jsonp1285197073066({
   "data": [
      {
         "id": "45075726743_434059296743",
         "from": {
            "name": "Jason John Adams",
            "id": "1205845313"
         },
         "to": {
            "data": [
               {
                  "name": "Pretty Klicks",
                  "category": "Artist",
                  "id": "45075726743"
               }
            ]
         },
.
.
.
});
On the client-side, the JQuery's ajax() function has some checking for whether the Ajax() call actually seems to have a regexp check to see if there is a & or ? appended at the end of the URL call for JSON Ajax calls. It uses this regexp to replace the ? with a generated function name to call.
jsre = /=\?(&|$)/,
This section seems to rename the callback to ?jsonp=jsc (i.e. jsonp1285193963567).
var jsc = now(),
.
.
// Build temporary JSONP function
  if ( s.dataType === "json" && (s.data && jsre.test(s.data) || jsre.test(s.url)) ) {
   jsonp = s.jsonpCallback || ("jsonp" + jsc++);



   // Replace the =? sequence both in the query string and the data
   if ( s.data ) {
    s.data = (s.data + "").replace(jsre, "=" + jsonp + "$1");
   }


This section then creates a function based on the callback name (jsonp):

s.url = s.url.replace(jsre, "=" + jsonp + "$1");

   // We need to make sure
   // that a JSONP style response is executed properly
   s.dataType = "script";

   // Handle JSONP-style loading
   window[ jsonp ] = window[ jsonp ] || function( tmp ) {
    data = tmp;
    success();
    complete();
    // Garbage collect
    window[ jsonp ] = undefined;

    try {
     delete window[ jsonp ];
    } catch(e) {}

    if ( head ) {
     head.removeChild( script );
    }
   };
  }
So what's happening is that jQuery creates a function that will take as an argument tmp, which corresponds to the data from the JSON response. So when the script finishes loading, it will be executed and the Ajax callback is called.
function success() {
   // If a local callback was specified, fire it and pass it the data
   if ( s.success ) {
    s.success.call( callbackContext, data, status, xhr );
   }

   // Fire the global callback
   if ( s.global ) {
    trigger( "ajaxSuccess", [xhr, s] );
   }
  }
The last part of the equation is that script tags are inserted in the HEAD document with the src= set to the external URL. By doing is this way, the result will be evaluated by the function that we previously added to the window object.
// If we're requesting a remote document
  // and trying to load JSON or Script with a GET
  if ( s.dataType === "script" && type === "GET" && remote ) {
   var head = document.getElementsByTagName("head")[0] || document.documentElement;
   var script = document.createElement("script");
   script.src = s.url;
   if ( s.scriptCharset ) {
    script.charset = s.scriptCharset;
   }

   // Use insertBefore instead of appendChild  to circumvent an IE6 bug.
   // This arises when a base node is used (#2709 and #4378).
   head.insertBefore( script, head.firstChild );

   // We handle everything using the script element injection
   return undefined;
  }
This success function can then be used by the $.getJSON call from http://www.prettyklicks.com/demo/fbjson.php to retrieve and process the data:

function(json){
      var html = "
    "; //loop through and within data array's retrieve the message variable. $.each(json.data,function(i,fb){ html += "
  • " + fb.message + "
  • "; }); html += "
";

No comments:

Post a Comment