AngularJS: Inicjalizacja usługi za pomocą danych asynchronicznych

Mam usługę AngularJS, którą chcę zainicjować danymi asynchronicznymi. Coś takiego:

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

Oczywiście to nie zadziała, ponieważ jeśli coś spróbuje wywołać doStuff() przed powrotem myData, otrzymam wyjątek wskaźnika null. Z tego co wiem po przeczytaniu kilku innych pytań zadanych tutaj i tutaj mam kilka opcji, ale żadna z nich nie wydaje się zbyt czysta (być może czegoś mi brakuje): {]}

Konfiguracja usługi z "run"

Podczas konfigurowania mojej aplikacji Zrób to:

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

Wtedy mój serwis wyglądałby tak:

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

To działa przez jakiś czas, ale jeśli dane asynchroniczne zajdą dłużej niż potrzeba, aby wszystko zostało zainicjowane, dostaję wyjątek wskaźnika null, gdy wywołuję doStuff()

Użyj obiektów obietnicy

To prawdopodobnie zadziała. Jedynym minusem jest to, że wszędzie nazywam MyService będę musiał wiedzieć, że doStuff () zwraca obietnicę / align = "center" bgcolor = "# e0ffe0 " / cesarz chin / / align = center / Wolałbym po prostu poczekać, aż myData wróci przed załadowaniem mojej aplikacji.

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 Mogę wysłać mój JSON bezpośrednio do globalnej zmiennej Javascript:

HTML:

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

Data.js:

var dataForMyService = { 
// myData here
};

Wtedy będzie on dostępny przy inicjalizacji MyService:

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

To też by zadziałało, ale wtedy mam globalną zmienna javascript, która źle pachnie.

Czy to moje jedyne wyjście? Czy jedna z tych opcji jest lepsza od pozostałych? Wiem, że to dość długie pytanie, ale chciałem pokazać, że próbowałem zbadać wszystkie moje opcje. Wszelkie wskazówki będą mile widziane.
Author: Community, 2013-04-29

10 answers

