AngularJS:使用异步数据初始化服务

tes*_*123 472 javascript service asynchronous angularjs angular-promise

我有一个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();
        }
    };
});
Run Code Online (Sandbox Code Playgroud)

显然这不会起作用,因为如果某些东西doStuff()myData返回之前尝试调用,我将得到一个空指针异常.据我所知,从这里这里提出的其他一些问题,我有几个选择,但没有一个看起来很干净(也许我错过了一些东西):

使用"运行"设置服务

设置我的应用时,请执行以下操作:

myApp.run(function ($http, MyService) {
    $http.get('data.json').success(function (data) {
        MyService.setData(data);
    });
});
Run Code Online (Sandbox Code Playgroud)

然后我的服务看起来像这样:

myModule.service('MyService', function() {
    var myData = null;
    return {
        setData: function (data) {
            myData = data;
        },
        doStuff: function () {
            return myData.getSomeData();
        }
    };
});
Run Code Online (Sandbox Code Playgroud)

这在某些时候有用,但是如果异步数据发生的时间比一切都要初​​始化的时间要长,我在调用时会得到一个空指针异常 doStuff()

使用promise对象

这可能会奏效.我称之为MyService的唯一缺点是我必须知道doStuff()返回一个promise,所有代码都必须让我们then与promise进行交互.在加载我的应用程序之前,我宁愿等到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
    });
});
Run Code Online (Sandbox Code Playgroud)

全局Javascript Var 我可以将我的JSON直接发送到全局Javascript变量:

HTML:

<script type="text/javascript" src="data.js"></script>
Run Code Online (Sandbox Code Playgroud)

data.js:

var dataForMyService = { 
// myData here
};
Run Code Online (Sandbox Code Playgroud)

然后在初始化时可用MyService:

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

这也可行,但后来我有一个全局的javascript变量闻起来很糟糕.

这些是我唯一的选择吗?这些选项中的一个比其他选项更好吗?我知道这是一个很长的问题,但我想表明我试图探索我的所有选择.非常感谢任何指导.

joa*_*mbl 326

你看过了$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();
      }
    };
});
Run Code Online (Sandbox Code Playgroud)

添加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;
      }
    }})
  }):
Run Code Online (Sandbox Code Playgroud)

在解决所有依赖项之前,您的控制器不会被实例化:

app.controller('MainCtrl', function($scope,MyService) {
  console.log('Promise is now resolved: '+MyService.doStuff().data)
  $scope.data = MyService.doStuff();
});
Run Code Online (Sandbox Code Playgroud)

我在plnkr上做了一个例子:http://plnkr.co/edit/GKg21XH0RwCMEQGUdZKH?p = preview

  • 这里有一个问题 - 如何将`resolve`属性分配给`$ routeProvider`中未提及的控制器.例如,`<div ng-controller ="IndexCtrl"> </ div>`.这里,明确提到控制器,而不是通过路由加载.在这种情况下,如何延迟控制器的实例化呢? (43认同)
  • 嗯,如果你不使用路由怎么办?这几乎就像是说除非使用路由,否则无法使用异步数据编写角度应用程序.将数据导入应用程序的推荐方法是异步加载它,但只要你有多个控制器并且你投入服务,BOOM就不可能. (19认同)
  • 我尝试了这个并且仍然遇到了一些问题,因为我有指令和其他控制器(我使用$ routeProvider的控制器处理主要的,辅助导航的东西......那是'MyOtherService')需要等到'MyService '已经解决了.我将继续努力,并在我取得任何成功的同时更新这一点.我只是希望在初始化我的控制器和指令之前,我可以等待数据返回.再次感谢你的帮助.如果我有一个主控制器包装了一切,那将是有效的. (3认同)
  • 我想我会在MyOtherService中链接承诺 - 我用链接和一些评论更新了plunker - 这看起来怎么样?http://plnkr.co/edit/Z7dWVNA9P44Q72sLiPjW?p=preview (2认同)

JBC*_*BCP 88

基于Martin Atkins的解决方案,这是一个完整,简洁的纯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']);
        });
    }
  );
})();
Run Code Online (Sandbox Code Playgroud)

此解决方案使用自执行匿名函数来获取$ http服务,请求配置,并在可用时将其注入名为CONFIG的常量.

完成后,我们等到文档准备就绪,然后引导Angular应用程序.

这比Martin的解决方案略有改进,该解决方案推迟获取配置,直到文档准备好.据我所知,没有理由延迟$ http呼叫.

单元测试

注意:我发现当代码包含在您的app.js文件中时,单元测试时此解决方案不能正常工作.原因是上面的代码在加载JS文件时立即运行.这意味着测试框架(在我的情况下是Jasmine)没有机会提供模拟实现$http.

