it-swarm.com.de

AngularJS: Dienst mit asynchronen Daten initialisieren

Ich habe einen AngularJS-Dienst, den ich mit einigen asynchronen Daten initialisieren möchte. Etwas wie das:

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

Das funktioniert natürlich nicht, denn wenn etwas versucht, doStuff() aufzurufen, bevor myData zurückkehrt, bekomme ich eine Nullzeiger-Ausnahme. Soweit ich das Lesen einiger der anderen Fragen, die hier und hier gestellt wurden, feststellen kann, habe ich einige Optionen, aber keine davon scheint sehr sauber zu sein (vielleicht fehlt mir etwas)

Setup Service mit "run"

Gehen Sie beim Einrichten meiner App folgendermaßen vor:

myApp.run(function ($http, MyService) {
    $http.get('data.json').success(function (data) {
        MyService.setData(data);
    });
});

Dann würde mein Service so aussehen:

myModule.service('MyService', function() {
    var myData = null;
    return {
        setData: function (data) {
            myData = data;
        },
        doStuff: function () {
            return myData.getSomeData();
        }
    };
});

Dies funktioniert zeitweise, aber wenn asynchrone Daten länger dauern, als alles initialisiert werden kann, erhalte ich eine Ausnahme mit einem Nullzeiger, wenn ich doStuff() aufrufe.

Versprechungsobjekte verwenden

Das würde wahrscheinlich funktionieren. Der einzige Nachteil ist, dass ich überall MyService anrufe. Ich muss wissen, dass doStuff () ein Versprechen zurückgibt und der gesamte Code uns then hat, um mit dem Versprechen zu interagieren. Ich würde lieber warten, bis myData wieder da ist, bevor ich meine Bewerbung lade.

Manual 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 Ich könnte meine JSON direkt an eine globale Javascript-Variable senden:

HTML:

<script type="text/javascript" src="data.js"></script>

data.js:

var dataForMyService = { 
// myData here
};

Dann wäre es beim Initialisieren von MyService verfügbar:

myModule.service('MyService', function() {
    var myData = dataForMyService;
    return {
        doStuff: function () {
            return myData.getSomeData();
        }
    };
});

Das würde auch funktionieren, aber dann habe ich eine globale Javascript-Variable, die schlecht riecht.

Sind dies meine einzigen Möglichkeiten? Ist eine dieser Optionen besser als die anderen? Ich weiß, das ist eine ziemlich lange Frage, aber ich wollte zeigen, dass ich versucht habe, alle meine Optionen zu erkunden. Jede Anleitung wäre sehr dankbar. 

458
testing123