Czy przyjrzałeś się $routeProvider.when('/path',{ resolve:{...}? To może sprawić, że podejście obietnicy będzie nieco czystsze: {]}

Ujawnić obietnicę w służbie:

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

Dodaj resolve do konfiguracji trasy:

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

Twój kontroler nie zostanie utworzony, zanim wszystkie zależności zostaną rozwiązane:

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

Zrobiłem przykład w plnkr: http://plnkr.co/edit/GKg21XH0RwCMEQGUdZKH?p=preview

 317
Author: joakimbl,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-06-27 10:25:57

Na podstawie rozwiązania Martina Atkinsa, oto kompletne, zwięzłe rozwiązanie czysto Kątowe:

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

To rozwiązanie używa samoczynnie wykonującej się funkcji anonimowej, aby uzyskać usługę $ http, zażądać konfiguracji i wstrzyknąć ją do stałej o nazwie CONFIG, gdy stanie się dostępna.

Po całkowitym zakończeniu czekamy, aż dokument będzie gotowy, a następnie uruchamiamy aplikację Angular.

Jest to niewielkie ulepszenie w stosunku do rozwiązania Martina, które odroczyło pobieranie config aż do dokument jest gotowy. O ile wiem, nie ma powodu, aby opóźniać wywołanie $ http w tym celu.

Testy Jednostkowe

Uwaga: odkryłem, że to rozwiązanie nie działa dobrze podczas testów jednostkowych, gdy kod jest zawarty w pliku app.js. Powodem tego jest to, że powyższy kod uruchamia się natychmiast po załadowaniu pliku JS. Oznacza to, że Framework testowy (w moim przypadku Jasmine)nie ma możliwości dostarczenia wzorcowej implementacji $http.

Moje rozwiązanie, którym jestem nie do końca zadowolony z, było przeniesienie tego kodu do naszego pliku index.html, więc Infrastruktura testów jednostkowych Grunt/Karma/Jasmine go nie widzi.

 86
Author: JBCP,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-03-07 17:47:33

Użyłem podobnego podejścia do tego opisanego przez @XMLilley, ale chciałem mieć możliwość korzystania z usług AngularJS, takich jak $http, aby załadować konfigurację i wykonać dalszą inicjalizację bez użycia niskopoziomowych API lub jQuery.

Używanie resolve na trasach również nie było opcją, ponieważ potrzebowałem, aby wartości były dostępne jako stałe podczas uruchamiania aplikacji, nawet w blokach module.config().

Stworzyłem małą aplikację AngularJS, która ładuje config, ustawia je jako stałe na rzeczywistym aplikacja i bootstraps go.

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

Zobacz go w akcji (używając $timeout zamiast $http) tutaj: http://plnkr.co/edit/FYznxP3xe8dxzwxs37hi?p=preview

UPDATE

Zalecałbym zastosowanie podejścia opisanego poniżej przez Martina Atkinsa i JBCP.

UPDATE 2

Ponieważ potrzebowałem go w wielu projektach, właśnie wydałem moduł bowera, który zajmuje się tym: https://github.com/philippd/angular-deferred-bootstrap

Przykład, który ładuje dane z zaplecza i ustawia stałą o nazwie APP_CONFIG na module AngularJS:

deferredBootstrapper.bootstrap({
  element: document.body,
  module: 'MyApp',
  resolve: {
    APP_CONFIG: function ($http) {
      return $http.get('/api/demo-config');
    }
  }
});
 48
Author: philippd,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-03-18 07:32:06

Obudowa "manual bootstrap" może uzyskać dostęp do usług Angular poprzez ręczne utworzenie wtryskiwacza przed bootstrap. Ten początkowy wtryskiwacz będzie samodzielny (nie będzie dołączony do żadnych elementów) i będzie zawierał tylko podzbiór modułów, które są ładowane. Jeśli potrzebujesz tylko podstawowych usług kątowych, wystarczy załadować ng, tak:

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

Możesz na przykład użyć mechanizmu module.constant, Aby udostępnić dane swojej aplikacji:

myApp.constant('myAppConfig', data);

To myAppConfig można teraz wstrzyknąć tylko jak każda inna usługa, a w szczególności jest dostępna w fazie konfiguracji:

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

Lub, w przypadku mniejszej aplikacji, możesz po prostu wprowadzić globalną konfigurację bezpośrednio do swojego serwisu, kosztem rozpowszechniania wiedzy na temat formatu konfiguracji w całej aplikacji.

Oczywiście, ponieważ operacje asynchroniczne tutaj zablokują bootstrap aplikacji, a tym samym zablokują kompilację / łączenie szablonu, mądrze jest użyć dyrektywy ng-cloak, aby zapobiec nieużywany szablon z pokazywania się podczas pracy. Możesz również podać jakiś rodzaj wskazania ładowania w DOM, podając jakiś kod HTML, który jest wyświetlany tylko do momentu inicjalizacji AngularJS:

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

Stworzyłem kompletny, działający przykład tego podejścia na Plunkerze, ładując konfigurację ze statycznego pliku JSON jako przykład.

 42
Author: Martin Atkins,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-12-08 01:13:26

Miałem ten sam problem: kocham obiekt resolve, ale to działa tylko dla zawartości ng-view. Co zrobić, jeśli masz Kontrolery (powiedzmy, dla najwyższego poziomu nav), które istnieją poza ng-view i które muszą zostać zainicjowane danymi, zanim routing zacznie się odbywać? Jak uniknąć błaznowania po stronie serwera tylko po to, aby to zadziałało?

Użyj ręcznego Bootstrapa i stałej kątowej . Naiive XHR dostaje Twoje dane, a Ty bootstrap angular w jego callback, który zajmuje się problemami asynchronicznymi. W poniższym przykładzie nie trzeba nawet tworzyć zmiennej globalnej. Zwracane dane istnieją tylko w zakresie kątowym jako iniekcja i nie są nawet obecne wewnątrz sterowników, usług itp. chyba, że wstrzykniesz. (Podobnie jak wstrzyknięcie wyjścia obiektu resolve do kontrolera w celu przekierowania widoku.) Jeśli wolisz później wchodzić w interakcje z tymi danymi jako usługą, możesz utworzyć usługę, wstrzyknąć Dane i nikt nigdy nie będzie mądrzej.

Przykład:

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

Teraz, twoja NavData stała istnieje. Można go wprowadzić do kontrolera lub serwisu:

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

Oczywiście, użycie gołego obiektu XHR usuwa wiele elementów, które $http lub JQuery dadzą ci radę, ale ten przykład działa bez specjalnych zależności, przynajmniej dla prostego get. Jeśli chcesz uzyskać trochę więcej mocy na swoje żądanie, załaduj zewnętrzną bibliotekę, która Ci pomoże. Ale nie wydaje mi się, żeby to było możliwe. angular ' s $http lub innych narzędzi w tym kontekście.

(SO related post)

 13
Author: XML,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-05-23 12:02:51

To, co możesz zrobić, jest w Twoim .config dla aplikacji to Utwórz obiekt resolve dla trasy i w funkcji pass w $q (obiekt promise) I nazwę usługi, od której zależy, i rozwiąż obietnicę w funkcji zwrotnej dla $http w usłudze w następujący sposób:

ROUTE CONFIG

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 nie wyrenderuje szablonu ani nie udostępni kontrolera do czasu odroczenia.resolve() została wywołana. Możemy to zrobić w naszym serwis:

Serwis

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

Teraz, gdy MyService ma dane przypisane do jego właściwości danych, a obietnica w obiekcie route resolve została rozwiązana, nasz kontroler dla trasy uruchamia się i możemy przypisać dane z usługi do naszego obiektu kontrolera.

CONTROLLER

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

Teraz wszystkie nasze powiązania w zakresie kontrolera będą mogły korzystać z danych pochodzących z MyService.

 8
Author: dewd,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-05-25 21:52:13

Więc znalazłem rozwiązanie. Stworzyłem usługę angularJS, nazwiemy ją MyDataRepository i stworzyłem do niej moduł. Następnie podaję ten plik javascript z mojego kontrolera po stronie serwera:

HTML:

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

Server-side:

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

Mogę następnie wstrzykiwać MyDataRepository tam, gdzie będę tego potrzebował:

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

To działało świetnie dla mnie, ale jestem otwarty na wszelkie opinie, jeśli ktoś ma jakieś. }

 5
