Wstrzykiwanie $ scope do kątowej funkcji usługi ()

108

Mam usługę:

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

Ale kiedy dzwonię save(), nie mam dostępu do $scopei otrzymuję ReferenceError: $scope is not defined. Więc logicznym krokiem (dla mnie) jest zapewnienie funkcji save () z rozszerzeniem $scope, a zatem muszę również dostarczyć / wstrzyknąć go do service. Więc jeśli zrobię to w ten sposób:

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

Otrzymuję następujący błąd:

Błąd: [$ injector: unpr] Nieznany dostawca: $ scopeProvider <- $ scope <- StudentService

Link w błędzie (wow, to fajne!) Informuje mnie, że jest to związane z wtryskiwaczem i może mieć związek z kolejnością deklaracji plików js. Próbowałem zmienić ich kolejność w programie index.html, ale myślę, że jest to coś prostszego, na przykład sposób, w jaki je wstrzykuję.

Korzystanie z Angular-UI i Angular-UI-Router

chris Frisina
źródło

Odpowiedzi:

183

To $scope, co widzisz, jest wstrzykiwane do kontrolerów, nie jest jakąś usługą (jak reszta rzeczy do wstrzykiwania), ale jest obiektem Scope. Można utworzyć wiele obiektów zakresu (zwykle prototypowo dziedzicząc z zakresu nadrzędnego). Podstawą wszystkich zakresów jest $rootScopei można utworzyć nowy zakres podrzędny przy użyciu $new()metody dowolnego zakresu (w tym $rootScope).

Celem zakresu jest „sklejenie” prezentacji i logiki biznesowej aplikacji. Przekazywanie a $scopedo usługi nie ma większego sensu .

Usługi to pojedyncze obiekty używane (między innymi) do udostępniania danych (np. Między kilkoma kontrolerami) i generalnie hermetyzowania fragmentów kodu wielokrotnego użytku (ponieważ można je wstrzykiwać i oferować swoje „usługi” w dowolnej części aplikacji, która ich potrzebuje: kontrolery, dyrektywy, filtry, inne usługi itp.).

Jestem pewien, że sprawdzą się u Ciebie różne podejścia. Jedna z nich jest następująca:
ponieważ StudentServicejest odpowiedzialny za przetwarzanie danych uczniów, możesz StudentServiceprzechowywać ich tablicę i „dzielić się” nimi z każdym, kto może być zainteresowany (np. Twoim $scope). Ma to jeszcze większy sens, jeśli istnieją inne widoki / kontrolery / filtry / usługi, które muszą mieć dostęp do tych informacji (jeśli nie ma ich teraz, nie zdziw się, jeśli zaczną się pojawiać wkrótce).
Za każdym razem, gdy dodawany jest nowy uczeń (przy użyciu metody usługi save()), własna tablica uczniów usługi będzie aktualizowana, a każdy inny obiekt udostępniający tę tablicę również zostanie automatycznie zaktualizowany.

Bazując na podejściu opisanym powyżej, Twój kod mógłby wyglądać następująco:

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.
        });
      };
    }
]);

Jedną rzeczą, na którą należy zachować ostrożność podczas korzystania z tego podejścia, jest to, aby nigdy nie ponownie przypisywać tablicy usługi, ponieważ wtedy wszelkie inne komponenty (np. Zakresy) będą nadal odwoływać się do oryginalnej tablicy, a aplikacja ulegnie awarii.
Np., Aby wyczyścić tablicę w StudentService:

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

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

Zobacz także to krótkie demo .


MAŁA AKTUALIZACJA:

Kilka słów, aby uniknąć nieporozumień, które mogą powstać podczas mówienia o korzystaniu z usługi, ale nie podczas tworzenia jej za pomocą service()funkcji.

Cytując dokumenty na$provide :

Kątowym usługa jest pojedyncza obiekt stworzony przez fabrykę usług . Te fabryki usług to funkcje, które z kolei są tworzone przez usługodawcę . Do usługodawcy są funkcje konstruktora. Po utworzeniu muszą zawierać właściwość nazwaną $get, która przechowuje funkcję fabryki usług .
[...]
... $provideusługa posiada dodatkowe pomocnicze metody rejestracji usług bez określania dostawcy:

  • provider (provider) - rejestruje usługodawcę w $ injectorze
  • stała (obj) - rejestruje wartość / obiekt, do którego mają dostęp dostawcy i usługi.
  • wartość (obj) - rejestruje wartość / obiekt, do którego dostęp mają tylko usługi, a nie dostawcy.
  • factory (fn) - rejestruje funkcję fabryki usług, fn, która zostanie opakowana w obiekt dostawcy usług, którego właściwość $ get będzie zawierać daną funkcję fabryki.
  • service (class) - rejestruje funkcję konstruktora, klasę, która zostanie opakowana w obiekt dostawcy usług, której właściwość $ get utworzy instancję nowego obiektu przy użyciu podanej funkcji konstruktora.

Zasadniczo mówi się, że każda usługa Angular jest rejestrowana przy użyciu $provide.provider(), ale istnieją metody „skrótów” dla prostszych usług (z których dwie to service()i factory()).
To wszystko „sprowadza się” do usługi, więc nie ma większego znaczenia, której metody użyjesz (o ile wymagania dotyczące Twojej usługi mogą być objęte tą metodą).

BTW, providervs servicevs factoryto jedna z najbardziej zagmatwanych koncepcji dla nowicjuszy Angulara, ale na szczęście istnieje wiele zasobów (tutaj na SO), które ułatwiają życie. (Po prostu poszukaj.)

