Il y a 7 ans -
Temps de lecture 6 minutes
Lazy loading avec WebPack & AngularJS
Les outils de packaging web adoptent généralement deux approches opposées. Soit comme gulp et autre grunt, ils construisent un seul gros fichier concaténé et minifié, ce qui résulte en un temps de chargement initial important, notamment sur les supports mobiles. Soit une requête par fichier, comme require.js, ce qui entraîne le surcoût des requêtes et la latence lors du chargement de chaque page.
Seul Webpack fait les choses correctement !
Dans cet article je montre un cas pratique d’utilisation du lazy loading des ressources AngularJS avec Webpack.
Actuellement, je travaille pour une startup qui développe une solution de gestion de voitures connectées. Le début du projet remonte à la deuxième moitié de 2014. La stack front-office est classique pour cette « époque » : AngularJS 1.x en ES5 construit par Gulp, les dépendances gérées par Bower, feuilles de styles écrites en SASS.
Au fil des sprints, la base de code de l’application AngularJS a beaucoup grossi et l’importance des dépendances a augmenté de façon significative.
L’ensemble du code applicatif : Javascript, templates html ainsi que les bibliothèques externes forment un seul package « indivisible » . Même minifié, sa taille dépasse les 3 Mo. Cela peut sensiblement dégrader l’expérience utilisateur lors de l’ouverture d’application, surtout sur les terminaux mobiles et les réseaux 3G.
En fonction du rôle attribué à l’utilisateur, il accède à un sous-ensemble de l’application. La version mobile, implémentée en Responsive Web Design (RWD), donne accès à une page unique de l’application. Cependant, dans tous les cas, l’utilisateur télécharge l’ensemble de l’application dont une grande partie ne sera jamais exécuté.
Webpack à la rescousse
Si vous ne connaissez pas Webpack, je vous recommande vivement de lire l’excellent article Webpack, ES6 (ES2015) & Babel 6 pour modulariser son application Angular sur notre blog.
La plupart des articles qui expliquent comment combiner Webpack avec AngularJS laissent de côté un aspect important de Webpack : le Lazy Loading des dépendances, qui permet de charger uniquement les ressources nécessaires pour l’affichage d’une page ou de l’ensemble des pages liées fonctionnellement.
Webpack permet de configurer de multiples points d’entrées dans l’application. Ensuite, il construit une arbre de dépendances permettant de grouper et de récupérer uniquement les ressources nécessaires à chaque point d’entrée ainsi que les ressources communes aux différents points d’entrée. Chaque bloc de ce type est appelé bundle.
AngularJS
Cette approche n’est pas compatible avec AngularJS qui attend une déclaration de l’application à l’aide d’une directive ng-app. Heureusement, Webpack est très flexible et permet de déclarer les points de rupture en runtime dans le code :
[js]class Greet {
constructor() {
this.name = ‘I\’am Groot!’;
}
tell() {
console.log(‘Greet: ‘ + this.name);
}
}
module.exports = Greet;[/js]
[js]app.controller(‘appCtrl’, () => {
require([‘./Greet.js’], (Greet) => {
var greet = new Greet();
greet.tell();
});
});[/js]
La syntaxe require(['file.js'], function(file) {})
permet d’indiquer à Webpack que la liste de fichiers spécifiés ainsi que leurs dépendances doivent être placés dans un bundle suivant :
Ici, 1.bundle.js
contient uniquement la classe Greet, et bundle.js
– le reste de l’application.
Problème…
J’ai rencontré un autre problème lorsque j’ai essayé de charger de la même façon un contrôleur ou un service d’AngularJS. Les dépendances chargées dynamiquement n’étaient pas visibles par le framework.
AngularJS n’a pas été conçu pour charger les dépendances dynamiquement. Une fois que l’application est initialisée – phase de configuration passée – la déclaration des nouveaux composants n’est plus possible de façon habituelle. C’est le cas pour tous les building blocks d’Angular: modules, services, contrôleurs et autres directives.
Heureusement, il y a un moyen de surmonter les limitations du framework. L’astuce est de rendre disponible en runtime les providers
qui servent à déclarer les ressources AngularJS. Normalement, ces providers
sont disponibles uniquement en phase de configuration.
Voici l’exemple du code permettant de faire l’ajustement :
[js]app.config(($controllerProvider, $compileProvider, $filterProvider, $provide) => {
app.register = {
controller: $controllerProvider.register,
directive: $compileProvider.directive,
filter: $filterProvider.register,
factory: $provide.factory,
service: $provide.service
};
}
);[/js]
Par la suite :
[js]app.register
.service(‘aboutService’, AboutService)[/js]
Notez que je passe par l’objet register
créé précédemment pour déclarer un service.
Faire ce genre de manipulations manuellement est plutôt fastidieux. De plus, il est impossible de charger dynamiquement un module entier.
Heureusement, il existe une bibliothèque ad hoc qui a pour but de télécharger les modules AngularJS et de les déclarer dynamiquement: oclazyload. Étant donné que Webpack prend déjà en charge le chargement des ressources, nous utiliserons le oclazyload
uniquement pour la déclaration dynamique des modules :
[bash]$ npm install oclazyload –save[/bash]
Pour le routage nous utilisons, comme la plupart des projets Angular 1.x, une excellente bibliothèque : ui-router. Notez que les exemples suivants doivent également fonctionner avec le routeur
de base.
L’idée principale est d’organiser chaque state
(ou route) de l’application en modules angular qui seront chargés dynamiquement et indépendamment l’un de l’autre. Tout le découpage de code en « chunks » se fera au niveau de la configuration du routeur – très localisé et sans le boilerplate indésirable :
[js]import angular from ‘angular’;
import uirouter from ‘angular-ui-router’;
import oclazyLoad from ‘oclazyload’;
angular.module(‘app’, [oclazyLoad, uirouter])
.config(function routing($stateProvider) {
$stateProvider
.state(‘hello’, {
url: ‘/hello’,
template: require(‘./hello/hello.html’),
controller: ‘helloCtrl as ctrl’,
resolve: {
loadModule: ($q, $ocLazyLoad) => {
return $q((resolve) => {
require([‘./hello/hello.module.js’], (module) => {
resolve($ocLazyLoad.load({name: module.default}));
});
});
}
}
})
.state(‘about’, {
// …
});
});[/js]
Le module app.hello
n’a rien de particulier :
[js]export default require(‘angular’)
.module(‘app.hello’, [])
.name;
require(‘./hello.ctrl’);[/js]
Le code de ce module ainsi que ses dépendances éventuelles seront chargés dynamiquement et en un seul bundle uniquement quand l’utilisateur accédera à /hello
.
Lors de la connexion initiale à /about
:
bundle.js
– contient l’application angular avec les dépendances communes (angular.js, ui-router, etc.)2.bundle.js
– moduleapp.about
chargé dynamiquement
Quand nous allons sur /hello
:
1.bundle.js
– contient le module'app.hello'
avec ses dépendances.
Conclusion
La technique de lazy loading n’est pas indispensable pour toutes les applications. L’amélioration de l’expérience utilisateur sera surtout notable dans le cas des grosses applications qui comptent plusieurs dizaines de dépendances et qui sont accessibles via les terminaux mobiles. Cette solution présente aussi un intérêt lorsque les fonctionnalités accessibles varient beaucoup selon les rôles des utilisateurs.
Si vous pensez que votre application a besoin de la gestion de dépendances chargées à la demande, Webpack avec son bundling intelligent est un outil idéal pour votre application AngularJS.
Le code de l’application est disponible sur notre github.
Commentaire
0 réponses pour " Lazy loading avec WebPack & AngularJS "
Published by Maxime Gris , Il y a 7 ans
Bon article sur le lazy loading.
Trois questions (liées) me viennent à l’esprit :
Comment se passe la phase de création du projet pour production? Les dépendances définies en lazy loading peuvent être regroupées et minifiées? Ou doit-on subir un appel ajax pour chacune d’elles? (Pas tip top non plus)
Published by Disfigure , Il y a 7 ans
@Maxime Gris
Les dependences en lazyloading je cite
« Soit comme gulp et autre grunt, ils construisent un seul gros fichier concaténé et minifié, ce qui résulte en un temps de chargement initial important, notamment sur les supports mobiles »
« L’ensemble du code applicatif : Javascript, templates html ainsi que les bibliothèques externes forment un seul package « indivisible » . Même minifié, sa taille dépasse les 3 Mo. Cela peut sensiblement dégrader l’expérience utilisateur lors de l’ouverture d’application, surtout sur les terminaux mobiles et les réseaux 3G. »
« La technique de lazy loading n’est pas indispensable pour toutes les applications. L’amélioration de l’expérience utilisateur sera surtout notable dans le cas des grosses applications qui comptent plusieurs dizaines de dépendances et qui sont accessibles via les terminaux mobiles. Cette solution présente aussi un intérêt lorsque les fonctionnalités accessibles varient beaucoup selon les rôles des utilisateurs. »
Resumé en gros faut lire de A à Z,
by the way très bon article sur un point tres injustement oublié ou carrement méconnu des autres blogs owner.
Regards.