AngularJS:非同期データでサービスを初期化する


475

非同期データで初期化したい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();
        }
    };
});

何かが戻ってくるdoStuff()前に呼び出そうとするとmyData、nullポインタ例外が発生するため、これは明らかに機能しません。ここここで尋ねられた他のいくつかの質問を読んでわかる限り、私にはいくつかのオプションがありますが、どれも非常にきれいに見えません(おそらく私は何かを見逃しています):

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

これは時々機能しますが、非同期データがすべてが初期化されるのにかかるよりも時間がかかる場合、呼び出し時にnullポインタ例外が発生します doStuff()

promiseオブジェクトを使用する

これはおそらく機能します。MyServiceを呼び出すすべての場所での唯一の欠点は、doStuff()がPromiseを返し、Promiseと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変数があります。

これらは私の唯一のオプションですか?これらのオプションの1つは他のオプションよりも優れていますか?これはかなり長い質問であることはわかっていますが、すべての選択肢を模索したことを示したいと思います。どんなガイダンスもいただければ幸いです。


angular-ブートストラップは非同期でコードをウォークスルーしてサーバーからデータをプル$httpし、サービスにデータを保存してからアプリをブートストラップします。
Steven Wexler、2016

回答:


327

ご覧になりました$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


1
お返事ありがとうございます!MyServiceを使用する解決マップにサービスがない場合は、うまくいきます。私の状況であなたのプランカーを更新しました:plnkr.co/edit/465Cupaf5mtxljCl5NuF?p=preview。MyServiceが初期化されるまでMyOtherServiceを待機させる方法はありますか?
Testing123 2013

2
MyOtherServiceのpromiseをチェーニングすると思います-チェッカーといくつかのコメントでプランカーを更新しました-これはどのように見えますか?plnkr.co/edit/Z7dWVNA9P44Q72sLiPjW?p=preview
joakimbl

3
私はこれを試してみましたが、ディレクティブや他のコントローラー($ routeProviderで使用するコントローラーは、プライマリ、セカンダリナビゲーションなどの「MyOtherService」を処理しています)があるため、いくつかの問題が発生しました。 'は解決されました。私はこれを試み続け、成功したらこれを更新します。コントローラとディレクティブを初期化する前に、データが戻るのを待つことができる角度のフックがあったらいいのにと思います。ご協力いただきありがとうございます。私がすべてをラップするメインコントローラーがあれば、これはうまくいきました。
testing123

43
ここで問題-どのように割り当てるresolveには言及されていないコントローラにプロパティを$routeProvider。たとえば、<div ng-controller="IndexCtrl"></div>。ここでは、コントローラーは明示的に言及されており、ルーティングを介して読み込まれていません。そのような場合、コントローラーのインスタンス化をどのように遅らせるでしょうか?
callmekatootie 2013年

19
ええと、ルーティングを使用していない場合はどうなりますか?これは、ルーティングを使用しない限り、非同期データで角度付きアプリを作成できないと言っているようなものです。アプリにデータを取得するための推奨される方法は、非同期で読み込むことですが、複数のコントローラーがあり、サービスを投入するとすぐに、BOOMは不可能になります。
wired_in

88

Martin Atkinsのソリューションに基づく、完全で簡潔な純粋な角度のソリューションを次に示します。

