486 Stimmen

AngularJS: Initialisiere den Service mit asynchronen Daten.

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

Offensichtlich wird dies nicht funktionieren, weil wenn etwas versucht, doStuff() aufzurufen, bevor myData zurückkommt, werde ich eine Nullzeiger-Ausnahme erhalten. So weit ich aus dem Lesen einiger der anderen gestellten Fragen hier und hier erkennen kann, habe ich ein paar Optionen, aber keine davon scheint sehr sauber zu sein (vielleicht übersehe ich etwas):

Service mit "run" einrichten

Wenn ich meine App einrichte, mache dies:

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

Dann würde mein Dienst so aussehen:

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

Dies funktioniert manchmal, aber wenn die asynchronen Daten länger dauern als alles initialisiert wird, erhalte ich eine Nullzeiger-Ausnahme, wenn ich doStuff() aufrufe

Versprechenobjekte verwenden

Dies würde wahrscheinlich funktionieren. Der einzige Nachteil ist, dass überall, wo ich MyService aufrufe, wissen muss, dass doStuff() ein Versprechen zurückgibt und der gesamte Code das verwenden muss then, um mit dem Versprechen zu interagieren. Ich würde lieber einfach warten, bis myData zurück ist, bevor ich meine Anwendung lade.

Manuelles Bootstrapping

angular.element(document).ready(function() {
    $.getJSON("data.json", function (data) {
       // kann die Daten hier nicht initialisieren, weil der Dienst noch nicht existiert
       angular.bootstrap(document);
       // hier zu spät zu initialisieren, weil möglicherweise schon etwas versucht hat, doStuff() aufzurufen und eine Nullzeiger-Ausnahme erhalten hätte
    });
});

Globaler Javascript-Var Ich könnte meine JSON direkt an eine globale Javascript-Variable senden:

HTML:

data.js:

var dataForMyService = { 
// myData hier
};

Dann wäre es verfügbar, wenn MyService initialisiert wird:

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

Dies würde auch funktionieren, aber dann hätte ich eine globale Javascript-Variable, was nicht gut ist.

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

329voto

joakimbl Punkte 18071

