They do come with two caveats however.
Dynamic script nodes also allow one to do cross-domain remoting without setting off sirens and flashing lights in the browser. This opens up an inherrent security problem. If you - as the developer of this application - do not have control over the source of your script, then you cannot trust that it will do nothing malicious. I'll skirt the issue in this article which concentrates on issue number two.
Using dynamic script nodes involves adding a <script> node to the DOM. For a simple application that just makes one or two calls, this isn't much of an issue, but for a complex application, the DOM can easily grow large, which starts to push memory limits of browsers.
An application that makes use of the flickr APIs or Yahoo! APIs that return JSON data suitably wrapped in a callback of your choice could hit these limits if built entirely in Javascript.
The call would be something like this:
var scr = document.createElement("script"); scr.type="text/javascript"; scr.src = "http://api.flickr.com/services/rest/?method=flickr.interestingness.getList" + "&api_key=xxxxxxxxx&format=json&jsoncallback=myfunc"; document.body.appendChild(scr);And a large number of such calls will add a lot of <script> nodes to the document, all of which have already served their purpose and are no longer needed.
These script nodes can be safely removed as soon as they've called their callback function. One needn't even wait for the callback to return, which means that the removal could be done within the callback itself.
While simple on the face of it, there's a bunch of housekeeping that goes with making this possible, and this isn't logic that all developers should be required to code into all their callbacks. It really should be generic enough to be separated out.
The code I came up with looks like this:
// The global callbacks array that stores a reference to all // active callbacks. This array is sparse. var callbacks = []; function call_api_method(url, callback) { // We create the script element first so that it will // be available to the closure var scr = document.createElement("script"); // Now add our custom callback to the callback array // so that the added script node can access it var i = callbacks.length; callbacks[i] = function(json) { // first remove the script node since we no longer // need it document.body.removeChild(scr); // On failure display a generic error message if(json.stat == 'fail') alert("Error calling method: " + json.errorString); // On success, call the callback else callback(json); // Clear out our entry in the callback array since we // don't need it anymore callbacks[i] = null; delete callbacks[i]; // and clear out all outer variables referenced in the // closure to prevent memory leaks in some browsers i = scr = callback = null; }; scr.type="text/javascript"; // add our own callback function to the script url // the resource sitting at the other end of this url // needs to know what to do with this argument scr.src = url + '&callback=' + encodeURIComponent('callbacks["' + i + '"]'); // finally, add the script node to initiate data transfer document.body.appendChild(scr); }A few things stand out:
- The reference to the script node is held by the callback since it's a closure
- A global reference to the callback is necessary so that the script can access it
- We need to clean up in a particular order to avoid memory leaks caused by circular references
This code is moderately simplified from what it would be if it were truly generic. Additions that would need to be made include:
- Encapsulate the callbacks array and the method into an object so that we don't pollute global space.
- Instead of accepting a callback function, accept a callback object with separate methods for success and failure as well as callback arguments and execution scope.
- For most applications, the url would be similar for various calls differring only in small parts, eg: the method name in the Flickr API. It would be good if the above class were an abstract base class with specialisations for different services providing the base API url.
This is what I did for the flickr API:
var apikey = "xxxxxxxxx"; var apiurl = "http://api.flickr.com/services/rest?api_key=" + apikey + "&format=json&method=flickr."; var usernamemap = {}; function call_api_method(method, uname, callback, params) { var scr = document.createElement("script"); var cb = function(json) { document.body.removeChild(scr); usernamemap[uname].callback = null; if(json.stat == 'fail') alert("Error " + json.code + " calling flickr." + method + ": " + json.message); else callback(json, uname); scr = uname = method = callback = null; }; usernamemap[uname] = usernamemap[uname] || {}; usernamemap[uname].callback = cb; scr.type="text/javascript"; var url = apiurl + method + "&jsoncallback=usernamemap" + encodeURIComponent("['" + uname + "']") + ".callback"; for(var k in params) if(params.hasOwnProperty(k)) url += '&' + encodeURIComponent(k) + '=' + encodeURIComponent(params[k]); scr.src = url; document.body.appendChild(scr); }
And this is how you call methods on flickr:
call_api_method("people.findByUsername", uname, map_user_to_nsid, {"username":uname}); call_api_method("photosets.getList", uname, show_set_list, {"user_id":nsid});
PS: I didn't mention it earlier, but caveat number three is the maximum size restriction on GET requests. There's nothing that can be done to get around this since the browser is going to make a GET request. Just use this for API calls that don't send too much data to the server. You can receive as much as you want.
4 comments :
Sweet. I like the idea of removing the 'script' bitches after they've been used. :)
Ok, so maybe I'm missing something but if you have a string which is some code you want to execute once only, why are you bothering creating dom nodes running the code and then deleting them, why not just "eval" the code?
I don't have the string. The string is returned by the url that I create the DOM node for. I can't XHR it because it's on a different domain.
to other anonymous.
Ajax works by the XHR transport mechanism but this article shows how to use script elements to pull in external logic &code. Just walk it through in your head, what would you evaluate? The creation of the script node? That still leaves a new node in the DOM. You need your browser to resolve the script tag and fetch the external code.
Post a Comment