Iniettare $ scope in una funzione di servizio angolare ()


108

Ho un servizio:

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

Ma quando chiamo save(), non ho accesso a $scope, e get ReferenceError: $scope is not defined. Quindi il passaggio logico (per me) è fornire save () con il $scope, e quindi devo anche fornirlo / iniettarlo al service. Quindi, se lo faccio in questo modo:

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

Ottengo il seguente errore:

Errore: [$ injector: unpr] Provider sconosciuto: $ scopeProvider <- $ scope <- StudentService

Il collegamento nell'errore (wow che è pulito!) Mi fa sapere che è correlato all'iniettore e potrebbe avere a che fare con l'ordine di dichiarazione dei file js. Ho provato a riordinarli in index.html, ma penso che sia qualcosa di più semplice, come il modo in cui li sto iniettando.

Utilizzo di Angular-UI e Angular-UI-Router

Risposte:


183

Quello $scopeche vedi essere iniettato nei controller non è un servizio (come il resto delle cose iniettabili), ma è un oggetto Scope. È possibile creare molti oggetti ambito (di solito ereditando in modo prototipico da uno scope padre). La radice di tutti gli ambiti è $rootScopeed è possibile creare un nuovo ambito figlio utilizzando il $new()metodo di qualsiasi ambito (incluso il $rootScope).

Lo scopo di uno Scope è "incollare" la presentazione e la logica di business della tua app. Non ha molto senso passare a $scopein un servizio.

I servizi sono oggetti singoli utilizzati (tra le altre cose) per condividere dati (ad esempio tra diversi controller) e generalmente incapsulano pezzi di codice riutilizzabili (poiché possono essere iniettati e offrono i loro "servizi" in qualsiasi parte della tua app che ne ha bisogno: controller, direttive, filtri, altri servizi ecc.).

Sono sicuro che vari approcci funzionerebbero per te. Uno è questo:
dal momento che StudentServiceè responsabile della gestione dei dati degli studenti, puoi avere la possibilità di StudentServicetenere un array di studenti e lasciarlo "condividere" con chiunque possa essere interessato (ad esempio il tuo $scope). Questo ha ancora più senso, se ci sono altre visualizzazioni / controller / filtri / servizi che devono avere accesso a quelle informazioni (se non ce ne sono in questo momento, non sorprenderti se iniziano a comparire presto).
Ogni volta che viene aggiunto un nuovo studente (utilizzando il save()metodo del servizio ), l'array di studenti del servizio verrà aggiornato e anche ogni altro oggetto che condivide quell'array verrà aggiornato automaticamente.

In base all'approccio descritto sopra, il tuo codice potrebbe essere simile a questo:

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

Una cosa a cui dovresti prestare attenzione quando utilizzi questo approccio è di non riassegnare mai l'array del servizio, perché in tal caso qualsiasi altro componente (ad esempio gli ambiti) farà ancora riferimento all'array originale e l'app si interromperà.
Ad esempio, per cancellare l'array in StudentService:

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

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

Vedi anche questa breve demo .


PICCOLO AGGIORNAMENTO:

Poche parole per evitare la confusione che può sorgere parlando di utilizzare un servizio, ma non crearlo con la service()funzione.

Citando i documenti su$provide :

Un servizio angolare è un oggetto singleton creato da una fabbrica di servizi . Queste fabbriche di servizi sono funzioni che, a loro volta, vengono create da un fornitore di servizi . I fornitori di servizi sono funzioni di costruzione. Quando vengono istanziati, devono contenere una proprietà chiamata $get, che contiene la funzione di fabbrica di servizi .
[...]
... il $provideservizio ha metodi di supporto aggiuntivi per registrare i servizi senza specificare un provider:

  • provider (provider) - registra un provider di servizi con $ injector
  • costante (obj) - registra un valore / oggetto a cui possono accedere fornitori e servizi.
  • value (obj) - registra un valore / oggetto a cui possono accedere solo i servizi, non i provider.
  • factory (fn) - registra una funzione factory di servizi, fn, che verrà racchiusa in un oggetto provider di servizi, la cui proprietà $ get conterrà la funzione factory data.
  • service (class) - registra una funzione di costruzione, classe che sarà racchiusa in un oggetto del fornitore di servizi, la cui proprietà $ get istanzerà un nuovo oggetto utilizzando la funzione di costruzione data.

Fondamentalmente, quello che dice è che ogni servizio Angular è registrato usando $provide.provider(), ma ci sono metodi "scorciatoia" per servizi più semplici (due dei quali sono service()e factory()).
Tutto "si riduce" a un servizio, quindi non fa molta differenza quale metodo usi (a patto che i requisiti per il tuo servizio possano essere coperti da quel metodo).

