我有一个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()返回一个承诺和所有的代码将不得不我们然后与承诺交互。我宁愿只是等待,直到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>
data.js:
var dataForMyService = {
// myData here
};
然后在初始化MyService时可用:
myModule.service('MyService', function() {
var myData = dataForMyService;
return {
doStuff: function () {
return myData.getSomeData();
}
};
});
这也可以工作,但我有一个全局javascript变量闻起来很糟糕。
这是我唯一的选择吗?这些选项中是否有一个比其他选项更好?我知道这是一个相当长的问题,但我想表明我已经尝试了所有的选择。任何指导都将不胜感激。
你可以在应用的。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的数据。
我使用了类似于@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');
}
}
});
你看过$routeProvider吗?当(/路径,{解决:{…}?它可以让承诺的方式更简洁:
在你的服务中暴露一个承诺:
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
“手动引导”的情况可以通过在引导之前手动创建注入器来访问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']);
}
);
}
);
例如,您可以使用模块。常量机制,使数据可用于你的应用程序:
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文件加载配置为例。
所以我找到了一个解决方案。我创建了一个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...
}
这对我来说很有效,但如果有人有任何反馈,我愿意接受。
}