Angular Promise, вызванный дважды, откладывается только до первого вызывающего абонента

Я работаю над приложением для серии видео, которое настроено так:

angular.module('videoSeries', ['ngAnimate', 'ui.router'])
  .config(config)
  .factory('episodes', episodesFactory)
  .controller('MainCtrl', MainCtrl)
  .controller('EpisodeCtrl', EpisodeCtrl);

Фабричные «эпизоды» загружают файл JSONP, из которого оба контроллера должны читать. Чтобы контроллеры дождались успешного возврата JSONP, я установил обещание на фабрике следующим образом (как предложено здесь):

episodesFactory.$inject = ['$http', '$q'];
function episodesFactory($http, $q) {
  var pub = {},
      jsonUrl = 'http://i.cdn.turner.com/nba/nba/.element/media/2.0/teamsites/warriors/json/json-wgtv.js?callback=JSON_CALLBACK' + (new Date().getTime()),
      cachedResponse;

  pub.getData = function() {
    var deferred = $q.defer();

    if (cachedResponse) {
      deferred.resolve(cachedResponse);
    }

    else {
      $http.jsonp(jsonUrl);

      window.jsonCallbackWGTV = function(data) {
        cachedResponse = data;
        deferred.resolve(data);
      }
    }

    return deferred.promise;
  }

  return pub;
}

И затем оба контроллера выполняются после возврата .then следующим образом:

MainCtrl.$inject = ['$scope', 'episodes'];
function MainCtrl($scope, episodes) {
  episodes.getData().then(function(data) {
    $scope.episodes = data.episodes;
  });
}

EpisodeCtrl.$inject = ['$scope', '$stateParams', 'episodes'];
function EpisodeCtrl($scope, $stateParams, episodes) {
  episodes.getData().then(function(data) {
    $scope.episode = data.episodes[$stateParams.episodeIndex];
  });
}

Это отлично работает, если только один контроллер вызывает .getData() за раз, но если оба вызывают его одновременно (что происходит, если вы загружаетесь непосредственно в состояние эпизодов), похоже, что одно обещание забыто.

Это планкер. Обратите внимание, что для того, чтобы увидеть проблему, вы нажимаете на эпизод, а затем перезагружаетесь прямо на этот маршрут, но я не уверен, как это сделать в планкерах, поскольку URL-адрес не обновляется.


person dougmacklin    schedule 11.03.2015    source источник


Ответы (2)


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

