30

ho letto queste due grandi articoli:AngularJS: In che modo i controller e le fabbriche/servizi devono essere strutturati con un modello di oggetto ricco e gerarchico?

The state of angularjs controllers di Jonathan Creamer

e

Rethinking AngularJS Controllers da Todd Motto

In questi articoli, gli autori parlano il modo giusto di usare i controller (fare loro ponti anemici tra la vista e il modello) e fabbriche/servizi (dove la logica di business dovrebbe davvero vivere).

Questa è una grande informazione, ed ero davvero entusiasta di iniziare a refactoring i controller su uno dei miei progetti, ma ho subito scoperto che la struttura mostrata negli articoli si rompe se si dispone di un modello di oggetto ricco.

Ecco un riassunto del setup da "Ripensare Controllers Angularjs":

Ecco il controllore:

app.controller('InboxCtrl', function InboxCtrl (InboxFactory) { 

    var vm = this; 

    vm.messages = InboxFactory.messages; 

    vm.openMessage = function (message) { 
     InboxFactory.openMessage(message); 
    }; 

    vm.deleteMessage = function (message) { 
     InboxFactory.deleteMessage(message); 
    }; 

    InboxFactory 
     .getMessages() 
     .then(function() { 
     vm.messages = InboxFactory.messages; 
    }); 

}); 

ed ecco la fabbrica:

app.factory('InboxFactory', function InboxFactory ($location, NotificationFactory) { 

    factory.messages = []; 

    factory.openMessage = function (message) { 
    $location.search('id', message.id).path('/message'); 
    }; 

    factory.deleteMessage = function (message) { 
    $http.post('/message/delete', message) 
    .success(function (data) { 
     factory.messages.splice(index, 1); 
     NotificationFactory.showSuccess(); 
    }) 
    .error(function() { 
     NotificationFactory.showError(); 
    }); 
    }; 

    factory.getMessages = function() { 
    return $http.get('/messages') 
    .success(function (data) { 
     factory.messages = data; 
    }) 
    .error(function() { 
     NotificationFactory.showError(); 
    }); 
    }; 

    return factory; 

}); 

Questo è grande e perché providers (la fabbrica) sono singleton, i dati vengono mantenuti attraverso le visualizzazioni e sono accessibili senza dover ricaricarli dall'API.

Questo funziona perfettamente se messages è un oggetto di livello superiore. Ma cosa succede se non lo sono? Che cosa succede se questa è un'app per sfogliare le caselle di posta di altri utenti? Forse sei un amministratore e vuoi essere in grado di gestire e sfogliare le caselle di posta di qualsiasi utente. Forse hai bisogno di più caselle di posta degli utenti caricate contemporaneamente. Come funziona? Il problema è che i messaggi di posta in arrivo sono memorizzati nel servizio, ad esempio InboxFactory.messages.

Che cosa succede se la gerarchia è come questo:

      Organization 
           | 
       __________________|____________________ 
      |     |     | 
     Accounting  Human Resources   IT 
      |     |     | 
    ________|_______  _____|______  ______|________ 
    |  |  | |  |  |  |  |  | 
    John  Mike Sue Tom Joe Brad May Judy  Jill 
    |  |  | |  |  |  |  |  | 
    Inbox Inbox Inbox Inbox Inbox Inbox Inbox Inbox Inbox 

Ora messages sono diversi livelli di profondità nella gerarchia, e non hanno alcun significato per conto proprio. Non è possibile memorizzare i messaggi in fabbrica, InboxFactory.messages perché è necessario recuperare i messaggi per più utenti alla volta.

Ora si avrà OrganizationFactory, DepartmentFactory, UserFactory e InboxFactory. Il recupero dei "messaggi" deve essere nel contesto di un user, che si trova nel contesto di un department, che si trova nel contesto di un organization. Come e dove devono essere memorizzati i dati? Come dovrebbe essere retreived?

Quindi come dovrebbe essere risolto? Come dovrebbero essere strutturati i controller, le fabbriche/i servizi e i modelli di oggetti ricchi?

A questo punto del mio pensiero, mi sto appoggiando solo a mantenerlo snello e senza avere un modello di oggetto ricco. È sufficiente archiviare gli oggetti sull'oggetto $ iniettato nel controller e, se si passa a una nuova vista, ricaricare dall'API. Se hai bisogno di dati alcuni persistono attraverso le viste, è possibile creare quel ponte con un servizio o fabbrica, ma non dovrebbe essere il modo di fare cose più.

Come hanno risolto gli altri questo? Ci sono dei modelli là fuori per questo?

+0

Se questo non dovesse essere un compito per il backend endpoint? Quindi se hai effettuato l'accesso come Jonh 'GET/messages' restituisce i messaggi solo per John e l'interfaccia è diversa da quella che il capo dell'organizzazione vede quando ha effettuato l'accesso. – Terafor

risposta

0

Dopo MOLTO armeggiare e provare approcci diversi, la mia decisione finale è che non si debba mantenere il modello di oggetto avanzato attraverso le viste.

