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

429

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

Использовать объекты обещания

Это, вероятно, сработает. Единственный недостаток, который я везде называю 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, которая плохо пахнет.

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

Теги:
asynchronous
service
angular-promise

10 ответов

292
Лучший ответ

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

  • 1
    Большое спасибо за ваш ответ! Это сработало бы для меня, если бы у меня еще не было службы на карте разрешения, использующей MyService. Я обновил ваш плункер в моей ситуации: plnkr.co/edit/465Cupaf5mtxljCl5NuF?p=preview . Есть ли способ заставить MyOtherService ждать инициализации MyService?
  • 2
    Думаю, я бы связал обещания в MyOtherService - я обновил плункер цепочкой и некоторыми комментариями - как это выглядит? plnkr.co/edit/Z7dWVNA9P44Q72sLiPjW?p=preview
Показать ещё 9 комментариев
81

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

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

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

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

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

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

  • 1
    Такое правило, как «не загрязнять глобальную область видимости», должно соблюдаться только в той мере, в которой они делают наш код лучше (менее сложным, более обслуживаемым, более безопасным и т. Д.). Я не вижу, как это решение лучше, чем просто загрузка данных в одну глобальную переменную. Что мне не хватает?
  • 4
    Это позволяет вам использовать систему внедрения зависимостей Angular для доступа к константе 'CONFIG' в модулях, которые в ней нуждаются, но вы не рискуете засорять другие модули, которые этого не делают. Например, если вы использовали глобальную переменную 'config', есть вероятность, что другой сторонний код также может искать ту же переменную.
Показать ещё 2 комментария
50

Я использовал аналогичный подход к описанию, описанному в @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

UPDATE

Я бы рекомендовал использовать подход, описанный ниже Мартином Аткинсом и 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');
    }
  }
});
  • 10
    deferredBootstrapper - это путь
41

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

  • 0
    Я не думаю, что вам нужно отложить $ http.get () до тех пор, пока документ не будет готов.
  • 0
    @JBCP да, вы правы, что это работает так же хорошо, если вы меняете события так, что мы не ждем, пока документ станет готовым, до тех пор, пока не будет возвращен ответ HTTP, с преимуществом возможности начать HTTP запрос быстрее. Только вызов начальной загрузки должен ждать, пока DOM не будет готов.
Показать ещё 2 комментария
13

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

Использовать ручную загрузку и константу angular. Найденный XHR получает ваши данные, а вы загружаете angular в своем обратном вызове, который имеет дело с вашими асинхронными проблемами. В приведенном ниже примере вам даже не нужно создавать глобальную переменную. Возвращенные данные существуют только в области 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 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 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 addressable in your templates 
}]);

Конечно, использование голого объекта XHR удаляет ряд тонкостей, которые $http или JQuery позаботятся о вас, но этот пример работает без особых зависимостей, по крайней мере, для простого get. Если вы хотите немного больше энергии для своего запроса, загрузите внешнюю библиотеку, чтобы помочь вам. Но я не считаю возможным доступ к angular $http или другим инструментам в этом контексте.

(SO связанный пост)

8

Что вы можете сделать в вашем .config для приложения, создайте объект разрешения для маршрута и в проходе функции в $q (объект обещания) и имя службы, на которую вы зависите, и разрешите обещание в функции обратного вызова для $http в сервисе, например:

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 не будет отображать шаблон или сделать доступным контроллер до тех пор, пока не будет вызван 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 the resolve function in our
                 //route config
                 q.resolve();
             }
       }

       return MyService;
});

Теперь, когда у MyService есть данные, привязанные к этому свойству данных, и обещание в разрешении объекта маршрута было разрешено, наш контроллер для маршрута запускается в жизнь, и мы можем назначить данные из службы нашему объекту контроллера.

КОНТРОЛЛЕР

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

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

  • 0
    Я дам этому шанс, когда у меня будет больше времени. Это похоже на то, что другие пытались сделать в ngModules.
  • 1
    Мне нравится этот подход, и я использовал его раньше, но в настоящее время я пытаюсь понять, как сделать это аккуратно, когда у меня есть несколько маршрутов, каждый из которых может зависеть или не зависеть от предварительно выбранных данных. Есть мысли по этому поводу?
Показать ещё 5 комментариев
5

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

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

  • 0
    Мне нравится ваш модульный подход. Я обнаружил, что $ routeScope доступен службе, запрашивающей данные, и вы можете назначить ей данные в обратном вызове $ http.success. Однако использование $ routeScope для неглобальных элементов создает запах, и данные должны быть действительно назначены контроллеру $ scope. К сожалению, я думаю, что ваш подход, хотя и новаторский, не идеален (хотя уважение к тому, чтобы найти что-то, что работает для вас). Я просто уверен, что должен быть ответ только на стороне клиента, который каким-то образом ожидает данные и позволяет присваивать область. Поиски продолжаются!
  • 0
    В случае, если это кому-то пригодится, я недавно увидел несколько разных подходов к просмотру модулей, которые другие люди написали и добавили на сайт ngModules. Когда у меня будет больше времени, мне придется либо начать использовать один из них, либо выяснить, что они сделали, и добавить его к своим материалам.
2

Кроме того, вы можете использовать следующие методы для предоставления услуг по всему миру до того, как будут выполнены реальные контроллеры: https://stackoverflow.com/questions/27050496/run-controllers-only-after-initialization-is-complete-in-angularjs. Просто разрешите данные в глобальном масштабе, а затем передайте их в свою службу в блок run, например.

0

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

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

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

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

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

Ещё вопросы

Сообщество Overcoder
Наверх
Меню