Hast du dir schon $routeProvider.when('/path',{ resolve:{...} angesehen? Es kann den Promise-Ansatz etwas sauberer machen:

Exponiere einen Promise in deinem Service:

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üge resolve zu deiner Routenkonfiguration hinzu:

app.config(function($routeProvider){
  $routeProvider
    .when('/',{controller:'MainCtrl',
    template:'Von MyService:{{data | json}}',
    resolve:{
      'MyServiceData':function(MyService){
        // MyServiceData wird auch in deinem Controller injectable sein, wenn du das nicht möchtest, könntest du einen neuen Promise mit dem $q-Service erstellen
        return MyService.promise;
      }
    }})
  }):

Dein Controller wird nicht instanziiert, bevor alle Abhängigkeiten aufgelöst sind:

app.controller('MainCtrl', function($scope,MyService) {
  console.log('Promise ist jetzt aufgelöst: '+MyService.doStuff().data)
  $scope.data = MyService.doStuff();
});

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

89voto

JBCP Punkte 12445

Auf der Grundlage der Lösung von Martin Atkins hier eine vollständige, prägnante, rein auf Angular basierende Lösung:

(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 zu erhalten, die Konfiguration anzufordern und sie in eine Konstante mit dem Namen CONFIG einzufügen, wenn sie verfügbar ist.

Sobald alles fertig ist, warten wir darauf, dass das Dokument bereit ist, und starten dann die Angular-App.

Dies ist eine leichte Verbesserung gegenüber Martins Lösung, die das Abrufen der Konfiguration verschoben hat, bis das Dokument bereit ist. Soweit ich weiß, gibt es keinen Grund, den $http-Aufruf zu verzögern.

Unit Testing

Hinweis: Ich habe festgestellt, dass diese Lösung nicht gut funktioniert, wenn sie während des Unit-Tests in der app.js-Datei enthalten ist. Der Grund dafür ist, dass der obige Code sofort ausgeführt wird, wenn die JS-Datei geladen wird. Das bedeutet, dass das Test-Framework (in meinem Fall Jasmine) keine Gelegenheit hat, eine Mock-Implementierung von $http bereitzustellen.

Meine Lösung, mit der ich nicht vollständig zufrieden bin, war es, diesen Code in unsere index.html-Datei zu verschieben, damit die Grunt/Karma/Jasmine-Unit-Testinfrastruktur ihn nicht sieht.

49voto

philippd Punkte 606

Ich habe einen ähnlichen Ansatz wie der von @XMLilley beschriebene verwendet, wollte aber die Möglichkeit haben, AngularJS-Services wie $http zu verwenden, um die Konfiguration zu laden und weitere Initialisierungen ohne die Verwendung von Low-Level-APIs oder jQuery durchzuführen.

Die Verwendung von resolve auf Routen war auch keine Option, weil ich die Werte als Konstanten verfügbar haben musste, wenn meine App gestartet wird, auch in module.config() Blöcken.

Ich habe eine kleine AngularJS-App erstellt, die die Konfiguration lädt, sie als Konstanten auf der tatsächlichen App setzt und sie startet.

// Definiere das Modul deiner App
angular.module('MyApp', []);

// Definiere das Modul der Bootstrap-App
var bootstrapModule = angular.module('bootstrapModule', []);

// Der Bootstrapper-Service lädt die Konfiguration und startet die angegebene App
bootstrapModule.factory('bootstrapper', function ($http, $log, $q) {
  return {
    bootstrap: function (appName) {
      var deferred = $q.defer();

      $http.get('/some/url')
        .success(function (config) {
          // Setze alle zurückgegebenen Werte als Konstanten auf der App...
          var myApp = angular.module(appName);
          angular.forEach(config, function(value, key){
            myApp.constant(key, value);
          });
          // ...und starte die eigentliche App.
          angular.bootstrap(document, [appName]);
          deferred.resolve();
        })
        .error(function () {
          $log.warn('Anwendung konnte nicht initialisiert werden, Konfiguration konnte nicht geladen werden.');
          deferred.reject();
        });

      return deferred.promise;
    }
  };
});

// Erstelle ein div, das als Wurzel der Bootstrap-App verwendet wird
var appContainer = document.createElement('div');

// In der run() Funktion kannst du jetzt den Bootstrapper-Service verwenden und die Bootstrapping-App nach der Initialisierung deiner tatsächlichen App herunterfahren
bootstrapModule.run(function (bootstrapper) {

  bootstrapper.bootstrap('MyApp').then(function () {
    // Entfernen des Containers wird die Bootstrap-App zerstören
    appContainer.remove();
  });

});

// Stelle sicher, dass der DOM vollständig geladen ist, bevor das Bootstrapping erfolgt.
angular.element(document).ready(function() {
  angular.bootstrap(appContainer, ['bootstrapModule']);
});

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

UPDATE

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

UPDATE 2

Weil ich es in mehreren Projekten benötigte, 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 auf dem AngularJS-Modul setzt:

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

44voto

Martin Atkins Punkte 45938

Der "manuelle Bootstrap"-Fall kann auf Angular-Services zugreifen, indem vor dem Bootstrap manuell ein Injector erstellt wird. Dieser erste Injector wird alleine stehen (nicht mit Elementen verbunden) und nur eine Teilmenge der geladenen Module enthalten. Wenn alles, was Sie brauchen, Core Angular-Services sind, genügt es, nur ng zu laden, wie hier:

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;
               // Fügen Sie zusätzliche Services/Konstanten/Variablen zu Ihrer App hinzu,
               // und starten Sie das schließlich:
               angular.bootstrap(document, ['myApp']);
            }
        );
    }
);

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

myApp.constant('myAppConfig', data);

Dieses myAppConfig kann nun wie jeder andere Service auch injiziert werden und ist insbesondere während der Konfigurationsphase verfügbar:

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

Oder für eine kleinere App könnten Sie die globale Konfiguration direkt in Ihren Service injizieren, auf Kosten der Verbreitung von Kenntnissen über das Konfigurationsformat in der gesamten Anwendung.

Natürlich, da die asynchronen Operationen hier den Bootstrap der Anwendung blockieren werden und somit die Kompilierung/Verknüpfung der Vorlage blockieren werden, ist es ratsam, die ng-cloak-Direktive zu verwenden, um zu verhindern, dass die unanalysierte Vorlage während der Arbeit angezeigt wird. Sie könnten auch irgendeine Art von Ladeanzeige im DOM bereitstellen, indem Sie etwas HTML bereitstellen, das nur angezeigt wird, bis AngularJS initialisiert ist:

    Lade die App.....

    Die App wurde geladen!