Author: testing123,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-05-02 14:15:20

Ponadto, możesz użyć następujących technik, aby świadczyć swoje usługi globalnie, zanim rzeczywiste Kontrolery zostaną wykonane: https://stackoverflow.com/a/27050497/1056679 . po prostu rozwiąż swoje dane globalnie, a następnie przekaż je do usługi na przykład w bloku run.

 2
Author: Slava Fomin II,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-05-23 10:31:11

Możesz użyć JSONP do asynchronicznie wczytywania danych usługi. Żądanie JSONP zostanie wykonane podczas początkowego ładowania strony, a wyniki będą dostępne przed rozpoczęciem aplikacji. W ten sposób nie będziesz musiał nadmuchiwać routingu redundantnymi rozwiązaniami.

HTML wyglądałby tak:

<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
Author: hegemon,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-07-06 13:55:13

Najprostszy sposób na pobranie dowolnej inicjalizacji użyj katalogu ng-INIT.

Po prostu umieść zakres ng-INIT div tam, gdzie chcesz pobrać dane init

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

Indeks.js

$scope.init=function(){
    $http({method:'GET',url:'/countries/countries.json'}).success(function(data){
      alert();
           $scope.countries = data;
    });
  };

UWAGA: możesz użyć tej metodologii, jeśli nie masz tego samego kodu więcej niż jedno miejsce.

 -1
Author: Roni,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-03-16 13:59:46