いくつかの非同期データで初期化したい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()
を呼び出そうとするとnullポインタ例外が発生するので、これはうまくいきません。 here および here 私はいくつかの選択肢がありますが、それらのどれも非常にきれいには見えません(おそらく私は何かを見逃しています)。
"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()
を呼び出すときにnullポインタ例外が発生します。
約束のオブジェクトを使う
これはおそらくうまくいくでしょう。私が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
});
});
Global 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変数があります。
これらは私の唯一の選択肢ですか?これらの選択肢の1つは他の選択肢より優れていますか?私はこれがかなり長い質問であることを知っています、しかし私は私がすべての私の選択肢を探求しようとしたことを示したかったです。任意のガイダンスは大歓迎です。
$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
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のソリューションをわずかに拡張したもので、ドキュメントの準備ができるまでconfigの取得を延期しました。私の知る限りでは、$ httpの呼び出しを遅らせる理由はありません。
ユニットテスト
注:コードがapp.js
ファイルに含まれている場合、この解決策が単体テストするときにうまく機能しないことがわかりました。これは、JSファイルがロードされるとすぐに上記のコードが実行されるためです。これはテストフレームワーク(私の場合はJasmine)が$http
のモックな実装を提供する機会がないことを意味します。
私の解決策は、私が完全に満足しているわけではないが、このコードを私たちのindex.html
ファイルに移動することでした。そのため、Grunt/Karma/Jasmineの単体テストインフラストラクチャには表示されません。
@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
_ update _
Martin AtkinsとJBCPによる下記のアプローチを使用することをお勧めします。
UPDATE 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');
}
}
});
「手動ブートストラップ」の場合は、ブートストラップの前に手動でインジェクタを作成することでAngularサービスにアクセスできます。この最初のインジェクタは独立しており(どの要素にもアタッチされていない)、ロードされているモジュールのサブセットのみを含みます。必要なのがcore 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
ディレクティブを使用するのが賢明です。 AngularJSが初期化されるまでの間だけ表示されるHTMLを提供することで、DOMに何らかのロード指示を提供することもできます。
<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>
例として静的JSONファイルから構成をロードして、Plunker上でこのアプローチの 完全で実用的な例 を作成しました。
私は同じ問題を抱えていました。私はresolve
オブジェクトが大好きですが、それはng-viewの内容に対してのみ機能します。 ng-viewの外部に存在し、ルーティングが開始される前でさえもデータで初期化される必要があるコントローラ(トップレベルのナビゲーションのために言ってみましょう)を持っているとしたらどうでしょうか?それを機能させるためだけに、サーバー側で混乱を回避するにはどうすればよいでしょうか。
手動ブートストラップと角度定数 を使用します。ナイーブなXHRはあなたにあなたのデータを取得します、そしてあなたはあなたの非同期問題に対処するそのコールバックで角度付きブートストラップをします。以下の例では、グローバル変数を作成する必要すらありません。返されたデータは注入可能なものとして角度範囲内にのみ存在し、注入しない限り、コントローラ、サービスなどの内部にさえ存在しません。 (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
や他のツールにアクセスすることは可能ではないと思います。
(SO 関連記事 )
あなたがすることができるのは、アプリケーションの.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;
}
})
}
Angularは、defer.resolve()が呼び出されるまでテンプレートをレンダリングしたり、コントローラを使用可能にしたりしません。私達は私達のサービスでそれをすることができます:
サービス
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にデータプロパティが割り当てられ、route resolveオブジェクトのプロミスが解決されたので、ルートのコントローラが起動し、サービスからのデータをコントローラオブジェクトに割り当てることができます。
コントローラ
app.controller('homeCtrl',function($scope,MyService){
$scope.servicedata = MyService.data;
});
これで、コントローラの範囲内のすべてのバインディングが、MyServiceから発生したデータを使用できるようになります。
そこで私は解決策を見つけました。 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...
}
これは私にとって素晴らしい仕事でした、しかし、誰かが何かを持っているならば、私はどんなフィードバックにでもオープンです。 }
また、実際のコントローラが実行される前に、次のテクニックを使用してサービスをグローバルにプロビジョニングすることもできます。 https://stackoverflow.com/a/27050497/1056679 。データをグローバルに解決し、それをrun
ブロックなどでサービスに渡します。
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>