(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ファイルがロードされるとすぐに上記のコードが実行されるためです。これは、テストフレームワーク(私の場合はジャスミン)がのモック実装を提供する機会がないことを意味します$http

私が完全に満足していない私の解決策は、このコードをindex.htmlファイルに移動することでした。そのため、Grunt / Karma / Jasmineユニットテストインフラストラクチャはそれを認識しません。


1
「グローバルスコープを汚染しない」などのルールは、コードを改善する範囲でのみ従う必要があります(複雑さを減らし、保守性を高め、安全性を高めるなど)。このソリューションがデータを単一のグローバル変数に単にロードするよりも優れていることはわかりません。何が欠けていますか?
david004 2014

4
Angularの依存性注入システムを使用して、それを必要とするモジュールの「CONFIG」定数にアクセスできますが、それを必要としない他のモジュールを壊すリスクはありません。たとえば、グローバルの「config」変数を使用した場合、他のサードパーティのコードも同じ変数を探している可能性があります。
JBCP 2014

1
私はここで、角度の初心者だいくつかのメモは、私は私のアプリで解決コンフィグモジュールの依存得た方法である: gist.github.com/dsulli99/0be3e80db9b21ce7b​​989 REFを:tutorials.jenkov.com/angularjs/...は、このソリューションをありがとうございました。
dps 2015

7
これは、以下の他の手動ブートストラップソリューションのいずれかのコメントで言及されていますが、これを見つけられなかった角度のある初心者として、これが正しく機能するためには、htmlコードのng-appディレクティブを削除する必要があることを指摘できます。 -自動ブートストラップ(ng-app経由)をこの手動の方法に置き換えます。ng-appを削除しないと、アプリケーションは実際に動作する可能性がありますが、コンソールにさまざまな不明なプロバイダーエラーが表示されます。
IrishDubGuy 2015

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

11
deferredBootstrapperは進むべき道です
fatlinesofcode

44

「手動ブートストラップ」のケースでは、ブートストラップの前にインジェクターを手動で作成することにより、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ディレクティブを使用して、解析されていないテンプレートが作業中に表示されないようにするのが賢明です。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>

Plunkerでこのアプローチの完全で実用的な例を作成し、例として静的JSONファイルから構成をロードしました。


ドキュメントの準備ができるまで$ http.get()を延期する必要はないと思います。
JBCP 2014年

@JBCPはい、HTTP応答が返されるまでドキュメントの準備が整うのを待たないようにイベントを交換した場合と同じように機能し、HTTPを開始できるという利点があります。より速くリクエストします。DOMの準備ができるまで待機する必要があるのは、ブートストラップ呼び出しのみです。
Martin Atkins

2
私はあなたのアプローチでbower
philippd

@MartinAtkins、私はあなたの素晴らしいアプローチがAngular v1.1 +では機能しないことを発見しました。Angularの初期のバージョンは、アプリケーションがブートストラップされるまで「その後」を理解できないようです。Plunkで表示するには、Angular URLをcode.angularjs.org/1.1.5/angular.min.jsに
vkelman

16

私は同じ問題を抱えていました:私は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ます。リクエストにもう少し力を入れたい場合は、外部ライブラリをロードして支援してください。しかし、私は$httpこの文脈でangular や他のツールにアクセスすることは可能ではないと思います。

(SO 関連投稿


8

あなたができることは、アプリの.configで、ルートの解決オブジェクトを作成し、$ q(promiseオブジェクト)の関数パスと依存しているサービスの名前を作成し、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にdataプロパティが割り当てられ、ルート解決オブジェクトのプロミスが解決されたので、ルートのコントローラーが起動し、サービスからコントローラーオブジェクトにデータを割り当てることができます。

コントローラ

  app.controller('homeCtrl',function($scope,MyService){
       $scope.servicedata = MyService.data;
  });

これで、コントローラーのスコープ内のすべてのバインディングで、MyServiceから発生したデータを使用できるようになります。


時間があれば、これを試してみます。これは、他の人がngModulesでやろうとしたことと似ています。
testing123 2013年

1
私はこのアプローチが好きで、以前にそれを使用しましたが、現在、プリフェッチされたデータに依存する場合としない場合があるルートがいくつかある場合に、これをクリーンな方法で実行する方法を考えています。それについての考えは?
ivarni 2013年

ところで、私は、プリフェッチされたデータを必要とする各サービスが初期化されたときにリクエストを作成し、Promiseを返し、さまざまなルートで必要なサービスでresolve-objectsを設定するようにしています。もっと簡潔な方法があったらいいのにと思います。
ivarni 2013年

1
@dewdそれが私が目指していたものですが、解決ブロックを繰り返さなくても、「どのルートがロードされているかに関係なく、最初にこのすべてのものをフェッチする」と言う方法があったら、私はずっと望みます。彼らはすべて彼らが依存している何かを持っています。しかし、それは大したことではありません、それはもう少しドライを感じるだけです:)
ivarni 2013年

2
これは、resolve関数であるプロパティを持つオブジェクトを作成する必要があったことを除いて、私が最終的に取ったルートです。結局それはresolve:{ dataFetch: function(){ // call function here } }
aron.duby 2013年

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...
}

これは私にとってはうまくいきましたが、誰かフィードバックがあれば教えてもらえます。}


私はあなたのモジュラーアプローチが好きです。$ routeScopeはデータを要求するサービスで利用可能であり、$ http.successコールバックでデータを割り当てることができることがわかりました。ただし、非グローバルアイテムに$ routeScopeを使用するとにおいが発生し、データはコントローラー$ scopeに実際に割り当てられるはずです。残念ながら、革新的ではありますが、あなたのアプローチは理想的ではないと思います(ただし、自分に合った方法を見つけることは尊重してください)。どういうわけかデータを待ってスコープへの割り当てを可能にするクライアント側のみの回答が必要であると私は確信しています。検索は続きます!
dewd

誰かに役立つ場合のために、私は最近、他の人々がngModulesのWebサイトに書き込んで追加したモジュールを検討するいくつかの異なるアプローチを見ました。時間が増えると、そのうちの1つを使い始めるか、何をしているかを把握して自分のものに追加する必要があります。
Testing123 2013年


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>

-1

初期化をフェッチする最も簡単な方法は、ng-initディレクトリを使用します。

初期データをフェッチしたい場所にng-init divスコープを置くだけです

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

注:同じ方法が1つ以上ない場合は、この方法を使用できます。


次のドキュメントに従ってngInitを使用することはお勧めしません:docs.angularjs.org/api/ng/directive/ngInit
fodma1
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.