【问题标题】:Replacing $http with Fetch API用 Fetch API 替换 $http
【发布时间】:2018-01-31 08:04:06
【问题描述】:

我将 $http 替换为 Fetch API,并将 $q 替换为 Promise API。因此,Angular 不再运行摘要循环,因此 UI 没有呈现。为了解决这个问题,我尝试了 Zone.js,这似乎部分解决了我们的问题。不幸的是,它的 API 在 0.6 中完全改变了,所以我们使用 legacy 0.5.15

现在是实际问题。

当刷新页面时,Angular 会像预期的那样配置和引导应用程序。在这个阶段,我正在初始化区域并用区域和$rootScope.$digest() 装饰$rootScope.apply。现在,当我在状态/路由(使用 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');

您可以在上面看到,当区域初始化开始、结束和运行时(+ 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!

如您所见,Zone/digest 在获取数据后没有运行,这就是数据和 UI 没有呈现在页面上的原因。

【问题讨论】:

  • 该问题不包含 plunk 或有助于调试问题的内容,这是必须的。 $apply 补丁看起来很可疑。从 $q 和 $http 切换到原生 Promise 和 Fetch 的原因是什么?您有效地消除了 Angular 作为框架的一堆好处,反而产生了一堆问题。看起来不是一个好的权衡。
  • 那是因为我无法重现这个。从 $q 和 $http 切换到原生 Promise 和 Fetch 的原因是因为最终目标是用 React 替换 Angular。 ui-router(我们用于路由)现在支持路由到 React 组件。
  • 我明白了。一般来说,我建议避免使用隐式摘要,因为它们会导致无法控制的混乱和糟糕的性能。一个合理的中间解决方案是创建一个用 $q 承诺包装fetch 结果的服务(也适合测试)。您还可以检查另一种解决 A1 中原生承诺的方法,stackoverflow.com/questions/39943937/…
  • $q 从异步函数内部返回。它不会那样工作。您可以检查返回的承诺仍然是instanceof Promise。这并不意味着这是您遇到的问题,但这绝对是一个问题。顺便说一句,你永远不应该依赖旧的 Zone,Zone 的全部意义在于它不断修复,因为它是一个巨大的 hack。
  • 确实如此,但这不是 async/await 的工作方式。避免这种情况的最简单方法是不要在此处使用async 函数。

标签: javascript angularjs angular-promise fetch-api angularjs-http


【解决方案1】:

fetch API 创建的 ES6 承诺转换为带有 $q.when 的 AngularJS $q 承诺。

使用 $q.when 将 ES6 承诺转换为 AngularJS 承诺1

AngularJS 通过提供自己的事件处理循环来修改正常的 JavaScript 流程。这将 JavaScript 拆分为经典和 AngularJS 执行上下文。只有应用在 AngularJS 执行上下文中的操作才能受益于 AngularJS 数据绑定、异常处理、属性监视等...2 由于 promise 来自 AngularJS 框架之外,因此框架不知道模型的变化,也不更新 DOM。

使用$q.when 将外部promise 转换为Angular 框架promise:

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

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

使用与 AngularJS 框架及其摘要周期正确集成的 $q 服务承诺。

$q.when

将一个可能是值或(第 3 方)then-able 承诺的对象包装到 $q 承诺中。当您处理的对象可能是也可能不是 Promise 时,或者当 Promise 来自不可信的来源时,这很有用。

-- AngularJS $q Service API Reference - $q.when

【讨论】:

  • 我明白,但我想尽可能地将它与 Angular 分开,因为最终目标是替换 Angular。我已经将所有控制器和服务重构为纯 ES6 并使用 1.5 组件。但是,正如您在上面看到的,我正在使用 Zone.js 来实现这一点并且它可以工作,但是由于某些奇怪的原因,它在获取数据后最终没有运行最后一个摘要循环。
  • 我尝试将 Fetch Promise 包装在 $q.when() 中,但它的行为完全相同。
  • 正如@estus 所说,将$q 承诺返回到ES6 承诺的.then 方法会返回一个新的ES6 承诺。相反,.then 方法如果 $q 承诺返回一个新的 $q 承诺。只有$q 承诺的.then 中的范围更改才会应用于AngularJS 执行上下文。
  • 所以唯一的办法就是回到$http。一定有什么办法可以打猴子补丁什么的?
【解决方案2】:

基本原理

包装$q.when 会起作用,但根据我团队的经验,它会非常挑剔并且容易出错。举个例子,从 Promise.then 函数体内返回 $q.when 仍然会像常规的 Promise 一样链接,并且您不会在回调中获得 $digest。

它还要求所有作者了解两个外观非常相似的构造 (Promise/$q) 之间的区别,并关心它们在异步调用的每个级别的具体类型。如果您使用像 async/await (进一步抽象了 Promise 类型)这样的现代便利设施,那么您将遇到更多麻烦。突然之间,您的任何代码都不能与框架无关。

我们的团队认为值得提交一个大型猴子补丁以确保所有承诺(以及 async/await 关键字)“正常工作”而无需额外思考。

丑吗?是的。但我们认为这是一个不错的权衡。


修补 Promise 回调以始终应用 $rootScope

首先我们在angular.run 块中安装针对Promise 的补丁:

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 以始终将语法实现为 Promises。我们使用了babel-plugin-transform-async-to-promises

【讨论】:

    猜你喜欢
    • 2020-02-29
    • 2019-06-28
    • 2018-10-28
    • 1970-01-01
    • 1970-01-01
    • 2018-08-08
    • 2016-11-06
    • 2022-11-27
    • 2021-07-13
    相关资源
    最近更新 更多