2
\$\begingroup\$

I wrote a small, cross browser compliant implementation of xhr requests in client JavaScript that closely follows the jQuery $.ajax() API and standards. The goal is to keep the xhr() very small in size, yet cover most common xhr scenarios and needs.

I'd like code review for performance, general best practice, and the most browser support (IE8+, Firefox, Chrome, Safari).

function xhr(options) {
    if(!options) {
        options = {};
    }

    var url = options.url ? options.url : window.location.href;
    var method = options.method ? options.method.toUpperCase() : 'GET';
    var data = options.data ? JSON.stringify(options.data) : null;
    var contentType = (typeof options.contentType !== 'undefined') ? options.contentType : null;
    var headers = (options.headers && typeof options.headers === 'object') ? options.headers : {};
    var username = options.username ? options.username : null;
    var password = options.password ? options.password : null;
    var timeout = options.timeout ? options.timeout : 0;
    var withCredentials = (typeof options.withCredentials !== 'undefined') ? options.withCredentials : false;
    var onSuccess = (options.onSuccess && typeof options.onSuccess === 'function') ? options.onSuccess : null;
    var onError = (options.onError && typeof options.onError === 'function') ? options.onError : null;
    var onTimeout = (options.onTimeout && typeof options.onTimeout === 'function') ? options.onTimeout : null;

    var _xhr = new window.XMLHttpRequest();
    _xhr.open(method, url, true, username, password);

    _xhr.timeout = timeout;

    _xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');

    if(contentType) {
        _xhr.setRequestHeader('Content-Type', contentType);
    } else if(contentType !== false && data) {
        _xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded; charset=UTF-8');
    }

    if(withCredentials) {
        _xhr.withCredentials = withCredentials;
    }

    for (var key in headers) {
        _xhr.setRequestHeader(key, headers[key]);
    }

    _xhr.onreadystatechange = function() {
        if (this.readyState === 4) {
            var responseHeaders = this.getAllResponseHeaders();

            if (this.status >= 200 && this.status < 400) {
                if(onSuccess) {
                    onSuccess(this.responseText, this.status, responseHeaders, this);
                }
            } else {
                if(onError) {
                    onError(this, this.status, responseHeaders);
                }
            }
        }
    };

    if(onTimeout) {
        _xhr.ontimeout = onTimeout;
    }

    _xhr.send(data);
    _xhr = null;
}

Usage examples:

xhr({
    url: "http://foo.bar"
});

xhr({
    url: "http://foo.bar/create",
    method: "POST",
    data: 'foo=bar'
});

xhr({
    url: "http://foo.bar/create",
    method: "POST",
    contentType: "application/json; charset=UTF-8",
    data: { foo: "bar" },
    headers: {
        "X-Key": "Value"
    },
    timeout: 5000,
    onSuccess: function(response, statusCode, responseHeaders, xhr) {     
    },
    onError: function(xhr, statusCode, responseHeaders) {
    },
    onTimeout: function(event) {
    }
});
\$\endgroup\$
1
\$\begingroup\$

Unfortunately, there is a new API for AJAX called fetch. It even has a polyfill. It even has a polyfill for older browsers which looks to me like its already doing what you aim to do.

But anyways, onto your code.

I see you have a long list of ternaries to set values or a default to the options. Instead of that, create an object hosting your defaults, and use Object.assign to merge it and the user config into an object. This throws away the need for doing a ternary/if-else on your code by providing sensible defaults.

var defaults = {
  method: 'GET',
  url: window.location.href,
  username: undefined,
  password: undefined,
  withCredentials: undefined
};

var mergedOptions = Object.assign({}, defaults, options);

var _xhr = new window.XMLHttpRequest();

_xhr.open(mergedOptions.method, mergedOptions.url, true, mergedOptions.username, mergedOptions.password);

_xhr.withCredentials = mergedOptions.withCredentials

For calling the callbacks, consider using call with a context of null. This way, the callback has no assumptions on what the context is and code doesn't accidentally bind something to the global scope.

Alternatively, consider returning a Promise from your function. This way, it acts more like jQuery and the standard fetch.

function xhr(options){
  return new Promise(function(resolve, reject){
    // setup XHR

    if (this.status >= 200 && this.status < 400) resolve(this.responseText, this.status, responseHeaders, this);
    else reject(this, this.status, responseHeaders);

    // send XHR
  });
}

xhr({...}).then(onSuccess, onError);
\$\endgroup\$
  • \$\begingroup\$ Thanks for your comments. It appears Object.assign() is not widely supported. The most important thing is code that is cross browser compliant. The same with using Promises \$\endgroup\$ – Justin May 5 '16 at 19:20
  • \$\begingroup\$ Also, fetch() and the GitHub polyfill looks awesome, but need at least IE9 support. \$\endgroup\$ – Justin May 5 '16 at 19:28

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.