# Webpack, Babel et ES6 dans une app Ionic — un pipeline JS moderne en 2016 > Retour technique sur la mise en place d'un pipeline JavaScript moderne — Webpack, Babel 6, modules ES6, SCSS et PostCSS — dans une app Ionic 1 / AngularJS de taille conséquente. Date : 28/10/2016 Auteur : Aurélien N. Tags : Webpack, Babel, ES6, JavaScript, Ionic, SCSS --- On vient de boucler [Hively](/blog/beespoke-marketplace-collaborative-ionic-rails), une app mobile Ionic pour une marketplace de services à domicile. Six mois de développement, une centaine de fichiers JavaScript, autant de fichiers SCSS, des dizaines de templates HTML. Sur un projet de cette taille, l'outillage fait la différence entre un code maintenable et un plat de spaghetti. On a fait le choix d'un pipeline qui n'est pas encore la norme dans l'écosystème Ionic — la plupart des projets Cordova qu'on voit passent encore par Gulp avec des fichiers concaténés, du JavaScript ES5 et des variables globales Angular. On a pris le parti inverse : Webpack, Babel, modules ES6 natifs, SCSS modulaire avec BEM. Le même outillage que les équipes React les plus avancées utilisent chez Facebook ou Airbnb — appliqué à un projet Ionic. Cet article détaille ce pipeline et pourquoi on pense que c'est la direction à suivre. ## Pourquoi on a quitté Gulp Sur nos projets précédents — dont l'app [Simone.paris](/blog/simone-paris-ios-retour-experience), livrée l'an dernier — on utilisait Gulp. Ça marchait. Les tâches s'enchaînaient : compiler le Sass, transpiler le JS, copier les assets, watcher les changements. Classique. Mais Gulp raisonne en tâches. On décrit des flux : "prends ces fichiers, applique cette transformation, écris le résultat là". Quand le projet grossit, les dépendances entre tâches deviennent implicites. On se retrouve à orchestrer l'ordre d'exécution, à gérer les cas où une tâche doit attendre la fin d'une autre, à débugger des race conditions entre le watcher SCSS et le watcher JS. **Webpack raisonne en graphe de dépendances.** On lui donne un point d'entrée — `app/index.js` — et il résout l'arbre complet : ce fichier importe tel module, qui importe tel template, qui référence telle image. Webpack comprend la structure du projet, pas juste une liste de fichiers à transformer. Le résultat concret : un seul fichier de config — pas de `gulpfile.js` de 200 lignes avec des tâches `watch`, `build:dev`, `build:prod`, `clean`, `copy-fonts`, `inject-css`. On a une config de base, une extension pour le développement, une pour la production. C'est tout. ## La config Webpack en détail ### La config de base Le socle commun entre dev et production : ```javascript // webpack.config/base.js const path = require('path'); const webpack = require('webpack'); const HtmlWebpackPlugin = require('html-webpack-plugin'); module.exports = { context: path.resolve(__dirname, '..'), output: , resolve: { alias: }, module: { rules: [ , { test: /\.html$/, loader: 'html-loader', options: }, , { test: /\.(woff|woff2|ttf|eot)(\?.+)?$/, loader: 'file-loader', options: } ] }, plugins: [ new webpack.DefinePlugin(), new HtmlWebpackPlugin() ] }; ``` Quelques choix notables : **L'alias `Commons`.** Plutôt que des `import` avec des chemins relatifs interminables (`../../../Common/Models/User`), on écrit `import User from 'Commons/Models/User'`. C'est un détail, mais sur un projet avec cinq niveaux d'imbrication dans les onglets, ça change la lisibilité. **`DefinePlugin` pour l'environnement.** L'URL de l'API, la clé Google Maps, le mode NODE_ENV — tout est injecté au build. Pas de fichier de config à charger au runtime, pas de risque de déployer avec l'URL de dev en production. **Le hash dans les noms de fichiers.** `bundle-app-[hash].js` génère un nom unique à chaque build. Le navigateur (ou la WebView Cordova) cache le fichier tant que le contenu ne change pas. Quand on déploie une nouvelle version, le hash change, le cache est invalidé. C'est du cache-busting gratuit. ### La config de production C'est là que ça devient intéressant : ```javascript // webpack.config/production.js const ExtractTextPlugin = require('extract-text-webpack-plugin'); const webpack = require('webpack'); module.exports = { entry: , module: { rules: [ { test: /\.scss$/, use: ExtractTextPlugin.extract() }, { test: /\.(png|jpe?g|gif|svg)$/, loader: 'image-webpack-loader', options: { pngquant: , mozjpeg: , svgo: { plugins: [] } } } ] }, plugins: [ new ExtractTextPlugin('bundle-[name]-[contenthash].css'), new webpack.optimize.UglifyJsPlugin({ compress: }), new webpack.optimize.ModuleConcatenationPlugin() ] }; ``` Trois optimisations clés : **Séparation vendor / app.** Le SDK Ionic fait ~300 Ko minifié. Il ne change jamais entre nos releases. En le mettant dans un bundle séparé, le navigateur le cache une fois et ne le retélécharge pas quand on met à jour le code applicatif. **`ModuleConcatenationPlugin`.** C'est une nouveauté de Webpack 3, baptisée "scope hoisting". Au lieu d'envelopper chaque module dans une fonction wrapper (le comportement par défaut), Webpack concatène les modules dans une seule portée quand c'est possible. Le résultat : un code plus petit et plus rapide à exécuter. **L'optimisation des images.** `image-webpack-loader` passe chaque image dans des compresseurs spécialisés — pngquant pour les PNG, mozjpeg pour les JPEG, svgo pour les SVG. Sur notre projet, avec une centaine d'icônes SVG et des images de catégories en PNG, on gagne ~40% de taille sans perte de qualité visible. ## Babel 6 : écrire du JavaScript du futur ### La configuration Notre `.babelrc` est sobre : ```json { "presets": [ ["env", { "targets": }] ], "plugins": [ "transform-object-rest-spread", "transform-runtime" ] } ``` `babel-preset-env` est la vraie révolution de Babel 6. Au lieu de charger tous les transforms à l'aveugle (`es2015` incluait tout, même ce que le navigateur supportait déjà), `env` cible des environnements spécifiques. On déclare nos cibles — iOS last 2 versions et Android 4+ — et Babel ne transpile que ce qui est nécessaire. Concrètement, sur iOS 9 et les WebViews Android 4.4+ (basées sur Chromium), les arrow functions et les template literals passent en natif. Babel ne les transforme pas. Il transpile les `class`, les `import`/`export`, le destructuring — ce qui n'est pas encore supporté. Moins de code généré, un bundle plus léger. ### ES6 en pratique dans AngularJS Le vrai gain n'est pas syntaxique — il est architectural. Les modules ES6 donnent une structure explicite au code qu'AngularJS ne fournit pas. Avant, tout passait par `angular.module('app')` — un namespace global implicite. L'ordre de chargement des fichiers importait, les dépendances étaient invisibles. Avec les modules ES6, chaque fichier déclare exactement ce qu'il importe et ce qu'il exporte : ```javascript // app/Common/Models/Booking.factory.js constructor(data) { this.id = data.id; this.state = data.state; this.beginningAt = moment.utc(data.beginning_at); this.endingAt = moment.utc(data.ending_at); this.duration = data.duration; this.frequency = data.frequency; this.options = data.options || ; } get isUpcoming() get isPending() get isPartOfPlan() static findOrCreate(data) { if (Booking.cache[data.id]) Booking.cache[data.id] = new Booking(data); return Booking.cache[data.id]; } } Booking.cache = ; ``` On lit ce fichier et on sait tout : il dépend de `moment` et de `lodash`, il exporte une classe `Booking`, cette classe a des getters pour l'état et un cache statique. Pas besoin de chercher dans le `angular.module` pour comprendre d'où viennent les dépendances. ### Le service API : un exemple complet Le service API illustre bien la combinaison ES6 + AngularJS. Une classe ES6 qui encapsule tous les appels HTTP, avec des méthodes qui utilisent les template literals, le destructuring et les arrow functions : ```javascript constructor($http) // Recherche de prestataires searchProviders() { return this.$http.get(`$/providers/search`, { params: }).then(() => data); } // Disponibilités d'un prestataire getAvailabilities(providerId, ) { return this.$http .get(`$/providers/$/availabilities`, { params: }) .then(() => data.availabilities); } // Création de réservation createBooking(booking) { return this.$http .post(`$/bookings`, ) .then(() => data); } } Api.$inject = ['$http']; ``` Le destructuring des paramètres (``) rend les signatures de méthode auto-documentées. Le destructuring de la réponse (``) évite la verbosité de `response.data`. Les template literals éliminent la concaténation de chaînes pour les URLs. Ce sont de petits gains individuels, mais cumulés sur une centaine de fichiers, ils transforment la lisibilité du projet. ## SCSS + PostCSS : le pipeline CSS ### Organisation modulaire Chaque composant a son fichier `.scss`. L'import se fait dans le module Angular — Webpack comprend les `import` de SCSS depuis du JavaScript : ```javascript // app/Tab/Home/Searches/Providers/Providers.module.js .module('tab.home.searches.providers', []) .config(ProvidersState) .controller('ProvidersController', ProvidersController) .name; ``` C'est contre-intuitif pour un développeur habitué à Gulp — importer du CSS dans du JavaScript ? Mais c'est exactement le modèle mental de Webpack : chaque module déclare ses dépendances, y compris visuelles. En production, `ExtractTextPlugin` extrait tout le CSS dans un fichier séparé. En développement, `style-loader` l'injecte directement dans le DOM pour le hot reload. ### BEM et la convention de nommage Sur un projet avec autant de directives Angular (on en a une trentaine), les collisions de noms CSS sont un vrai risque. On utilise **BEM** systématiquement : ```scss // Convention : .DirectiveName__element--modifier .StatusBadge { display: inline-flex; align-items: center; padding: 4px 10px; border-radius: 12px; font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; &--pending &--confirmed &--cancelled &--completed } .ProviderCard { display: flex; align-items: center; padding: 12px 16px; border-bottom: 1px solid rgba(0, 0, 0, 0.06); &__avatar &__info &__name &__rating } ``` Chaque directive est un bloc BEM autonome. Pas de cascade involontaire, pas de `!important`, pas de sélecteurs trop spécifiques. Le nesting SCSS rend la structure BEM naturelle — le `&__element` et le `&--modifier` se lisent d'un coup d'œil. ### Flexbox partout On a fait le choix de Flexbox pour tous les layouts. Les WebViews qu'on cible (iOS 8+ avec WKWebView, Android 4.4+ avec Chromium) le supportent correctement. Le support global est passé au-dessus de 93% cette année — c'est le moment de basculer. L'écran de filtres en est un bon exemple. Avant, on aurait utilisé des `float` avec des `width` en pourcentage et des clearfix. Maintenant : ```scss .SearchFilters { display: flex; flex-wrap: wrap; &__group &__fullwidth } ``` Deux lignes de Flexbox remplacent dix lignes de float-based layout. Et quand l'écran est trop étroit pour deux colonnes, `flex-wrap` et `min-width` gèrent le passage à une colonne. Pas de media query, pas de breakpoint — le layout est fluide par construction. ### Les astuces SCSS qui font la différence SCSS n'est pas juste "du CSS avec du nesting". Sur un projet de cette taille, les features avancées de Sass changent la productivité. **Les variables comme design tokens.** On centralise toutes les couleurs, tailles et espacements dans un fichier `_variables.scss`. Quand le client demande "un peu plus de jaune", on change une variable et tout suit : ```scss // _variables.scss — la source de vérité du design $color-primary: #E8B830; $color-primary-dark: #3C3216; $color-text: #212121; $color-text-light: #757575; $color-border: rgba(0, 0, 0, 0.06); $spacing-xs: 4px; $spacing-sm: 8px; $spacing-md: 16px; $spacing-lg: 24px; $font-size-body: 14px; $font-size-small: 11px; $font-size-title: 18px; $radius-card: 4px; $radius-pill: 20px; $radius-circle: 50%; ``` **Les mixins pour la cohérence mobile.** Les interactions tactiles ont des subtilités que le CSS standard ne couvre pas. On a un mixin `tap-highlight` qui élimine le flash bleu par défaut sur Android, un `truncate` pour les textes longs, et un `safe-area` pour les modales : ```scss @mixin tap-highlight @mixin truncate($lines: 1) { @if $lines == 1 @else } @mixin card-shadow ``` Le `-webkit-line-clamp` est un hack WebKit non standard, mais il fonctionne dans toutes les WebViews qu'on cible — et il n'existe aucune alternative CSS standard pour tronquer à N lignes. On le wrappe dans un mixin pour le jour où une solution standard arrivera. **Le nesting intelligent avec `&`.** L'opérateur `&` de SCSS est sous-estimé. Au-delà du BEM (`.Block { &__element }`), on l'utilise pour les états contextuels : ```scss .BookingCard { padding: $spacing-md; background: white; @include card-shadow; // État selon le status — classe parente .state-pending & .state-confirmed & // Variation dans une liste dense .compact-list & { padding: $spacing-sm; &__title } } ``` Le `&` inversé (`.parent &`) permet de styliser un composant différemment selon son contexte, sans toucher au composant lui-même. C'est du scoping CSS avant l'heure — en attendant que CSS offre nativement ce que Sass fait depuis des années. ### SCSS vs CSS natif : ce qui manque encore On utilise Sass parce que CSS n'a pas encore ce dont on a besoin. Les variables CSS (Custom Properties) commencent à arriver dans les navigateurs — Chrome 49 les supporte depuis mars, Firefox les a depuis un moment. Mais le support est loin d'être universel, et surtout : les Custom Properties ne couvrent qu'une fraction de ce que Sass offre. Ce qui nous manque en CSS natif : - **Le nesting.** Écrire `.card { .title }` au lieu de `.card .title `, c'est fondamental pour la lisibilité. Il y a des discussions au W3C — Tab Atkins a proposé un brouillon de spécification — mais rien de concret à l'horizon. - **Les mixins.** Réutiliser des blocs de déclarations, avec des paramètres. CSS n'a rien d'équivalent. - **Les fonctions de couleur.** `darken($color, 10%)`, `lighten()`, `mix()` — on les utilise partout. CSS a `calc()`, mais pas de fonctions de manipulation de couleurs. - **Les partials et l'import modulaire.** `@import` en CSS crée une requête HTTP. En Sass, c'est de la concaténation au build. Webpack résout ce problème pour nous via les loaders. PostCSS est une alternative intéressante — on l'utilise déjà pour Autoprefixer, et le plugin `postcss-nesting` implémente la spec CSS Nesting brouillon. Mais on préfère Sass pour l'instant : l'écosystème est plus mature, la documentation est meilleure, et nos développeurs le connaissent. On migrera vers du CSS natif quand les navigateurs le permettront — pas avant. ## Performances Ionic : les optimisations qui comptent Sur mobile, la performance est l'expérience utilisateur. Une app qui lag à 20 fps perd ses utilisateurs — peu importe la qualité du design. Voici les optimisations qu'on a mises en place et celles qu'on recommande. ### collection-repeat au lieu de ng-repeat C'est l'optimisation la plus impactante de toute l'app. Le `ng-repeat` standard d'AngularJS crée un nœud DOM pour chaque élément de la liste, même ceux qui sont hors écran. Sur une liste de 200 prestataires, ça fait 200 éléments DOM, 200 cycles de watchers, et un scroll qui rame. Le `collection-repeat` d'Ionic fonctionne comme un `UITableView` sur iOS : il ne rend que les éléments visibles et recycle les nœuds DOM quand l'utilisateur scrolle. On l'utilise pour les sélecteurs de dates dans le parcours de réservation : ```html {} {} ``` L'astuce : toujours fournir `item-height` et `item-width` explicitement. Sans ça, Ionic doit calculer les dimensions de chaque élément au runtime, ce qui annule une bonne partie du gain. ### track by dans les ng-repeat Quand on ne peut pas utiliser `collection-repeat` (listes courtes, layouts complexes), on ajoute systématiquement `track by` : ```html ... ... ``` Sans `track by`, AngularJS détruit et recrée l'intégralité des nœuds DOM chaque fois que la collection change — même si un seul élément a été ajouté. Avec `track by provider.id`, Angular identifie les éléments existants et ne met à jour que ce qui a changé. Sur une liste de résultats de recherche qui se rafraîchit à chaque filtre, c'est la différence entre une transition fluide et un flash blanc. ### Animations CSS accélérées par GPU Sur mobile, une animation CSS en `top` / `left` déclenche un reflow complet du layout — le CPU recalcule la position de chaque élément. Une animation en `transform: translateX()` est composée par le GPU — le CPU ne fait rien, le GPU déplace une texture. La différence : 15 fps vs 60 fps. ```scss // ❌ Mauvais — déclenche un reflow à chaque frame .SlideIn { transition: left 300ms ease; left: -100%; &.active } // ✅ Bon — composé par le GPU, 60 fps .SlideIn { transition: transform 300ms ease; transform: translateX(-100%); &.active } ``` On applique le même principe aux modales, aux transitions de page et aux animations de feedback. Ionic utilise déjà des `transform` pour ses transitions internes — on s'assure que nos composants custom suivent la même logique. ### Désactiver le debug Angular en production Un one-liner qui change les performances perçues : ```javascript // app.config.js if (process.env.NODE_ENV === 'production') } ``` En mode debug (le défaut), Angular ajoute des classes CSS et des attributs de data à chaque élément lié à un scope — utile pour les outils comme Batarang, mais qui double la quantité de nœuds DOM et ralentit le rendu. En production, on les désactive. Le `process.env.NODE_ENV` est injecté par le `DefinePlugin` de Webpack — le code mort est éliminé au build. ### Le cache d'identité des modèles On en a parlé plus haut, mais c'est aussi une optimisation de performance. Chaque modèle (User, Provider, Booking) maintient un cache statique keyed par ID. Quand l'API renvoie un provider déjà connu, on met à jour l'instance existante plutôt que d'en créer une nouvelle : ```javascript static findOrCreate(data) { if (Provider.cache[data.id]) Provider.cache[data.id] = new Provider(data); return Provider.cache[data.id]; } ``` C'est un pattern d'**identity map** — chaque objet n'existe qu'une fois en mémoire. Quand plusieurs vues référencent le même provider, elles pointent vers la même instance. Une mise à jour via l'API met à jour toutes les vues d'un coup, sans broadcast manuel. Et on économise des allocations mémoire — important sur des appareils avec 1 Go de RAM. ## Les directives Angular comme composants ### La philosophie On n'avait pas de "component library" en 2014 quand on a commencé Ionic. Angular 2 parle de composants, React parle de composants — en AngularJS 1, on a les **directives**. Le concept est le même : un élément d'interface encapsulé, avec son template, son style et sa logique. On a construit une trentaine de directives pour Hively. Chaque directive suit le même pattern : ``` Directive/ ├── Directive.directive.js # logique + lien scope ├── Directive.template.html # template HTML ├── Directive.scss # styles (BEM) └── images/ # assets propres ``` ### Le cache intelligent de l'autocomplétion La directive `AddressSearchField` interroge l'API Google Places à chaque frappe. Google facture ces appels. On a ajouté un cache en mémoire — si l'utilisateur tape la même requête deux fois (ce qui arrive souvent avec le backspace), on sert le résultat depuis le cache sans rappeler Google : ```javascript return { restrict: 'E', scope: , template: require('./AddressSearchField.template.html'), link(scope) { const _cache = ; const geocoder = new google.maps.Geocoder(); scope.search = (query) => { if (_cache[query]) geocoder.geocode(, (results, status) => { if (status === google.maps.GeocoderStatus.OK) { _cache[query] = results; scope.$apply(() => ); } }); }; scope.select = (place) => { scope.onChange(); }; } }; } ``` Le `require('./AddressSearchField.template.html')` est géré par le `html-loader` de Webpack — le template est inliné dans le bundle JavaScript. Pas de requête HTTP supplémentaire pour charger un template, pas de problème de chemin relatif dans Cordova. ## Ce qu'on en retient Six mois avec ce stack, voilà ce qu'on recommande à ceux qui s'y mettent : 1. **Webpack vaut l'investissement.** La courbe d'entrée est plus raide que Gulp — la documentation de Webpack 2 est franchement meilleure que celle de Webpack 1, mais on a commencé avant. Une fois en place, le gain est quotidien. On ne reviendra pas à Gulp. 2. **Babel + ES6 ne sont pas optionnels.** Écrire du JavaScript moderne, c'est écrire du code plus lisible et plus maintenable. Le coût de transpilation est négligeable. `babel-preset-env` est la bonne réponse à "quoi transpiler" — pas de config manuelle, juste des cibles. 3. **BEM + SCSS est le bon combo** pour un projet avec beaucoup de composants. Les CSS Modules viendront peut-être, mais en 2016, BEM reste la solution la plus pragmatique pour éviter les collisions. 4. **Flexbox est prêt pour la production mobile.** On n'a pas rencontré de bug bloquant sur iOS 8+ et Android 4.4+. Le layout est plus simple, plus prévisible, et plus facile à maintenir que les float-based layouts. Le JavaScript évolue vite — ES2016 vient de sortir avec `Array.prototype.includes()`, et les propositions TC39 avancent à un rythme soutenu. On utilise déjà des propositions stage 3 comme le spread sur les objets. La plupart des équipes front attendent que ces features "arrivent dans les navigateurs" — avec Babel, on les utilise en production aujourd'hui, sans compromis sur la compatibilité. Les outils sont là. Le State of JavaScript de cette année montre que Webpack a 1,5 fois plus d'intérêt que n'importe quel autre build tool. `babel-preset-env` rend la transpilation intelligente. PostCSS et Autoprefixer éliminent les préfixes vendeurs manuels. On n'a plus à choisir entre écrire du code moderne et supporter les vieux appareils — on fait les deux. Il n'y a plus d'excuse pour écrire du JavaScript de 2012.