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

我有一个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(),我将得到一个空指针异常。据我所知,从阅读一些其他问题在这里在这里中,我有一些选择,但没有一个看起来很干净(也许我遗漏了一些东西):

带有“run”的安装服务

当设置我的应用程序这样做:

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变量闻起来很糟糕。

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

220304 次浏览

你有看过$routeProvider.when('/path',{ resolve:{...}吗?它可以让承诺的方式更简洁:

在你的服务中暴露一个承诺:

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中创建了一个例子

所以我找到了一个解决方案。我创建了一个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...
}
这对我来说很有效,但如果任何人有任何反馈,我都愿意接受。 } < / p >

你可以在应用的。config中为路由创建resolve对象,在函数中传入$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;
}
})
}

在调用deferred .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的数据。

我有同样的问题:我喜欢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对象剥夺了$http或JQuery会为你照顾的一些细节,但这个例子没有特殊的依赖关系,至少对于简单的get是这样。如果您希望为您的请求提供更强大的功能,可以加载一个外部库来帮助您。但我认为在这种情况下不可能访问angular的$http或其他工具。

(所以相关的职位)

我使用了类似于@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

因为我在多个项目中需要它,所以我刚刚发布了一个处理这个问题的模块: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');
}
}
});

“手动引导”的情况可以通过在引导之前手动创建注入器来访问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中提供一些加载指示,通过提供一些HTML,这些HTML只在AngularJS初始化之前显示:

<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文件加载配置为例。

基于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的解决方案的轻微改进,Martin的解决方案将获取配置延迟到文档准备好之后。据我所知,没有理由为此延迟$http调用。

单元测试

注意:我发现当代码包含在你的app.js文件中时,这个解决方案在单元测试时不能很好地工作。这样做的原因是上述代码在加载JS文件时立即运行。这意味着测试框架(在我的例子中是Jasmine)没有机会提供$http的模拟实现。

我的解决方案,我不完全满意,是移动这段代码到我们的index.html文件,所以Grunt/Karma/Jasmine单元测试基础设施看不到它。

同样,在实际控制器执行之前,您可以使用以下技术全局地提供您的服务:只需要全局解析你的数据,然后在run块中传递给你的服务。

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

只要把ng-init div scope放在你想获取init数据的地方

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

注意:你可以使用这种方法,如果你没有相同的代码不止一个地方。

你可以使用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>