AngularJS:使用异步数据初始化服务

IT技术 javascript angularjs asynchronous service angular-promise
2021-02-02 17:31:09

我有一个 AngularJS 服务,我想用一些异步数据初始化它。像这样的东西:

myModule.service('MyService', function($http) {
    var myData = null;

    $http.get('data.json').success(function (data) {
        myData = data;
    });

    return {
        setData: function (data) {
            myData = data;
        },
        doStuff: function () {
            return myData.getSomeData();
        }
    };
});

显然这行不通,因为如果doStuff()myData返回之前尝试调用某些东西,我将收到空指针异常。据我阅读这里这里提出的其他一些问题我有一些选择,但它们似乎都不是很干净(也许我遗漏了一些东西):

使用“运行”设置服务

设置我的应用程序时,请执行以下操作:

myApp.run(function ($http, MyService) {
    $http.get('data.json').success(function (data) {
        MyService.setData(data);
    });
});

然后我的服务看起来像这样:

myModule.service('MyService', function() {
    var myData = null;
    return {
        setData: function (data) {
            myData = data;
        },
        doStuff: function () {
            return myData.getSomeData();
        }
    };
});

这在某些时候有效,但如果异步数据恰好花费比一切都初始化所需的时间更长的时间,我在调用时会得到一个空指针异常 doStuff()

使用Promise对象

这可能会奏效。唯一的缺点是我调用 MyService 的所有地方我都必须知道 doStuff() 返回一个Promise,并且所有代码都必须让我们then与Promise进行交互。我宁愿等到 myData 恢复后再加载我的应用程序。

手动引导

angular.element(document).ready(function() {
    $.getJSON("data.json", function (data) {
       // can't initialize the data here because the service doesn't exist yet
       angular.bootstrap(document);
       // too late to initialize here because something may have already
       // tried to call doStuff() and would have got a null pointer exception
    });
});

全局 Javascript Var 我可以将我的 JSON 直接发送到全局 Javascript 变量:

HTML:

<script type="text/javascript" src="data.js"></script>

数据.js:

var dataForMyService = { 
// myData here
};

然后它在初始化时可用MyService

myModule.service('MyService', function() {
    var myData = dataForMyService;
    return {
        doStuff: function () {
            return myData.getSomeData();
        }
    };
});

这也可以,但是我有一个全局 javascript 变量,它闻起来很糟糕。

这些是我唯一的选择吗?这些选项之一是否比其他选项更好?我知道这是一个很长的问题,但我想表明我已尝试探索所有选项。任何指导将不胜感激。

6个回答