Haben Sie einen Blick auf $routeProvider.when('/path',{ resolve:{...} geworfen? Es kann den Versprechungsansatz etwas sauberer machen:

Legen Sie ein Versprechen in Ihrem Service vor:

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

Fügen Sie resolve zu Ihrer Routenkonfiguration hinzu:

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

Ihr Controller wird erst instanziiert, wenn alle Abhängigkeiten aufgelöst sind:

app.controller('MainCtrl', function($scope,MyService) {
  console.log('Promise is now resolved: '+MyService.doStuff().data)
  $scope.data = MyService.doStuff();
});

Ich habe ein Beispiel bei plnkr gemacht: http://plnkr.co/edit/GKg21XH0RwCMEQGUdZKH?p=preview

319
joakimbl

Basierend auf der Lösung von Martin Atkins, hier eine vollständige, prägnante Lösung mit reinem Winkel:

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

Diese Lösung verwendet eine selbstausführende anonyme Funktion, um den $ http-Dienst abzurufen, die config anzufordern und in eine Konstante namens CONFIG einzubringen, sobald sie verfügbar ist. 

Sobald wir vollständig sind, warten wir, bis das Dokument fertig ist, und bootstrapen dann die Angular-App. 

Dies ist eine geringfügige Verbesserung der Lösung von Martin, die das Abrufen der Konfiguration verzögerte, bis das Dokument fertig ist. Meines Wissens gibt es keinen Grund, den $ http-Aufruf dafür zu verzögern. 

Stückprüfung

Hinweis: Ich habe festgestellt, dass diese Lösung beim Komponententest nicht gut funktioniert, wenn der Code in Ihrer app.js-Datei enthalten ist. Der Grund dafür ist, dass der obige Code sofort ausgeführt wird, wenn die JS-Datei geladen wird. Dies bedeutet, dass das Testframework (in meinem Fall Jasmine) keine Chance hat, eine Scheinimplementierung von $http bereitzustellen. 

Meine Lösung, mit der ich nicht ganz zufrieden bin, bestand darin, diesen Code in unsere index.html-Datei zu verschieben, sodass die Infrastruktur des Grunt/Karma/Jasmine-Gerätetests ihn nicht erkennt.

86
JBCP

Ich habe einen ähnlichen Ansatz wie den von @XMLilley beschriebenen verwendet, wollte aber die Möglichkeit haben, AngularJS-Dienste wie $http zu verwenden, um die Konfiguration zu laden und die weitere Initialisierung ohne die Verwendung von APIs oder jQuery auf niedriger Ebene durchzuführen.

Die Verwendung von resolve auf Routen war auch keine Option, da die Werte beim Starten meiner App als Konstanten verfügbar waren, selbst in module.config()-Blöcken.

Ich habe eine kleine AngularJS-App erstellt, die die Konfiguration lädt, sie als Konstanten für die eigentliche App festlegt und diese bootst.

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

Sehen Sie es in Aktion (mit $timeout anstelle von $http) hier: http://plnkr.co/edit/FYznxP3xe8dxzwxs37hi?p=preview

UPDATE

Ich würde empfehlen, den unten beschriebenen Ansatz von Martin Atkins und JBCP zu verwenden.

UPDATE 2

Da ich es in mehreren Projekten brauchte, habe ich gerade ein Bower-Modul veröffentlicht, das sich darum kümmert: https://github.com/philippd/angular-deferred-bootstrap

Beispiel, das Daten vom Backend lädt und eine Konstante namens APP_CONFIG im Modul AngularJS setzt:

deferredBootstrapper.bootstrap({
  element: document.body,
  module: 'MyApp',
  resolve: {
    APP_CONFIG: function ($http) {
      return $http.get('/api/demo-config');
    }
  }
});
48
philippd

Der Fall "Manual Bootstrap" kann auf Angular-Dienste zugreifen, indem vor dem Bootstrap ein Injektor manuell erstellt wird. Dieser anfängliche Injektor steht für sich alleine (kann nicht an Elemente angehängt werden) und enthält nur eine Teilmenge der geladenen Module. Wenn Sie nur Angular-Kerndienste benötigen, reicht es aus, einfach ng zu laden:

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

Sie können beispielsweise den Mechanismus module.constant verwenden, um Daten für Ihre App verfügbar zu machen:

myApp.constant('myAppConfig', data);

Diese myAppConfig kann nun wie jeder andere Dienst eingefügt werden und ist insbesondere in der Konfigurationsphase verfügbar:

myApp.config(
    function (myAppConfig, someService) {
        someService.config(myAppConfig.someServiceConfig);
    }
);

bei einer kleineren App können Sie einfach die globale Konfiguration direkt in Ihren Dienst einfügen, auf Kosten der Kenntnis des Konfigurationsformats in der gesamten Anwendung.

Da die asynchronen Vorgänge hier den Bootstrap der Anwendung blockieren und somit die Erstellung/Verknüpfung der Vorlage blockieren, ist es natürlich ratsam, die ng-cloak-Direktive zu verwenden, um zu verhindern, dass die nicht geparste Vorlage während der Arbeit angezeigt wird. Sie können auch eine Art Ladeanzeige im DOM bereitstellen, indem Sie HTML-Code bereitstellen, der nur angezeigt wird, wenn AngularJS initialisiert wird:

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

Ich habe ein vollständiges Arbeitsbeispiel dieses Ansatzes in Plunker erstellt und die Konfiguration als Beispiel aus einer statischen JSON-Datei geladen.

42
Martin Atkins

Ich hatte das gleiche Problem: Ich liebe das resolve-Objekt, aber das funktioniert nur für den Inhalt von ng-view. Was ist, wenn Sie Controller (für die oberste Ebene der Navigation) haben, die außerhalb von ng-view vorhanden sind und die mit Daten initialisiert werden müssen, bevor das Routing überhaupt beginnt? Wie vermeiden wir es, auf der Serverseite herumzumachen, nur um das zu erreichen?

Manuelle Bootstrap und eine Winkelkonstante verwenden. Ein naiive XHR holt Ihnen Ihre Daten und Sie können den Callback, der sich mit Ihren async-Problemen befasst, in eckigen Winkeln ausführen. Im folgenden Beispiel müssen Sie nicht einmal eine globale Variable erstellen. Die zurückgegebenen Daten sind nur im Winkelbereich als injizierbares Material vorhanden und nicht einmal in Controllern, Diensten usw. vorhanden, sofern Sie sie nicht injizieren. (So ​​wie Sie die Ausgabe Ihres resolve-Objekts für eine geroutete Ansicht in den Controller injizieren würden.) Wenn Sie es vorziehen, mit diesen Daten als Dienst zu interagieren, können Sie einen Dienst erstellen, die Daten einfügen, und niemand wird dies tun klüger.

Beispiel:

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

Nun existiert Ihre Konstante NavData. Fahren Sie fort und injizieren Sie es in eine Steuerung oder einen Dienst: 

angular.module('MyApp')
    .controller('NavCtrl', ['NavData', function (NavData) {
        $scope.localObject = NavData; //now it's addressable in your templates 
}]);

Wenn Sie ein nacktes XHR-Objekt verwenden, werden natürlich einige der Feinheiten entfernt, die $http oder JQuery für Sie übernehmen würden. Dieses Beispiel funktioniert jedoch ohne besondere Abhängigkeiten, zumindest für eine einfache get. Wenn Sie mehr Leistung für Ihre Anfrage benötigen, laden Sie eine externe Bibliothek, um Ihnen zu helfen. Ich denke jedoch nicht, dass es möglich ist, auf den $http von Winkle oder andere Tools in diesem Zusammenhang zuzugreifen. 

(SO verwandte post )

14
XML

Sie können in Ihrer .config für die App das Auflösungsobjekt für die Route erstellen und in der Funktion in $ q (Versprechungsobjekt) und den Namen des Dienstes übergeben, von dem Sie abhängig sind Callback-Funktion für den $ http im Service wie folgt:

ROUTE KONFIG

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 wird die Vorlage nicht rendern oder den Controller erst verfügbar machen, wenn defer.resolve () aufgerufen wurde. Das können wir in unserem Service tun:

BEDIENUNG

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

Nachdem MyService die Daten seiner Dateneigenschaft zugewiesen hat und das Versprechen im Routenauflösungsobjekt gelöst wurde, wird unser Controller für die Route zum Leben erweckt, und wir können die Daten des Services unserem Controllerobjekt zuweisen.

REGLER

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

Alle unsere Bindungen im Rahmen des Controllers können nun die Daten verwenden, die von MyService stammen.

8
dewd

Also habe ich eine Lösung gefunden. Ich habe einen angleJS-Dienst erstellt, wir nennen ihn MyDataRepository und ich habe ein Modul dafür erstellt. Ich serviere dann diese Javascript-Datei von meinem serverseitigen Controller aus:

HTML:

<script src="path/myData.js"></script>

Serverseite:

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

Ich kann MyDataRepository dann an jeder beliebigen Stelle injizieren:

someOtherModule.service('MyOtherService', function(MyDataRepository) {
    var myData = MyDataRepository.getData();
    // Do what you have to do...
}

Das hat für mich super funktioniert, aber ich bin offen für jegliches Feedback, wenn jemand etwas hat .}

5
testing123

Sie können auch die folgenden Techniken verwenden, um Ihren Service global bereitzustellen, bevor tatsächliche Controller ausgeführt werden: https://stackoverflow.com/a/27050497/1056679 . Lösen Sie Ihre Daten einfach global auf und übergeben Sie sie beispielsweise in run block an Ihren Dienst.

2
Slava Fomin II

Sie können JSONP verwenden, um Servicedaten asynchron zu laden. __ Die JSONP-Anforderung wird während des ersten Seitenladens gestellt und die Ergebnisse stehen vor dem Start Ihrer Anwendung zur Verfügung. Auf diese Weise müssen Sie Ihr Routing nicht mit redundanten Auflösungen aufblähen.

Sie würden html so aussehen:

<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>
0
hegemon