Cross browser cross domain ajax requests

When programming JavaScript you will eventually hit several cross browser inconsistencies. One of the most frustrating is the ajax request. This allows you to send and load data from other files or urls. A quick search will show that the modern browsers support a simple syntax:


var xhr = new XMLHttpRequest();
xhr.addEventListener('load', function(e) {
  console.log(e.target.responseText);
})
xhr.open('GET', 'http://echo.jsontest.com/key/value/one/two');
xhr.send();

This works in Chrome, Safari and other modern browsers. But what happens when we need to older browsers such as Internet Explorer and Firefox 3.6? Well a simple search returns almost every answer as "use jQuery".

The reason behind this is that there are many inconsistencies between browsers and whether requests are cross domain, when the server requires credentials and pre-flighted request that it becomes a nightmare to support.


What happens if we don't want to include a 30KB library just for ajax requests? Well it is fairly simple to support cross browser ajax requests if you know how.

The first step is to check if the browser is IE then use an ActiveXObject to support local requests (This can be used for cross domain requests but it is limited by browser security restrictions, so may not be ideal for some users e.g. inside company internal network). I've also defined success and error functions which we will reference later in several places.



var xhr = window.XMLHttpRequest ? new XMLHttpRequest() : new ActiveXObject('Microsoft.XMLHTTP');
var success = function(e) { console.log(e); }
var error = function(e) { console.log(e); }
if (window.XDomainRequest && !this.sameOrigin(url)) { xhr = new XDomainRequest(); xhr.onload = success; }



Now we need to reference those complete and error functions which will be fired based on whether onload or on readystatechange functions are available. If the request is crossdomain use XDomainRequest for IE only.


if (window.XDomainRequest && !sameOrigin(url)) { xhr = new XDomainRequest(); xhr.onload = success; }
xhr.onerror = error;
xhr.onreadystatechange = function(e) { if (xhr.readyState == 4 && xhr.status == 200) { success(e); } }


If you would like to load images then you will need to override the mimetype to grab the data

if (file == 'image' && xhr.overrideMimeType) { xhr.overrideMimeType('text/plain; charset=x-user-defined'); }

Also if the request needs credentials then you need to send the value true through, we will surround this with a try catch block as IE throws an access denied error for request without CORS enabled. If you get the error message you need to ensure the feed header has 'access-control-allow-origin: *'


try {
    if ('withCredentials' in xhr) { xhr.open(type, url, true); }
    else { xhr.open(type, url); }
    xhr.send(null);
}
catch(e) { error(e); }


There you have a working cross browser ajax request. Great, but not exactly a good idea to duplicate this into every part for our app. Lets convert this into a reusable function which I store in my Utils module.

function ajax(url, callback, filetype, type) {


    filetype = filetype ? filetype : 'json';
    type = type ? type : 'GET';
    var xhr = window.XMLHttpRequest ? new XMLHttpRequest() : new ActiveXObject('Microsoft.XMLHTTP');
    var success = function(e) {
        var items = '';
        switch(filetype) {
            case 'csv': items = csv(xhr.responseText); break;
            case 'json': items = JSON.parse(xhr.responseText); break;
            default: items = xhr.responseText; break;
        }
        callback(items);
    }
    var error = function(e) {
        console.log('Please enabled CORS using  access-control-allow-origin');
    }
    if (window.XDomainRequest && !sameOrigin(url)) { xhr = new XDomainRequest(); xhr.onload = success; }
    if (filetype == 'image' && xhr.overrideMimeType) { xhr.overrideMimeType('text/plain; charset=x-user-defined'); }
    xhr.onerror = error;
    xhr.onreadystatechange = function(e) { if (xhr.readyState == 4 && xhr.status == 200) { success(e); } }
    try {
        if ('withCredentials' in xhr) { xhr.open(type, url, true); }
        else { xhr.open(type, url); }
        xhr.send(null);
    }
    catch(e) { error(e); }


}

The check for sameDomain matches the url against the current window location url:

function sameOrigin(url){
    var split = url.split('/');
    if (split[0]+'//' == window.location.protocol+'//') { return split[2] != window.location.host ? false : true; }
    else { return true; }
}


And to use it you would write:

ajax('http://echo.jsontest.com/key/value/one/two', function(e) {
    console.log(e);
});

I'm using this technique combined with a csv to json convertor script to load data into my site. My csv to json function:

function csv(string, divider) {
    divider = divider || ',';
    var regex = new RegExp(("(\\"+divider+"|\\r?\\n|\\r|^)"+"(?:\"([^\"]*(?:\"\"[^\"]*)*)\"|" +"([^\"\\"+divider+"\\r\\n]*))"), 'gi');
    var regex2 = new RegExp("\"\"", 'g');
    var items = [];
    var fields = [];
    var matches = null;
    while (matches = regex.exec(string)) {
        var match = matches[1];
        var value = '';
        if (match.length && match != divider) { items.push({}); }
        if (matches[2]) { value = matches[2].replace(regex2, '\"'); }
        else { value = matches[3]; }
        if (items.length == 0) { fields.push(value); }
        else {
            var index = Utils.size(items[items.length-1]);
            var name = fields[index];
            items[items.length-1][name] = value;
        }
    }
    return items;
}

If you want to test it working you can try these test feeds:


// cross domain json feed without CORS
var url = 'http://www.kimturley.co.uk/data/projects.json';

// cross domain json feed with CORS
var url = 'http://pipes.yahooapis.com/pipes/pipe.run?_id=giWz8Vc33BG6rQEQo_NLYQ&_render=json';

// same domain json feed
var url = '/echo/json/';

// google docs example
var url = 'https://docs.google.com/spreadsheet/pub?key=0AsnymCBa0S5PdGh4ZVF5Uktfc2hyYm5ibHduTHAzQ1E&output=csv';

Here is a working example with all of the code:
http://jsfiddle.net/kmturley/4RCBg/

Hope that helps some of you guys

No comments:

Post a Comment