Let's Make a Framework: Ajax Improvements

2011-09-15 15:00

Let's Make a Framework: Ajax Improvements

by

at 2011-09-15 07:00:00

original http://feedproxy.google.com/~r/dailyjs/~3/TURKqg_iMqk/framework-80

Let’s Make a Framework is an ongoing series about building a JavaScript framework from the ground up.

These articles are tagged with lmaf. The project we’re creating is called Turing. Documentation is available at turingjs.com.

API Redesign

As I mentioned last week, I thought it would be interesting to change the turing.net module to have a chained API that looks more like TJ Holowaychuk’s Superagent library. Part of the reason for doing this is to again demonstrate how easy it is to make a simple and consistent chained API with JavaScript. However, TJ’s reasoning behind Superagent’s design is worth thinking about, because he makes some good points about the inconsistencies found in many modern frameworks.

jQuery’s API generally looks like this:

$.get('/user/1', function(data, textStatus, xhr) {
  // Callback manages result
});

TJ argued that reducing the arity in the callback would make it more friendly:

request.get('/user/1', function(res) {
  // The abstracted `res` object contains status code, result data, etc.
});

The res object neatly encapsulates everything you need to deal with the server’s response.

Another issue is any configuration requires falling back to the full-blown $.ajax() method. And, much like my initial Turing design, we end up with calls like this:

$.ajax({
  url: '/api/pet',
  type: 'POST',
  data: { name: 'Manny', species: 'cat' },
  headers: { 'X-API-Key': 'foobar' }
}).success(function(res) {

}).error(function() {

});

Whenever you’re designing a JavaScript API and you find yourself creating large configuration options, it’s often worth sketching out what a chained API would look like. I agree with TJ’s example that this looks better:

request
  .post('/api/pet')
  .data({ name: 'Manny', species: 'cat' })
  .set('X-API-Key', 'foobar')
  .set('Accept', 'application/json')
  .end(function(res) {

  });

Rather than providing two callbacks, users of the library can process res however they wish. They don’t depend on the framework’s interpretation of HTTP status codes.

Tests

To convert Turing’s API to a Superagent-inspired chained API, I started by writing tests based on my idealised API:

$t.post('/post-test')
  .data({ key: 'value' })
  .end(function(res) {
    assert.equal('value', res.responseText);
  });

Compare that to the old style:

$t.post('/post-test', {
  postBody: { key: 'value' },
  success: function(r) {
    assert.equal('value', r.responseText);
  },
  error: function() {
    assert.ok(false);
  }
});

Chains

The network methods call and return an internal method, ajax. This was returning an object for chaining then (the promise API), so I extended it:

function ajax(url, options) {
  var chain = {};

  // All the old ajax stuff goes here

  chain = {
    set: function(key, value) {
      options.headers[key] = value;
      return chain;
    },

    send: function(data, callback) {
      options.postBody = net.serialize(data);
      options.callback = callback;
      send();
      return chain;
    },

    end: function(callback) {
      options.callback = callback;
      send();
      return chain;
    },

    data: function(data) {
      options.postBody = net.serialize(data);
      return chain;
    },

    then: function() {
      chain.end();
      if (promise) promise.then.apply(promise, arguments);
      return chain;
    }
  };

  return chain;
}

As with all chained APIs, the trick is to just return an object with the expected methods. Here I return the same object from each method. The only other things I had to change were to make respondToReadyState always call options.callback if present (else it looks for success/error callbacks from the promise API or old style API), and I made net.serialize cope with strings.

Conclusion

As we’ve seen before in this series, implementing chainable APIs is pretty easy. In this case, a chainable network API seems a lot cleaner than large configuration objects

These changes can be reviewed in commit 5263c6c.