AngularJS: инициализировать сервис с асинхронными данными

У меня есть служба 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()

Используйте обещанные объекты

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

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

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.

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

Модульное тестирование

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

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

Я использовал подход, аналогичный описанному @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 вместо $http) здесь: http://plnkr.co/edit/FYznxP3xe8dxzwxs37hi?p=preview

ОБНОВИТЬ

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

ОБНОВЛЕНИЕ 2

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

Пример, который загружает данные из серверной части и устанавливает константу с именем APP_CONFIG в модуле AngularJS:

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

В случае "ручной начальной загрузки" можно получить доступ к службам Angular, вручную создав инжектор перед начальной загрузкой. Этот начальный инжектор будет автономным (не привязанным к каким-либо элементам) и будет включать только подмножество загруженных модулей. Если все, что вам нужно, это основные сервисы Angular, достаточно просто загрузить 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 директива, предотвращающая появление необработанного шаблона во время работы. Вы могли бы также предоставить некоторую индикацию загрузки в 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 объект, но это работает только для содержимого ng-view. Что если у вас есть контроллеры (скажем, для навигации верхнего уровня), которые существуют за пределами ng-view и которые нужно инициализировать данными, прежде чем маршрутизация даже начнет происходить? Как мы можем избежать обмана на стороне сервера только для того, чтобы заставить это работать?

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

(ТАК связанный пост)

В вашем.config для приложения вы можете создать объект разрешения для маршрута и передать функцию в $q (объект обещания) и имя службы, от которой вы зависите, и разрешить обещание в Функция обратного вызова для $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, и обещание в объекте разрешения маршрута разрешено, наш контроллер для маршрута запускается, и мы можем назначить данные из сервиса нашему объекту контроллера.

КОНТРОЛЛЕР

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

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

Кроме того, вы можете использовать следующие методы для предоставления вашей услуги в глобальном масштабе, прежде чем будут выполнены фактические контроллеры: /questions/43292623/zapuskat-kontrolleryi-tolko-posle-zaversheniya-initsializatsii-v-angularjs/43292626#43292626. Просто разрешите ваши данные глобально, а затем передайте их вашей службе в 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, где вы хотите получить данные инициализации

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

index.js

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

ПРИМЕЧАНИЕ: вы можете использовать эту методологию, если у вас нет одного и того же кода более чем в одном месте.

Другие вопросы по тегам