483

いくつかの非同期データで初期化したい 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尋ねられた他の質問のいくつかを読んでわかる限り、いくつかのオプションがありますが、どれもあまりきれいに見えません(おそらく何かが欠けています):

「実行」によるサービスのセットアップ

アプリをセットアップするときは、次のようにします。

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()

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 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 つは、他のオプションよりも優れていますか? これがかなり長い質問であることは承知していますが、すべての選択肢を検討しようとしたことを示したいと思います。任意のガイダンスをいただければ幸いです。

4

10 に答える 10

89

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 のソリューションをわずかに強化したものです。私の知る限り、そのために $http 呼び出しを遅らせる理由はありません。

単体テスト

app.js注:コードがファイルに含まれている場合、単体テストを行うと、このソリューションがうまく機能しないことがわかりました。これは、JS ファイルが読み込まれるとすぐに上記のコードが実行されるためです。これは、テスト フレームワーク (私の場合は Jasmine) が のモック実装を提供する機会がないことを意味します$http

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

于 2014-01-17T15:04:21.900 に答える
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ここで(の代わりに使用して)動作中を参照してください$httphttp://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');
    }
  }
});
于 2013-11-09T01:27:37.203 に答える
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 プロパティにデータが割り当てられ、ルート解決オブジェクトの promise が解決されたので、ルートのコントローラーが有効になり、サービスからのデータをコントローラー オブジェクトに割り当てることができます。

コントローラ

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

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

于 2013-05-25T21:41:20.877 に答える
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...
}

これは私にとってはうまくいきましたが、誰かが何かあればフィードバックをお待ちしています. }

于 2013-05-02T14:15:20.467 に答える