Injizieren von $ scope in eine Winkeldienstfunktion ()

107

Ich habe einen Service:

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

Aber wenn ich anrufe save(), habe ich keinen Zugang zu $scopeund bekomme ReferenceError: $scope is not defined. Der logische Schritt (für mich) besteht also darin, save () mit dem zu versehen $scope, und daher muss ich es auch bereitstellen / injizieren service. Also, wenn ich das so mache:

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

Ich erhalte folgende Fehlermeldung:

Fehler: [$ injor: unpr] Unbekannter Anbieter: $ scopeProvider <- $ scope <- StudentService

Der Link im Fehler (wow, das ist ordentlich!) Lässt mich wissen, dass er mit dem Injektor zusammenhängt und möglicherweise mit der Reihenfolge der Deklaration der js-Dateien zu tun hat. Ich habe versucht, sie in der neu zu ordnen index.html, aber ich denke, es ist etwas einfacher, wie die Art, wie ich sie injiziere.

Verwenden von Angular-UI und Angular-UI-Router

Chris Frisina
quelle

Antworten:

183

Das $scope, was Sie sehen, wie es in Controller injiziert wird, ist kein Dienst (wie der Rest des injizierbaren Materials), sondern ein Scope-Objekt. Es können viele Bereichsobjekte erstellt werden (normalerweise prototypisch von einem übergeordneten Bereich erben). Die Wurzel aller Bereiche ist der, $rootScopeund Sie können einen neuen untergeordneten Bereich mit der $new()Methode eines beliebigen Bereichs (einschließlich des $rootScope) erstellen .

Der Zweck eines Bereichs besteht darin, die Präsentation und die Geschäftslogik Ihrer App "zusammenzukleben". Es macht nicht viel Sinn, a zu bestehen$scope an einen Dienst .

Services sind Singleton-Objekte, die (unter anderem) zum Teilen von Daten (z. B. zwischen mehreren Controllern) verwendet werden und im Allgemeinen wiederverwendbare Codeteile kapseln (da sie injiziert werden können und ihre "Services" in jedem Teil Ihrer App anbieten, der sie benötigt: Controller, Richtlinien, Filter, andere Dienste usw.).

Ich bin sicher, dass verschiedene Ansätze für Sie funktionieren würden. Eines ist das Folgende:
Da das StudentServicefür den Umgang mit Studentendaten zuständig ist, können Sie StudentServiceeine Reihe von Studenten behalten und es mit jedem "teilen" lassen, der interessiert sein könnte (z $scope. B. Ihrem ). Dies ist umso sinnvoller, wenn andere Ansichten / Controller / Filter / Dienste Zugriff auf diese Informationen benötigen (wenn derzeit keine verfügbar sind, wundern Sie sich nicht, wenn sie bald auftauchen).
Jedes Mal, wenn ein neuer Schüler hinzugefügt wird (unter Verwendung der save()Dienstmethode), wird das eigene Schülerarray des Dienstes aktualisiert, und jedes andere Objekt, das dieses Array gemeinsam nutzt , wird ebenfalls automatisch aktualisiert.

Basierend auf dem oben beschriebenen Ansatz könnte Ihr Code folgendermaßen aussehen:

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

Eine Sache, bei der Sie bei diesem Ansatz vorsichtig sein sollten, ist, das Array des Dienstes niemals neu zuzuweisen, da dann alle anderen Komponenten (z. B. Bereiche) weiterhin auf das ursprüngliche Array verweisen und Ihre App beschädigt wird.
ZB um das Array zu löschen in StudentService:

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

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

Siehe auch diese kurze Demo .


KLEINES UPDATE:

Ein paar Worte, um die Verwirrung zu vermeiden, die auftreten kann, wenn über die Verwendung eines Dienstes gesprochen wird, dieser jedoch nicht mit der service()Funktion erstellt wird.

Zitieren der Dokumente auf$provide :

