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

AngularJS : Initialize service with asynchronous data

我有一个AngularJS服务,我想用一些异步数据初始化它。像这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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();
        }
    };
});

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

"运行"的安装服务

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

1
2
3
4
5
myApp.run(function ($http, MyService) {
    $http.get('data.json').success(function (data) {
        MyService.setData(data);
    });
});

那么我的服务应该是这样的:

1
2
3
4
5
6
7
8
9
10
11
myModule.service('MyService', function() {
    var myData = null;
    return {
        setData: function (data) {
            myData = data;
        },
        doStuff: function () {
            return myData.getSomeData();
        }
    };
});

这在某些情况下是可行的,但是如果异步数据的时间比初始化所有内容所需的时间长,那么当调用doStuff()时,我会得到一个空指针异常。

使用承诺对象

这可能有效。唯一的缺点是,在我调用myService时,我必须知道dostuff()返回一个承诺,并且所有代码都必须使用then来与该承诺交互。我宁愿等到我的数据返回后再加载我的应用程序。

手动引导

1
2
3
4
5
6
7
8
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变量我可以将JSON直接发送到一个全局javascript变量:

HTML:

1
<script type="text/javascript" src="data.js">

DAT.JS:

1
2
3
var dataForMyService = {
// myData here
};

然后在初始化MyService时可用:

1
2
3
4
5
6
7
8
myModule.service('MyService', function() {
    var myData = dataForMyService;
    return {
        doStuff: function () {
            return myData.getSomeData();
        }
    };
});

这也可以,但是我有一个全局javascript变量,闻起来很糟糕。

这是我唯一的选择吗?其中一个选项比其他选项更好吗?我知道这是一个很长的问题,但我想证明我已经尝试了探索我所有的选择。任何指导都将不胜感激。


你看过$routeProvider.when('/path',{ resolve:{...}吗?它可以使Promise方法更清晰:

在您的服务中公开承诺:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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添加到路由配置中:

1
2
3
4
app.config(function($routeProvider){
  $routeProvider
    .when('/',{controller:'MainCtrl',
    template:'From MyService:[cc lang="javascript"]{{data | json}}

解析:{"myServiceData":函数(myService){//myServiceData也可以在控制器中注入,如果不希望这样做,可以用$Q服务创建一个新的承诺。返回myservice.promise;}})}:< /代码>

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

1
2
3
4
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=预览


基于Martin Atkins的解,这里有一个完整、简洁的纯角度解:

1
2
3
4
5
6
7
8
9
10
11
12
13
(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单元测试基础结构看不到它。


我使用了与@xmilley描述的方法类似的方法,但希望能够使用AngularJS服务(如$http)来加载配置并在不使用低级API或jquery的情况下进行进一步的初始化。

在路由上使用resolve也不是一个选项,因为我需要在应用程序启动时将这些值作为常量提供,即使在module.config()块中也是如此。

我创建了一个小的AngularJS应用程序来加载配置,将它们设置为实际应用程序上的常量并引导它。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
// 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(使用$timeout而不是$http)。P=预览

更新

我建议使用下面由Martin Atkins和JBCP描述的方法。

更新2

因为我在多个项目中需要它,所以我刚刚发布了一个Bower模块来处理这个问题:https://github.com/philippd/angular-deferred-bootstrap

从后端加载数据并在AngularJS模块上设置一个名为app_config的常量的示例:

1
2
3
4
5
6
7
8
9
deferredBootstrapper.bootstrap({
  element: document.body,
  module: 'MyApp',
  resolve: {
    APP_CONFIG: function ($http) {
      return $http.get('/api/demo-config');
    }
  }
});


"手动引导"案例可以通过在引导前手动创建注入器来访问角度服务。这个初始注入器将独立(不连接到任何元素),并且只包含加载模块的一个子集。如果您只需要核心角度服务,那么只需加载ng就足够了,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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机制将数据提供给您的应用程序:

1
myApp.constant('myAppConfig', data);

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

1
2
3
4
5
myApp.config(
    function (myAppConfig, someService) {
        someService.config(myAppConfig.someServiceConfig);
    }
);

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

当然,因为这里的异步操作会阻塞应用程序的引导,从而阻塞模板的编译/链接,所以最好使用ng-cloak指令来防止未分析的模板在工作期间出现。您还可以在DOM中提供某种加载指示,方法是提供一些仅在AngularJS初始化之前才会显示的HTML:

1
2
3
4
5
6
7
8
9
10
    <!-- initialLoad never gets set, so this div vanishes as soon as Angular is done compiling -->
    <p>
Loading the app.....
</p>


    <!-- ng-cloak attribute is removed once the app is done bootstrapping -->
    <p>
Done loading the app!
</p>

我在Plugker上创建了一个完整的、可工作的这种方法的示例,并以从静态JSON文件加载配置为例。


我也有同样的问题:我喜欢resolve对象,但这只适用于NG视图的内容。如果您有位于ng视图之外的控制器(例如顶级导航),并且在路由开始之前需要用数据初始化这些控制器,该怎么办?我们如何避免在服务器端乱搞以使其正常工作?

使用手动引导和角度常量。Naive XHR为您提供数据,并在回调中引导Angular,处理异步问题。在下面的示例中,您甚至不需要创建全局变量。返回的数据只作为可注入数据存在于角度范围内,并且甚至不存在于控制器、服务等内部,除非您注入它。(就像您将resolve对象的输出注入到控制器中以获得路由视图一样。)如果您以后更愿意将该数据作为服务进行交互,那么您可以创建一个服务,注入数据,而没有人会更明智。

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
//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常数存在。继续将其注入控制器或服务:

1
2
3
4
angular.module('MyApp')
    .controller('NavCtrl', ['NavData', function (NavData) {
        $scope.localObject = NavData; //now it's addressable in your templates
}]);

当然,使用一个裸露的XHR对象会去掉$http或jQuery为您处理的许多细节,但是这个示例没有特殊的依赖关系,至少对于一个简单的get。如果您需要更多的电力来满足您的请求,请加载一个外部库来帮助您解决问题。但我认为在这种情况下,不可能访问Angular的$http或其他工具。

(相关岗位)


您可以在应用程序的.config中创建路由的resolve对象,在函数pass in$q(promise object)中创建所依赖的服务的名称,并在回调函数中为服务中的$http解析promise,如下所示:

路由配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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;
          }
      })
}

在调用defer.resolve()之前,Angular不会呈现模板或使控制器可用。我们可以在我们的服务中做到这一点:

服务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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已经为它的数据属性分配了数据,并且路由解析对象中的承诺已经得到了解决,我们的路由控制器开始工作,我们可以将来自服务的数据分配给我们的控制器对象。

控制器

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

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


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

HTML:

1
<script src="path/myData.js">

服务器端:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
@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);
}

然后,我可以在需要的地方注入我的数据存储库:

1
2
3
4
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如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js">


function MyService {
  this.getData = function(){
    return   MyService.data;
  }
}
MyService.setData = function(data) {
  MyService.data = data;
}

angular.module('main')
.service('MyService', MyService)


<script src="/some_data.php?jsonp=MyService.setData">

获取任何初始化的最简单方法是使用ng init目录。

只需将ng init div scope放在要获取init数据的位置

索引文件

1
2
3
4
        <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>

索引文件

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

NOTE: you can use this methodology if you do not have same code more then one place.