将$scope注入到Angular服务函数中

112

我有一个服务:

angular.module('cfd')
  .service('StudentService', [ '$http',
    function ($http) {
    // get some data via the $http
    var path = 'data/people/students.json';
    var students = $http.get(path).then(function (resp) {
      return resp.data;
    });     
    //save method create a new student if not already exists
    //else update the existing object
    this.save = function (student) {
      if (student.id == null) {
        //if this is new student, add it in students array
        $scope.students.push(student);
      } else {
        //for existing student, find this student using id
        //and update it.
        for (i in students) {
          if (students[i].id == student.id) {
            students[i] = student;
          }
        }
      }
    };

但是当我调用save()时,我无法访问$scope,会收到ReferenceError: $scope未定义的错误。因此,对我来说,逻辑上的下一步是为save()提供$scope,因此我还必须将其提供/注入到service中。所以如果我这样做:

  .service('StudentService', [ '$http', '$scope',
                      function ($http, $scope) {
我遇到了以下错误:

Error: [$injector:unpr] 未知的提供者: $scopeProvider <- $scope <- StudentService

错误信息中的链接(哇,这很不错!)让我知道这是关于注入的,并且可能与JavaScript文件的声明顺序有关。我尝试在index.html中重新排列它们,但我认为问题更简单,比如我注入它们的方式。
使用Angular-UI和Angular-UI-Router。
7个回答

186
你在控制器中看到被注入的$scope并不是某个服务(像其他可注入的东西一样),而是一个Scope对象。可以创建许多作用域对象(通常从父作用域原型继承)。所有作用域的根是$rootScope,你可以使用任何作用域(包括$rootScope)的$new()方法创建一个新的子作用域。
Scope的目的是将应用程序的演示和业务逻辑“粘合”在一起。将$scope传递给服务没有太多意义。
服务是单例对象,用于共享数据(例如,在多个控制器之间)并封装可重用的代码片段(因为它们可以被注入,并在需要它们的应用程序的任何部分提供其“服务”:控制器、指令、过滤器、其他服务等)。
我相信,有各种方法适合你。其中之一是:
由于StudentService负责处理学生数据,你可以让StudentService保持一个学生数组,并让它与可能感兴趣的人(例如你的$scope)“共享”。如果还有其他视图/控制器/过滤器/服务需要访问该信息,则更有意义(如果现在没有任何视图/控制器/过滤器/服务需要访问该信息,则不要惊讶,如果它们很快开始出现)。
每次添加新学生(使用服务的save()方法)时,服务自己的学生数组将被更新,并且共享该数组的每个其他对象也将自动更新。
基于上述方法,你的代码可能如下所示:
angular.
  module('cfd', []).

  factory('StudentService', ['$http', '$q', function ($http, $q) {
    var path = 'data/people/students.json';
    var students = [];

    // In the real app, instead of just updating the students array
    // (which will be probably already done from the controller)
    // this method should send the student data to the server and
    // wait for a response.
    // This method returns a promise to emulate what would happen 
    // when actually communicating with the server.
    var save = function (student) {
      if (student.id === null) {
        students.push(student);
      } else {
        for (var i = 0; i < students.length; i++) {
          if (students[i].id === student.id) {
            students[i] = student;
            break;
          }
        }
      }

      return $q.resolve(student);
    };

    // Populate the students array with students from the server.
    $http.get(path).then(function (response) {
      response.data.forEach(function (student) {
        students.push(student);
      });
    });

    return {
      students: students,
      save: save
    };     
  }]).

  controller('someCtrl', ['$scope', 'StudentService', 
    function ($scope, StudentService) {
      $scope.students = StudentService.students;
      $scope.saveStudent = function (student) {
        // Do some $scope-specific stuff...

        // Do the actual saving using the StudentService.
        // Once the operation is completed, the $scope's `students`
        // array will be automatically updated, since it references
        // the StudentService's `students` array.
        StudentService.save(student).then(function () {
          // Do some more $scope-specific stuff, 
          // e.g. show a notification.
        }, function (err) {
          // Handle the error.
        });
      };
    }
]);

当使用这种方法时,您需要注意不要重新分配服务的数组,否则任何其他组件(例如作用域)仍将引用原始数组,并且您的应用程序将会崩溃。
例如,在StudentService中清除数组:

/* DON'T DO THAT   */  
var clear = function () { students = []; }

/* DO THIS INSTEAD */  
var clear = function () { students.splice(0, students.length); }

请参考这个简短的演示


小更新:

关于使用服务,但不是通过service()函数创建服务时可能会产生混淆,以下是引用$provide文档

Angular中的服务是由服务工厂创建的单例对象。这些服务工厂是函数,而这些函数则由服务提供者创建。这些服务提供者是构造函数。当它们被实例化时,必须包含一个名为$get的属性,该属性保存着服务工厂函数。
[...]
...$provide服务还有其他辅助方法来注册服务而不需要指定提供者:

  • provider(provider) - 在$injector中注册服务提供者
  • constant(obj) - 注册可以由提供者和服务访问的值/对象。
  • value(obj) - 注册只能由服务访问而不能由提供者访问的值/对象。
  • factory(fn) - 注册服务工厂函数fn,它将被包装在一个服务提供者对象中,该对象的$get属性将包含给定的工厂函数。
  • service(class) - 注册构造函数class,该构造函数将被包装在一个服务提供者对象中,该对象的$get属性将使用给定的构造函数实例化一个新对象。

基本上,每个Angular服务都是使用$provide.provider()注册的,但也有用于更简单服务的“快捷”方法(其中两个是service()factory())。
最终都归结为一个服务,因此使用哪种方法并没有太大区别(只要你的服务的要求可以通过这种方法满足即可)。

顺便说一下,provider vs service vs factory是Angular新手最容易混淆的概念之一,但幸运的是有很多资源(在SO上)可以让事情变得更容易。(只要搜索一下即可。)

(我希望这能澄清问题-如果不能,请让我知道。)


1
一个问题。你说服务,但你的代码示例使用工厂。我刚开始理解工厂、服务和提供者之间的区别,只是想确认选择工厂是否是最佳选项,因为我一直在使用服务。从你的示例中学到了很多。感谢你提供的帮助和非常清晰的解释。 - chris Frisina
3
@chrisFrisina:更新了答案并加入了一些解释。基本上,使用servicefactory没有太大区别 - 最终你都会得到一个Angular服务。只要确保你理解每个选项的工作原理,并确定它是否适合你的需求即可。 - gkalpak
@ExpertSystem 如果ajax调用没有完成,$scope.students会是空的吗?或者如果这个代码块正在进行中,$scope.students会部分填充吗?students.push(student); - Yc Zhang
愚蠢的问题,为什么你要把data中的每个元素都推入students中?为什么不能将students设置为data? - JohnVanDijk
@JohnVanDijk,我们使用students的方式(即导出对初始students数组的引用),如果您执行students = data,则只有您内部引用会更改。导出的students数组(尤其是我们在控制器中抓取的副本)仍将引用旧数组。 - gkalpak
显示剩余4条评论

18

不要试图在服务中修改$scope,你可以在控制器中实现$watch以监视服务上的属性变化,然后更新$scope上的属性。以下是您可能在控制器中尝试的示例:

angular.module('cfd')
    .controller('MyController', ['$scope', 'StudentService', function ($scope, StudentService) {

        $scope.students = null;

        (function () {
            $scope.$watch(function () {
                return StudentService.students;
            }, function (newVal, oldVal) {
                if ( newValue !== oldValue ) {
                    $scope.students = newVal;
                }
            });
        }());
    }]);

需要注意的一点是,在您的服务中,为了使 students 属性可见,它需要位于 Service 对象或 this 上,如下所示:

this.students = $http.get(path).then(function (resp) {
  return resp.data;
});

12

如果你坚持在服务中要有$scope访问权限,你可以:

创建一个getter/setter服务

ngapp.factory('Scopes', function (){
  var mem = {};
  return {
    store: function (key, value) { mem[key] = value; },
    get: function (key) { return mem[key]; }
  };
});

将其注入并将控制器作用域存储其中

ngapp.controller('myCtrl', ['$scope', 'Scopes', function($scope, Scopes) {
  Scopes.store('myCtrl', $scope);
}]);

现在,在另一个服务中获取范围

ngapp.factory('getRoute', ['Scopes', '$http', function(Scopes, $http){
  // there you are
  var $scope = Scopes.get('myCtrl');
}]);

作用域是如何被销毁的? - JK.

9

服务是单例的,将作用域注入到服务中是不合逻辑的(确实是这种情况,不能在服务中注入作用域)。您可以将作用域作为参数传递,但这也是一种糟糕的设计选择,因为您将使多个地方对作用域进行修改,从而使调试变得困难。处理作用域变量的代码应该放在控制器中,而服务调用应该放在服务中。


我理解你的意思。但是在我的情况下,我有很多控制器,我想使用非常相似的一组$watches来配置它们的作用域。你会在哪里做这个?目前,我确实将作用域作为参数传递给一个服务,该服务设置$watches。 - moritz
@moritz 可以考虑实现一个次要指令(具有范围:false,因此它使用其他指令定义的范围),并且该指令使观察者的绑定以及您需要的任何其他内容。这样,您可以在需要定义此类观察器的任何位置使用该其他指令。因为将范围传递给服务确实非常糟糕 :)(相信我,我已经尝试过,并最终撞了墙) - tfrascaroli
@TIMINeutron,这听起来比传递范围好多了,下次遇到这种情况我会尝试一下!谢谢! - moritz

3

你可以让你的服务完全不考虑作用域,但是在控制器中允许异步更新作用域。

你遇到的问题是因为你没有意识到http调用是异步进行的,这意味着你不会立即得到一个值。例如,

var students = $http.get(path).then(function (resp) {
  return resp.data;
}); // then() returns a promise object, not resp.data

有一种简单的方法可以解决这个问题,那就是提供一个回调函数。

.service('StudentService', [ '$http',
    function ($http) {
    // get some data via the $http
    var path = '/students';

    //save method create a new student if not already exists
    //else update the existing object
    this.save = function (student, doneCallback) {
      $http.post(
        path, 
        {
          params: {
            student: student
          }
        }
      )
      .then(function (resp) {
        doneCallback(resp.data); // when the async http call is done, execute the callback
      });  
    }
.controller('StudentSaveController', ['$scope', 'StudentService', function ($scope, StudentService) {
  $scope.saveUser = function (user) {
    StudentService.save(user, function (data) {
      $scope.message = data; // I'm assuming data is a string error returned from your REST API
    })
  }
}]);

表格:
<div class="form-message">{{message}}</div>

<div ng-controller="StudentSaveController">
  <form novalidate class="simple-form">
    Name: <input type="text" ng-model="user.name" /><br />
    E-mail: <input type="email" ng-model="user.email" /><br />
    Gender: <input type="radio" ng-model="user.gender" value="male" />male
    <input type="radio" ng-model="user.gender" value="female" />female<br />
    <input type="button" ng-click="reset()" value="Reset" />
    <input type="submit" ng-click="saveUser(user)" value="Save" />
  </form>
</div>

为了简化业务逻辑,我删除了一些内容,并且没有实际测试这段代码,但是像这样的代码可以工作。主要的概念是将回调从控制器传递到服务中,稍后在未来被调用。如果您熟悉NodeJS,则这是相同的概念。


这种方法并不推荐。请参阅为什么从Promise的.then方法中使用回调是一种反模式 - georgeawg

0

遇到了同样的困境。最终我得出了以下结论。在这里,我没有将作用域对象注入到工厂中,而是使用$http服务返回的promise概念,在控制器本身中设置了$scope

(function () {
    getDataFactory = function ($http)
    {
        return {
            callWebApi: function (reqData)
            {
                var dataTemp = {
                    Page: 1, Take: 10,
                    PropName: 'Id', SortOrder: 'Asc'
                };

                return $http({
                    method: 'GET',
                    url: '/api/PatientCategoryApi/PatCat',
                    params: dataTemp, // Parameters to pass to external service
                    headers: { 'Content-Type': 'application/Json' }
                })                
            }
        }
    }
    patientCategoryController = function ($scope, getDataFactory) {
        alert('Hare');
        var promise = getDataFactory.callWebApi('someDataToPass');
        promise.then(
            function successCallback(response) {
                alert(JSON.stringify(response.data));
                // Set this response data to scope to use it in UI
                $scope.gridOptions.data = response.data.Collection;
            }, function errorCallback(response) {
                alert('Some problem while fetching data!!');
            });
    }
    patientCategoryController.$inject = ['$scope', 'getDataFactory'];
    getDataFactory.$inject = ['$http'];
    angular.module('demoApp', []);
    angular.module('demoApp').controller('patientCategoryController', patientCategoryController);
    angular.module('demoApp').factory('getDataFactory', getDataFactory);    
}());

0

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接