Замена $http на Fetch API

Я заменил $http на Fetch API, а $q заменил на Promise API. Из-за этого Angular больше не запускал циклы дайджеста, поэтому пользовательский интерфейс не отображался. Чтобы решить эту проблему, я попробовал Zone.js, и это частично решило наши проблемы. К сожалению, его API полностью изменился в версии 0.6, поэтому мы используем устаревшую версию 0.5.15. .

Теперь к самой проблеме.

При обновлении страницы Angular конфигурирует и загружает приложение, как и ожидалось. На этом этапе я инициализирую Зону и украшаю $rootScope.apply Зоной и $rootScope.$digest(). Теперь, когда я переключаюсь между состояниями/маршрутами (с помощью ui-router), все работает, как и ожидалось, но при полном обновлении возникает состояние гонки, и зона/дайджест работает неправильно. Я не знаю, как это исправить.

У меня есть следующий код в блоке angular.run():

console.log('Zone setup begin');
const scopePrototype = $rootScope.constructor.prototype;
const originalApply = scopePrototype.$apply;
const zoneOptions = {
    afterTask: function afterTask() {
        try {
            $rootScope.$digest();
        } catch (e) {
            $exceptionHandler(e);
            throw e;
        }
    }
};

scopePrototype.$apply = function $applyFn() : void {
    const scope = this;
    const applyArgs = arguments;

    window.zone.fork(zoneOptions).run(() => {
        originalApply.apply(scope, applyArgs);
        console.log('Zone + $digest run!');
    });
};
console.log('Zone setup end');

Выше вы можете видеть, что я захожу в консоль, когда начинается инициализация Zone, когда она заканчивается и когда она запускается (+ цикл дайджеста Angular). В моем контроллере, где я извлекаю данные через Fetch API, я добавил console.log('Data fetched!');, чтобы знать, когда данные были извлечены.

Теперь вывод в консоли:

Переход состояния с помощью ui-router (отлично работает)

Обратите внимание, что дайджест запускается в конце.

Zone setup begin
Zone setup end
Zone + $digest run!
Zone + $digest run!
Zone + $digest run!
Zone + $digest run!
Data fetched!
Zone + $digest run!

Полное обновление состояния/маршрута (не запускается в конце)

Zone setup begin
Zone setup end
Zone + $digest run!
Zone + $digest run!
Zone + $digest run!
Zone + $digest run!
Data fetched!

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


person Gaui    schedule 22.08.2017    source источник
comment
Вопрос не содержит планка или чего-то, что могло бы помочь отладить проблему, что здесь обязательно. Патч $apply выглядит очень подозрительно. Что послужило причиной перехода с $q и $http на нативные промисы и Fetch? Вы эффективно устраняете кучу преимуществ Angular как фреймворка и вместо этого создаете кучу проблем. Не похоже на хороший обмен.   -  person Estus Flask    schedule 23.08.2017
comment
Это потому, что я не могу воспроизвести это. Причина перехода с $q и $http на нативные промисы и Fetch заключалась в том, что конечной целью является замена Angular на React. ui-router (который мы используем для маршрутизации) теперь поддерживает маршрутизацию к компонентам React.   -  person Gaui    schedule 23.08.2017
comment
Я понимаю. Как правило, я бы посоветовал избегать неявных дайджестов, потому что они приведут к неконтролируемому беспорядку с низкой производительностью. Разумным промежуточным решением было бы создание службы, которая оборачивает результат fetch обещанием $q (тоже подходит для тестирования). Вы также можете проверить другой подход, который вообще касается нативных промисов в A1, stackoverflow.com/questions/39943937/   -  person Estus Flask    schedule 23.08.2017
comment
Смешной. Я обернул fetch с помощью $q.when (патч здесь), и он по-прежнему не запускает последний дайджест. Думаю, проблема в другом. Это объясняет, почему это работало на главной странице с некоторыми запросами API.   -  person Gaui    schedule 23.08.2017
comment
$q возвращается из асинхронной функции. Это не сработает так. Вы можете проверить, что возвращенное обещание все еще instanceof Promise. Это не означает, что это проблема, с которой вы столкнулись, но это определенно проблема. Кстати, вы никогда не должны полагаться на более старую Zone, весь смысл Zone в том, что она постоянно исправлена, потому что это один огромный взлом.   -  person Estus Flask    schedule 23.08.2017
comment
По словам @georgeawg, именно так это и работает.   -  person Gaui    schedule 23.08.2017
comment
Действительно, но async/await работает иначе. Самый простой способ избежать этого — не использовать здесь функцию async.   -  person Estus Flask    schedule 23.08.2017
comment
Я удалил асинхронную функцию. Та же проблема. Я попытаюсь добавить Plunkr, воспроизводящий это.   -  person Gaui    schedule 23.08.2017


Ответы (2)


Преобразуйте промисы ES6, созданные fetch API, в AngularJS $ q обещания с $q.when.