Ich habe auf Plunker ein komplettes, funktionierendes Beispiel für diesen Ansatz erstellt, bei dem die Konfiguration aus einer statischen JSON-Datei geladen wird.

16voto

XML Punkte 18668

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 Top-Level-Navigation, sagen wir mal) haben, die außerhalb von ng-view existieren und die mit Daten initialisiert werden müssen, bevor die Routing überhaupt beginnt? Wie vermeiden wir es, auf der Serverseite herumzufummeln, nur um das zum Laufen zu bringen?

Verwenden Sie manuelles Bootstrap und eine Angular-Konstante. Ein einfacher XHR-Aufruf liefert Ihre Daten, und Sie booten Angular im Rückruf, der sich mit Ihren asynchronen Problemen befasst. Im folgenden Beispiel müssen Sie nicht einmal eine globale Variable erstellen. Die zurückgegebenen Daten existieren nur im Angular-Scope als injizierbares Element und sind nicht einmal in Controllern, Services usw. vorhanden, es sei denn, Sie injizieren sie. (So wie Sie die Ausgabe Ihres resolve-Objekts in den Controller für eine geroutete Ansicht injizieren würden.) Wenn Sie danach lieber mit diesen Daten als Service interagieren möchten, können Sie einen Service erstellen, die Daten injizieren, und niemand wird davon erfahren.

Beispiel:

//Zunächst müssen wir das Angular-Modul erstellen, weil alle anderen JS-Dateien geladen werden, während wir Daten abrufen und booten, und sie in der Lage sein müssen, an es anzuhängen.
var MyApp = angular.module('MyApp', ['dependency1', 'dependency2']);

// Verwenden Sie die Angular-Version von document.ready(), um sicherzustellen, dass das DOM vollständig geladen ist, bevor Sie booten. Dies ist wahrscheinlich optional, da der asynchrone Datenabruf wahrscheinlich deutlich länger dauern wird als das Laden des DOM. Ihre Meinung dazu kann variieren.
// Hat den zusätzlichen Vorteil, Ihren XHR-Müll aus dem globalen Scope zu halten.
angular.element(document).ready(function() {

    //zuerst erstellen wir das Callback, das nach dem Herunterladen der Daten ausgelöst wird
    function xhrCallback() {
        var myData = this.responseText; // die XHR-Ausgabe

        // hier hängen wir eine Konstante an, die die API-Daten für unser App-Modul enthält. Vergessen Sie nicht, JSON zu parsen, was `$http` normalerweise für Sie erledigt.
        MyApp.constant('NavData', JSON.parse(myData));

        // und jetzt führen Sie alle anderen endgültigen Konfigurationen Ihres Angular-Moduls durch.
        MyApp.config(['$routeProvider', function ($routeProvider) {
            $routeProvider
              .when('/someroute', {configs})
              .otherwise({redirectTo: '/someroute'});
          }]);

        // Und zuletzt booten Sie die App. Stellen Sie sicher, dass Sie `ng-app` aus Ihrem index.html entfernen.
        angular.bootstrap(document, ['NYSP']);
    };

    //hier die grundlegende Mechanik des XHR, die Sie anpassen können.
    var oReq = new XMLHttpRequest();
    oReq.onload = xhrCallback;
    oReq.open("get", "/api/overview", true); // Ihre spezifische API-URL
    oReq.send();
})

Jetzt existiert Ihre NavData-Konstante. Gehen Sie vor und injizieren Sie sie in einen Controller oder Service:

angular.module('MyApp')
    .controller('NavCtrl', ['NavData', function (NavData) {
        $scope.localObject = NavData; // jetzt ist es in Ihren Templates ansprechbar
}]);

Natürlich entfernt die Verwendung eines nackten XHR-Objekts eine Reihe von Annehmlichkeiten, die $http oder JQuery bereits für Sie erledigen würden, aber dieses Beispiel funktioniert ohne besondere Abhängigkeiten, zumindest für ein einfaches get. Wenn Sie etwas mehr Leistung für Ihre Anfrage möchten, laden Sie eine externe Bibliothek, um Ihnen zu helfen. Aber ich glaube nicht, dass es möglich ist, auf Angulars $http oder andere Tools in diesem Kontext zuzugreifen.

(Siehe auch verwandten Beitrag)

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X