.state('root', {
    url: '',
    abstract: true,
    views: {
      'episodesList': {
        templateUrl: '/episodelist.html',
        controller: 'MainCtrl'
      }
    },
    resolve: {
        'cachedEpisodes': ['episodes', function(episodes){
            return episodes.getData();
        }]
    }
  }

В этом случае разрешение должно происходить только один раз, когда какое-либо дочернее состояние активирует это корневое состояние.

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

Функция разрешения не будет выполняться до тех пор, пока состояние не станет активным, в этот момент она будет иметь доступ ко всем инжектам среды выполнения (не конфигурации).

Может показаться немного странным, что я оборачиваю функцию в массив. Каждое значение массива является именем вводимого объекта, а конечное значение массива должно быть функцией, в которую они вводятся. Это включено в angular, чтобы помочь предотвратить проблемы с оптимизаторами, которые искажают имена аргументов (которые в противном случае использовались бы для определения внедряемых).

О, также любые функции разрешения, которые возвращают обещания, будут полностью разрешены angular до того, как изменение маршрута будет завершено, и их разрешенное значение - это то, что вводится в контроллеры.

MainCtrl.$inject = ['$scope', 'cachedEpisodes'];
function MainCtrl($scope, data) {
  $scope.episodes = data.episodes;
}

EpisodeCtrl.$inject = ['$scope', '$stateParams', 'cachedEpisodes'];
function EpisodeCtrl($scope, $stateParams, data) {
  var i = 0, len = data.episodes.length, episode;
  for (i = 0; i < len; i++) {
    if (data.episodes[i].season === $stateParams.season && data.episodes[i].episode === $stateParams.episode) {
      episode = data.episodes[i];
      break;
    }
  }
  $scope.episode = episode;
}
person Scott Vickers    schedule 13.03.2015
comment
не могли бы вы включить немного больше кода? нужно ли вводить «эпизоды» в config(), чтобы это работало? и должны ли контроллеры по-прежнему включать «эпизоды» или теперь они будут включать «кэшированные эпизоды»? Кроме того, вы работаете с моим кодом обещания или включаете изменения @Roamer-1888? извините, не очень знаком с разрешением, я очень ценю помощь - person dougmacklin; 13.03.2015
comment
спасибо за добавление дополнительной информации, это все очень полезно. однако теперь главная страница пуста, getData(), кажется, никогда не вызывается: plnkr.co/edit/AGR90r4zJHzk4jfch2Uq?p=preview - person dougmacklin; 13.03.2015
comment
Получаете какие-либо сообщения об ошибках? Каково значение cachedEpisodes в основном контроллере? В данный момент не вижу plnkr на мобильном устройстве - person Scott Vickers; 13.03.2015
comment
нет сообщений об ошибках, когда я запускаю его локально, и он никогда не достигает основного контроллера :( - person dougmacklin; 14.03.2015
comment
А, я вижу ошибку. Извините, глупая ошибка. Я забыл поместить аргумент «эпизоды» в функцию разрешения. я отредактирую ответ - person Scott Vickers; 14.03.2015
comment
ах, хорошо, он снова появился, но по-прежнему загрузка напрямую на страницу эпизода не работает, вот код, если вы хотите посмотреть локально: dropbox.com/s/dy9pp2ycxbhffh0/video-series.zip?dl=0 - person dougmacklin; 14.03.2015
comment
Хм, вы выбираете эпизод с помощью тега ui-sref в html и указываете значение EpisodeIndex в параметрах состояния, но у вас нет никакого способа получить это значение через URL-адрес. Поэтому, если вы загружаете URL-адрес эпизода напрямую, значение 'episodeIndex' равно нулю. - person Scott Vickers; 14.03.2015
comment
@dougmacklin См. новое редактирование. Я изменил контроллер эпизода, чтобы идентифицировать эпизод по сезону и номеру эпизода, а не по индексу. - person Scott Vickers; 14.03.2015
comment
Давайте продолжим обсуждение в чате. - person dougmacklin; 14.03.2015

Ваша проблема в основном вызвана тем, что удаленный ресурс всегда возвращает свои данные, завернутые в jsonCallbackWGTV() {...}, в результате чего ваши два одновременных вызова соперничают за window.jsonCallbackWGTV(). и кто смеется последним, тот смеется громче всех.

Ресурсы JSONP действительно должны позволять клиенту указывать имя обратного вызова, но это, по-видимому, вне вашего контроля.

Проблема должна быть преодолена путем кэширования отложенных, а не доставленных в них данных. Таким образом, исходный вызов .getData() и все последующие вызовы могут возвращать обещание, полученное из того же отложенного.

episodesFactory.$inject = ['$http', '$q'];
function episodesFactory($http, $q) {
    var jsonUrl = 'http://i.cdn.turner.com/nba/nba/.element/media/2.0/teamsites/warriors/json/json-wgtv.js',
    deferred;
    return {
        getData: function() {
            if (!deferred) {
                deferred = $q.defer();
                $http.jsonp(jsonUrl);
                window.jsonCallbackWGTV = function(data) {
                    deferred.resolve(data);
                    delete window.jsonCallbackWGTV;
                }
            }
            return deferred.promise;
        }
    };
}   

Как правило, вы кэшируете промис отложенного, а не само отложенное, но проблема jsonCallbackWGTV делает необходимым сохранение ссылки на отложенное в любом случае, и кешированное отложенное может также использоваться для обеих целей.

person Roamer-1888    schedule 11.03.2015
comment
спасибо @ Roamer-1888, хотя все, что вы сказали, имеет смысл, проблема все еще возникает. отдельные эпизоды по-прежнему не загружаются, если вы переходите сразу к одному (название и описание не отображаются), а не начинаете с домашней страницы, в то время как список эпизодов по-прежнему загружается правильно - person dougmacklin; 11.03.2015
comment
Можно только думать, что происходит что-то еще. Должен признаться, у меня сейчас нет идей. - person Roamer-1888; 12.03.2015