Внедрение $ scope в служебную функцию angular ()

108

У меня есть услуга:

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 is not defined. Таким образом, логический шаг (для меня) - предоставить save () с расширением $scope, и поэтому я также должен предоставить / внедрить его в service. Итак, если я сделаю это так:

  .service('StudentService', [ '$http', '$scope',
                      function ($http, $scope) {

Я получаю следующую ошибку:

Ошибка: [$ injector: unpr] Неизвестный поставщик: $ scopeProvider <- $ scope <- StudentService

Ссылка в ошибке (вау, это здорово!) Позволяет мне узнать, что это связано с инжектором и, возможно, связано с порядком объявления файлов js. Я попытался переупорядочить их в index.html, но я думаю, что это что-то более простое, например, способ их введения.

Использование Angular-UI и Angular-UI-Router

Крис Фризина
источник

Ответы:

184

То, $scopeчто вы видите, внедряемое в контроллеры, не является какой-то службой (как и остальная часть вводимого материала), а является объектом Scope. Могут быть созданы многие объекты области видимости (обычно прототипически наследуемые от родительской области). Корнем всех областей видимости является область, $rootScopeи вы можете создать новую дочернюю область, используя $new()метод любой области (включая $rootScope).

Цель Scope - «склеить» презентацию и бизнес-логику вашего приложения. Нет смысла передавать a $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 (поставщик) - регистрирует поставщика услуг с помощью инжектора $
  • константа (obj) - регистрирует значение / объект, к которому могут получить доступ провайдеры и службы.
  • value (obj) - регистрирует значение / объект, к которому могут получить доступ только службы, а не поставщики.
  • factory (fn) - регистрирует функцию фабрики служб, fn, которая будет заключена в объект поставщика службы, чье свойство $ get будет содержать данную фабричную функцию.
  • service (class) - регистрирует функцию-конструктор, класс, который будет заключен в объект поставщика услуг, чье свойство $ get будет создавать экземпляр нового объекта с использованием данной функции-конструктора.

По сути, это говорит о том, что каждая служба Angular регистрируется с использованием $provide.provider(), но есть "ярлыки" для более простых служб (два из которых - service()и factory()).
Все «сводится» к сервису, поэтому не имеет большого значения, какой метод вы используете (если требования к вашему сервису могут быть удовлетворены этим методом).

Кстати, providervs servicevs factory- одна из самых запутанных концепций для новичков в Angular, но, к счастью, есть много ресурсов (здесь, на SO), чтобы упростить задачу. (Просто поищите вокруг.)

(Надеюсь, это проясняет ситуацию - дайте мне знать, если это не так.)

гкалпак
источник
1
Один вопрос. Вы говорите сервис, но в вашем примере кода используется фабрика. Я только начинаю понимать разницу между фабриками, услугами и поставщиками, просто хочу быть уверенным, что использование фабрики - лучший вариант, поскольку я использовал услугу. Многому научился на твоем примере. Спасибо за скрипку и ОЧЕНЬ четкое объяснение.
Крис Фризина
3
@chrisFrisina: Обновил ответ с небольшим пояснением. По сути, не имеет большого значения, используете ли вы serviceили factory- вы закончите u службой и Angular . Просто убедитесь, что вы понимаете, как каждый из них работает и соответствует ли он вашим потребностям.
gkalpak
Хороший пост! Это мне очень помогает !
Oni1
Спасибо брат! вот хорошая статья на подобную тему stsc3000.github.io/blog/2013/10/26/…
Terafor
@ExpertSystem Будет $scope.studentsпустым, если вызов ajax не завершен? Или $scope.studentsбудет частично заполнен, если этот блок кода работает? students.push(student);
Yc Zhang
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доступе внутри службы, вы можете:

Создать службу получения / установки

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

Сервисы являются одиночными, и нелогично внедрять область видимости в службу (а это действительно так, вы не можете внедрить область в службу). Вы можете передать область видимости в качестве параметра, но это также плохой выбор дизайна, потому что область видимости будет редактироваться в нескольких местах, что затруднит отладку. Код для работы с переменными области действия должен находиться в контроллере, а вызовы службы - в службу.

Эрмин Дедович
источник
Я понимаю, что вы говорите. Однако в моем случае у меня много контроллеров, и я хотел бы настроить их области действия с очень похожим набором $ watch. Как / где бы вы это сделали? В настоящее время я действительно передаю область видимости в качестве параметра службе, которая устанавливает $ watch.
moritz
@moritz, возможно, реализует вторичную директиву (ту, которая имеет scope: false, поэтому она использует область, определенную другими директивами), и эта директива делает привязки watchchess, а также все, что вам нужно. Таким образом, вы можете использовать эту другую директиву в любом месте, где вам нужно определить такие часы. Потому что передать прицел в сервис действительно ужасно :) (поверьте, я был там, сделал это, в конце концов ударился головой о стену)
tfrascaroli
@TIMINeutron звучит намного лучше, чем передача по прицелу, я попробую это в следующий раз, когда появится сценарий! Спасибо!
moritz
Конечно. Я все еще учусь, и эту конкретную проблему я недавно решил таким особым способом, и это сработало для меня как прелесть.
tfrascaroli
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, это та же концепция.

2upmedia
источник
0

Попал в такое же затруднительное положение. В итоге я получил следующее. Итак, здесь я не внедряю объект области видимости в фабрику, а устанавливаю область $ в самом контроллере, используя концепцию обещания, возвращаемого службой $ http .

(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

Код для работы с переменными области действия должен находиться в контроллере, а вызовы службы - в службу.

Вы можете вводить $rootScopeс целью использования $rootScope.$broadcastи $rootScope.$on.

В противном случае избегайте инъекций $rootScope. Видеть

Джорджог
источник