本文翻译自:AngularJS : Initialize service with asynchronous data
I have an AngularJS service that I want to initialize with some asynchronous data. 我有一个AngularJS服务,我想用一些异步数据进行初始化。 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. 显然这不起作用,因为如果某些东西试图在myData
返回之前调用doStuff()
,我将得到一个空指针异常。 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()
这种情况在某些时候有效,但是如果异步数据发生的时间比初始化所有内容要花费的时间长,那么当我调用doStuff()
时会得到一个空指针异常
Use promise objects 使用promise对象
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. 我称之为MyService的唯一缺点就是我必须知道doStuff()会返回一个promise, then
我们需要所有代码与promise进行交互。 I would rather just wait until myData is back before loading the my application. 在加载我的应用程序之前,我宁愿等到myData返回。
Manual Bootstrap 手动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: 全局Javascript Var我可以将我的JSON直接发送到全局Javascript变量:
HTML: HTML:
<script type="text/javascript" src="data.js"></script>
data.js: data.js:
var dataForMyService = {
// myData here
};
Then it would be available when initializing MyService
: 然后在初始化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. 这也可行,但后来我有一个全局的javascript变量闻起来很糟糕。
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. 非常感谢任何指导。
#1楼
参考:https://stackoom.com/question/16KtB/AngularJS-使用异步数据初始化服务
#2楼
Have you had a look at $routeProvider.when('/path',{ resolve:{...}
? It can make the promise approach a bit cleaner: 您是否看过$routeProvider.when('/path',{ resolve:{...}
?它可以使承诺更清晰:
Expose a promise in your service: 在您的服务中披露承诺:
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();
}
};
});
Add resolve
to your route config: 添加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;
}
}})
}):
Your controller won't get instantiated before all dependencies are resolved: 在解决所有依赖项之前,您的控制器不会被实例化:
app.controller('MainCtrl', function($scope,MyService) {
console.log('Promise is now resolved: '+MyService.doStuff().data)
$scope.data = MyService.doStuff();
});
I've made an example at plnkr: http://plnkr.co/edit/GKg21XH0RwCMEQGUdZKH?p=preview 我在plnkr上做了一个例子: http ://plnkr.co/edit/GKg21XH0RwCMEQGUdZKH?p = preview
#3楼
So I found a solution. 所以我找到了解决方案。 I created an angularJS service, we'll call it MyDataRepository and I created a module for it. 我创建了一个angularJS服务,我们称之为MyDataRepository,我为它创建了一个模块。 I then serve up this javascript file from my server-side controller: 然后我从服务器端控制器提供这个javascript文件:
HTML: HTML:
<script src="path/myData.js"></script>
Server-side: 服务器端:
@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);
}
I can then inject MyDataRepository where ever I need it: 然后,我可以在需要它的地方注入MyDataRepository:
someOtherModule.service('MyOtherService', function(MyDataRepository) {
var myData = MyDataRepository.getData();
// Do what you have to do...
}
This worked great for me, but I am open to any feedback if anyone has any. 这对我很有用,但如果有人有任何反馈,我愿意接受任何反馈。 } }
#4楼
What you can do is in your .config for the app is create the resolve object for the route and in the function pass in $q (promise object) and the name of the service you're depending on, and resolve the promise in the callback function for the $http in the service like so: 您可以做的是在您的.config中为app创建路由的解析对象,并在函数传递$ q(promise对象)和您依赖的服务名称,并解决服务中$ http的回调函数如下:
ROUTE CONFIG ROUTE CONFIG
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;
}
})
}
Angular won't render the template or make the controller available until defer.resolve() has been called. 在调用defer.resolve()之前,Angular不会渲染模板或使控制器可用。 We can do that in our service: 我们可以在服务中做到这一点:
SERVICE 服务
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;
});
Now that MyService has the data assigned to it's data property, and the promise in the route resolve object has been resolved, our controller for the route kicks into life, and we can assign the data from the service to our controller object. 既然MyService已经为其数据属性分配了数据,并且路径解析对象中的promise已经解析,我们的路由控制器就会生效,我们可以将服务中的数据分配给控制器对象。
CONTROLLER CONTROLLER
app.controller('homeCtrl',function($scope,MyService){
$scope.servicedata = MyService.data;
});
Now all our binding in the scope of the controller will be able to use the data which originated from MyService. 现在,我们在控制器范围内的所有绑定都将能够使用源自MyService的数据。
#5楼
I had the same problem: I love the resolve
object, but that only works for the content of ng-view. 我有同样的问题:我喜欢resolve
对象,但这只适用于ng-view的内容。 What if you have controllers (for top-level nav, let's say) that exist outside of ng-view and which need to be initialized with data before the routing even begins to happen? 如果您在ng-view之外存在控制器(用于顶层导航,并且在路由开始发生之前需要使用数据进行初始化),该怎么办? How do we avoid mucking around on the server-side just to make that work? 我们如何避免在服务器端乱搞只是为了让它工作?
Use manual bootstrap and an angular constant . 使用手动自举和角度常数 。 A naiive XHR gets you your data, and you bootstrap angular in its callback, which deals with your async issues. 一个简单的XHR可以获取您的数据,并在其回调中引导角度,这将处理您的异步问题。 In the example below, you don't even need to create a global variable. 在下面的示例中,您甚至不需要创建全局变量。 The returned data exists only in angular scope as an injectable, and isn't even present inside of controllers, services, etc. unless you inject it. 返回的数据仅作为可注入的角度范围存在,并且除非您注入它,否则甚至不存在于控制器,服务等内部。 (Much as you would inject the output of your resolve
object into the controller for a routed view.) If you prefer to thereafter interact with that data as a service, you can create a service, inject the data, and nobody will ever be the wiser. (就像你将resolve
对象的输出注入控制器以获取路由视图一样。)如果您希望此后作为服务与该数据进行交互,您可以创建服务,注入数据,并且没有人会成为聪明。
Example: 例:
//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();
})
Now, your NavData
constant exists. 现在,您的NavData
常量存在。 Go ahead and inject it into a controller or service: 继续将其注入控制器或服务:
angular.module('MyApp')
.controller('NavCtrl', ['NavData', function (NavData) {
$scope.localObject = NavData; //now it's addressable in your templates
}]);
Of course, using a bare XHR object strips away a number of the niceties that $http
or JQuery would take care of for you, but this example works with no special dependencies, at least for a simple get
. 当然,使用裸XHR对象会消除$http
或JQuery会为您处理的一些细节,但是这个示例没有特殊的依赖关系,至少对于一个简单的get
。 If you want a little more power for your request, load up an external library to help you out. 如果您想为请求提供更多功能,请加载外部库以帮助您。 But I don't think it's possible to access angular's $http
or other tools in this context. 但我不认为在这种情况下可以访问angular的$http
或其他工具。
(SO related post ) (SO 相关帖子 )
#6楼
I used a similar approach to the one described by @XMLilley but wanted to have the ability to use AngularJS services like $http
to load the configuration and do further initialization without the use of low level APIs or jQuery. 我使用了与@XMLilley描述的方法类似的方法,但希望能够使用像$http
这样的AngularJS服务来加载配置并在不使用低级API或jQuery的情况下进行进一步的初始化。
Using resolve
on routes was also not an option because I needed the values to be available as constants when my app is started, even in module.config()
blocks. 在路由上使用resolve
也不是一个选项,因为我需要在我的应用程序启动时将值作为常量使用,即使在module.config()
块中也是如此。
I created a small AngularJS app that loads the config, sets them as constants on the actual app and bootstraps it. 我创建了一个小的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']);
});
See it in action (using $timeout
instead of $http
) here: http://plnkr.co/edit/FYznxP3xe8dxzwxs37hi?p=preview 在此处查看它(使用$timeout
而不是$http
): $http
: //plnkr.co/edit/FYznxP3xe8dxzwxs37hi?p = preview
UPDATE UPDATE
I would recommend to use the approach described below by Martin Atkins and JBCP. 我建议使用Martin Atkins和JBCP下面介绍的方法。
UPDATE 2 更新2
Because I needed it in multiple projects, I just released a bower module that takes care of this: https://github.com/philippd/angular-deferred-bootstrap 因为我在多个项目中需要它,所以我刚刚发布了一个bower模块来处理这个问题: https : //github.com/philippd/angular-deferred-bootstrap
Example that loads data from the back-end and sets a constant called APP_CONFIG on the AngularJS module: 从后端加载数据并在AngularJS模块上设置一个名为APP_CONFIG的常量的示例:
deferredBootstrapper.bootstrap({
element: document.body,
module: 'MyApp',
resolve: {
APP_CONFIG: function ($http) {
return $http.get('/api/demo-config');
}
}
});