A proposito, providervs servicevs factoryè uno dei concetti più confusi per i nuovi arrivati ​​di Angular, ma fortunatamente ci sono molte risorse (qui su SO) per rendere le cose più facili. (Basta cercare in giro.)

(Spero che questo chiarisca tutto, fammi sapere se non lo fa.)


1
Una domanda. Dici servizio, ma il tuo esempio di codice utilizza la fabbrica. Sto appena iniziando a capire la differenza tra fabbriche, servizi e fornitori, voglio solo essere sicuro che andare con una fabbrica sia l'opzione migliore, dato che stavo usando un servizio. Ho imparato molto dal tuo esempio. Grazie per il violino e per la spiegazione MOLTO chiara.
chris Frisina

3
@chrisFrisina: aggiornata la risposta con una piccola spiegazione. Fondamentalmente, non fa molta differenza se usi serviceo factory- ti finirai con un servizio Angular . Assicurati solo di capire come funzionano e se si adattano alle tue esigenze.
gkalpak

Bel post! Mi aiuta molto !
Oni1

Grazie fratello! ecco un bell'articolo su argomenti simili stsc3000.github.io/blog/2013/10/26/…
Terafor

@ExpertSystem $scope.studentsSarà vuoto, se la chiamata ajax non è finita? O $scope.studentsverrà riempito parzialmente, se questo blocco di codice è in corso? students.push(student);
Yc Zhang

18

Invece di provare a modificare il $scopeall'interno del servizio, puoi implementare un $watchall'interno del tuo controller per guardare una proprietà sul tuo servizio per le modifiche e quindi aggiornare una proprietà sul $scope. Ecco un esempio che potresti provare in un controller:

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

Una cosa da notare è che all'interno del tuo servizio, affinché la studentsproprietà sia visibile, deve essere sull'oggetto Servizio o in questo thismodo:

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

12

Beh (lungo) ... se insisti per avere $scopeaccesso all'interno di un servizio puoi:

Crea un servizio getter / setter

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

Iniettalo e memorizza l'ambito del controller in esso

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

Ora, ottieni l'ambito all'interno di un altro servizio

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

Come vengono distrutti gli ambiti?
JK.

9

I servizi sono singleton e non è logico che un ambito venga inserito nel servizio (il che è il caso in cui non è possibile iniettare l'ambito nel servizio). È possibile passare l'ambito come parametro, ma questa è anche una cattiva scelta di progettazione, perché si dovrebbe modificare l'ambito in più posizioni, rendendo difficile il debug. Il codice per la gestione delle variabili di ambito deve essere inserito nel controller e le chiamate di servizio vanno al servizio.


Capisco quello che stai dicendo. Tuttavia, nel mio caso, ho molti controller e vorrei configurare i loro ambiti con un set di $ watch molto simile. Come / dove lo faresti? Attualmente, in effetti passo l'ambito come parametro a un servizio che imposta $ orologi.
moritz

@moritz potrebbe implementare una direttiva secondaria (una che ha scope: false, quindi usa l'ambito definito da altre direttive) e quella fa i binding di watchess, così come qualsiasi altra cosa di cui hai bisogno. In questo modo potresti usare quell'altra direttiva in qualsiasi posto tu abbia bisogno di definire tali orologi. Perché passare l'ambito a un servizio è davvero orribile :) (credimi, ci sono stato, l'ho fatto, alla fine ho sbattuto la testa contro il muro)
tfrascaroli

@TIMINeutron che suona molto meglio del passaggio intorno all'ambito, lo proverò la prossima volta che verrà fuori lo scenario! Grazie!
moritz

Sicuro. Sto ancora imparando da solo, e questo particolare problema è uno che ho affrontato di recente in questo modo particolare, e ha funzionato come un fascino per me.
tfrascaroli

3

È possibile rendere il servizio completamente inconsapevole dell'ambito, ma nel controller consentire l'aggiornamento dell'ambito in modo asincrono.

Il problema che stai riscontrando è perché non sei consapevole che le chiamate http vengono effettuate in modo asincrono, il che significa che non ottieni un valore immediatamente come potresti. Per esempio,

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

C'è un modo semplice per aggirare questo problema ed è fornire una funzione di callback.

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

Il modulo:

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

Ciò ha rimosso parte della logica aziendale per brevità e in realtà non ho testato il codice, ma qualcosa del genere avrebbe funzionato. Il concetto principale è passare una richiamata dal controller al servizio che viene richiamato in seguito in futuro. Se hai familiarità con NodeJS, questo è lo stesso concetto.


Questo approccio non è consigliato. Vedere Perché i callback dai .thenmetodi Promise sono un anti-pattern .
georgeawg

0

Sono entrato nella stessa situazione. Ho finito con quanto segue. Quindi qui non sto iniettando l'oggetto scope nella fabbrica, ma impostando $ scope nel controller stesso usando il concetto di promessa restituito dal servizio $ 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);    
}());

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.