Mantengo il modello di oggetto super snello e carico solo ciò che mi serve per ogni vista. Vi sono dati di alto livello che mantengo in giro (informazioni utente come nome, id, email, ecc. E dati di organizzazione come l'organizzazione con cui sono loggati), ma tutto il resto viene caricato per la vista corrente.

Con questo approccio lean, ecco cosa mia fabbrica sarà simile:

app.factory('InboxFactory', function InboxFactory ($location, NotificationFactory) { 

factory.messages = []; 

factory.openMessage = function (message) { 
    $location.search('id', message.id).path('/message'); 
}; 

factory.deleteMessage = function (message) { 
    $http.post('/message/delete', message) 
    .success(function (data) { 
    NotificationFactory.showSuccess(); 
    return data; 
    }) 
    .error(function() { 
    NotificationFactory.showError(); 
    return null; 
    }); 
}; 

factory.getMessages = function (userId) { 
    return $http.get('/messages/user/id/'+userId) 
    .success(function (data) { 
    return data; 
    }) 
    .error(function() { 
    NotificationFactory.showError(); 
    return null; 
    }); 
}; 

return factory; 

}); 

E il controllore:

app.controller('InboxCtrl', function InboxCtrl (InboxFactory) { 

    var vm = this; 

    vm.messages = {}; 

    vm.openMessage = function (message) { 
    InboxFactory.openMessage(message); 
    }; 

    vm.deleteMessage = function (message) { 
    InboxFactory.deleteMessage(message); 
    }; 

    InboxFactory 
    .getMessages(userId) //you can get the userId from anywhere you want. 
    .then(function (data) { 
     vm.messages = data; 
    }); 

}); 

I benefici finora sono:

  • logica applicativa semplificata
  • magro e medio, leggero (carico solo proprio quello che mi serve per lo stato attuale)
  • meno l'utilizzo della memoria, che si traduce in prestazioni complessive migliori, in particolare sul mobile
3

È possibile utilizzare un modello di oggetto ricco, ma per gli oggetti che non sono di primo livello, le loro fabbriche dovrebbero esporre un'API per la creazione di nuove istanze anziché essere utilizzate come singleton. Questo è in qualche modo contrario al design di molte app che vedete in questi giorni, che sono più funzionali di quelle orientate agli oggetti - Non sto commentando i pro e i contro di entrambi gli approcci, e non penso che Angular ti costringa ad adottare uno o l'altro.

Il vostro esempio, ridisegnato, in pseudocodice:

app.controller('InboxCtrl', function InboxCtrl (InboxFactory) { 

    var inbox = InboxFactory.createInbox(); 

    $scope.getMessages = function(){ 
     inbox.getMessages() 
      .then(...) 

    $scope.deleteMessages = function(){ 
     inbox.deleteMessages() 
      .then(...) 

}); 
+0

Grazie AlexMa. Questo è quello a cui mi sto appoggiando. Facendolo in questo modo, vorresti che il grafico dell'oggetto fosse memorizzato da qualche parte in modo persistente tra le viste? O preferisci difendere il grafico ricreato (se necessario) per ogni vista applicabile? Mi sto orientando verso il futuro per mantenere l'app snella e leggera. – richard

+1

Sarei d'accordo; Cerco di evitare di rendere l'app più complessa di quanto dovrebbe essere, poiché crea effetti collaterali ed è più difficile da testare e modificare in seguito. Il router ui – AlexMA

2

La vostra situazione diventa molto più semplice se si adotta un approccio basato percorso (alla ngRoute o qualcosa di simile). Prendi in considerazione questa alternativa: codice di verifica non verificato:

app.config(function($routeProvider) { 
    $routeProvider 
    .when('/inbox/:inboxId', 
     templateUrl: 'views/inbox.html', 
     controller: 'InboxCtrl', 
     controllerAs: 'inbox', 
     resolve: { 
     inboxMessages: function(InboxFactory) { 
      // Use use :inboxId route param if you need to work with multiple 
      // inboxes. Taking some libery here, we'll assuming 
      // `InboxFactory.getMessages()` returns a promise that will resolve to 
      // an array of messages; 
      return InboxFactory.getMessages(); 
     } 
     } 
    // ... other routes 
    .otherwise: { 
     // ... 
    }; 
}); 

app.controller('InboxCtrl', function InboxCtrl (InboxFactory, inboxMessages) { 
    var vm = this; 
    vm.messages = inboxMessages; 
    vm.openMessage = InboxFactory.openMessage; 
    vm.deleteMessage = InboxFactory.deleteMessage; 
}); 

Guarda quanto è sottile il controller ora! Certo, ho fatto uso di una sintassi più compatta in un paio di punti, ma questo mette in evidenza come il nostro controller stia semplicemente incollando le cose insieme.

Possiamo ulteriormente ottimizzare le cose eliminando InboxFactory.messages, quando lo utilizzeremmo effettivamente? Ci si garantisce che sia necessario che venga popolato dopo le risoluzioni InboxFactory.getMessages, quindi facciamo in modo che questa promessa risolva i messaggi stessi.

Memorizzare i dati in singlet in questo modo può essere la soluzione più semplice in alcuni casi, ma rende la vita difficile quando i dati devono essere recuperati al volo. Avrai la meglio sulle API e sulle fabbriche (come suggerisce AlexMA), tirando giù i dati necessari ogni volta che un percorso cambia (ad es. L'utente vuole guardare una casella di posta diversa) e iniettare quei dati direttamente nel controller appropriato.

Un altro vantaggio di questo modulo è che possiamo avere i nostri dati in mano nel momento in cui il controllore viene istanziato. Non dobbiamo destreggiarsi tra stati asincroni o preoccuparci di inserire un sacco di codice nei callback. Come corollario, arriviamo a rilevare gli errori di caricamento dei dati prima di visualizzare una nuova vista della casella di posta in arrivo e l'utente non si blocca in uno stato semi-cotto.

Oltre al punto della domanda, tuttavia, si noti che l'onere di sapere come si combina la ricca struttura del modello non è più il problema del controller. Ottiene solo alcuni dati ed espone un sacco di metodi alla vista.

+0

offre percorsi nidificati ed è molto conveniente per questi casi d'uso – Eloims

Problemi correlati