Ce guide est la traduction francaise de AngularJS style guide.
Le but de ce guide de style est d'exposer un ensemble de meilleures pratiques et directives de style pour une application AngularJS. Elles proviennent:
- du code source d'AngularJS
- du code source ou des articles que j'ai lus
- de ma propre expérience.
Note 1: ce guide est encore à l'état d'ébauche. Son principal objectif est d'être développé par la communauté, donc combler les lacunes sera grandement apprécié par l'ensemble de la communauté.
Note 2: avant de suivre certaines directives des traductions du document original en anglais, assurez-vous qu'elles sont à jour avec la dernière version.
Dans ce document, vous ne trouverez pas de directives générales concernant le développement en JavaScript. Vous pouvez les trouver dans les documents suivants:
- Google's JavaScript style guide
- Mozilla's JavaScript style guide
- Douglas Crockford's JavaScript style guide
- Airbnb JavaScript style guide
Pour le développement d'AngularJS, le guide recommandé est Google's JavaScript style guide.
Dans le wiki Github d'AngularJS, il y a une section similaire de ProLoser, vous pouvez la consulter ici.
- Général
- Modules
- Contrôleurs
- Directives
- Filtres
- Services
- Gabarits
- Routage
- Tests E2E
- i18n
- Performance
- Contribution
Étant donné qu'une grosse application AngularJS a beaucoup de composants, il est préférable de la structurer en une hiérarchie de répertoires. Il existe deux approches principales:
- Créer une division de haut niveau par types de composants et une division inférieure par fonctionnalité.
De cette façon, la structure de répertoires ressemblera à:
.
├── app
│ ├── app.js
│ ├── controllers
│ │ ├── home
│ │ │ ├── FirstCtrl.js
│ │ │ └── SecondCtrl.js
│ │ └── about
│ │ └── ThirdCtrl.js
│ ├── directives
│ │ ├── home
│ │ │ └── directive1.js
│ │ └── about
│ │ ├── directive2.js
│ │ └── directive3.js
│ ├── filters
│ │ ├── home
│ │ └── about
│ └── services
│ ├── CommonService.js
│ ├── cache
│ │ ├── Cache1.js
│ │ └── Cache2.js
│ └── models
│ ├── Model1.js
│ └── Model2.js
├── partials
├── lib
└── test
- Créer une division de haut niveau par fonctionnalité et de niveau inférieur par type de composants.
Voici son schéma:
.
├── app
│ ├── app.js
│ ├── common
│ │ ├── controllers
│ │ ├── directives
│ │ ├── filters
│ │ └── services
│ ├── home
│ │ ├── controllers
│ │ │ ├── FirstCtrl.js
│ │ │ └── SecondCtrl.js
│ │ ├── directives
│ │ │ └── directive1.js
│ │ ├── filters
│ │ │ ├── filter1.js
│ │ │ └── filter2.js
│ │ └── services
│ │ ├── service1.js
│ │ └── service2.js
│ └── about
│ ├── controllers
│ │ └── ThirdCtrl.js
│ ├── directives
│ │ ├── directive2.js
│ │ └── directive3.js
│ ├── filters
│ │ └── filter3.js
│ └── services
│ └── service3.js
├── partials
├── lib
└── test
- Dans l'éventualité ou le nom du dossier contient plusieurs mots, utilisez la syntaxe lisp-case comme suit:
app
├── app.js
└── mon-module-complexe
├── controllers
├── directives
├── filters
└── services
- Lors de la création de directives, placez tous les fichiers associés à une directive (gabarits, fichiers CSS / SASS, JavaScript) dans un seul dossier. Si vous choisissez d'utiliser ce style d'arborescence, soyez cohérent et utilisez-le partout dans votre projet.
app
└── directives
├── directive1
│ ├── directive1.html
│ ├── directive1.js
│ └── directive1.sass
└── directive2
├── directive2.html
├── directive2.js
└── directive2.sass
Cette approche peut être combinée avec les deux structures de répertoires ci-dessus.
- Une dernière petite variation des deux structures de répertoires est celle utilisée dans ng-boilerplate. Dans celle-ci, les tests unitaires pour un composant donné sont dans le même dossier que le composant. De cette façon, quand vous modifiez un composant donné, il est facile de trouver ses tests. Les tests tiennent aussi lieu de documentation et démontrent des cas d'usage.
services
├── cache
│ ├── cache1.js
│ └── cache1.spec.js
└── models
├── model1.js
└── model1.spec.js
- Le fichier
app.js
devrait contenir la définition des routes, la configuration et/ou l'amorçage manuel (si nécessaire). - Chaque fichier JavaScript ne devrait contenir qu'un seul composant. Le fichier doit être nommé avec le nom du composant.
- Utilisez un modèle de structure de projet pour Angular tel que Yeoman ou ng-boilerplate.
Les conventions sur le nommage des composants peuvent être trouvées dans la section de chaque composant.
TLDR; Placer les scripts tout en bas.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>MyApp</title>
</head>
<body>
<div ng-app="myApp">
<div ng-view></div>
</div>
<script src="angular.js"></script>
<script src="app.js"></script>
</body>
</html>
Garder les choses simples et placez les directives spécifiques d'AngularJS en dernier, après les attributs HTML standard. De cette façon, il sera plus facile de parcourir votre code et de le maintenir puisque vos attributs seront groupés et positionnés de manière cohérente et régulière.
<form class="frm" ng-submit="login.authenticate()">
<div>
<input class="ipt" type="text" placeholder="name" require ng-model="user.name">
</div>
</form>
Les autres attributs HTML devraient suivre les recommandations du Code Guide de Mark Otto.
The tableau suivant présente des conventions de nommage pour chaque élément:
Élément | Style de nommage | Exemple | Usage |
---|---|---|---|
Modules | lowerCamelCase | angularApp | |
Contrôleurs | Fonctionnalité + 'Ctrl' | AdminCtrl | |
Directives | lowerCamelCase | userInfo | |
Filtres | lowerCamelCase | userFilter | |
Services | UpperCamelCase | User | constructor |
Factories | lowerCamelCase | dataFactory | autres |
- Utilisez:
$timeout
au lieu desetTimeout
$interval
au lieu desetInterval
$window
au lieu dewindow
$document
au lieu dedocument
$http
au lieu de$.ajax
$location
au lieur dewindow.location
ou$window.location
$cookies
au lieu dedocument.cookie
Cela rendra vos tests plus facile à faire et, dans certains cas, évitera les comportements inattendus (par exemple, si vous avez oublié $scope.$apply
dans setTimeout
).
-
Automatisez votre flux de travail en utilisant des outils comme:
-
Utilisez des promises (
$q
) au lieu de rappels (callback). Cela rendra votre code plus élégant et propre, et vous sauvera de l'enfer des callbacks. -
Utilisez
$resource
au lieu de$http
lorsque possible. Un niveau d'abstraction plus élevé diminuera la redondance. -
Utilisez un pré-minifier AngularJS (ng-annotate) pour la prévention des problèmes après minification.
-
N'utilisez pas de variables globales. Résolvez toutes les dépendances en utilisant l'injection de dépendances, cela préviendra les bugs et le monkey-patching lorsqu'en phase de test.
-
Eliminez les variables globales en utilisant Grunt/Gulp pour englober votre code dans des Expressions de Fonction Immédiatement Invoquée (Immediately Invoked Function Expression, (IIFE)). Vous pouvez utiliser des plugins tel que grunt-wrap ou gulp-wrap pour cet usage. Exemple (avec Gulp)
gulp.src("./src/*.js") .pipe(wrap('(function(){\n"use strict";\n<%= contents %>\n})();')) .pipe(gulp.dest("./dist"));
-
Ne polluez pas votre portée
$scope
. Ajoutez uniquement sur celle-ci les fonctions et les variables qui sont utilisés dans les gabarits. -
Préférez l'utilisation de contrôleurs au lieu de
ngInit
. Il n'y a que quelques utilisations appropriées de ngInit tel que pour spécifier des propriétés spécifiques de ngRepeat, et pour injecter des données via des scripts côté serveur. Outre ces quelques cas, vous devez utiliser les contrôleurs plutôt quengInit
pour initialiser les valeurs sur une portée. L'expression passée àngInit
doit être analysée et évaluée par l'interpréteur d'Angular implémenté dans le service$parse
. Ceci mène à:- Des impacts sur la performance, car l'interpréteur est implémenté en JavaScript
- La mise en cache des expressions passées au service
$parse
n'a pas réellement de sens dans la plus part des cas, étant donnée que l'expressionngInit
n'est évaluée qu'une seule fois - Ecrire des chaînes de caractère dans votre HTML est enclin à causer des erreurs, puisqu'il n'y a pas d'auto-complétion ou de support par votre éditeur de texte
- Aucune levée d'exceptions à l'éxécution
-
Ne pas utiliser le prefixe
$
pour les noms de variables, les propriétés et les méthodes. Ce préfixe est réservé à l'usage d'AngularJS. -
Lors de la résolution des dépendances par le système d'injection de dépendances d'AngularJS, triez les dépendances par leur type - les dépendances intégrées à AngularJS en premier, suivies des vôtres :
module.factory('Service', function ($rootScope, $timeout, MyCustomDependency1, MyCustomDependency2) {
return {
//Something
};
});
-
Les modules devraient être nommés en lowerCamelCase. Pour indiquer que le module
b
est un sous-module du modulea
, vous pouvez les imbriquer en utlisant un espace de noms tel quea.b
.Les deux façons habituelles de structurer les modules sont:
- par fonctionnalité
- par type de composant.
Actuellement, il n'y a pas une grande différence entre les deux mais la première semble plus propre. En outre, si le chargement paresseux des modules est implémenté (actuellement il ne figure pas sur la feuille de route d'AngularJS), il permettra d'améliorer les performances de l'application.
-
Ne manipulez pas le DOM dans vos contrôleurs. Cela rendra vos contrôleurs plus difficiles à tester et violerait le principe de séparation des préoccupations. Utilisez plutôt les directives.
-
Le nom d'un contrôleur s'obtient à partir de sa fonction (par exemple panier, page d'accueil, panneau d'administration) suffixée par
Ctrl
. -
Les contrôleurs sont des constructeurs javascript ordinaires, ils sont donc nommés en UpperCamelCase (
HomePageCtrl
,ShoppingCartCtrl
,AdminPanelCtrl
, etc.) -
Les contrôleurs ne devraient pas être définis dans le contexte global (bien qu'AngularJS le permet, c'est une mauvaise pratique de polluer l'espace de noms global).
-
Utilisez la syntaxe suivante pour définir des contrôleurs:
function MyCtrl(dependency1, dependency2, ..., dependencyn) { // ... } module.controller('MyCtrl', MyCtrl);
Une telle définition évite les problèmes avec la minification. Vous pouvez générer automatiquement la définition du tableau à l'aide d'outils comme ng-annotate (et la tâche grunt grunt-ng-annotate).
Une autre alternative serait d'utiliser
$inject
comme suit:angular .module('app') .controller('HomepageCtrl', Homepage); HomepageCtrl.$inject = ['$log', '$http', 'ngRoute']; function HomepageCtrl($log, $http, ngRoute) { // ... }
-
Utilisez les noms d'origine des dépendances du contrôleur. Cela vous aidera à produire un code plus lisible:
module.controller('MyCtrl', ['$scope', function (s) {
//...body
}]);
est moins lisible que
module.controller('MyCtrl', ['$scope', function ($scope) {
//...body
}]);
Cela s'applique particulièrement à un fichier qui a tellement de lignes de code que vous devrez les faire défiler. Cela pourrait vous faire oublier quelle variable est liée à quelle dépendance.
-
Faites les contrôleurs aussi simples que possible. Extrayez les fonctions couramment utilisées dans un service.
-
Communiquez entre les différents contrôleurs en utilisant l'appel de méthode (possible lorsqu'un enfant veut communiquer avec son parent) ou
$emit
,$broadcast
et$on
. Les messages émis et diffusés doivent être réduits au minimum. -
Faites une liste de tous les messages qui sont passés en utilisant
$emit
et$broadcast
, et gérez-la avec précaution à cause des conflits de nom et bugs éventuels. Exemple:// app.js /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * Custom events: - 'authorization-message' - description of the message - { user, role, action } - data format - user - a string, which contains the username - role - an ID of the role the user has - action - specific ation the user tries to perform * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
-
Si vous devez formater les données alors encapsulez la logique de mise en forme dans un filtre et déclarez-le comme dépendance:
module.filter('myFormat', function () {
return function () {
//body...
};
});
module.controller('MyCtrl', ['$scope', 'myFormatFilter', function ($scope, myFormatFilter) {
//body...
}]);
-
Il est préférable d'utiliser la syntaxe
controller as
:<div ng-controller="MainCtrl as main"> {{ main.title }} </div>
app.controller('MainCtrl', MainCtrl); function MainCtrl () { this.title = 'Some title'; }
Les principales avantages d'utiliser cette syntaxe:
- Créer des composants isolés - les propriétés liées ne font pas parties de la chaîne de prototypage de
$scope
. C'est une bonne pratique depuis que l'héritage du prototype de$scope
a quelques inconvénients majeurs (c'est probablement la raison pour laquelle il serat supprimé d'Angular 2):- Il est difficile de suivre la trace des données pour savoir où elles vont.
- Le changement de valeur d'un scope peut affecter certaines portées de façon inattendue.
- Plus difficile à réusiner (refactoring).
- La règle des points : 'dot rule(in English)'.
- Ne pas utiliser
$scope
sauf pour le besoin d'opérations spéciales (comme$scope.$broadcast
) est une bonne preparation pour AngularJS V2. - La syntaxe est plus proche du constructeur Javascript brut ('vanilla').
Decouvrez la syntaxe
controller as
en détail: adoptez-la-syntaxe-controller-as - Créer des composants isolés - les propriétés liées ne font pas parties de la chaîne de prototypage de
-
Eviter d'écrire la logique métier dans le contrôleur. Déplacez la logique métier dans un
modèle
, grâce à un service. Par exemple://Ceci est un comportement répandu (mauvais exemple) d'utiliser le contrôleur pour implémenter la logique métier. angular.module('Store', []) .controller('OrderCtrl', function ($scope) { $scope.items = []; $scope.addToOrder = function (item) { $scope.items.push(item);//-->Logique métier dans le contrôleur }; $scope.removeFromOrder = function (item) { $scope.items.splice($scope.items.indexOf(item), 1);//-->Logique métier dans le contrôleur }; $scope.totalPrice = function () { return $scope.items.reduce(function (memo, item) { return memo + (item.qty * item.price);//-->Logique métier dans le contrôleur }, 0); }; });
Quand on utilise un service comme 'modèle' pour implémenter la logique métier, voici à quoi ressemble le contrôleur (voir 'utilisez un service comme votre Modèle' pour une implémentation d'un service-modèle):
//Order est utilisé comme un 'modèle' angular.module('Store', []) .controller('OrderCtrl', function (Order) { $scope.items = Order.items; $scope.addToOrder = function (item) { Order.addToOrder(item); }; $scope.removeFromOrder = function (item) { Order.removeFromOrder(item); }; $scope.totalPrice = function () { return Order.total(); }; });
Pourquoi la mise en place de la logique métier dans le contrôleur est une mauvaise pratique ?
- Les contrôleurs sont instanciés pour chaque vue HTML et sont détruits au déchargement de la vue.
- Les contrôleurs ne sont pas ré-utilisables - ils sont liés à la vue HTML.
- Les contrôleurs ne sont pas destinés à êtres injectés.
-
Dans le cas de contrôleurs imbriqués utilisez les portées emboitées (avec
controllerAs
):
app.js
module.config(function ($routeProvider) {
$routeProvider
.when('/route', {
templateUrl: 'partials/template.html',
controller: 'HomeCtrl',
controllerAs: 'home'
});
});
HomeCtrl
function HomeCtrl() {
this.bindingValue = 42;
}
template.html
<div ng-bind="home.bindingValue"></div>
- Nommez vos directives en lowerCamelCase
- Utilisez
scope
au lieu de$scope
dans votre fonction de lien. Dans la compilation, les fonctions de liaison pré/post compilation, vous avez déjà les arguments qui sont passés lorsque la fonction est appelée, vous ne serez pas en mesure de les modifier à l'aide de DI. Ce style est également utilisé dans le code source d'AngularJS. - Utilisez les préfixes personnalisés pour vos directives pour éviter les collisions de noms de bibliothèques tierces.
- Ne pas utiliser
ng
ouui
comme préfixe car ils sont réservés pour AngularJS et l'utilisation d'AngularJS UI. - Les manipulations du DOM doivent être effectués uniquement avec des directives.
- Créer un scope isolé lorsque vous développez des composants réutilisables.
- Utilisez des directives comme des attributs ou des éléments au lieu de commentaires ou de classes, cela va rendre le code plus lisible.
- Utilisez
$scope.$on('$destroy, fn)
pour le nettoyage de vos objects/variables. Ceci est particulièrement utile lorsque vous utilisez des plugins tiers comme directives. - Ne pas oublier d'utiliser
$sce
lorsque vous devez faire face à un contenu non approuvé.
- Nommez vos filtres en lowerCamelCase.
- Faites vos filtres aussi légers que possible. Ils sont souvent appelés lors de la boucle
$digest
, donc créer un filtre lent ralentira votre application. - Limitez vos filtres à une seule chose et gardez-les cohérents. Des manipulations plus complexes peuvent être obtenues en enchaînant des filtres existants.
La présente section contient des informations au sujet des composants service dans AngularJS. Sauf mention contraire, elles ne dépendent pas de la méthode utilisée pour définir les services (c.-à-d. provider
, factory
, service
).
- Nommez vos services en camelCase:
- UpperCamelCase (PascalCase) pour vos services utilisés comme constructeurs, c.-à.-d.:
module.controller('MainCtrl', function ($scope, User) {
$scope.user = new User('foo', 42);
});
module.factory('User', function () {
return function User(name, age) {
this.name = name;
this.age = age;
};
});
-
lowerCamel pour tous les autres services.
-
Encapsulez la logique métier dans des services.
-
La méthode
service
est préférable à la méthodefactory
. De cette façon, nous pouvons profiter de l'héritage classique plus facilement:
function Human() {
//body
}
Human.prototype.talk = function () {
return "I'm talking";
};
function Developer() {
//body
}
Developer.prototype = Object.create(Human.prototype);
Developer.prototype.code = function () {
return "I'm codding";
};
myModule.service('Human', Human);
myModule.service('Developer', Developer);
- Pour un cache de session, vous pouvez utiliser
$cacheFactory
. Il devrait être utilisé pour mettre en cache les résultats des requêtes ou des calculs lourds. - Si un service donné nécessite une configuration, définissez le service comme un provider et configurez-le ainsi dans la fonction de rappel
config
:
angular.module('demo', [])
.config(function ($provide) {
$provide.provider('sample', function () {
var foo = 42;
return {
setFoo: function (f) {
foo = f;
},
$get: function () {
return {
foo: foo
};
}
};
});
});
var demo = angular.module('demo');
demo.config(function (sampleProvider) {
sampleProvider.setFoo(41);
});
- Utilisez
ng-bind
oung-cloak
au lieu de simples{{ }}
pour prévenir les collisions de contenus - Eviter d'écrire du code complexe dans les gabarits
- Quand vous avez besoin de définir le
src
d'une image dynamiquement, utilisezng-src
au lieu desrc
avec{{}}
dans le gabarit. Ceci pour permettre un refresh dynamique ? (NLDT) - Au lieu d'utiliser la variable $scope en tant que chaîne et de l'utiliser avec l'atribut
style
et{{}}
, utilisez la directiveng-style
avec les paramètres de l'objet comme et les variables de scope comme valeurs:
<script>
...
$scope.divStyle = {
width: 200,
position: 'relative'
};
...
</script>
<div ng-style="divStyle">my beautifully styled div which will work in IE</div>;
- Utilisez
resolve
pour résoudre les dépendances avant que la vue ne soit affichée. - Ne placez pas d'appels REST à l'intérieur du callback
resolve
. Isolez les requêtes à l'intérieur de services appropriés. De cette manière, vous pourrez activer la mise en cache et appliquer le principe de séparation des problèmes (separation of concerns).
Les tests E2E sont la prochaine étape logique après les tests unitaires. Cette étape permet de retracer les bugs et les erreurs dans le comportement de votre système. Ils confirment que les scénarios les plus communs de l'utilisation de votre application sont fonctionnels. De cette manière, vous pouvez automatiser le processus et l'exécuter à chaque fois que vous déployez votre application.
Idéallement, les tests E2E Angular sont écris avec Jasmine. Ces tests sont exécutés en utilisant l'exécuteur de tests Protractor E2E qui utilise des évènements natifs et qui possèdes des fonctionnalités spécifiques aux applications Angular.
Arborescence:
.
├── app
│ ├── app.js
│ ├── home
│ │ ├── home.html
│ │ ├── controllers
│ │ │ ├── FirstCtrl.js
│ │ │ ├── FirstCtrl.spec.js
│ │ ├── directives
│ │ │ └── directive1.js
│ │ │ └── directive1.spec.js
│ │ ├── filters
│ │ │ ├── filter1.js
│ │ │ └── filter1.spec.js
│ │ └── services
│ │ ├── service1.js
│ │ └── service1.spec.js
│ └── about
│ ├── about.html
│ ├── controllers
│ │ └── ThirdCtrl.js
│ │ └── ThirdCtrl.spec.js
│ └── directives
│ ├── directive2.js
│ └── directive2.spec.js
├── partials
├── lib
└── e2e-tests
├── protractor.conf.js
└── specs
├── home.js
└── about.js
- Pour les versions les plus récentes du framework (>=1.4.0), utilisez les outils i18n intégrés. Lorsque vous utilisez de versions antérieures(<1.4.0), utilisez
angular-translate
.
- Surveiller seulement les variables les plus importantes (par exemple, lors de l'utilisation de communication en temps réel, ne pas provoquer une boucle
$digest
dans chaque message reçu). - Pour un contenu initialisé une seule fois et qui ensuite ne change pas, utiliser des observateurs à évaluation unique comme bindonce.
- Faire les calculs dans
$watch
aussi simples que possible. Faire des calculs lourds et lents dans un unique$watch
va ralentir l'ensemble de l'application (la boucle$digest
s'exécute dans un seul thread en raison de la nature mono-thread de JavaScript). - Mettre le troisième paramètre de la fonction
$timeout
à false pour éviter la boucle$digest
lorsqu'aucune des variables observées n'est impactée par la fonction de rappel$timeout
.
Puisque ce guide de style a pour but d'être un projet communautaire, les contributions sont très appréciées. Par exemple, vous pouvez contribuer en développant la section Tests ou en traduisant le guide dans votre langue.