Guida stilistica dogmatica ad AngularJS per i team di @john_papa
Traduzido por Angelo Chiello
The original English version is the source of truth, as it is maintained and updated first.
Se stai cercando una guida stilistica dogmatica per le sintassi, convenzioni e struttura di applicazioni AngularJS, allora questo fa per te. Gli stili sono basati sulla mia esperienza di sviluppo con AngularJS, presentazioni, corsi di formazioni di Pluralsight e del lavoro in team.
Se ti piace questa guida, dai un'occhiata al mio corso AngularJS Patterns: Clean Code (in inglese) su Pluralsight.
L'obbiettivo di questa guida stilistica è di fare da vademecum alla costruzione di applicazioni con AngularJS mostrando le convenzioni che uso e, più importante, perché le uso.
Mai lavorare nel vuoto. Ritengo che la comunità intorno ad AngularJS sia un gruppo incredibile con la passione di condividere le esperienze. Perciò, Todd Motto, un amico ed un esperto di AngularJS, ed io abbiamo collaborato su molti stili e convenzioni. Su molto siamo d'accordo, su altro meno. Ti invito a controllare le linee guida di Todd per avere cognizione del suo approccio e di come paragonarle.
Many of my styles have been from the many pair programming sessions Ward Bell and I have had. While we don't always agree, my friend Ward has certainly helped influence the ultimate evolution of this guide.
Molti dei mie stili sono frutto di parecchie sessioni di pair programming che Ward Bell ed io abbiamo avuto. Seppur non sempre in sintonia, il mio amico Ward ha di certo una influenza sull'evoluzione finale di questa guida.
Nonostante questa guida spieghi i cosa, come e perché, trovo che sia di aiuto vederle in pratica. Questa guida è accompagnata da una applicazione di esempio che segue questi stili e schemi. Troverai l'applicazione di esempio (chiamata modular) qui nella cartella modular
. Prendila, clonala o fanne un fork liberamente. Le istruzioni su come eseguirla sono nel proprio readme.
##Traduzioni Traduzioni di questa guida stilistica ad AngularJS sono gestite dalla comunità e possono essere trovate qui.
- Responsabilità singola
- IIFE
- Moduli
- Controller
- Service
- Factory
- Data Service
- Directive
- Risoluzioni di promesse per un controller
- Annotazioni manuali per la Dependency Injection
- Minificazione e Annotazioni
- Gestione delle eccezioni
- Nomenclatura
- Principio "LIFT" per la struttura dell'applicazione
- Struttura dell'applicazione
- Modularità
- Logica di Startup
- Wrapper dei Servizi $ di Angular
- Test
- Animazioni
- Commenti
- JSHint
- Costanti
- File Template e Snippet
- Documentazione di AngularJS
- Contribuire
- Licenza
-
Definire 1 componente per file.
Il seguente esempio definisce il modulo
app
e le proprie dipendenze, definisce un controller e definisce una factory tutto nel medesimo file.
/* evitare */
angular
.module('app', ['ngRoute'])
.controller('SomeController' , SomeController)
.factory('someFactory' , someFactory);
function SomeController() { }
function someFactory() { }
Gli stessi componenti sono ora separati nei loro file.
/* consigliato */
// app.module.js
angular
.module('app', ['ngRoute']);
/* consigliato */
// someController.js
angular
.module('app')
.controller('SomeController' , SomeController);
function SomeController() { }
/* consigliato */
// someFactory.js
angular
.module('app')
.factory('someFactory' , someFactory);
function someFactory() { }
- Racchiudi i componenti di AngularJS in una Immediately Invoked Function Expression (IIFE) (Espressione di funzione immediatamente chiamata).
Perché?: Una IIFFE rimuove le variabili dallo scope globale. Questo aiuta a prevenire che variabili e funzioni vivano più del previsto nello scope globale, che inoltre aiuta ad evitare la collisione di variabili. Perché?: Quando il tuo codice è minificato e raggruppato in un file singolo per il rilascio ad un server di produzione, potresti avere collisioni di variabili e parecchie variabili globali. Una IIFE ti protegge in entrambi i casi fornendo uno scope variabile per ogni file.
/* evitare */
// logger.js
angular
.module('app')
.factory('logger', logger);
// La funzione logger è aggiunta come variabile globale
function logger() { }
// storage.js
angular
.module('app')
.factory('storage', storage);
// La funzione storage è aggiunta come variabile globale
function storage() { }
/**
* consigliato
*
* non ci sono più variabili globali
*/
// logger.js
(function() {
'use strict';
angular
.module('app')
.factory('logger', logger);
function logger() { }
})();
// storage.js
(function() {
'use strict';
angular
.module('app')
.factory('storage', storage);
function storage() { }
})();
-
Nota: Per essere più coincisi, il resto degli esempi in questa guida potrebbe omettere l'uso della sintassi IIFE.
-
Nota: Le IIFE evitano che il codice di test possa raggiungere membri privati come regular expression o funzioni di supporto le quali sono spesso oggetto dei propri unit test. In ogni caso, queste possono essere testate per mezzo di membri accessibili o attraverso l'esposizione di propri componenti. Per esempio ponendo funzioni di supporto, regular expression o costanti nelle proprie factory o costanti.
- Usa una convenzione unica per i nomi con separatori per sotto moduli.
Perché?: Nomi unici aiutano ad evitare la collisione di nomi dei moduli. I separatori aiutano a definire gerarchie di moduli e dei propri sotto moduli. Per esempio app
potrebbe essere il modulo principale mentre app.dashboard
e app.users
potrebbero essere moduli che sono usati come dipendenze di app
.
-
Dichiara moduli senza una variabile usando la sintassi setter.
Perché?: con 1 componente per file, raramente c'è la necessità di introdurre una variabile per il modulo.
/* evitare */
var app = angular.module('app', [
'ngAnimate',
'ngRoute',
'app.shared',
'app.dashboard'
]);
Invece usa la più semplice sintassi setter.
/* consigliato */
angular
.module('app', [
'ngAnimate',
'ngRoute',
'app.shared',
'app.dashboard'
]);
-
Usando un modulo, evita l'uso di una variabile e piuttosto usa la concatenazione con la sintassi getter.
Perché? : Ciò produce un codice maggiormente leggibile ed evita la collisione di variabili o buchi.
/* evitare */
var app = angular.module('app');
app.controller('SomeController' , SomeController);
function SomeController() { }
/* consigliato */
angular
.module('app')
.controller('SomeController' , SomeController);
function SomeController() { }
-
Setta solo una volta e prendi (get) per tutte le altre istanze.
Perché?: Un modulo dovrebbe essere creato solamente una volta, quindi recuperato da lì in avanti.
- Usa
angular.module('app', []);
per settare un modulo. - Usa
angular.module('app');
per prendere (get) un modulo.
- Usa
-
Usa funzioni che hanno un nome piuttosto che passare una funzione anonima come in una callback.
Perché?: Ciò produce codice maggiormente leggibile, è più facile farne il debug, e riduce la quantità di codice posto dentro una callback.
/* evitare */
angular
.module('app')
.controller('Dashboard', function() { });
.factory('logger', function() { });
/* consigliato */
// dashboard.js
angular
.module('app')
.controller('Dashboard', Dashboard);
function Dashboard() { }
// logger.js
angular
.module('app')
.factory('logger', logger);
function logger() { }
-
Usa la sintassi
controllerAs
al posto della sintassiclassico controller con $scope
.Perché?: I controller sono costruiti, fatti nuovi e forniti con un nuova istanza singola, inoltre la sintassi
controllerAs
è più somigliante ad un costruttore JavaScript che lasintassi classica con $scope
.Perché?: Promuove l'uso del binding ad un oggetto che "usa il punto" nella View (p.e.
customer.name
invece diname
), il quale è più contestuale, facile da leggere ed evita qualunque questione di riferimenti che potrebbe accadere senza "uso del punto".Perché?: Aiuta ad evitare l'uso di chiamate a
$parent
nelle View che hanno controller nidificati.
<!-- evitare -->
<div ng-controller="Customer">
{{ name }}
</div>
<!-- consigliato -->
<div ng-controller="Customer as customer">
{{ customer.name }}
</div>
-
Usa la sintassi
controllerAs
al posto della sintassiclassico controller con $scope
. -
La sintassi
controllerAs
usathis
all'interno dei controller che fanno uso di$scope
Perché?: controllerAs
è una semplificazione sintattica per $scope
. Puoi ancora fare il binding con la View ed accedere ai metodi di $scope
.
Perché?: Aiuta ad evitare la tentazione ad usare i metodi di $scope
dentro un controller quando sarebbe meglio evitare o spostarli in una factory. Considera l'uso di $scope
in una factory o, se in un controller, soltanto quando necessario. Per esempio, quando si pubblicano o sottoscrivono eventi usando $emit
, $broadcast
, o $on
considera di spostare questi tipi di utilizzi in una facotry e di invocarli da un controller.
/* evitare */
function Customer($scope) {
$scope.name = {};
$scope.sendMessage = function() { };
}
/* consigliato - tuttavia vedi la prossima sezione */
function Customer() {
this.name = {};
this.sendMessage = function() { };
}
- Usa una variabile che "catturi"
this
quando si utilizza la sintassicontrollerAs
. Scegli un nome della variabile consistente comevm
, che sta per ViewModel.
Perché?: La keyword this
è contestuale e quando usata all'interno di una funzione dentro un controller può cambiare il proprio contesto. Catturare il contesto di this
evita di incorrere in questo problema.
/* evitare */
function Customer() {
this.name = {};
this.sendMessage = function() { };
}
/* consigliato */
function Customer() {
var vm = this;
vm.name = {};
vm.sendMessage = function() { };
}
Nota: Puoi evitare ogni warning di jshint ponendo il commento sotto riportato al di sopra della linea di codice.
/* jshint validthis: true */
var vm = this;
Nota: Quando di creano watch in un controller usando controller as
, puoi fare il watch del membro vm.*
usando la seguente sintassi. (Crea watch con cautela poiché aggiungono carico al ciclo di digest.)
$scope.$watch('vm.title', function(current, original) {
$log.info('vm.title was %s', original);
$log.info('vm.title is now %s', current);
});
-
Poni i membri che possono fare il bind in cima al controller, in ordine alfabetico, piuttosto che dispersi in tutto il codice del controller.
Perché?: Porre i membri che posso fare il bind in cima rende semplice la lettura e aiuta l'istantanea identificazione di quali membri del controller possono essere collegati ed usati in una View.
Perché?: Settare funzioni anonime nella medesima linea è semplice, tuttavia quando queste funzioni sono più lunghe di 1 linea di codice possono ridurre la leggibilità. Definire le funzione al di sotto i membri che possono fare il bind (funzioni che saranno chiamate) spostano l'implementazione in basso, tengono i membri che possono fare il bind in cima e rendono il codice più facile da leggere.
/* evitare */
function Sessions() {
var vm = this;
vm.gotoSession = function() {
/* ... */
};
vm.refresh = function() {
/* ... */
};
vm.search = function() {
/* ... */
};
vm.sessions = [];
vm.title = 'Sessions';
/* consigliato */
function Sessions() {
var vm = this;
vm.gotoSession = gotoSession;
vm.refresh = refresh;
vm.search = search;
vm.sessions = [];
vm.title = 'Sessions';
////////////
function gotoSession() {
/* */
}
function refresh() {
/* */
}
function search() {
/* */
}
![Controller che usa "Above the Fold"](https://raw.githubusercontent.com/johnpapa/angularjs-styleguide/master/assets/above-the-fold-1.png)
Nota: Se la funzione è di 1 linea considera di poterla lasciare in cima fino a che la leggibilità non ne è compromessa.
/* evitare */
function Sessions(data) {
var vm = this;
vm.gotoSession = gotoSession;
vm.refresh = function() {
/**
* lines
* of
* code
* affects
* readability
*/
};
vm.search = search;
vm.sessions = [];
vm.title = 'Sessions';
/* consigliato */
function Sessions(dataservice) {
var vm = this;
vm.gotoSession = gotoSession;
vm.refresh = dataservice.refresh; // 1 liner is OK
vm.search = search;
vm.sessions = [];
vm.title = 'Sessions';
-
Usa le dichiarazioni di funzione per nascondere i dettagli di implementazione. Tieni i membri che possono fare il binding in cima. Quando necessiti di fare binding a una funzione nel controller, puntalo ad una dichiarazione di funzione che compaia dopo nel file. Questo è direttamente collegabile con la sezione Membri che possono fare il binding in cima. Per ulteriori dettagli guarda questo post (in inglese).
Perché?: Porre i membri che possono fare il binding in cima rende semplice la lettura ed aiuta l'immediata identificazione dei membri del controller che possono fare il binding ed usati nella View. (Come sopra.)
Perché?: Porre i dettagli di implementazione di una funzione in seguito nel file sposta la complessità fuori dalla vista così che puoi vedere le cose importanti in cima.
Perché?: Dichiarazioni di funzioni che sono chiamate così che non c'è rischio dell'uso di una funzione prima che sia definita (come sarebbe in caso di espressioni di funzione).
Perché?: Non ti devi preoccupare di dichiarazioni di funzione che sposta
var a
prima divar b
che romperà il codice perchéa
dipende dab
.Perché?: Con le espressioni di funzione l'ordine è critico.
/**
* evitare
* Uso di espressioni di funzione.
*/
function Avengers(dataservice, logger) {
var vm = this;
vm.avengers = [];
vm.title = 'Avengers';
var activate = function() {
return getAvengers().then(function() {
logger.info('Activated Avengers View');
});
}
var getAvengers = function() {
return dataservice.getAvengers().then(function(data) {
vm.avengers = data;
return vm.avengers;
});
}
vm.getAvengers = getAvengers;
activate();
}
Nota come le cose importanti, nell'esempio precedente, sono disseminate. Nell'esempio sotto, nota che le cose importanti sono in cima. Per esempio, i membri collegati al controller come vm.avengers
e vm.title
. I dettagli di implementazione sono in fondo. Questo è certamente più facile da leggere.
/*
* consigliato
* Usare dichiarazione di funzione
* e mebri che fanno in binding in cima.
*/
function Avengers(dataservice, logger) {
var vm = this;
vm.avengers = [];
vm.getAvengers = getAvengers;
vm.title = 'Avengers';
activate();
function activate() {
return getAvengers().then(function() {
logger.info('Activated Avengers View');
});
}
function getAvengers() {
return dataservice.getAvengers().then(function(data) {
vm.avengers = data;
return vm.avengers;
});
}
}
-
Rimandare la logica in un controller delegandola ai service e factory.
Perché?: La logica può essere riutilizzata da più controller quando posta in un service ed esposta tramite una funzione.
Perché?: La logica posta in un service può essere più facilmente isolata in una unit test, mentre la call della logica nel controller può essere più facile di farne un mock.
Perché?: Rimuove dipendenze e nasconde dettagli di implementazione dal controller.
/* evitare */
function Order($http, $q) {
var vm = this;
vm.checkCredit = checkCredit;
vm.total = 0;
function checkCredit() {
var orderTotal = vm.total;
return $http.get('api/creditcheck').then(function(data) {
var remaining = data.remaining;
return $q.when(!!(remaining > orderTotal));
});
};
}
/* consigliato */
function Order(creditService) {
var vm = this;
vm.checkCredit = checkCredit;
vm.total = 0;
function checkCredit() {
return creditService.check();
};
}
-
Definisci un controller per vista e prova a non utilizzare il controller per altre view. Piuttosto, sposta la logica riutilizzabile alle factory e mantieni il controller semplice ed a fuoco sulla propria view.
Perché?: Riutilizzare i controller con diverse view è precario e sono necessari dei buoni test end to end (e2e) per assicurarne la stabilità in applicazioni su larga scala.
-
Quando un controller deve essere accoppiato ad una view ed un componente può essere riutilizzato da altri controller o view, definisci i controller insieme alle loro route.
Nota: Se una View è caricata attraverso altri mezzi che una route, allora usa la sintassi
ng-controller="Avengers as vm"
.Perché?: Accoppiare il controller in una route consente a route diverse di invocare diversi accoppiamenti di controller e view. Quando i controller sono assegnati in una view usando
ng-controller
, quella view sarà sempre associata al medesimo controller.
/* evitare - quando usato con una route ed è desiderata una dinamicità negli accoppiamenti */
// route-config.js
angular
.module('app')
.config(config);
function config($routeProvider) {
$routeProvider
.when('/avengers', {
templateUrl: 'avengers.html'
});
}
<!-- avengers.html -->
<div ng-controller="Avengers as vm">
</div>
/* consigliato */
// route-config.js
angular
.module('app')
.config(config);
function config($routeProvider) {
$routeProvider
.when('/avengers', {
templateUrl: 'avengers.html',
controller: 'Avengers',
controllerAs: 'vm'
});
}
<!-- avengers.html -->
<div>
</div>
-
I Service sono istanziati con la keyword
new
, usathis
per metodi e variabili pubbliche. Dal momento che sono molto simili alle factory, usa queste ultime per consistenza.Nota: Tutti i servizi di AngularJS sono singleton. Questo significa che c'è soltanto una istanza di un dato servizio per iniettore.
// service
angular
.module('app')
.service('logger', logger);
function logger() {
this.logError = function(msg) {
/* */
};
}
// factory
angular
.module('app')
.factory('logger', logger);
function logger() {
return {
logError: function(msg) {
/* */
}
};
}
- Le factory dovrebbero avere la singola responsabilità che è incapsulata nel proprio contesto. Una volta che una factory eccede quello che è un singolo scopo, una nuova factory dovrebbe essere creata.
-
Le factory sono singleton e ritornano un oggetto che contiene i membri del servizio.
-
Esponi tutti i membri richiamabili del servizio (l'interfaccia) in cima, usando una tecnica derivata dal Revealing Module Pattern.
Perché?: Porre i membri richiamabili in cima lo rende semplice da leggere e aiuta ad identificare istantaneamente quali membri del servizio possono essere richiamati ed essere oggetto di unit test (e/o simulati).
Perché?: Questo è particolarmente utile quando i file iniziano ad allungarsi così come aiuta la necessità di scorrere per leggere cosa è esposto.
Perché?: Settare funzioni mentre procedi può essere facile ma quando tali funzioni sono più lunghe di 1 linea di codice possono ridurre la leggibilità e causare maggiore scorrimento. Definire l'interfaccia richiamabile attraverso i servizi ritornati sposta i dettagli di implementazione in basso, tiene l'interfaccia richiamabile in cima e rende più facile al lettura.
/* evitare */
function dataService() {
var someValue = '';
function save() {
/* */
};
function validate() {
/* */
};
return {
save: save,
someValue: someValue,
validate: validate
};
}
/* consigliato */
function dataService() {
var someValue = '';
var service = {
save: save,
someValue: someValue,
validate: validate
};
return service;
////////////
function save() {
/* */
};
function validate() {
/* */
};
}
In questo modo i binding si riflettono in tutto l'oggetto host, i valori di base non possono essere solamente aggiornati usando il revealing module pattern.
![Factory che usano "Above the Fold"](https://raw.githubusercontent.com/johnpapa/angularjs-styleguide/master/assets/above-the-fold-2.png)
-
Usa le dichiarazioni di funzioni per nascondere i dettagli di implementazione. Tieni i membri accessibili della factory in cima. Puntali alle dichiarazioni di funzioni che compaiono dopo nel file. Per ulteriori dettagli guarda questo post (in inglese).
Perché?: Porre i membri richiamabili in cima lo rende semplice da leggere e aiuta ad identificare istantaneamente quali funzioni della factory possono accessibili esternamente.
Perché?: Porre i dettagli di implementazione di una funzione dopo nel file sposta la complessità fuori dalla vista così che puoi vedere le cose importanti in cima.
Perché?: Le dichiarazioni di funzione sono richiamate così da non avere preoccupazioni circa l'uso di una funzione prima della sua definizione (come sarebbe nel caso di espressioni di funzione).
Perché?: Non dovrai mai preoccuparti di dichiarazioni di funzione che spostano
var a
prima divar b
rompendo il codice perchéa
dipende dab
.Perché?: Con le espressioni di funzione l'ordine è critico.
/**
* evita
* Uso di espressioni di funzioni
*/
function dataservice($http, $location, $q, exception, logger) {
var isPrimed = false;
var primePromise;
var getAvengers = function() {
// dettagli di implementazione vanno qui
};
var getAvengerCount = function() {
// dettagli di implementazione vanno qui
};
var getAvengersCast = function() {
// dettagli di implementazione vanno qui
};
var prime = function() {
// dettagli di implementazione vanno qui
};
var ready = function(nextPromises) {
// dettagli di implementazione vanno qui
};
var service = {
getAvengersCast: getAvengersCast,
getAvengerCount: getAvengerCount,
getAvengers: getAvengers,
ready: ready
};
return service;
}
/**
* consigliato
* Uso di dichiarazioni di funzioni
* e membri accessibili in cima.
*/
function dataservice($http, $location, $q, exception, logger) {
var isPrimed = false;
var primePromise;
var service = {
getAvengersCast: getAvengersCast,
getAvengerCount: getAvengerCount,
getAvengers: getAvengers,
ready: ready
};
return service;
////////////
function getAvengers() {
// dettagli di implementazione vanno qui
}
function getAvengerCount() {
// dettagli di implementazione vanno qui
}
function getAvengersCast() {
// dettagli di implementazione vanno qui
}
function prime() {
// dettagli di implementazione vanno qui
}
function ready(nextPromises) {
// dettagli di implementazione vanno qui
}
}
-
Rivedi la logica per gestire le operazioni con i dati e con la loro interazione delegandola ad una factory.
Perché?: La responsabilità del controller è per la presentazione e raccolta di informazioni dalla view. Non dovrebbe occuparsi di come recuperare i dati, soltanto sapere a chi chiederli. La separazione dei servizi per i dati sposta la logica su come reperirli al servizio dei dati, rendendo il controller più semplice e più focalizzato sulla view.
Perché?: Ciò rende più semplice da testare (vere o simulate) le chiamate ai dati quando si testa un controller che usa un servizio ai dati.
Perché?: L'implementazione di un servizio ai dati può avere del codice molto specifico su come trattare i repository dei dati. Questo può includere header, come comunicare con i dati o altri servizi quali $http. Separare la logica in un servizio ai dati incapsula questa logica in un posto unico nascondendo l'implementazione ai consumatori esterni (forse un controller), rendendo inoltre più semplice cambiarne l'implementazione.
/* consigliato */
// factory del servizio ai dati
angular
.module('app.core')
.factory('dataservice', dataservice);
dataservice.$inject = ['$http', 'logger'];
function dataservice($http, logger) {
return {
getAvengers: getAvengers
};
function getAvengers() {
return $http.get('/api/maa')
.then(getAvengersComplete)
.catch(getAvengersFailed);
function getAvengersComplete(response) {
return response.data.results;
}
function getAvengersFailed(error) {
logger.error('XHR Failed for getAvengers.' + error.data);
}
}
}
Nota: Il servizio ai dati è chiamato dai consumatori, come un controller, nascondendo l'implementazione ai consumatori, come mostrato sotto.
/* consigliato */
// controller che chiama la factory del servizio ai dati
angular
.module('app.avengers')
.controller('Avengers', Avengers);
Avengers.$inject = ['dataservice', 'logger'];
function Avengers(dataservice, logger) {
var vm = this;
vm.avengers = [];
activate();
function activate() {
return getAvengers().then(function() {
logger.info('Activated Avengers View');
});
}
function getAvengers() {
return dataservice.getAvengers()
.then(function(data) {
vm.avengers = data;
return vm.avengers;
});
}
}
-
Quando si chiama un servizio ai dati che ritorna una promessa come $http, ritorna a tua volta una promessa nella tua funzione di chiamata.
Perché?: Puoi concatenare le promesse insieme e prendere ulteriori azioni dopo che la chiamata ai dati è completata e risolvere o rigettare la promessa.
/* consigliato */
activate();
function activate() {
/**
* Passo 1
* Chiedi alla funzione getAvengers per i
* dati sugli avenger e aspetta la promessa
*/
return getAvengers().then(function() {
/**
* Passo 4
* Produci un'azione sulla risoluzione della promessa conclusiva
*/
logger.info('Activated Avengers View');
});
}
function getAvengers() {
/**
* Passo 2
* Chiedi al servizio i dati e aspetta
* la promessa
*/
return dataservice.getAvengers()
.then(function(data) {
/**
* Passo 3
* set the data and resolve the promise setta i dati e risolvi la promessa
*/
vm.avengers = data;
return vm.avengers;
});
}
**[Torna all'inizio](#tavola-dei-contenuti)**
-
Crea una directive per file. Nomina il file per la directive.
Perché?: È facile mescolare tutte le directive in un unico file ma difficoltoso da separarle così che alcune siano condivise tra le applicazioni, alcune tra moduli, altre solo per un module.
Perché?: Una directive per file è semplice da manutenere.
/* evitare */
/* directives.js */
angular
.module('app.widgets')
/* directive di ordini che è specifica per il modulo degli ordini */
.directive('orderCalendarRange', orderCalendarRange)
/* directive delle vendite che può essere usata dovunque nelle app di vendita */
.directive('salesCustomerInfo', salesCustomerInfo)
/* dirctive dello spinner che può essere usata dovunque nelle app */
.directive('sharedSpinner', sharedSpinner);
function orderCalendarRange() {
/* dettagli di implementazione */
}
function salesCustomerInfo() {
/* dettagli di implementazione */
}
function sharedSpinner() {
/* dettagli di implementazione */
}
/* consigliato */
/* calendarRange.directive.js */
/**
* @desc directive di ordini che è specifica al modulo ordini in una azienda di nome Acme
* @example <div acme-order-calendar-range></div>
*/
angular
.module('sales.order')
.directive('acmeOrderCalendarRange', orderCalendarRange);
function orderCalendarRange() {
/* dettagli di implementazione */
}
/* consigliato */
/* customerInfo.directive.js */
/**
* @desc directive dello spinner che può essere usato dovunque nella applicazione di vendita di una azienda di nome Acme
* @example <div acme-sales-customer-info></div>
*/
angular
.module('sales.widgets')
.directive('acmeSalesCustomerInfo', salesCustomerInfo);
function salesCustomerInfo() {
/* dettagli di implementazione */
}
/* consigliato */
/* spinner.directive.js */
/**
* @desc directive dello spinner che può essere usato dovunque nella applicazione di vendita di una azienda di nome Acme
* @example <div acme-shared-spinner></div>
*/
angular
.module('shared.widgets')
.directive('acmeSharedSpinner', sharedSpinner);
function sharedSpinner() {
/* dettagli di implementazione */
}
Nota: Ci sono molte opzioni per i nomi delle directive, in particolare dal momento che possono essere usate in ambiti stretti o larghi. Scegline uno che sia chiaro e distino che dia senso alla directive e il suo nome del file. Alcuni esempi sono sotto ma vedi la sezione sulla nomenclatura per maggiori raccomandazioni.
-
Quando devi manipolare direttamente il DOM, usa una directive. Se possono essere usate delle alternative come settare stili CSS o i servizi di animazione, templating di Angular,
ngShow
oppurengHide
, piuttosto usa questi. Per esempio, se la directive semplicemente nasconde e mostra, usa ngHide/ngShow.Perché?: Manipolare il DOM può essere difficoltoso da testare, debuggare e spesso ci sono modi migliori (p.e. CSS, animazioni, template)
-
Utilizza un corto, unico e descrittivo prefisso alla directive come
acmeSalesCustomerInfo
che è dichiarato in HTML comeacme-sales-customer-info
.Perché?: L'unico breve prefisso identifica il contesto delle directive e l'origine. Per esempio un prefisso
cc-
potrebbe indicare che la directive è parte di una app CodeCamper mentreacme-
potrebbe indicare una direttiva per l'azienda Acme.Nota: Evita
ng-
poiché queste sono riservate per le directive di AngularJS. Cerca directive che sono largamente utilizzate per evitare il conflitto di nomi, comeion-
per il Framework Ionic .
-
Quando crei una directive che abbia senso come elemento a se stante, considera la restrizione a
E
(elemento custom) e facoltativamente restringere aA
(attributo custom). In generale, se può essere il suo stesso controllo,E
è appropriato. Le linee guida generali sono di permettereEA
ma tendono verso l'implementazione come un elemento quando è a se stante e come attributo quando accresce il proprio elemento DOM esistente.Perché?: È sensato.
Perché?: Mentre è possibile consentire che la directive sia usata come una classe, se questa agisce davvero con un elemento è più sensato usarla un elemento o al meno come un attributo.
Nota: EA è il default per AngularJS 1.3 e successivi
<!-- evitare -->
<div class="my-calendar-range"></div>
/* evitare */
angular
.module('app.widgets')
.directive('myCalendarRange', myCalendarRange);
function myCalendarRange() {
var directive = {
link: link,
templateUrl: '/template/is/located/here.html',
restrict: 'C'
};
return directive;
function link(scope, element, attrs) {
/* */
}
}
<!-- consigliato -->
<my-calendar-range></my-calendar-range>
<div my-calendar-range></div>
/* consigliato */
angular
.module('app.widgets')
.directive('myCalendarRange', myCalendarRange);
function myCalendarRange() {
var directive = {
link: link,
templateUrl: '/template/is/located/here.html',
restrict: 'EA'
};
return directive;
function link(scope, element, attrs) {
/* */
}
}
-
Usa la sintassi
controller as
con una directive per essere consistente con l'utilizzo dicontroller as
con una coppia di view e controller.Perché?: È sensato e non è difficile.
Nota: Le directive sotto dimostrano alcuni dei modi in cui puoi usare lo scope all'interno di link e controller di directive usando controllerAs. Ho usato sulla stessa linea il template solo per mettere tutto in un unico posto.
<div my-example max="77"></div>
angular
.module('app')
.directive('myExample', myExample);
function myExample() {
var directive = {
restrict: 'EA',
templateUrl: 'app/feature/example.directive.html',
scope: {
max: '='
},
link: linkFunc,
controller : ExampleController,
controllerAs: 'vm'
};
return directive;
ExampleController.$inject = ['$scope'];
function ExampleController($scope) {
// Iniettare $scope solo per confronto
/* jshint validthis:true */
var vm = this;
vm.min = 3;
vm.max = $scope.max;
console.log('CTRL: $scope.max = %i', $scope.max);
console.log('CTRL: vm.min = %i', vm.min);
console.log('CTRL: vm.max = %i', vm.max);
}
function linkFunc(scope, el, attr, ctrl) {
console.log('LINK: scope.max = %i', scope.max);
console.log('LINK: scope.vm.min = %i', scope.vm.min);
console.log('LINK: scope.vm.max = %i', scope.vm.max);
}
}
/* example.directive.html */
<div>hello world</div>
<div>max={{vm.max}}<input ng-model="vm.max"/></div>
<div>min={{vm.min}}<input ng-model="vm.min"/></div>
-
Risolvi la logica di start-up per un controller in una funzione
activate
.Perché?: Porre la logica di start-up in una posizione consistente nel controller la rende semplice da localizzare, più consistente da testare e aiuta a prevenire la diffusione di logica su tutto il controller.
Nota: Se hai necessità di annullare condizionalmente il route prima di iniziare ad usare il controller, usa piuttosto una risoluzione nella route.
/* evitare */
function Avengers(dataservice) {
var vm = this;
vm.avengers = [];
vm.title = 'Avengers';
dataservice.getAvengers().then(function(data) {
vm.avengers = data;
return vm.avengers;
});
}
/* consigliato */
function Avengers(dataservice) {
var vm = this;
vm.avengers = [];
vm.title = 'Avengers';
activate();
////////////
function activate() {
return dataservice.getAvengers().then(function(data) {
vm.avengers = data;
return vm.avengers;
});
}
}
-
Quando un controller dipende dalla dal fatto che una promessa sia risolta risolvi queste dipendenze nel
$routeProvider
prima che la logica del controller sia eseguita. Se hai bisogno di annullare condizionalmente una route prima che il controller sia attivato, usa un resolver della route.Perché?: Un controller può richiedere dei dati prima che si carichi. Quei dati potrebbero venire da una promessa di una factory su misura oppure $http. Usando un resolver della route acconsenti che la promessa sia risolta prima che la logica del controller sia eseguita, così da poter prendere decisioni basandosi sui dati provenienti dalla promessa.
/* evitare */
angular
.module('app')
.controller('Avengers', Avengers);
function Avengers(movieService) {
var vm = this;
// non risolta
vm.movies;
// risolta in modo asincrono
movieService.getMovies().then(function(response) {
vm.movies = response.movies;
});
}
/* migliore */
// route-config.js
angular
.module('app')
.config(config);
function config($routeProvider) {
$routeProvider
.when('/avengers', {
templateUrl: 'avengers.html',
controller: 'Avengers',
controllerAs: 'vm',
resolve: {
moviesPrepService: function(movieService) {
return movieService.getMovies();
}
}
});
}
// avengers.js
angular
.module('app')
.controller('Avengers', Avengers);
Avengers.$inject = ['moviesPrepService'];
function Avengers(moviesPrepService) {
/* jshint validthis:true */
var vm = this;
vm.movies = moviesPrepService.movies;
}
Nota: La dipendenza del codice di esempio da `movieService` non è a prova di minificazione in se stessa. Per i dettagli su come rendere questo codice a prova di minificazione, vedi la sezione sulla [dependency injection](#manual-annotating-for-dependency-injection) e sulla [minificazione e annotazione](#minification-and-annotation).
-
Evita di usare abbreviazioni sintattiche per la dichiarazione di dipendenze senza usare un approccio a prova di minificazione.
Perché?: I parametri dei componenti (p.e. controller, factory, etc.) saranno convertiti in variabili dal nome ridotto. Per esempio,
common
edataservice
potrebbero diventarea
ob
e non essere piò ritrovate da AngularJS./* evita - non a prova di minificazione*/ angular .module('app') .controller('Dashboard', Dashboard); function Dashboard(common, dataservice) { }
Questo codice può produrre variabili da nome ridotto e perciò causare errori a runtime.
/* evita - non a prova di minificazione*/ angular.module('app').controller('Dashboard', d);function d(a, b) { }
-
Usa
$inject
per identificare manualmente le tue dipendenze per i componenti di AngularJS.Perché?: Questa tecnica rispecchia la tecnica usata da
ng-annotate
, che raccomando per l'automazione della creazione della minificazione che sia a sicura per le dipendenze. Seng-annotate
rileva che una iniezione è stata fatta, non la duplicherà.Perché?: Questo salvaguarda le tue dipendenze dal essere vulnerabili alla questione della minificazione quando i parametri possono essere passati con nomi ridotti. Per esempio,
common
edataservice
possono diventarea
ob
e non essere più trovati da AngularJS.Perché?: Evita la creazione di dipendenze sulla stessa linea dal momento che lunghe liste possono essere difficili da leggere nell'array. Inoltre può essere fuorviante che l'array è una serie di stringhe mentre l'ultimo elemento è una funzione.
/* evitare */ angular .module('app') .controller('Dashboard', ['$location', '$routeParams', 'common', 'dataservice', function Dashboard($location, $routeParams, common, dataservice) {} ]);
/* evitare */ angular .module('app') .controller('Dashboard', ['$location', '$routeParams', 'common', 'dataservice', Dashboard]); function Dashboard($location, $routeParams, common, dataservice) { }
/* consigliato */ angular .module('app') .controller('Dashboard', Dashboard); Dashboard.$inject = ['$location', '$routeParams', 'common', 'dataservice']; function Dashboard($location, $routeParams, common, dataservice) { }
Nota: Quando la tua funzione si trova sotto una dichiarazione di return, $inject potrebbe essere non raggiungibile (ciò può accadere in una directive). Puoi risolvere ciò sia spostando l'$inject sopra la dichiarazione di return oppure usando la sintassi di array di iniezione alternativa.
Nota:
ng-annotate 0.10.0
introduce una caratteristica che sposta l'$inject
dove è raggiungibile.// dentro la definizione di una directive function outer() { return { controller: DashboardPanel, }; DashboardPanel.$inject = ['logger']; // Unreachable function DashboardPanel(logger) { } }
// dentro la definizione di una directive function outer() { DashboardPanel.$inject = ['logger']; // reachable return { controller: DashboardPanel, }; function DashboardPanel(logger) { } }
-
Usa $inject per identificare manualmente le tue dipendenze di resolver della route per i componenti di AngularJS.
Perché?: Questa tecnica evade le funzioni anonime per il di resolver della route, rendendolo più semplice da leggere.
Perché?: Una dichiarazione
$inject
può facilmente precedere il resolver della route per gestire la produzione di dipendenze che siano a prova di minificazione./* consigliato */ function config($routeProvider) { $routeProvider .when('/avengers', { templateUrl: 'avengers.html', controller: 'Avengers', controllerAs: 'vm', resolve: { moviesPrepService: moviePrepService } }); } moviePrepService.$inject = ['movieService']; function moviePrepService(movieService) { return movieService.getMovies(); }
-
Usa ng-annotate per Gulp o Grunt e commenta le funzioni che necessitano di automatizzare il dependency injection usando
/** @ngInject */
Perché?: Questo salvaguarda il tuo codice da ogni dipendenza che non segua le pratiche a prova di minificazione
Perché?:
ng-min
is deprecated Perché?:ng-min
è deprecato.Preferisco Gulp poiché lo ritengo più semplice da scrivere, leggere e fare il debug.
Il codice che segue non usa dipendenze che sono a prova di minificazione.
angular .module('app') .controller('Avengers', Avengers); /* @ngInject */ function Avengers(storageService, avengerService) { var vm = this; vm.heroSearch = ''; vm.storeHero = storeHero; function storeHero(){ var hero = avengerService.find(vm.heroSearch); storageService.save(hero.name, hero); } }
Quando il codice soprastante è eseguito da ng-annotate produce il seguente output con l'annotazione
$inject
e diventa a prova di minificazione.angular .module('app') .controller('Avengers', Avengers); /* @ngInject */ function Avengers(storageService, avengerService) { var vm = this; vm.heroSearch = ''; vm.storeHero = storeHero; function storeHero(){ var hero = avengerService.find(vm.heroSearch); storageService.save(hero.name, hero); } } Avengers.$inject = ['storageService', 'avengerService'];
Nota: Se
ng-annotate
rileva che l'iniezione è già stata fatta (p.e.@ngInject
è stato rilevato), non duplicherà il codice di$inject
.Nota: Quando si usa un resolver della route, puoi fare precedere il resolver della funzione con
/* @ngInject */
e ciò produrrà codice opportunamente annotato, mantenendo ogni iniezione delle dipendenze a prova di minificazione.// Usare l'annotazione @ngInject function config($routeProvider) { $routeProvider .when('/avengers', { templateUrl: 'avengers.html', controller: 'Avengers', controllerAs: 'vm', resolve: { /* @ngInject */ moviesPrepService: function(movieService) { return movieService.getMovies(); } } }); }
Nota: A partire da AngularJS 1.3 usa il parametro
ngStrictDi
della directivengApp
. Quando presente, l'iniettore sarà creato in modalità "strict-di" causando il fallimento dell'invocazione di funzioni che non fanno uso esplicito di annotazione delle funzioni da parte dell'applicazione (queste potrebbero non essere a prova di minificazione). Informazioni di debug saranno mostrate nella console per aiutare nel tracciare il codice non confacente.<body ng-app="APP" ng-strict-di>
-
Usa gulp-ng-annotate o grunt-ng-annotate in un task di build automatizzato. Inietta
/* @ngInject */
prima di qualunque funzione che abbia delle dipendenze.Perché?: ng-annotate carpirà la maggior parte delle dipendenze ma talvolta necessita dell'uso del suggerimento sintattico
/* @ngInject */
.Il seguente codice è un esempio di un task di gulp che utilizza ngAnnotate.
gulp.task('js', ['jshint'], function() { var source = pkg.paths.js; return gulp.src(source) .pipe(sourcemaps.init()) .pipe(concat('all.min.js', {newLine: ';'})) // Annota prima di fare l'uglify così che il codice sarà minificato correttamente. .pipe(ngAnnotate({ // true aiuta ad aggiunge @ngInject dove non usato. Inferisce. // Non funzione con resolve, quindi deve essere esplicitato qui add: true })) .pipe(bytediff.start()) .pipe(uglify({mangle: true})) .pipe(bytediff.stop()) .pipe(sourcemaps.write('./')) .pipe(gulp.dest(pkg.paths.dev)); });
-
Usa un decorator, al momento del config una un servizio
$provide
, sul servizio$exceptionHandler
per eseguire azioni ad hoc quando l'eccezione occorre.Perché?: Fornisci un modo consistente per la gestione delle eccezioni che non trattate da AngularJS sia durante lo sviluppo che a runtime.
Nota: Un'altra opzione è di fare l'override del servizio invece che usare un decorator. Questa è una buona opzione ma se vuoi tenere il comportamento di default ed estenderlo un decorator è consigliato.
/* consigliato */ angular .module('blocks.exception') .config(exceptionConfig); exceptionConfig.$inject = ['$provide']; function exceptionConfig($provide) { $provide.decorator('$exceptionHandler', extendExceptionHandler); } extendExceptionHandler.$inject = ['$delegate', 'toastr']; function extendExceptionHandler($delegate, toastr) { return function(exception, cause) { $delegate(exception, cause); var errorData = { exception: exception, cause: cause }; /** * Potresti aggiungere l'errore ad una collezione del servizio, * aggiungere l'errore a $rootScope, fare il log degli errori ad un server web remoto, * oppure farlo localmente. O lanciare l'eccezione solamente. Sta del tutto a te. * lancia l'eccezione; */ toastr.error(exception.msg, errorData); }; }
-
Crea una factory che espone un'interfaccia per ricevere ed elegantemente gestire le eccezioni.
Perché?: Fornisce un modo consistente di ricevere le eccezioni che possono essere lanciate nel tuo codice (p.e. durante una chiamata XHR o il fallimento di promesse).
Nota: Il ricevitore di eccezioni è buono per ricevere e reagire a specifiche eccezioni da chiamate che sai ne possono generare una. Per esempio, quando fai una chiamata XHR per il recupero di dati da un servizio di un server web remoto e vuoi ricevere qualsiasi eccezione da ciò e reagire univocamente.
/* consigliato */ angular .module('blocks.exception') .factory('exception', exception); exception.$inject = ['logger']; function exception(logger) { var service = { catcher: catcher }; return service; function catcher(message) { return function(reason) { logger.error(message, reason); }; } }
-
Gestisti e fai il log di tutti gli errori di routing usando
$routeChangeError
.Perché?: Fornisce un modo consistente di gestire tutti gli errori di routing.
Perché?: Potenzialmente fornisce una migliore esperienza all'utente se si verifica un errore di routing e li puoi indirizzare ad una schermata di aiuto o con opzioni di recupero.
/* consigliato */ function handleRoutingErrors() { /** * Annullamento del route: * Su un errore di routing, vai alla dashboard. * Fornisci una clausola di uscita se tenta di farlo per una seconda volta. */ $rootScope.$on('$routeChangeError', function(event, current, previous, rejection) { var destination = (current && (current.title || current.name || current.loadedTemplateUrl)) || 'unknown target'; var msg = 'Error routing to ' + destination + '. ' + (rejection.msg || ''); /** * Optionally log using a custom service or $log. * (Don't forget to inject custom service) */ /** * A scelta fai il log usando un servizio ad hoc o $log. * (Non dimenticare di iniettare il servizio ad hoc) */ logger.warning(msg, [current]); } ); }
-
Usa nomi consistenti per tutti i componenti seguendo uno schema che descriva le funzionalità dei componenti e poi (a scelta) il suo tipo. Lo schema che consiglio è
feature.type.js
. Ci sono 2 nomi per la maggior parte dei componenti:- il nome del file (
avengers.controller.js
) - il nome del componente registrato con Angular (
AvengersController
)
Perché?: Convezioni sui nomi aiutano a fornire un modo consistente per trovate i contenuti a colpo d'occhio. Essere consistenti in un progetto è vitale. Essere consistenti in un team è importante. Essere consistenti nell'insieme di un'azienda è tremendamente efficiente.
Perché?: Le convezioni sulla nomenclatura dovrebbe semplicemente aiutare a trovare il tuo codice più rapidamente e renderlo più semplice da comprendere.
- il nome del file (
-
Usa nomi consistenti per tutti i componenti seguendo uno schema che descriva le funzionalità dei componenti e poi (a scelta) il suo tipo. Lo schema che consiglio è
feature.type.js
.Perché?: Fornisce un modo consistente per identificare facilmente i componenti.
Perché?: Fornisce uno schema di corrispondenza per qualsiasi processo di automatizzazione.
/** * opzioni comuni */ // Controllers avengers.js avengers.controller.js avengersController.js // Services/Factories logger.js logger.service.js loggerService.js
/** * consigliato */ // controllers avengers.controller.js avengers.controller.spec.js // services/factories logger.service.js logger.service.spec.js // constants constants.js // module definition avengers.module.js // routes avengers.routes.js avengers.routes.spec.js // configuration avengers.config.js // directives avenger-profile.directive.js avenger-profile.directive.spec.js
Nota: Un'altra convenzione comune è dare il nome al file del controller senza la parola controller
nel nome del file come avengers.js
invece di avengers.controller.js
. Tutte le altre convenzioni continuano ancora a mantenere il suffisso del tipo. I controller sono i tipi di componenti più comuni perciò questo risparmia digitazione continuando ad essere facilmente identificabili. Consiglio di scegliere 1 convenzione e rimanere consistente nel tuo team.
```javascript
/**
* consigliato
*/
// Controllers
avengers.js
avengers.spec.js
```
-
Nomina le specifiche dei test in modo similare al componente che testano aggiundendo il suffisso
spec
.Perché?: Fornisce un modo consistente per identificare facilmente i componenti.
Perché?: Fornisce uno schema di corrispondenza per karma o altri esecutori di test.
/** * consigliato */ avengers.controller.spec.js logger.service.spec.js avengers.routes.spec.js avenger-profile.directive.spec.js
-
Usa nomi consistenti per tutti i controller nominandoli come le loro funzionalità. Usa UpperCamelCase per i controller, dal momento che sono costruttori.
Perché?: Fornisce un modo consistente per identificare e referenziare facilmente i controller.
Perché?: UpperCamelCase è una convezione per identificare un oggetto che può essere istanziato usando un costruttore.
/** * consigliato */ // avengers.controller.js angular .module .controller('HeroAvengers', HeroAvengers); function HeroAvengers(){ }
-
Aggiungi
Controller
alla fine del nome del controller o no. Segli 1 non entrambi.Perché?: Il suffisso
Controller
è quello più comunemente usato ed è più esplicitamente descrittivo.Perché?: L'omissione del suffisso è più coinciso ed il controller è spesso facilmente identificabile anche senza suffisso.
/** * consigliato: Opzione 1 */ // avengers.controller.js angular .module .controller('Avengers', Avengers); function Avengers(){ }
/** * consigliato: Opzione 2 */ // avengers.controller.js angular .module .controller('AvengersController', AvengersController); function AvengersController(){ }
-
Usa una nomenclatura consistente per tutte le factory dando i nomi date le loro funzionalità. Usa il camel-case per service e factory.
Perché?: Fornisce un modo consistente per identificare facilmente e referenziare le factory.
/** * consigliato */ // logger.service.js angular .module .factory('logger', logger); function logger(){ }
-
Usa nomi consistenti per putte le directive usando il camel-case. Usa un breve prefisso che descriva l'area alla quale la directive appartiene (alcuni esempi sono prefissi relativi all'azienda o al progetto).
Perché?: Fornisce un modo consistente per identificare e referenziare facilmente i componenti.
/** * consigliato */ // avenger.profile.directive.js angular .module .directive('xxAvengerProfile', xxAvengerProfile); // usage is <xx-avenger-profile> </xx-avenger-profile> function xxAvengerProfile(){ }
- Quando i sono moduli multipli, il modulo principale è nominato come
app.module.js
mentre altri moduli dipendenti prendono i nomi da ciò che rappresentano. Per esempio, un modulo admin è nominatoadmin.module.js
. I rispettivi nomi con i quali sono registrati sarannoapp
eadmin
. Una app a modulo singolo si chiameràapp.js
, omettendo l'appellativo module.
*Perché?*: Una app con 1 modulo si chiama `app.js`. È l'app, quindi perché non estremamente semplice.
*Perché?*: Fornisce consistenza per app che hanno più di un modulo e per poter espandere verso applicazioni a larga scala.
*Perché?*: Fornisci un modo semplice al fine di usare processi automatici per caricare prima tutte le definizioni di moduli, successivamente tutti gli altri file di Angular (per il bundling).
-
Separa la configurazione di un modulo nel proprio file chiamato come il modulo. Un file di configurazione per il modulo principale
app
è chiamatoapp.config.js
(o semplicementeconfig.js
). Un file di configurazione per un modulo chiamatoadmin.module.js
saràadmin.config.js
.Perché?: Separa la configurazione dalla definizione, componenti e codice di attivazione del modulo.
Perché?: Fornisci una posizione identificabile per settare la configurazione di un modulo.
- Separa la configurazione delle route nei propri file. Esempi possono essere
app.route.js
per il modulo principale eadmin.route.js
per il moduloadmin
. Anche in piccole app preferisco questa separazione dal resto della configurazione. Una alternativa è un nome più esteso qualeadmin.config.route.js
.
-
Struttura la tua app tale da poter
L
ocate (localizzare) il codice facilmente,I
dentify (identificare) il codice con uno sguardo, tenere la struttura piùF
lattest (piatta) che puoi, eT
ry (provare) a rimanere DRY (Don't Repeat Yourself - Non ripetersi). La struttura dovrebbe seguire queste 4 linee guida basilari.Perché LIFT?: Fornisce una struttura consistente che scala bene, è modulare e rende più semplice aumentare l'efficienza nel trovare facilmente il codice. Un altro modo per verificare la struttura della tua app è chiediti: Quanto rapidamente puoi aprire e lavorare ad una funzionalità in tutti i file che sono collegati?
Quando ritengo che la mia struttura non sia confortevole, torno indietro a rivedere le linee guida LIFT
L
ocalizzare il nostro codice con facilitàI
dentificare il codice a vistaF
lat (pitta) struttura quanto più possibileT
ry (prova) a restare DRY (Don’t Repeat Yourself) o T-DRY
-
Rendi intuitivo, semplice e facile localizzare il codice.
Perché?: Ritengo ciò essere estremamente importante per il progetto. Se il team non è in grado di trovare i file di cui necessita rapidamente, non sarà in grado di lavorare il più efficacemente possibile, per cui la struttura necessita un cambiamento. Potresti non sapere il nome del file o dove sono i file a questo correlati quindi posizionarli in nel posto più intuitivo e prossimi gli uni agli altri fa risparmiare un mucchio di tempo. Una descrittiva struttura delle cartelle può essere d'aiuto.
/bower_components /client /app /avengers /blocks /exception /logger /core /dashboard /data /layout /widgets /content index.html .bower.json
-
Guardando un file dovresti istantaneamente sapere ciò che contiene e cosa rappresenta.
Perché?: Spendi meno tempo a rintracciare e beccare il codice e diventa più efficiente. Se per fare ciò hai bisogno di nomi dei file più lunghi, fallo. Si descrittivo con i nomi dei file e tieni il contenuto del file con esattamente 1 componente. Evita file con più di un controller, diversi service o un misto. Ci sono delle eccezioni alla regola "1 per file" ovvero quando ho una serie di piccole funzionalità correlate l'un l'altra: continuano ad essere facilmente identificabili.
-
Tieni la struttura delle cartelle piatta il più a lungo possibile. Quando arrivi ad avere 7 o più file, inizia a considerarne una separazione.
Perché?: Nessuno vuole cercare 7 livelli di cartelle per trovare un file. Pensa ai menù di un sito web.. qualunque cosa oltre i 2 livelli dovrebbe esser presa in considerazione. Nella struttura di cartella non c'è una regola con un numero esattamente definito ma quando una cartella contiene 7-10 file, è il momento di creare una sottocartella. Basalo su un livello a te comodo. Usa una struttura più piatta fino a che c'è l'ovvia necessità (praticando il resto dei principi LIFT) di creare una nuova cartella.
-
Si DRY, ma non diventare pazzo e sacrificare la leggibilità.
Perché?: Non ripetersi è importante ma non è cruciale se sacrifica altri principi LIFT, per questo il principio è Try (provare) DRY. Non voglio digitare session-view.html perché è ovvio essere una view. Se non è ovvio o se per convenzione allora nominala così.
- Abbi una visione a breve termine dell'implementazione e una a lunga scadenza. In altre parole, parti in piccolo ma tieni in mente su dove l'app è diretta lungo il percorso. Tutto il codice dell'app va nella cartella principale chiamata
app
. Tutti i contenuti rispettano 1 funzione per file. Ogni controller, service, module, view nel proprio file. Tutti gli script di terze party sono poste in una altra cartella principale e non nella cartellaapp
. Non le ho scritte e non voglio facciano disordine nella mia app (bower_components
,scripts
,lib
).
Nota: Trovi più dettagli e le motivazioni di questa struttura nel [post originale sulla struttura delle applicazioni](http://www.johnpapa.net/angular-app-structuring-guidelines/) (in inglese).
-
Metti i componenti che definiscono il layout globale dell'applicazione in una cartella con il nome
layout
. Questi possono includere un shell view e controller che agiscono come contenitori per l'app, navigazione, menù, aree per i contenuti ed altre regioni.Perché?: Organizza tutto il layout in una sola posizione riutilizzabile lungo tutta l'applicazione.
-
Crea cartelle che abbiamo in nome per la funzionalità che rappresentano. Quando una cartella cresce fino a contenere più di 7 file, inizia a considerare la creazione di un'altra cartella. La tua soglia potrebbe essere differente, aggiustala di conseguenza.
Perché?: Uno sviluppatore può localizzare il codice, identificare ciò che rappresenta a colpo d'occhio, la struttura è piatta come deve essere e non c'è ripetitività o nomi ridondanti.
Perché?: Le linee guida LIFT sono tutte coperte.
Perché?: Aiuta a ridurre la possibilità che l'app divenga disordinata per mezzo dell'organizzazione dei contenuti mantenendola allineata con i principi LIFT.
Perché?: Quando ci sono parecchi file (più di 10) la loro localizzazione è più semplice con una struttura di cartelle che sia consistente e più difficile in una struttura piatta.
/** * raccomanadato */ app/ app.module.js app.config.js app.routes.js components/ calendar.directive.js calendar.directive.html user-profile.directive.js user-profile.directive.html layout/ shell.html shell.controller.js topnav.html topnav.controller.js people/ attendees.html attendees.controller.js speakers.html speakers.controller.js speaker-detail.html speaker-detail.controller.js services/ data.service.js localstorage.service.js logger.service.js spinner.service.js sessions/ sessions.html sessions.controller.js session-detail.html session-detail.controller.js
Nota: Non utilizzare una strutturazione del tipo cartella-per-tipo. Questo richiede spostarsi tra molte cartelle quando si lavora su una funzionalità e diventa rapidamente scomodo quando l'app cresce di 5, 10 o più di 25 tra view e controller (ed altre funzionalità), per cui è più difficile rispetto alla localizzazione basata su cartella-per-funzionalità.
/* * evitare * Alternativa cartella-per-tipo * Consiglio invece "cartella-per-funzionalità". */ app/ app.module.js app.config.js app.routes.js controllers/ attendees.js session-detail.js sessions.js shell.js speakers.js speaker-detail.js topnav.js directives/ calendar.directive.js calendar.directive.html user-profile.directive.js user-profile.directive.html services/ dataservice.js localstorage.js logger.js spinner.js views/ attendees.html session-detail.html sessions.html shell.html speakers.html speaker-detail.html topnav.html
-
Crea moduli piccoli che incapsulino una responsabilità.
Perché?: Applicazioni modulari rendono semplice l'inclusione dal momento che consentono ai team di sviluppo la costruzione di tagli verticali dell'applicazione e il suo roll out incrementale. Questo significa che è possibile aggiungere nuove funzionalità mentre vengono sviluppate.
-
Crea un modulo principale per l'applicazione il cui ruolo sia di mettere insieme tutti gli altri moduli e funzionalità della tua applicazione. Chiamalo con il nome della tua applicazione.
Perché?: AngularJS incoraggia la modularità e schemi di separazione. La creazione di un modulo principale il cui ruolo sia quello di legante tra gli altri moduli consente un modo lineare di aggiungere o rimuovere moduli dall'applicazione.
-
Nel modulo principale metti solo la logica che serva da collante per l'app. Lascia le funzioni ognuno al proprio modulo.
Perché?: L'aggiunta di ruoli addizionali al modulo principale per il recupero dei dati, il mostrare viste o altra logica non correlata al tenere insieme l'applicazione sporca il modulo principale e rende entrambi gli insiemi di funzionalità più complessi da riusare o rimuovere.
-
Crea moduli che rappresentino aree di funzionalità come layout, servizi riusabili e condivisi, pannelli di controllo e funzioni specifiche all'app (p.e. clienti, amministrazione, vendite).
Perché?: Moduli autonomi possono essere aggiunti all'applicazione con piccola o nessuna frizione.
Perché?: Sprint o iterazioni possono focalizzarsi sulle aree di funzionalità e renderle disponibili alla fine dello sprint o dell'iterazione.
Perché?: Separare aree di funzioni in moduli rende più semplice testare i moduli in isolamento e il riutilizzo del codice.
-
Crea moduli che rappresentino blocchi di applicazione riutilizzabili per servizi comuni quali la gestione delle eccezioni, il log, la diagnostica, sicurezza e il data stashing locale.
Perché?: Questi tipi di funzionalità sono richieste in molte applicazioni, perciò tenerle separate in moduli possono essere generiche l'applicazione e riutilizzate in applicazioni diverse.
-
Il modulo principale dell'applicazione dipende dai moduli di funzionalità specifiche dell'app, i moduli delle funzionalità non hanno dipendenze dirette, moduli trans-applicazione dipendono da moduli generici.
Perché?: Il modulo principale dell'app contiene un manifesto che sia facilmente identificabile con le funzionalità dell'applicazione.
Perché?: Funzionalità trans-applicazione diventano semplici da condividere. Le funzionalità generalmente dipendono dagli stessi moduli tras-applicazione, che sono consolidati in in singolo modulo (
app.core
nell'immagine).Perché?: Funzionalità intra-app come servizio ai dati condiviso diventano facilmente localizzabili da dentro
app.core
(questi il nome che più di piaccia per questo modulo).Nota: Questa è una strategia per la consistenza. Ci sono diverse buone opzioni in questo caso. Scegline una che sia consistente, segua le regole delle dipendenze di AngularJS e sia facile da manutenere e scalabile.
La mia struttura varia leggermente tra progetti ma tutti seguono queste linee guida per la strutturazione e modularità. L'implementazione può variare in relazione alle funzionalità ed al team. In altre parole, non ti bloccare su una struttura che sia esattamente uguale ma giustifica la tua struttura tenendo a mente l'uso di consistenza, manutenibilità ed efficienza.
-
Inietta codice nel modulo di configurazione che deve essere configurato prima dell'esecuzione dell'app angular. I candidati ideali includono provider e costanti.
Perché?: Questo rende più facile ottenere pochi posti atti alla configurazione
angular
.module('app')
.config(configure);
configure.$inject =
['routerHelperProvider', 'exceptionHandlerProvider', 'toastr'];
function configure (routerHelperProvider, exceptionHandlerProvider, toastr) {
exceptionHandlerProvider.configure(config.appErrorPrefix);
configureStateHelper();
toastr.options.timeOut = 4000;
toastr.options.positionClass = 'toast-bottom-right';
////////////////
function configureStateHelper() {
routerHelperProvider.configure({
docTitle: 'NG-Modular: '
});
}
}
-
Qualunque codice che necessiti di essere eseguito quando un'applicazione si avvia dovrebbe essere dichiarato in una factory, esposto tramite funzione ed iniettato nel blocco run.
Perché?: Codice posto direttamente in un blocco run può essere difficile da testate. Metterlo in una factory lo rende più astratto e simulabile (farne un mock).
angular
.module('app')
.run(runBlock);
runBlock.$inject = ['authenticator', 'translator'];
function runBlock(authenticator, translator) {
authenticator.initialize();
translator.initialize();
}
-
Usa
$document
e$window
al posto didocument
ewindow
.Perché?: Questi servizi sono gestiti da Angular e più facilmente testabili che l'uso di document e window nei test. Ciò ti aiuta ad evitare di fare mock di document e window.
-
Usa
$timeout
e$interval
al posto disetTimeout
esetInterval
.Perché?: Questi servizi sono gestiti da Angular e più facilmente testabili e trattano il ciclo di digest di AngularJS quindi tengono il data binding sincronizzato.
Gli unit test aiutano a mantenere il codice più chiaro, perciò ho incluso alcune mie raccomandazioni fondamentali per lo unit testing con link e ulteriori informazioni.
-
Scrivi un set di test per ogni storia. Inizia con un test vuoto e riempilo fino a scrivere il codice per la storia.
Perché?: Scrivere la descrizione del test aiuta a definire chiaramente cosa la tua stoia farà, non farà e come puoi misurarne il successo.
it('dovrebbe avere il controller Avenger', function() { //TODO }); it('dovrebbe trovare 1 Avenger quando filtrato per nome', function() { //TODO }); it('dovrebbe avere 10 Avenger', function() {} //TODO (fare un mock dei dati?) }); it('dovrebbe ritornare Avenger via XHR', function() {} //TODO ($httpBackend?) }); // continuare
-
Usa Jasmine oppure Mocha per lo unit testing.
Perché?: Sia Jasmine che Mocha sono largamente utilizzati nella comunità di AngularJS. Entrambi son stabili, ben manutenuti e forniscono funzionalità solide per i test.
Nota: Usando Mocha, tieni in considerazione di usare anche una libreria di asserzione come Chai.
-
Usa Karma come esecutore di test.
Perché?: Karma è facilmente configurabile per essere eseguito una sola volta o automaticamente quando cambia il tuo codice.
Perché?: Karma si aggancia facilmente al tuo processo di Integrazione Continua da solo o attraverso Grunt o Gulp.
Perché?: Alcuni IDE cominciamo ad integrare Karma, come WebStorm e Visual Studio.
Perché?: Karma lavora bene con leader di automazione di processo quali Grunt (con grunt-karma) e Gulp (con gulp-karma).
-
Usa Sinon per lo stubbing e spying.
Perché?: Sinon lavora bene sia con Jasmine che Mocha ed estende le funzionalità di stubbing e spying che questi offrono.
Perché?: Sinon rende più semplice il passaggio tra Jasmine e Mocha, nel caso voglia usarli entrambi.
-
Usa PhantomJS per eseguire i test su un server.
Perché?: PhantomJS è un headless browser che aiuta l'esecuzione di test senza la necessità di un browser "visuale". Quindi non devi installare Chrome, Safari, IE o altri browser sul server.
Nota: Dovresti in ogni caso testare tutti i browser del tuo ambiente, come appropriato per il pubblico che è il target.
-
Esegui JSHint sui tuoi test.
Perché?: I test sono codice. JSHint può aiutare ad identificare problemi di qualità del codice che causano l’improprio funzionamento del test.
-
Rilassa le regole sul codice dei test per consentendoli per variabili globali comuni quali
describe
edexpect
.Perché?: I tuoi test sono codice e richiedono al medesima attenzione e regole per la qualità del codice come tutto il resto del codice di produzione. Comunque, variabili globali usate dai framework di test, per esempio, possono essere rilassate includendole nelle specifiche dei test.
/* global sinon, describe, it, afterEach, beforeEach, expect, inject */
-
Usa sfumate animazioni con AngularJS per fare transizioni tra stati per viste ed elementi visuali primari. Includi il modulo ngAnimate. Le 3 chiavi sono sfumate, dolci e continue.
Perché?: Animazioni sfumate possono migliorare l'esperienza dell'utente (UX) quando usate in modo appropriato.
Perché?: Animazioni sfumate possono migliorare la percezione di prestazioni nella transizione tra le viste.
-
Usa animazioni che abbiano una durata breve. Generalmente parto con 300 ms e aggiusto finché non è appropriato.
Perché?: Long animations can have the reverse affect on User Experience and perceived performance by giving the appearance of a slow application.
-
Usa animate.css per animazioni convenzionali.
Perché?: Le animazione che animate.css fornisce sono veloci, dolci e facili da aggiungere alla tua applicazione.
Perché?: Da consistenza alle tue animazioni.
Perché?: animate.css è ampiamente usato e testato.
Nota: Leggi questo ottimo posto di Matias Niemelä sulle animazioni di AngularJS
-
Se hai intenzione di produrre documentazione, usa la sintassi di
jsDoc
per documentare nomi di funzione, descrizione, parametri e ciò che ritorna. Usa@namespace
e@memberOf
per adattarlo alla stuttura della tua app.Perché?: Puoi generare (e rigenerare) documentazione dal tuo codice, piuttosto che scriverlo partendo da zero.
Perché?: Fornisce consistenza usando un tool industriale comune.
/** * Factory di Logger * @namespace Factories */ (function() { angular .module('app') .factory('logger', logger); /** * @namespace Logger * @desc Logger di tutta l'applicazione * @memberOf Factories */ function logger($log) { var service = { logError: logError }; return service; //////////// /** * @name logError * @desc Log degli errori * @param {String} msg Messaggio di cui fare il log * @returns {String} * @memberOf Factories.Logger */ function logError(msg) { var loggedMsg = 'Error: ' + msg; $log.error(loggedMsg); return loggedMsg; }; } })();
-
Usa JS Hint per spazzolare il tuo JavaScript ed assicurati di ritagliare il file di opzioni di JS Hint e di includerlo nel source control . Vedi la documentazione di JS Hint per i dettagli sulle opzioni.
Perché?: Da un allerta iniziale prima di fare il commit di qualunque codice al source control.
Perché?: Fornisce consistenza all'interno del tuo team.
{ "bitwise": true, "camelcase": true, "curly": true, "eqeqeq": true, "es3": false, "forin": true, "freeze": true, "immed": true, "indent": 4, "latedef": "nofunc", "newcap": true, "noarg": true, "noempty": true, "nonbsp": true, "nonew": true, "plusplus": false, "quotmark": "single", "undef": true, "unused": false, "strict": false, "maxparams": 10, "maxdepth": 5, "maxstatements": 40, "maxcomplexity": 8, "maxlen": 120, "asi": false, "boss": false, "debug": false, "eqnull": true, "esnext": false, "evil": false, "expr": false, "funcscope": false, "globalstrict": false, "iterator": false, "lastsemic": false, "laxbreak": false, "laxcomma": false, "loopfunc": true, "maxerr": false, "moz": false, "multistr": false, "notypeof": false, "proto": false, "scripturl": false, "shadow": false, "sub": true, "supernew": false, "validthis": false, "noyield": false, "browser": true, "node": true, "globals": { "angular": false, "$": false } }
-
Crea una costante di AngularJS per le variabili globali delle librerie di terze parti.
Perché?: Fornisce in modo per iniettare librerie di terze parte che altrimenti sarebbero globali. Questo migliore la testabilità permettendoti più facilmente di sapere quali sono le dipendenze dei tuoi componenti. Ti consente inoltre di fare un mock di queste dipendenze, dove ciò ha senso.
// constants.js /* global toastr:false, moment:false */ (function() { 'use strict'; angular .module('app.core') .constant('toastr', toastr) .constant('moment', moment); })();
Usa file template o snippet che ti aiutino a seguire stili e schemi consistentemente. Qui trovi alcuni template e/o snippet per alcuni degli editor per lo sviluppo wbe e IDE.
-
Snippet di AngularJS che seguono questi stili e linee guida.
- Scarica gli snippet di Angular per Sublime
- Mettili nella tua cartella Packages
- Riavvia Sublime
- In un file JavaScript digita questi comandi seguiti da
TAB
ngcontroller // crea un controller Angular ngdirective // crea una directive Angular ngfactory // crea una factory Angular ngmodule // crea un modulo Angular
-
I file dei template per Angular che seguono questi stili e linee guida possono essere trovati su SideWaffle
- Scarica l'estensione SideWaffle per Visual Studio (file vsix)
- Esegui il file vsix
- Riavvia Visual Studio
-
Snippet di Angular JS e file di template che seguono queste linee guida. Le puoi importare dentro i tuoi settaggi di WebStorm:
- Scarica i file dei template e gli snippet diAngularJS per WebStorm
- Apri WebStorm e vai al menù
File
- Scegli la voce di menù
Import Settings
- Seleziona il file e clicca
OK
- In un file JavaScript digita questi comandi seguiti da
TAB
ng-c // crea un controller Angular ng-f // crea una factory Angular ng-m // crea un modulo Angular
For anything else, API reference, check the Angular documentation.
Apri prima una "issue" per discutere potenziali cambiamenti/aggiunte. Se hai delle domande relative alla guida, sentiti libero di porle come "issue" nel repository. Se trovi un errore di scrittura, crea una pull request. L'idea è quella di tenere aggiornato il contenuto e usare le funzioni native di Github per aiutare nel racconto della storia con issue e pull request che sono tutte ricercabili via Google. Perché? Il caso vuole che se hai una domanda, qualcun altro l'abbia pure. Puoi trovare di più qui su come contribuire.
Contribuendo a questo repository sei d'accordo a rendere il tuo contenuto soggetto alla licenza di questo repository
1. Discuti i cambiamenti in un Issue.
1. Apri una Pull Request, fai riferimento all issue e spiega i cambiamenti e perché questi aggiungono valore.
1. La Pull Request sarà vagliata e quindi fatto un merge o declinata.
tldr; Usa questa guida. I riferimenti sono apprezzati.
Copyright (c) 2014 John Papa
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.