你看过$routeProvider.when('/path',{ resolve:{...}吗?它可以使 promise 方法更简洁:

在您的服务中公开Promise:

app.service('MyService', function($http) {
    var myData = null;

    var promise = $http.get('data.json').success(function (data) {
      myData = data;
    });

    return {
      promise:promise,
      setData: function (data) {
          myData = data;
      },
      doStuff: function () {
          return myData;//.getSomeData();
      }
    };
});

添加resolve到您的路由配置:

app.config(function($routeProvider){
  $routeProvider
    .when('/',{controller:'MainCtrl',
    template:'<div>From MyService:<pre>{{data | json}}</pre></div>',
    resolve:{
      'MyServiceData':function(MyService){
        // MyServiceData will also be injectable in your controller, if you don't want this you could create a new promise with the $q service
        return MyService.promise;
      }
    }})
  }):

在解决所有依赖项之前,您的控制器不会被实例化:

app.controller('MainCtrl', function($scope,MyService) {
  console.log('Promise is now resolved: '+MyService.doStuff().data)
  $scope.data = MyService.doStuff();
});

我在 plnkr 做了一个例子:http ://plnkr.co/edit/GKg21XH0RwCMEQGUdZKH?p=preview

非常感谢你的回复!如果我在解析映射中还没有使用 MyService 的服务,它会为我工作。我根据我的情况更新了你的 plunker:plnkr.co/edit/465Cupaf5mtxljCl5NuF?p=preview有没有办法让 MyOtherService 等待 MyService 初始化?
2021-03-10 17:31:09
嗯,如果你不使用路由怎么办?这几乎就像说除非您使用路由,否则您无法使用异步数据编写角度应用程序。将数据导入应用程序的推荐方法是异步加载它,但是一旦您有多个控制器并投入服务,BOOM 就不可能了。
2021-03-12 17:31:09
我试过了,但仍然遇到了一些问题,因为我有指令和其他控制器(我与 $routeProvider 一起使用的控制器正在处理主要的、次要的导航内容……即“MyOtherService”),需要等到“MyService” ' 解决了。我将继续尝试并在取得任何成功时更新此内容。我只是希望有一个 angular 钩子,我可以在初始化我的控制器和指令之前等待数据返回。再次感谢你的帮助。如果我有一个包含所有内容的主控制器,这将起作用。
2021-03-18 17:31:09
我想我会在 MyOtherService 中链接Promise - 我已经用链接和一些评论更新了 plunker - 这看起来如何?plnkr.co/edit/Z7dWVNA9P44Q72sLiPjW?p=preview
2021-03-29 17:31:09
这里有一个问题 - 您将如何将resolve属性分配给$routeProvider. 例如,<div ng-controller="IndexCtrl"></div>在这里,控制器被明确提及,而不是通过路由加载。在这种情况下,如何延迟控制器的实例化呢?
2021-03-29 17:31:09

基于 Martin Atkins 的解决方案,这里有一个完整、简洁的纯 Angular 解决方案:

(function() {
  var initInjector = angular.injector(['ng']);
  var $http = initInjector.get('$http');
  $http.get('/config.json').then(
    function (response) {
      angular.module('config', []).constant('CONFIG', response.data);

      angular.element(document).ready(function() {
          angular.bootstrap(document, ['myApp']);
        });
    }
  );
})();

此解决方案使用自执行匿名函数来获取 $http 服务,请求配置,并在可用时将其注入名为 CONFIG 的常量。

完成后,我们等待文档准备就绪,然后引导 Angular 应用程序。

这是对 Martin 解决方案的轻微增强,后者将获取配置推迟到文档准备好之后。据我所知,没有理由为此延迟 $http 调用。

单元测试

注意:当代码包含在您的app.js文件中时,我发现此解决方案在进行单元测试时效果不佳这样做的原因是上面的代码在加载 JS 文件时立即运行。这意味着测试框架(在我的例子中是 Jasmine)没有机会提供$http.

我对此并不完全满意的解决方案是将此代码移动到我们的index.html文件中,因此 Grunt/Karma/Jasmine 单元测试基础架构看不到它。

它允许您使用 Angular 的依赖注入系统来访问需要它的module中的“CONFIG”常量,但您不会冒险破坏其他不需要它的module。例如,如果您使用了全局 'config' 变量,则其他 3rd 方代码也可能正在寻找相同的变量。
2021-03-16 17:31:09
我是一个 angular 新手,这里有一些关于如何在我的应用程序中解决配置module依赖项的说明: gist.github.com/dsulli99/0be3e80db9b21ce7b​​989 ref:tutorials.jenkov.com/angularjs/...感谢您的解决方案。
2021-03-21 17:31:09
诸如“不要污染全局范围”之类的规则应该只在它们使我们的代码更好(更简单、更易于维护、更安全等)的程度上遵循。我看不出这个解决方案比简单地将数据加载到单个全局变量中更好。我错过了什么?
2021-04-05 17:31:09
在下面的其他手动引导解决方案之一的评论中提到了它,但作为一个没有发现它的有角度的新手,我可以指出你需要在你的 html 代码中删除你的 ng-app 指令才能正常工作- 它正在用这种手动方法替换自动引导程序(通过 ng-app)。如果您不取出 ng-app,该应用程序实际上可以运行,但您会在控制台中看到各种未知的提供程序错误。
2021-04-07 17:31:09

我使用了与@XMLilley 描述的方法类似的方法,但希望能够使用 AngularJS 服务,例如$http加载配置并在不使用低级 API 或 jQuery 的情况下进行进一步初始化。

resolve在路由上使用也不是一种选择,因为我需要在我的应用程序启动时将这些值作为常量提供,即使在module.config()块中也是如此。

我创建了一个小的 AngularJS 应用程序来加载配置,将它们设置为实际应用程序上的常量并引导它。

// define the module of your app
angular.module('MyApp', []);

// define the module of the bootstrap app
var bootstrapModule = angular.module('bootstrapModule', []);

// the bootstrapper service loads the config and bootstraps the specified app
bootstrapModule.factory('bootstrapper', function ($http, $log, $q) {
  return {
    bootstrap: function (appName) {
      var deferred = $q.defer();

      $http.get('/some/url')
        .success(function (config) {
          // set all returned values as constants on the app...
          var myApp = angular.module(appName);
          angular.forEach(config, function(value, key){
            myApp.constant(key, value);
          });
          // ...and bootstrap the actual app.
          angular.bootstrap(document, [appName]);
          deferred.resolve();
        })
        .error(function () {
          $log.warn('Could not initialize application, configuration could not be loaded.');
          deferred.reject();
        });

      return deferred.promise;
    }
  };
});

// create a div which is used as the root of the bootstrap app
var appContainer = document.createElement('div');

// in run() function you can now use the bootstrapper service and shutdown the bootstrapping app after initialization of your actual app
bootstrapModule.run(function (bootstrapper) {

  bootstrapper.bootstrap('MyApp').then(function () {
    // removing the container will destroy the bootstrap app
    appContainer.remove();
  });

});

// make sure the DOM is fully loaded before bootstrapping.
angular.element(document).ready(function() {
  angular.bootstrap(appContainer, ['bootstrapModule']);
});

此处查看实际操作(使用$timeout而不是$http):http : //plnkr.co/edit/FYznxP3xe8dxzwxs37hi?p=preview

更新

我建议使用下面由 Martin Atkins 和 JBCP 描述的方法。

更新 2

因为我在多个项目中需要它,所以我刚刚发布了一个 bower module来处理这个:https : //github.com/philippd/angular-deferred-bootstrap

从后端加载数据并在 AngularJS module上设置名为 APP_CONFIG 的常量的示例:

deferredBootstrapper.bootstrap({
  element: document.body,
  module: 'MyApp',
  resolve: {
    APP_CONFIG: function ($http) {
      return $http.get('/api/demo-config');
    }
  }
});
deferredBootstrapper 是要走的路
2021-03-27 17:31:09

“手动引导”案例可以通过在引导之前手动创建注入器来访问 Angular 服务。这个初始注入器将是独立的(不附加到任何元素)并且只包含加载的module的一个子集。如果您只需要核心 Angular 服务,只需加载就足够了ng,如下所示:

angular.element(document).ready(
    function() {
        var initInjector = angular.injector(['ng']);
        var $http = initInjector.get('$http');
        $http.get('/config.json').then(
            function (response) {
               var config = response.data;
               // Add additional services/constants/variables to your app,
               // and then finally bootstrap it:
               angular.bootstrap(document, ['myApp']);
            }
        );
    }
);

例如,您可以使用该module.constant机制向您的应用程序提供数据:

myApp.constant('myAppConfig', data);

myAppConfig现在可以注入就像任何其他服务,尤其是在配置阶段是可用的:

myApp.config(
    function (myAppConfig, someService) {
        someService.config(myAppConfig.someServiceConfig);
    }
);

或者,对于较小的应用程序,您可以将全局配置直接注入您的服务,代价是在整个应用程序中传播有关配置格式的知识。

当然,由于这里的异步操作会阻塞应用程序的引导,从而阻塞模板的编译/链接,因此明智的做法是使用该ng-cloak指令来防止在工作期间出现未解析的模板。您还可以通过提供一些仅在 AngularJS 初始化之前显示的 HTML 来在 DOM 中提供某种加载指示:

<div ng-if="initialLoad">
    <!-- initialLoad never gets set, so this div vanishes as soon as Angular is done compiling -->
    <p>Loading the app.....</p>
</div>
<div ng-cloak>
    <!-- ng-cloak attribute is removed once the app is done bootstrapping -->
    <p>Done loading the app!</p>
</div>

在 Plunker 上创建这种方法的完整工作示例,以从静态 JSON 文件加载配置为例。

@JBCP 是的,你是对的,如果你交换事件,它也能正常工作,这样我们就不会等到文档准备好,直到 HTTP 响应返回之后,优点是可能能够开始 HTTP请求更快。只有引导程序调用需要等到 DOM 准备好。
2021-03-17 17:31:09
我用你的方法创建了一个凉亭module:github.com/philippd/angular-deferred-bootstrap
2021-03-30 17:31:09
@MartinAtkins,我刚刚发现您的好方法不适用于 Angular v1.1+。看起来早期版本的 Angular 在应用程序启动之前不理解“then”。要在您的 Plunk 中看到它,请将 Angular URL 替换为code.angularjs.org/1.1.5/angular.min.js
2021-04-02 17:31:09
我认为您不需要将 $http.get() 推迟到文档准备好之后。
2021-04-05 17:31:09

我有同样的问题:我喜欢这个resolve对象,但这仅适用于 ng-view 的内容。如果你有控制器(比如顶级导航)存在于 ng-view 之外并且需要在路由开始发生之前用数据初始化怎么办?我们如何避免在服务器端搞砸只是为了让它工作?

使用手动引导程序和角度常数一个天真的 XHR 为您获取数据,然后您在其回调中引导 angular,它可以处理您的异步问题。在下面的示例中,您甚至不需要创建全局变量。返回的数据仅作为可注入对象存在于 angular 范围内,甚至不存在于控制器、服务等内部,除非您注入它。(就像您将resolve对象的输出注入到路由视图的控制器中一样。)如果您希望此后将数据作为服务进行交互,您可以创建一个服务,注入数据,没有人会更聪明.

例子:

//First, we have to create the angular module, because all the other JS files are going to load while we're getting data and bootstrapping, and they need to be able to attach to it.
var MyApp = angular.module('MyApp', ['dependency1', 'dependency2']);

// Use angular's version of document.ready() just to make extra-sure DOM is fully 
// loaded before you bootstrap. This is probably optional, given that the async 
// data call will probably take significantly longer than DOM load. YMMV.
// Has the added virtue of keeping your XHR junk out of global scope. 
angular.element(document).ready(function() {

    //first, we create the callback that will fire after the data is down
    function xhrCallback() {
        var myData = this.responseText; // the XHR output

        // here's where we attach a constant containing the API data to our app 
        // module. Don't forget to parse JSON, which `$http` normally does for you.
        MyApp.constant('NavData', JSON.parse(myData));

        // now, perform any other final configuration of your angular module.
        MyApp.config(['$routeProvider', function ($routeProvider) {
            $routeProvider
              .when('/someroute', {configs})
              .otherwise({redirectTo: '/someroute'});
          }]);

        // And last, bootstrap the app. Be sure to remove `ng-app` from your index.html.
        angular.bootstrap(document, ['NYSP']);
    };

    //here, the basic mechanics of the XHR, which you can customize.
    var oReq = new XMLHttpRequest();
    oReq.onload = xhrCallback;
    oReq.open("get", "/api/overview", true); // your specific API URL
    oReq.send();
})

现在,你的NavData常数存在。继续并将其注入控制器或服务:

angular.module('MyApp')
    .controller('NavCtrl', ['NavData', function (NavData) {
        $scope.localObject = NavData; //now it's addressable in your templates 
}]);

当然,使用裸 XHR 对象会剥夺$httpJQuery 会为您处理的许多细节,但此示例没有特殊依赖项,至少对于简单的get. 如果您想为您的请求提供更多功能,请加载一个外部库来帮助您。但我认为$http在这种情况下不可能访问 angular或其他工具。

(SO相关帖子