Используйте $q.when для преобразования обещаний ES6 в обещания AngularJS1

AngularJS изменяет обычный поток JavaScript, предоставляя собственный цикл обработки событий. Это разделяет JavaScript на классический и контекст выполнения AngularJS. Только операции, которые применяются в контексте выполнения AngularJS, получат выгоду от привязки данных AngularJS, обработки исключений, наблюдения за свойствами и т. д....2 Поскольку обещание исходит из-за пределов фреймворка AngularJS, фреймворк не знает об изменениях в модели и не обновить ДОМ.

Используйте $q.when, чтобы преобразовать внешнее обещание в обещание фреймворка Angular:

var myRequest = new Request('flowers.jpg');

$q.when(fetch(myRequest)).then(function(response) {
    //code here
})

Используйте промисы $q Service, которые должным образом интегрированы с фреймворком AngularJS и его циклом дайджеста.

$q.когда

Оборачивает объект, который может быть значением или обещанием (стороннего производителя), в обещание $q. Это полезно, когда вы имеете дело с объектом, который может быть или не быть обещанием, или если обещание исходит из источника, которому нельзя доверять.

-- Справочник по API службы AngularJS $q – $q.when

person georgeawg    schedule 22.08.2017
comment
Я понимаю, но я хотел бы максимально отделить это от Angular, потому что конечная цель — заменить Angular. Я провел рефакторинг всех контроллеров и сервисов до чистого ES6 и с использованием компонентов версии 1.5. Однако, как вы видели выше, я использую Zone.js для достижения этой цели, и он работает, но по какой-то странной причине он не запускает последний цикл дайджеста в конце после выборки данных. - person Gaui; 23.08.2017
comment
Я попытался обернуть Fetch Promise в $q.when(), но он вел себя точно так же. - person Gaui; 23.08.2017
comment
Как заявил @estus, возврат обещания $q методу .then обещания ES6 возвращает новое обещание ES6. И наоборот, метод .then, если обещание $q возвращает новое обещание $q. В контексте выполнения AngularJS применяются только изменения объема в .then обещания $q. - person georgeawg; 24.08.2017
comment
Так что единственный способ — вернуться к $http. Должен же быть какой-то путь к обезьяньему патчу или что-то в этом роде? - person Gaui; 24.08.2017

Обоснование

Обертывание $q.when будет работать, но, по опыту моей команды, это будет очень привередливо и подвержено ошибкам. Например, возврат $q.when из тела функции Promise.then по-прежнему будет цепочкой, как обычный Promise, и вы не получите $digest при обратных вызовах.

Также требуется, чтобы все авторы понимали разницу между двумя очень похожими конструкциями (Promise/$q) и заботились об их конкретных типах для каждого уровня асинхронного вызова. Если вы используете современные удобства, такие как async/await (которые еще больше абстрагируют типы промисов), вы столкнетесь с еще большими проблемами. Внезапно ни один из ваших кодов не может быть независимым от фреймворка.

Наша команда решила, что стоит выпустить большой обезьяний патч, чтобы убедиться, что все промисы (и ключевые слова async/await) «просто работают», не требуя дополнительных размышлений.

Уродливый? Да. Но мы чувствовали, что это был хороший компромисс.


Исправлены обратные вызовы Promise, чтобы они всегда применяли $rootScope

Сначала мы устанавливаем патч против Promise в блоке angular.run:

angular.module(...).run(normalizePromiseSideEffects);

normalizePromiseSideEffects.$inject = ['$rootScope'];

function normalizePromiseSideEffects($rootScope) {
  attachScopeApplicationToPromiseMethod('then');
  attachScopeApplicationToPromiseMethod('catch');
  attachScopeApplicationToPromiseMethod('finally');

  function attachScopeApplicationToPromiseMethod(methodName) {
    const NativePromiseAPI = window.Promise;
    const nativeImplementation = NativePromiseAPI.prototype[methodName];

    NativePromiseAPI.prototype[methodName] = function(...promiseArgs) {
      const newPromiseArgs = promiseArgs.map(wrapFunctionInScopeApplication);
      return nativeImplementation.bind(this)(...newPromiseArgs);
    };
  }

  function wrapFunctionInScopeApplication(fn) {
    if (!isFunction(fn) || fn.isScopeApplicationWrapped) {
      return fn;
    }

    const wrappedFn = (...args) => {
      const result = fn(...args);
      // this API is used since it's $q was using in AngularJS src
      $rootScope.$evalAsync();
      return result;
    };
    wrappedFn.isScopeApplicationWrapped = true;
    return wrappedFn;
  }
}

Асинхронный/ожидание

Если вы хотите поддерживать использование async/await, вам также необходимо настроить Babel, чтобы он всегда реализовывал синтаксис в виде промисов. Мы использовали babel-plugin-transform-async-to-promises.

person cpimhoff    schedule 15.11.2019