AngularJS: инициализация службы с асинхронными данными

у меня есть служба AngularJS, которую я хочу инициализировать с некоторыми асинхронными данными. Что-то вроде этого:--13-->

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 возвращается я получу исключение нулевого указателя. Насколько я могу судить, прочитав некоторые из других заданных вопросов здесь и здесь у меня есть несколько вариантов, но ни один из них не кажется очень чистым (возможно, мне не хватает что-то):

настройка службы с помощью "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()

использовать объекты promise

это, вероятно, работа. Единственным недостатком этого везде, где я называю MyService, я должен буду знать, что doStuff() возвращает обещание, и весь код будет нам then взаимодействовать с обещанием. Я бы предпочел просто подождать, пока myData не вернется, прежде чем загружать мое приложение.

Ручной 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
    });
});

Глобальный Javascript Var Я мог бы отправить свой JSON непосредственно на глобальный Javascript переменная:

HTML-код:

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

данные.js:

var dataForMyService = { 
// myData here
};

тогда он будет доступен при инициализации MyService:

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

это тоже сработает, но тогда у меня есть глобальная переменная javascript, которая плохо пахнет.

это мои единственные варианты? Один из этих вариантов лучше других? Я знаю, что это довольно длинный вопрос, но я хотел показать, что я постарался изучить все варианты. Любое руководство будет значительно будьте благодарны.

10 ответов


вы посмотрели на $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


основываясь на решении Мартина Аткинса, вот полное, краткое чисто угловое решение:

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

Это небольшое повышение над решением Мартина, которое отложило выборку config до тех пор, пока документ не будет готов. Насколько я знаю, нет причин откладывать вызов $http для этого.

Тестирование

Примечание: я обнаружил, что это решение не работает хорошо, когда модульное тестирование, когда код включен в ваш . Причина этого заключается в том, что приведенный выше код запускается немедленно при загрузке JS-файла. Это означает, что тестовая среда (Jasmine в моем случае) не имеет возможности предоставить макет реализация $http.

моим решением, которым я не совсем доволен, было переместить этот код в наш index.html файл, поэтому инфраструктура модульных тестов Grunt/Karma/Jasmine не видит его.


я использовал аналогичный подход к описанному @XMLilley, но хотел иметь возможность использовать службы AngularJS, такие как $http для загрузки конфигурации и дальнейшей инициализации без использования низкоуровневых API или jQuery.

используя resolve на маршрутах также не было опции, потому что мне нужно было, чтобы значения были доступны в качестве констант при запуске моего приложения, даже в module.config() блоки.

Я создал небольшое приложение AngularJS, которое загружает конфигурацию, устанавливает их как константы на фактическом приложении и bootstraps его.

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

обновление

Я бы рекомендовал использовать подход, описанный ниже Мартином Аткинсом и JBCP.

обновление 2

потому что мне это было нужно в нескольких проектах, я только что выпустил модуль bower, который заботится это: https://github.com/philippd/angular-deferred-bootstrap

пример, который загружает данные из back-end и устанавливает константу APP_CONFIG в модуле AngularJS:

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

случай "ручной начальной загрузки" может получить доступ к угловым сервисам, вручную создав инжектор перед начальной загрузкой. Этот начальный инжектор будет стоять отдельно (не будет прикреплен к любым элементам) и включать только подмножество модулей, которые загружаются. Если все, что вам нужно, это основные угловые службы, достаточно просто загрузить 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 директива, чтобы предотвратить unparsed шаблон от отображения во время работы. Вы также можете предоставить некоторую индикацию загрузки в DOM, предоставив некоторый HTML, который отображается только до инициализации 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>

Я создал полный, рабочий пример этого подхода на Plunker, загрузка конфигурации из статического файла JSON в качестве примера.


у меня была та же проблема: я люблю resolve object, но это работает только для содержимого ng-view. Что делать, если у вас есть контроллеры (например, для nav верхнего уровня), которые существуют вне ng-view и которые должны быть инициализированы данными до начала маршрутизации? Как нам избежать возни на стороне сервера, чтобы это сработало?

используйте ручной bootstrap и угловую константу. Наивный XHR получает ваши данные, и вы bootstrap угловой в своем обратном вызове, который касается ваших асинхронных проблем. В приведенном ниже примере вам даже не нужно создавать глобальную переменную. Возвращаемые данные существуют только в угловой области как инъекционные и даже не присутствуют внутри контроллеров, служб и т. д. если только вы не введете его. (Так же, как вы вводите вывод вашего 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 или другие инструменты в этом контексте.

(т. похожие статьи)


что вы можете сделать это в вашем .конфигурация для приложения-создать объект разрешения для маршрута и в функции pass в $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;
          }
      })
}

угловой не будет отображать шаблон или сделать контроллер доступным до отложить.вызывается 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 имеет данные, назначенные его свойству данных, и обещание в объекте разрешения маршрута было разрешено, наш контроллер для маршрута запускается в жизнь, и мы можем назначить данные из службы нашему объекту контроллера.

контроллер

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

Теперь все наши привязки в области контроллера смогут использовать данные, которые возникли из MyService.


Итак, я нашел решение. Я создал сервис 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...
}

Это отлично сработало для меня, но я открыт для любой обратной связи, если у кого-то есть. }


кроме того, вы можете использовать следующие методы для глобального предоставления службы перед выполнением фактических контроллеров:https://stackoverflow.com/a/27050497/1056679. Просто разрешите свои данные глобально, а затем передайте их в службу в run блок например.


можно использовать 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>

самый простой способ получить любой инициализировать использование каталога ng-init.

просто поместите область ng-init div, где вы хотите получить данные init

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

.js

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

Примечание: вы можете использовать эту методологию, если у вас нет одного и того же кода более одного места.