483

I have an AngularJS service that I want to initialize with some asynchronous data. Something like this:

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();
        }
    };
});

Obviously this won't work because if something tries to call doStuff() before myData gets back I will get a null pointer exception. As far as I can tell from reading some of the other questions asked here and here I have a few options, but none of them seem very clean (perhaps I am missing something):

Setup Service with "run"

When setting up my app do this:

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

Then my service would look like this:

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

This works some of the time but if the asynchronous data happens to take longer than it takes for everything to get initialized I get a null pointer exception when I call doStuff()

Use promise objects

This would probably work. The only downside it everywhere I call MyService I will have to know that doStuff() returns a promise and all the code will have to us then to interact with the promise. I would rather just wait until myData is back before loading the my application.

Manual Bootstrap

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
    });
});

Global Javascript Var I could send my JSON directly to a global Javascript variable:

HTML:

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

data.js:

var dataForMyService = { 
// myData here
};

Then it would be available when initializing MyService:

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

This would work too, but then I have a global javascript variable which smells bad.

Are these my only options? Are one of these options better than the others? I know this is a pretty long question, but I wanted to show that I have tried to explore all my options. Any guidance would greatly be appreciated.

4

10 回答 10

329

你看过$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

于 2013-04-29T21:23:27.097 回答
89

基于 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 单元测试基础架构看不到它。

于 2014-01-17T15:04:21.900 回答
49

我使用了与@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 模块来处理这个问题: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');
    }
  }
});
于 2013-11-09T01:27:37.203 回答
44

“手动引导”案例可以通过在引导之前手动创建注入器来访问 Angular 服务。这个初始注入器将是独立的(不附加到任何元素)并且只包含加载的模块的子集。如果您只需要核心 Angular 服务,只需 load 就足够了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 上创建了一个完整的工作示例,从静态 JSON 文件加载配置作为示例。

于 2013-12-08T01:13:26.007 回答
16

我有同样的问题:我喜欢这个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 对象会去掉$httpJQuery 会为您处理的许多细节,但是这个示例没有特殊的依赖关系,至少对于一个简单的get. 如果您想为您的请求提供更多功能,请加载一个外部库来帮助您。但我认为$http在这种情况下不可能访问角度或其他工具。

(所以相关的帖子

于 2013-08-20T13:33:13.370 回答
8

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

路线配置

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 的数据。

于 2013-05-25T21:41:20.877 回答
5

所以我找到了解决方案。我创建了一个 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...
}

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

于 2013-05-02T14:15:20.467 回答
2

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

于 2014-11-25T15:21:28.967 回答
1

您可以使用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-06T13:55:13.083 回答
-1

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

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

索引.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;
    });
  };

注意:如果您没有多个相同的代码,则可以使用此方法。

于 2016-03-16T13:59:46.753 回答