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

служба установки с "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 вернется перед загрузкой моего приложения.

Ручной Загрузки

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 452

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 для этого.

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

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

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

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

используя resolve on routes также не был вариантом, потому что мне нужно было, чтобы значения были доступны в качестве констант при запуске моего приложения, даже в 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');
    }
  }
});

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

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

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

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

Angular не будет отображать шаблон или делать контроллер доступным до тех пор, пока не отложит.решения() был вызван. Мы можем сделать это в нашем сервис:

сервис

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

контроллер

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

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