Ein Angular- Service ist ein Singleton-Objekt, das von einer Service-Factory erstellt wurde . Diese Servicefabriken sind Funktionen, die wiederum von einem Service Provider erstellt werden . Die Dienstleister sind Konstruktorfunktionen. Wenn sie instanziiert werden, müssen sie eine aufgerufene Eigenschaft enthalten $get, die die Service Factory- Funktion enthält.
[...]
... der $provideDienst verfügt über zusätzliche Hilfsmethoden zum Registrieren von Diensten ohne Angabe eines Anbieters:

  • Provider (Provider) - registriert einen Service Provider beim $ Injector
  • Konstante (obj) - registriert einen Wert / ein Objekt, auf das bzw. das Anbieter und Dienste zugreifen können.
  • Wert (obj) - registriert einen Wert / ein Objekt, auf das nur von Diensten zugegriffen werden kann, nicht von Anbietern.
  • Fabrik (fn) - registriert eine Service-Factory-Funktion, fn, die in ein Service-Provider-Objekt eingeschlossen wird, dessen $ get-Eigenschaft die angegebene Factory-Funktion enthält.
  • service (class) - registriert eine Konstruktorfunktion, eine Klasse, die in ein Service Provider-Objekt eingeschlossen wird, dessen $ get-Eigenschaft ein neues Objekt mithilfe der angegebenen Konstruktorfunktion instanziiert.

Grundsätzlich heißt es, dass jeder Angular-Dienst mit registriert wird $provide.provider(), es gibt jedoch Verknüpfungsmethoden für einfachere Dienste (zwei davon sind service()und factory()).
Alles läuft auf einen Service hinaus, sodass es keinen großen Unterschied macht, welche Methode Sie verwenden (solange die Anforderungen für Ihren Service durch diese Methode abgedeckt werden können).

Übrigens providervs servicevs.factory ist eines der verwirrendsten Konzepte für Angular-Neulinge, aber zum Glück gibt es viele Ressourcen (hier auf SO), um die Dinge einfacher zu machen. (Suchen Sie einfach herum.)

(Ich hoffe, das klärt es auf - lass es mich wissen, wenn es nicht so ist.)

gkalpak
quelle
1
Eine Frage. Sie sagen Service, aber Ihr Codebeispiel verwendet die Factory. Ich fange gerade erst an, den Unterschied zwischen Fabriken, Dienstleistungen und Anbietern zu verstehen. Ich möchte nur sichergehen, dass die Entscheidung für eine Fabrik die beste Option ist, da ich einen Dienst in Anspruch genommen habe. Ich habe viel aus Ihrem Beispiel gelernt. Vielen Dank für die Geige und sehr klare Erklärung.
Chris Frisina
3
@chrisFrisina: Die Antwort wurde mit einer kleinen Erklärung aktualisiert. Grundsätzlich macht es keinen großen Unterschied, ob Sie serviceoder factory- Sie werden mit und Angular Service enden . Stellen Sie einfach sicher, dass Sie verstehen, wie jeder funktioniert und ob er Ihren Anforderungen entspricht.
Gkalpak
Netter Post! Es hilft mir sehr!
Oni1
Dank bro! Hier ist ein schöner Artikel über ähnliche Themen stsc3000.github.io/blog/2013/10/26/…
Terafor
@ExpertSystem Wird $scope.studentsleer sein, wenn der Ajax-Aufruf nicht beendet ist? Oder wird $scope.studentses teilweise gefüllt, wenn dieser Codeblock in Bearbeitung ist? students.push(student);
Yc Zhang
18

Anstatt zu versuchen, das $scopeinnerhalb des Dienstes zu ändern , können Sie ein $watchinnerhalb Ihres Controllers implementieren , um eine Eigenschaft in Ihrem Dienst auf Änderungen zu überwachen und dann eine Eigenschaft auf dem zu aktualisieren $scope. Hier ist ein Beispiel, das Sie in einem Controller versuchen könnten:

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

Beachten Sie, dass sich die studentsEigenschaft innerhalb Ihres Dienstes auf dem Dienstobjekt befinden muss, damit sie sichtbar ist, oder thisso:

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

