web-dev-qa-db-ja.com

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()を呼び出そうとすると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つは他の選択肢より優れていますか?私はこれがかなり長い質問であることを知っています、しかし私は私がすべての私の選択肢を探求しようとしたことを示したかったです。任意のガイダンスは大歓迎です。

464
testing123

$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

323
joakimbl

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の単体テストインフラストラクチャには表示されません。

86
JBCP

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

「手動ブートストラップ」の場合は、ブートストラップの前に手動でインジェクタを作成することで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上でこのアプローチの 完全で実用的な例 を作成しました。

42
Martin Atkins

私は同じ問題を抱えていました。私は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 関連記事

14
XML

あなたがすることができるのは、アプリケーションの.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から発生したデータを使用できるようになります。

8
dewd

そこで私は解決策を見つけました。 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...
}

これは私にとって素晴らしい仕事でした、しかし、誰かが何かを持っているならば、私はどんなフィードバックにでもオープンです。 }

5
testing123

また、実際のコントローラが実行される前に、次のテクニックを使用してサービスをグローバルにプロビジョニングすることもできます。 https://stackoverflow.com/a/27050497/1056679 。データをグローバルに解決し、それをrunブロックなどでサービスに渡します。

2
Slava Fomin II

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
hegemon