【问题标题】:AngularJS : Initialize service with asynchronous dataAngularJS:使用异步数据初始化服务
【发布时间】:2013-04-23 14:08:13
【问题描述】:

我有一个 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();
        }
    };
});

显然这不起作用,因为如果在myData 回来之前尝试调用doStuff(),我将得到一个空指针异常。据我通过阅读herehere 提出的其他一些问题可以看出,我有一些选择,但它们看起来都不是很干净(也许我遗漏了一些东西):

使用“运行”设置服务

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

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() 时会收到空指针异常

使用承诺对象

这可能会奏效。在我调用 MyService 的任何地方,唯一的缺点是我必须知道 doStuff() 返回一个承诺,并且所有代码都必须让我们then 与承诺进行交互。我宁愿等到 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 变量 我可以将我的 JSON 直接发送到一个全局 Javascript 变量:

HTML:

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

data.js:

var dataForMyService = { 
// myData here
};

那么在初始化MyService的时候就可以使用了:

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

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

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

【问题讨论】:

标签: javascript angularjs asynchronous service angular-promise


【解决方案1】:

你看过$routeProvider.when('/path',{ resolve:{...}吗?它可以让 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 初始化?
  • 我想我会在 MyOtherService 中链接承诺 - 我已经用链接和一些 cmets 更新了 plunker - 这看起来如何? plnkr.co/edit/Z7dWVNA9P44Q72sLiPjW?p=preview
  • 我试过了,但仍然遇到了一些问题,因为我有指令和其他控制器(我与 $routeProvider 一起使用的控制器正在处理主要的、次要的导航内容......那就是 'MyOtherService' ) 需要等到“MyService”解决。我将继续尝试并在我取得任何成功的情况下更新它。我只是希望在初始化我的控制器和指令之前我可以等待数据返回的角度挂钩。再次感谢你的帮助。如果我有一个包含所有内容的主控制器,这将起作用。
  • 这里有一个问题 - 如何将resolve 属性分配给$routeProvider 中未提及的控制器。例如,&lt;div ng-controller="IndexCtrl"&gt;&lt;/div&gt;。在这里,明确提到了控制器,而不是通过路由加载。在这种情况下,如何延迟控制器的实例化呢?
  • 嗯,如果您不使用路由怎么办?这几乎就像说除非您使用路由,否则您不能使用异步数据编写 Angular 应用程序。将数据导入应用程序的推荐方法是异步加载,但一旦您拥有多个控制器并投入服务,BOOM 就不可能了。
【解决方案2】:

所以我找到了解决方案。我创建了一个 angularJS 服务,我们将其命名为 MyDataRepository,并为它创建了一个模块。然后我从我的服务器端控制器提供这个 javascript 文件:

HTML:

<script src="path/myData.js"></script>

服务器端:

@RequestMapping(value="path/myData.js", method=RequestMethod.GET)
public ResponseEntity<String> getMyDataRepositoryJS()
{
    // Populate data that I need into a Map
    Map<String, String> myData = new HashMap<String,String>();
    ...
    // Use Jackson to convert it to JSON
    ObjectMapper mapper = new ObjectMapper();
    String myDataStr = mapper.writeValueAsString(myData);

    // Then create a String that is my javascript file
    String myJS = "'use strict';" +
    "(function() {" +
    "var myDataModule = angular.module('myApp.myData', []);" +
    "myDataModule.service('MyDataRepository', function() {" +
        "var myData = "+myDataStr+";" +
        "return {" +
            "getData: function () {" +
                "return myData;" +
            "}" +
        "}" +
    "});" +
    "})();"

    // Now send it to the client:
    HttpHeaders responseHeaders = new HttpHeaders();
    responseHeaders.add("Content-Type", "text/javascript");
    return new ResponseEntity<String>(myJS , responseHeaders, HttpStatus.OK);
}

然后我可以在任何需要的地方注入 MyDataRepository:

someOtherModule.service('MyOtherService', function(MyDataRepository) {
    var myData = MyDataRepository.getData();
    // Do what you have to do...
}

这对我很有用,但如果有人有任何反馈,我愿意接受。 }

【讨论】:

  • 我喜欢你的模块化方法。我发现 $routeScope 可用于请求数据的服务,您可以在 $http.success 回调中为其分配数据。但是,将 $routeScope 用于非全局项目会产生异味,并且数据实际上应该分配给控制器 $scope。不幸的是,我认为您的方法虽然具有创新性,但并不理想(尽管尊重找到适合您的东西)。我只是确定必须有一个仅限客户端的答案,它以某种方式等待数据并允许分配范围。搜索继续!
  • 如果它对某人有用,我最近看到了一些不同的方法来查看其他人编写并添加到 ngModules 网站的模块。当我有更多时间时,我将不得不开始使用其中一个,或者弄清楚他们做了什么并将其添加到我的东西中。
【解决方案3】:

您可以在应用程序的 .config 中为路由创建解析对象,并在函数中传入 $q(promise 对象)和您所依赖的服务的名称,然后解析服务中 $http 的回调函数中的 promise,如下所示:

路线配置

app.config(function($routeProvider){
    $routeProvider
     .when('/',{
          templateUrl: 'home.html',
          controller: 'homeCtrl',
          resolve:function($q,MyService) {
                //create the defer variable and pass it to our service
                var defer = $q.defer();
                MyService.fetchData(defer);
                //this will only return when the promise
                //has been resolved. MyService is going to
                //do that for us
                return defer.promise;
          }
      })
}

在调用 defer.resolve() 之前,Angular 不会渲染模板或使控制器可用。我们可以在我们的服务中做到这一点:

服务

app.service('MyService',function($http){
       var MyService = {};
       //our service accepts a promise object which 
       //it will resolve on behalf of the calling function
       MyService.fetchData = function(q) {
             $http({method:'GET',url:'data.php'}).success(function(data){
                 MyService.data = data;
                 //when the following is called it will
                 //release the calling function. in this
                 //case it's the resolve function in our
                 //route config
                 q.resolve();
             }
       }

       return MyService;
});

现在 MyService 已将数据分配给它的 data 属性,并且路由解析对象中的承诺已被解析,我们的路由控制器开始运行,我们可以将来自服务的数据分配给我们的控制器对象。

控制器

  app.controller('homeCtrl',function($scope,MyService){
       $scope.servicedata = MyService.data;
  });

现在我们在控制器范围内的所有绑定都将能够使用源自 MyService 的数据。

【讨论】:

  • 当我有更多时间时,我会试一试。这看起来类似于其他人在 ngModules 中尝试做的事情。
  • 我喜欢这种方法,并且我以前使用过它,但是当我有几条路线时,我目前正在尝试以一种干净的方式做到这一点,每条路线都可能取决于也可能不取决于预取数据。对此有什么想法吗?
  • 顺便说一句,我倾向于让每个需要预取数据的服务在初始化时发出请求并返回承诺,然后使用不同路由所需的服务设置解析对象。我只是希望有一种不那么冗长的方式。
  • @dewd 这就是我的目标,但如果有某种方式可以说“无论加载哪条路线,都先获取所有这些东西”,而不必重复我的解决方案,我会更喜欢 -块。他们都有自己所依赖的东西。但这并不是什么大不了的事,只是感觉更D.R.Y. :)
  • 这是我最终采取的路线,除了我必须将resolve 设为一个对象,其属性为函数。所以它最终成为resolve:{ dataFetch: function(){ // call function here } }
【解决方案4】:

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

使用手动引导和角度常数。一个简单的 XHR 会为您获取数据,并在其回调中引导 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 对象会消除 $http 或 JQuery 会为您处理的许多细节,但是这个示例没有特殊的依赖关系,至少对于简单的 get 来说是这样。如果您想为您的请求提供更多功能,请加载一个外部库来帮助您。但我认为在这种情况下无法访问 angular 的 $http 或其他工具。

(所以related post

【讨论】:

    【解决方案5】:

    我使用了与@XMLilley 描述的方法类似的方法,但希望能够使用像$http 这样的 AngularJS 服务来加载配置并进行进一步的初始化,而无需使用低级 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 模块来处理这个问题:https://github.com/philippd/angular-deferred-bootstrap

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

    deferredBootstrapper.bootstrap({
      element: document.body,
      module: 'MyApp',
      resolve: {
        APP_CONFIG: function ($http) {
          return $http.get('/api/demo-config');
        }
      }
    });
    

    【讨论】:

    • deferredBootstrapper 是要走的路
    【解决方案6】:

    “手动引导”案例可以通过在引导之前手动创建注入器来访问 Angular 服务。这个初始注入器将是独立的(不附加到任何元素)并且只包含加载的模块的子集。如果您只需要核心 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 指令来防止未解析的模板在工作期间出现.您还可以在 DOM 中提供某种加载指示,方法是提供一些仅在 AngularJS 初始化之前才显示的 HTML:

    <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 上创建了这种方法的a complete, working example,以从静态 JSON 文件加载配置为例。

    【讨论】:

    • 我认为您不需要将 $http.get() 推迟到文档准备好之后。
    • @JBCP 是的,你说得对,如果你交换事件,它也能正常工作,这样我们就不会等到返回 HTTP 响应后才等到文档准备好,这样做的好处是可能能够更快地开始 HTTP 请求。只有引导调用需要等待 DOM 准备好。
    • 我用你的方法创建了一个凉亭模块:github.com/philippd/angular-deferred-bootstrap
    • @MartinAtkins,我刚刚发现你的好方法不适用于 Angular v1.1+。看起来早期版本的 Angular 只是在应用程序被引导之前不理解“then”。要在您的 Plunk 中查看它,请将 Angular URL 替换为 code.angularjs.org/1.1.5/angular.min.js
    【解决方案7】:

    基于 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 的依赖注入系统来访问需要它的模块中的 'CONFIG' 常量,但你不会冒险破坏其他不需要它的模块。例如,如果您使用了全局“config”变量,则其他第 3 方代码也有可能也在寻找相同的变量。
    • 我是一个有角度的新手,这里有一些关于我如何在我的应用程序中解决配置模块依赖关系的说明:gist.github.com/dsulli99/0be3e80db9b21ce7b989 ref:tutorials.jenkov.com/angularjs/… 谢谢你的解决方案。
    • 以下其他手动引导解决方案之一的评论中提到了它,但作为一个没有发现它的角度新手,我可以指出您需要删除您的 ng-app 指令您的 html 代码使其正常工作 - 它正在用这种手动方法替换自动引导程序(通过 ng-app)。如果你不把 ng-app 拿出来,应用程序可能真的可以工作,但你会在控制台中看到各种未知提供程序错误。
    【解决方案8】:

    此外,您可以使用以下技术在执行实际控制器之前全局配置您的服务:https://stackoverflow.com/a/27050497/1056679。只需全局解析您的数据,然后在 run 块中将其传递给您的服务。

    【讨论】:

      【解决方案9】:

      获取任何初始化使用 ng-init 目录的最简单方法。

      只需将 ng-init div 范围放在要获取初始化数据的位置

      index.html

      <div class="frame" ng-init="init()">
          <div class="bit-1">
            <div class="field p-r">
              <label ng-show="regi_step2.address" class="show-hide c-t-1 ng-hide" style="">Country</label>
              <select class="form-control w-100" ng-model="country" name="country" id="country" ng-options="item.name for item in countries" ng-change="stateChanged()" >
              </select>
              <textarea class="form-control w-100" ng-model="regi_step2.address" placeholder="Address" name="address" id="address" ng-required="true" style=""></textarea>
            </div>
          </div>
        </div>
      

      index.js

      $scope.init=function(){
          $http({method:'GET',url:'/countries/countries.json'}).success(function(data){
            alert();
                 $scope.countries = data;
          });
        };
      

      注意:如果您的相同代码不止一处,您可以使用此方法。

      【讨论】:

      【解决方案10】:

      您可以使用JSONP 异步加载服务数据。 JSONP 请求将在初始页面加载期间发出,结果将在您的应用程序启动之前可用。这样一来,您就不必使用多余的解析来膨胀您的路由。

      你的 html 应该是这样的:

      <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
      <script>
      
      function MyService {
        this.getData = function(){
          return   MyService.data;
        }
      }
      MyService.setData = function(data) {
        MyService.data = data;
      }
      
      angular.module('main')
      .service('MyService', MyService)
      
      </script>
      <script src="/some_data.php?jsonp=MyService.setData"></script>
      

      【讨论】:

        猜你喜欢
        • 2016-07-23
        • 2013-10-03
        • 2016-09-17
        • 1970-01-01
        • 2015-12-04
        • 2022-01-25
        • 1970-01-01
        • 1970-01-01
        • 2014-06-23
        相关资源
        最近更新 更多