[javascript] 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()오기 전에 전화를 걸면 myDatanull 포인터 예외가 발생 하기 때문에 분명히 작동하지 않습니다 . 여기여기에 묻는 다른 질문 중 일부를 읽을 수 있는 한 몇 가지 옵션이 있지만 그중 어느 것도 매우 깨끗한 것처럼 보이지 않습니다 (아마도 뭔가 빠진 것 같습니다).

“실행”설정 서비스

내 앱을 설정할 때 다음을 수행하십시오.

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

이것은 약간의 시간 동안 작동하지만 모든 것이 초기화되는 데 걸리는 시간보다 비동기 데이터가 오래 걸리면 호출 할 때 null 포인터 예외가 발생합니다. 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
    });
});

전역 자바 스크립트 Var
내 JSON을 전역 자바 스크립트 변수로 직접 보낼 수 있습니다.

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

이것도 작동하지만 냄새가 나는 전역 자바 스크립트 변수가 있습니다.

이것들은 나의 유일한 옵션입니까? 이러한 옵션 중 하나가 다른 옵션보다 낫습니까? 나는 이것이 꽤 긴 질문이라는 것을 알고 있지만 모든 옵션을 탐색하려고 노력하고 있음을 보여주고 싶었습니다. 어떤 지침이라도 대단히 감사하겠습니다.



답변

봤어 $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


답변

Martin Atkins의 솔루션을 기반으로 한 완벽하고 간결한 순수 각도 솔루션은 다음과 같습니다.

(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 단위 테스트 인프라에서 볼 수 없었습니다.


답변

@XMLilley가 설명한 것과 비슷한 접근 방식을 사용했지만 $http구성을로드하고 낮은 수준의 API 또는 jQuery를 사용하지 않고 추가 초기화를 수행하는 것과 같은 AngularJS 서비스를 사용할 수있는 기능을 원했습니다 .

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

http://plnkr.co/edit/FYznxP3xe8dxzwxs37hi?p=preview 에서 실제로 사용하십시오 ( $timeout대신 사용 $http).

최신 정보

Martin Atkins와 JBCP가 아래에 설명하는 접근 방식을 사용하는 것이 좋습니다.

업데이트 2

여러 프로젝트에서 필요했기 때문에 방금 이것을 처리하는 bower 모듈을 출시했습니다. https://github.com/philippd/angular-deferred-bootstrap

백엔드에서 데이터를로드하고 AngularJS 모듈에서 APP_CONFIG라는 상수를 설정하는 예제 :

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가 차단되므로 구문 분석되지 않은 템플릿이 작업 중에 표시되지 않도록 지시문을 사용하는 것이 좋습니다 . AngularJS가 초기화 될 때까지만 표시되는 HTML을 제공하여 DOM에 일종의 로딩 표시를 제공 할 수도 있습니다.

<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 객체를 사용하면 $httpJQuery가 처리 할 수 있는 많은 장점을 제거 할 수 있지만이 예제는 최소한 간단한 의존성을 위해 특별한 종속성없이 작동합니다 get. 요청에 대해 더 많은 권한을 원할 경우 외부 라이브러리를로드하여 도움을 받으십시오. 그러나이 $http맥락에서 각도 또는 다른 도구에 액세스하는 것이 가능하지 않다고 생각합니다 .

(SO 관련 게시물 )


답변

당신이 할 수있는 일은 앱의 .config에서 경로에 대한 resolve 객체를 만들고 $ q (promise object)에 함수 패스와 의존하는 서비스 이름을 만들고, 약속을 해결하는 것입니다. 서비스에서 $ 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에 데이터 속성에 할당 된 데이터가 있고 경로 확인 객체의 약속이 해결되었으며 경로에 대한 컨트롤러가 실제로 작동하며 서비스의 데이터를 컨트롤러 객체에 할당 할 수 있습니다.

제어 장치

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

이것은 나에게 큰 효과가 있었지만, 누군가가 있다면 어떤 피드백에도 열려 있습니다. }