(Mam nadzieję, że to wyjaśnia sprawę - daj mi znać, jeśli nie.)

gkalpak
źródło
1
Jedno pytanie. Mówisz service, ale Twój przykład kodu korzysta z fabryki. Dopiero zaczynam rozumieć różnicę między fabrykami, usługami i dostawcami, po prostu chcę mieć pewność, że wybranie fabryki jest najlepszą opcją, ponieważ korzystałem z usługi. Wiele się nauczyłem z twojego przykładu. Dzięki za skrzypce i BARDZO jasne wyjaśnienie.
chris Frisina
3
@chrisFrisina: Zaktualizowałem odpowiedź, dodając małe wyjaśnienie. Zasadniczo nie ma większego znaczenia, jeśli używasz servicelub factory- skończysz z usługą Angular . Po prostu upewnij się, że rozumiesz, jak każdy z nich działa i czy odpowiada Twoim potrzebom.
gkalpak
Niezły post! To bardzo mi pomaga !
Oni1
Dzięki stary! tutaj jest fajny artykuł o podobnej sprawie stsc3000.github.io/blog/2013/10/26/…
Terafor
@ExpertSystem Czy $scope.studentsbędzie puste, jeśli wywołanie AJAX nie zostanie zakończone? A może $scope.studentsbędzie częściowo zapełniony, jeśli ten blok kodu jest w toku? students.push(student);
Yc Zhang
18

Zamiast próbować modyfikować $scopew usłudze, możesz zaimplementować $watchw kontrolerze, aby obserwować właściwość w usłudze pod kątem zmian, a następnie zaktualizować właściwość w $scope. Oto przykład, który możesz wypróbować w kontrolerze:

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

Należy zwrócić uwagę, że w ramach Twojej usługi, aby studentsnieruchomość była widoczna, musi znajdować się w obiekcie usługi lub w thispodobny sposób:

this.students = $http.get(path).then(function (resp) {
  return resp.data;
});
Keith Morris
źródło
12

Cóż (długi) ... jeśli nalegasz na $scopedostęp do usługi, możesz:

Utwórz usługę pobierającą / ustawiającą

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

Wstrzyknij go i zapisz w nim zakres kontrolera

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

Teraz uzyskaj zakres w innej usłudze

ngapp.factory('getRoute', ['Scopes', '$http', function(Scopes, $http){
  // there you are
  var $scope = Scopes.get('myCtrl');
}]);
Jonatas Walker
źródło
Jak niszczone są lunety?
JK.
9

Usługi są pojedynczymi usługami i nie jest logiczne, aby zakres był wstrzykiwany do usługi (w rzeczywistości nie można wstrzyknąć zakresu do usługi). Możesz przekazać zakres jako parametr, ale jest to również zły wybór projektowy, ponieważ zakres byłby edytowany w wielu miejscach, co utrudniłoby debugowanie. Kod do obsługi zmiennych zakresu powinien trafiać do kontrolera, a wywołania usługi - do usługi.

Ermin Dedovic
źródło
Rozumiem, co mówisz. Jednak w moim przypadku mam wiele kontrolerów i chciałbym skonfigurować ich lunety z bardzo podobnym zestawem zegarków $. Jak / gdzie byś to zrobił? Obecnie rzeczywiście przekazuję zakres jako parametr do usługi, która ustawia $ zegary.
moritz
@moritz może zaimplementować dodatkową dyrektywę (taką, która ma zakres: false, więc używa zakresu zdefiniowanego przez inne dyrektywy) i która tworzy powiązania zegarków, a także cokolwiek innego, czego potrzebujesz. W ten sposób możesz użyć tej innej dyrektywy w dowolnym miejscu, w którym potrzebujesz zdefiniować takie zegarki. Ponieważ przekazanie lunety do serwisu jest rzeczywiście dość okropne :) (uwierz mi, byłem tam, zrobiłem to,
uderzyłem
@TIMINeutron, który brzmi o wiele lepiej niż omijanie lunety, spróbuję tego następnym razem, gdy pojawi się scenariusz! Dzięki!
moritz
Pewnie. Wciąż się uczę, a ten konkretny problem jest tym, z którym ostatnio poradziłem sobie w ten szczególny sposób i zadziałał dla mnie jak urok.
tfrascaroli
3

Możesz sprawić, że usługa będzie całkowicie nieświadoma zakresu, ale w kontrolerze zezwalaj na asynchroniczne aktualizowanie zakresu.

Problem polega na tym, że nie jesteś świadomy tego, że wywołania http są wykonywane asynchronicznie, co oznacza, że ​​nie otrzymujesz wartości od razu, tak jak możesz. Na przykład,

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

Istnieje prosty sposób obejścia tego problemu i polega na dostarczeniu funkcji zwrotnej.

.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
    })
  }
}]);

Formularz:

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

To usunęło część logiki biznesowej dla zwięzłości i tak naprawdę nie testowałem kodu, ale coś takiego zadziała. Główną koncepcją jest przekazanie wywołania zwrotnego ze sterownika do usługi, która zostanie wywołana później w przyszłości. Jeśli znasz NodeJS, to jest ta sama koncepcja.

2upmedia
źródło
Takie podejście nie jest zalecane. Zobacz Dlaczego wywołania zwrotne z .thenmetod Promise są anty-wzorcem .
georgeawg
0

Wpadłem w tę samą sytuację. Skończyło się na tym. Więc tutaj nie wstrzykuję obiektu scope do fabryki, ale ustawiam $ scope w samym kontrolerze używając koncepcji obietnicy zwracanej przez usługę $ 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);    
}());
VivekDev
źródło