Nun (eine lange) ... wenn Sie darauf bestehen , $scopeZugang zu einem Dienst zu haben, können Sie:

Erstellen Sie einen Getter / Setter-Service

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

Injizieren Sie es und speichern Sie den Controller-Bereich darin

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

Holen Sie sich jetzt den Bereich in einen anderen Dienst

ngapp.factory('getRoute', ['Scopes', '$http', function(Scopes, $http){
  // there you are
  var $scope = Scopes.get('myCtrl');
}]);
Jonatas Walker
quelle
Wie werden die Zielfernrohre zerstört?
JK.
9

Dienste sind Singletons, und es ist nicht logisch, dass ein Bereich in den Dienst eingefügt wird (was in der Tat der Fall ist, dass Sie den Bereich nicht in den Dienst einfügen können). Sie können den Bereich als Parameter übergeben, dies ist jedoch auch eine schlechte Wahl für das Design, da der Bereich an mehreren Stellen bearbeitet werden muss, was das Debuggen erschwert. Der Code für den Umgang mit Bereichsvariablen sollte im Controller gespeichert werden, und Dienstaufrufe werden an den Dienst gesendet.

Ermin Dedovic
quelle
Ich verstehe was du sagst. In meinem Fall habe ich jedoch viele Controller und möchte deren Bereiche mit einem sehr ähnlichen Satz von $ watchs konfigurieren. Wie / wo würdest du das machen? Derzeit übergebe ich den Bereich tatsächlich als Parameter an einen Dienst, der die $ watchs festlegt.
Moritz
@moritz implementiert möglicherweise eine sekundäre Direktive (eine mit dem Gültigkeitsbereich: false, verwendet also den durch andere Direktiven definierten Gültigkeitsbereich) und erstellt die Bindungen der Watchess sowie alles andere, was Sie benötigen. Auf diese Weise können Sie diese andere Richtlinie an jedem Ort verwenden, an dem Sie solche Uhren definieren müssen. Weil es in der Tat ziemlich schrecklich ist, das Zielfernrohr an einen Dienst weiterzugeben :) (glauben Sie mir, ich war dort, habe das getan und am Ende meinen Kopf gegen die Wand geschlagen)
tfrascaroli
@TIMINeutron, das viel besser klingt, als das Zielfernrohr herumzugeben. Ich werde es versuchen, wenn das nächste Mal das Szenario auftaucht! Vielen Dank!
Moritz
Sicher. Ich lerne immer noch selbst und dieses spezielle Problem habe ich kürzlich auf diese spezielle Weise angegangen, und es hat für mich wie ein Zauber gewirkt.
Tfrascaroli
3

Sie können dafür sorgen, dass Ihr Dienst den Bereich überhaupt nicht kennt, aber in Ihrem Controller kann der Bereich asynchron aktualisiert werden.

Das Problem besteht darin, dass Sie nicht wissen, dass http-Aufrufe asynchron ausgeführt werden, was bedeutet, dass Sie nicht sofort einen Wert erhalten, wie Sie es könnten. Zum Beispiel,

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

Es gibt eine einfache Möglichkeit, dies zu umgehen und eine Rückruffunktion bereitzustellen.

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

Die Form:

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

Dies hat der Kürze halber einige Ihrer Geschäftslogiken entfernt, und ich habe den Code noch nicht getestet, aber so etwas würde funktionieren. Das Hauptkonzept besteht darin, einen Rückruf von der Steuerung an den Dienst weiterzuleiten, der später in der Zukunft aufgerufen wird. Wenn Sie mit NodeJS vertraut sind, ist dies das gleiche Konzept.

2upmedia
quelle
Dieser Ansatz wird nicht empfohlen. Siehe Warum Rückrufe von Versprechensmethoden .thenein Anti-Muster sind .
Georgiaeawg
0

Bin in die gleiche Situation geraten. Am Ende hatte ich Folgendes. Hier injiziere ich das Scope-Objekt also nicht in die Factory, sondern setze den $ scope im Controller selbst unter Verwendung des vom $ http- Dienst zurückgegebenen Versprechenskonzepts .

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