我的解决方案,我并不完全满意,是将此代码移动到我们的index.html文件中,因此Grunt/Karma/Jasmine单元测试基础架构看不到它.

  • 在下面的其他一个手动引导程序解决方案的评论中提到它,但作为一个没有发现它的角度新手我可以指出你需要在你的html代码中删除你的ng-app指令才能正常工作 - 它正在用这种手动方法替换自动引导程序(通过ng-app).如果你没有取出ng-app,应用程序实际上可以工作,但你会在控制台中看到各种未知的提供程序错误. (7认同)
  • 它允许您使用Angular的依赖注入系统来访问需要它的模块中的'CONFIG'常量,但是您不会冒险破坏其他不需要的模块.例如,如果您使用全局"config"变量,则其他第三方代码也可能正在寻找相同的变量. (4认同)

phi*_*ppd 49

我使用了与@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']);
});
Run Code Online (Sandbox Code Playgroud)

在此处查看它(使用$timeout而不是$http):http://plnkr.co/edit/FYznxP3xe8dxzwxs37hi?p = preview

UPDATE

我建议使用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');
    }
  }
});
Run Code Online (Sandbox Code Playgroud)

  • deferredBootstrapper是要走的路 (11认同)

Mar*_*ins 43

"手动引导程序"案例可以通过在引导程序之前手动创建注入器来访问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']);
            }
        );
    }
);
Run Code Online (Sandbox Code Playgroud)

例如,您可以使用该module.constant机制为您的应用提供数据:

myApp.constant('myAppConfig', data);
Run Code Online (Sandbox Code Playgroud)

myAppConfig可以像任何其他服务一样注入,特别是在配置阶段可用:

myApp.config(
    function (myAppConfig, someService) {
        someService.config(myAppConfig.someServiceConfig);
    }
);
Run Code Online (Sandbox Code Playgroud)

或者,对于较小的应用程序,您可以直接将全局配置注入您的服务,但代价是在整个应用程序中传播有关配置格式的知识.

当然,由于此处的异步操作将阻止应用程序的引导程序,从而阻止模板的编译/链接,因此使用该ng-cloak指令防止未分析的模板在工作期间出现是明智的.您还可以在DOM中提供某种加载指示,方法是提供一些只在AngularJS初始化之前显示的HTML:

<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>
Run Code Online (Sandbox Code Playgroud)

我在Plunker上创建这个方法的完整,有效的示例,从静态JSON文件加载配置作为示例.

  • 我用你的方法创建了一个bower模块:https://github.com/philippd/angular-deferred-bootstrap (2认同)

XML*_*XML 15

我有同样的问题:我喜欢这个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();
})
Run Code Online (Sandbox Code Playgroud)

现在,你的NavData常数存在.继续将其注入控制器或服务:

angular.module('MyApp')
    .controller('NavCtrl', ['NavData', function (NavData) {
        $scope.localObject = NavData; //now it's addressable in your templates 
}]);
Run Code Online (Sandbox Code Playgroud)

当然,使用一个简单的XHR对象会剥夺$http或者JQuery会为你处理的一些细节,但是这个例子没有特殊的依赖关系,至少对于一个简单的依赖get.如果您想为请求提供更多功能,请加载外部库以帮助您.但我不认为$http在这种情况下可以访问角度或其他工具.

(SO 相关帖子)


dew*_*ewd 8

您可以做的是在您的.config中为app创建路由的解析对象,并在函数传递$ q(promise对象)和您依赖的服务名称,并解决服务中$ 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;
          }
      })
}
Run Code Online (Sandbox Code Playgroud)

在调用defer.resolve()之前,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;
});
Run Code Online (Sandbox Code Playgroud)

既然MyService已经为其数据属性分配了数据,并且路径解析对象中的promise已经解析,我们的路由控制器就会生效,我们可以将服务中的数据分配给控制器对象.

CONTROLLER

  app.controller('homeCtrl',function($scope,MyService){
       $scope.servicedata = MyService.data;
  });
Run Code Online (Sandbox Code Playgroud)

现在,我们在控制器范围内的所有绑定都将能够使用源自MyService的数据.

  • 这是我最终采用的路线,除了我必须使用属性作为函数的`resolve`一个对象.所以它最终成为`resolve:{dataFetch:function(){// call function here}}` (2认同)

tes*_*123 5

所以我找到了解决方案.我创建了一个angularJS服务,我们称之为MyDataRepository,我为它创建了一个模块.然后我从服务器端控制器提供这个javascript文件:

HTML:

<script src="path/myData.js"></script>
Run Code Online (Sandbox Code Playgroud)

服务器端:

@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);
}
Run Code Online (Sandbox Code Playgroud)

然后,我可以在需要它的地方注入MyDataRepository:

someOtherModule.service('MyOtherService', function(MyDataRepository) {
    var myData = MyDataRepository.getData();
    // Do what you have to do...
}
Run Code Online (Sandbox Code Playgroud)

这对我很有用,但如果有人有任何反馈,我愿意接受任何反馈.}