Каков наиболее лаконичный способ использования отложенного канала jQuery?

Я разрабатываю API javascript, который обертывает мой REST API. Обычно я хочу избежать большого количества подробных и запутанных вложенных обратных вызовов и читал о достоинствах Deferred jQuery.

Давайте представим мою библиотеку myLib, которая представляет объекты людей и способы перемещения между объектами людей. У него есть куча методов, таких как «папа», «босс», «помощник» и т. д., которым необходимо выполнить запрос ajax, чтобы найти некоторые данные и вернуть другой связанный объект «люди». Но я хочу, чтобы они возвращали отложенный объект, который также имеет методы myLib, которые я могу связать вместе, чтобы написать действительно краткий простой код, например:


 myLib('me').dad().boss().assistant(function(obj){
   alert(obj.phone); // My dad's, bosses assistants phone number
 }, function(){
   alert('No such luck);
 });

Это создает объект «я», затем выполняет первый вызов ajax, чтобы найти мои данные, затем использует эти данные для выполнения другого вызова, чтобы узнать моего родителя, затем снова, чтобы найти моего босса, затем еще один, чтобы получить помощника, а затем наконец, это передается моему обратному вызову, и я обрабатываю его. Вроде как цепные методы обхода jQuery, но асинхронные.

Передача функции в любой точке, но обычно в последнем методе, будет вызываться внутренне, когда разрешается последний объект Deferred в цепочке. Вторая функция — это обратный вызов отказа, который вызывается, если какой-либо из отложенных объектов в цепочке отклонен.

Я думаю, мне нужно создать отложенный объект jQuery, а затем расширить его, но не уверен, что это «лучший» способ.

Итак, каков наилучший способ достижения моей минималистской цели API? В основном я хочу, чтобы все имена методов были на 100% в пространстве имен проблем домена и не были загрязнены множеством «когда», «сделано», «успех» и т. д.

И есть ли примеры подобных чистых API, которые я могу где-то эмулировать?


person Brendan Heywood    schedule 22.10.2011    source источник
comment
Я внес несколько больших изменений в свой ответ ниже (вместо того, чтобы создавать новый). Посмотрите и посмотрите, выглядит ли это осуществимым.   -  person Elliot Nelson    schedule 23.10.2011


Ответы (3)


Я собираюсь оставить свою реализацию Person в покое, так как я думаю, что она в основном выполняет свою задачу:

function Person(o) {
  this.id = o.id;
  this.name = o.name;
}

Person.prototype.dad = function(done, fail) {
  var promise = $.getJSON('/people/' + this.id + '/dad').pipe(Person, null);
  promise.then(done, fail);
  return new Person.Chain(promise);  
};

Person.prototype.boss = function(done, fail) {
  var promise = $.getJSON('/people/' + this.id + '/boss').pipe(Person, null);
  promise.then(done, fail);
  return new Person.Chain(promise);  
};

Для реализации Person.Chain у нас есть две проблемы: каждый раз, когда вы вызываете метод-получатель, он действительно должен возвращать новый Person.Chain, и этот новый Person.Chain должен быть "вложенным": он должен связывать результаты вызовов AJAX вместе. Это должно решить обе проблемы.

Этот подход требует нескольких строк клея, поэтому сначала давайте удостоверимся, что нам не нужно дублировать его снова и снова:

Person.Chain = function(promise) {
  this.promise = promise;
};

Person.Chain.prototype.assistant = function(done, fail) {
  return this.pipe('assistant', done, fail);
};

Person.Chain.prototype.dad = function(done, fail) {
  return this.pipe('dad', done, fail);
};

Person.Chain.prototype.boss = function(done, fail) {
  return this.pipe('boss', done, fail);
};

Нам просто нужно определить столько этих методов-оболочек, сколько есть методов-геттеров на Person. Теперь, чтобы реализовать pipe:

Person.Chain.prototype.pipe = function(f, done, fail) {
  var defer = new $.Deferred();
  defer.then(done, fail);

  this.promise.pipe(function(person) {
    person[f](function(person) {
      defer.resolve(person);
    }, function() {
      defer.reject();
    });
  }, function() {
    defer.reject();
  });

  return new Person.Chain(defer.promise());
}

Во-первых, мы явно создаем новый объект Deferred и прикрепляем к нему обработчики done и fail (если есть). Затем мы прикрепляем функцию, которая будет вызывать любое переданное f (папу, помощника, начальника и т. д.) на Person, которое будет возвращено предыдущей функцией. Наконец, когда разрешается эта функция, мы явно разрешаем созданный нами объект Deferred. Теперь мы можем объединить последовательные вызовы следующим образом:

jake = new Person({id: 3, name: 'Jake'});

jake.dad().boss().assistant(function(person) {
  alert("Jake's dad's boss's assistant is " + person.name);
});

Обратите внимание, что обработка сбоев довольно многословна, но нам это нужно, чтобы, если вы соедините кучу вызовов вместе, ранний сбой все равно передал бы свои reject() вызовов по линии вплоть до обратного вызова сбоя, заданного в конце.

Это также совершенно законно:

jake.dad(function(person) {
  alert('Dad is ' + person.name);
}, function() {
  alert('Dad call failed');
}).boss(function(person) {
  alert('Jake dad boss is ' + person.name);
}, function() {
  alert('One of the calls failed');
});

Если первый вызов завершится ошибкой, оба обратных вызова будут вызваны по порядку. Если только последний терпит неудачу, будет вызван только тот.

Большое предостережение, ни один из этих кодов не тестировался. Но, теоретически, это рабочий подход.

person Elliot Nelson    schedule 22.10.2011

Я думаю, что вам нужен построитель запросов, в котором методы, добавляющие критерии (такие как «папа» и «помощник», могут быть объединены в цепочку. Кроме того, вы хотите, чтобы в любой момент вы могли передать обратный вызов, а это означает выполнение запрос.

Поэтому я бы сделал это так:

function PersonQuery(personName) {
  this.criteria = [];
  criteria.push({name:personName});
}

PersonQuery.prototype.dad = function(doneCallback) {
    this.criteria.push({relationship:"dad"});
    _execute(doneCallback);
    return this;
}

PersonQuery.prototype.boss = function(doneCallback) {
    this.criteria.push({relationship:"boss"});
    _execute(doneCallback);
    return this;
}

PersonQuery.prototype.assistant = function(doneCallback) {
    this.criteria.push({relationship:"assistant"});
    _execute(doneCallback);
    return this;
}

PersonQuery.prototype._execute = function(doneCallback) {
    if (!doneCallback) {
       return;
    }
    $.ajax({"data": this.criteria}).success(function(responseData) {
       doneCallback(responseData);   
    });
}

Затем, чтобы использовать это, ваш пример станет:

   new PersonQuery("me").dad().boss().assistant(function(obj) { 
    alert(obj.phone); });
person mattsh    schedule 22.10.2011
comment
Да, это выглядит хорошо, за исключением того, что вызов ajax необходимо выполнять последовательно, чтобы получить фрагменты данных, необходимые для выполнения следующего вызова ajax. Я все еще мог бы связать их в конце, но все, что это делает, это просто переносит суть проблемы в другой фрагмент кода, на самом деле это не решает ее. - person Brendan Heywood; 23.10.2011

У меня это отлично работает с внутренним обещанием, поэтому я сразу же создаю объекты Person без данных. Все, что он содержит, — это обещание предоставить данные позже. Такой метод, как parent(), создает новое обещание, цепочка которого не соответствует текущему обещанию. Вероятно, есть способ сделать это проще, используя pipe(), но пока не совсем понял.


myLib = {
  find: function(id){
    var person = new Person();
    person.promise = $.ajax(.....);
  }
};

function Person(){
}
Person.prototype.parent(){
  var person = this;
  var parent = new Person();
  var deferred = $.Deferred();
  this.promise.then(function(data){
    var promise = $.ajax({url: '/person/'+data.parent+'/json'});
    promise.done(function(obj){
      person.data = obj.data;
      deferred.resolve(node);
    });
  }, function(error){
    deferred.fail();
  });
  parent.promise = deferred.promise();
  return parent;
}

Person.prototype.get(callback){
  this.promise.then(function(data){
    this.data = data;
    callback(this);
  });
}


Usage/Test:

myLib.find('12345').get(callback);
myLib.find('12345').parent().get(callback);



person Brendan Heywood    schedule 23.10.2011