AngularJS: `template` vs` templateUrl`

Au cours des derniers mois, j'ai étudié diverses manières d'améliorer les performances d'exécution du gigantesque SPA sur lequel je travaille chez Domo. Nous avons réalisé de sérieux progrès, mais avec un million de lignes de code dans un même SPA, certains changements ne sont pas toujours faciles. Un des membres de notre équipe a fait la découverte pour aider à ajouter du lazyloading aux projets AngularJS, et nous avons beaucoup investi dans ce domaine. Quelques membres différents de l’équipe (Jason et Tim) ont décidé de nous aider à mesurer le temps nécessaire à l’initialisation complète de notre application. Nous avons également utilisé webpack pour rationaliser la construction et modifier certains modèles que nous utilisons. En combinant webpack avec ocLazyload, nous avons trouvé un sérieux gain pour les projets AngularJS.

La semaine dernière, j'ai entrepris de modifier toutes les déclarations de modèle de composant / directive et de passer de templateUrl à template. Au lieu de déplacer manuellement tous les modèles de leurs fichiers .html distincts vers leurs fichiers JS respectifs, nous avons décidé d'utiliser un chargeur de pack Web et les requièrent sous forme de chaînes en ligne. Afin de mieux l'expliquer… laissez-moi vous montrer ce que je veux dire. Voici un exemple de composant AngularJS:

Comme vous pouvez le constater, dans le premier exemple, un composant utilise un templateUrl pour le charger. C'est au mieux problématique, IMO. Cela signifie que vous devez déployer le fichier foo / bar / myComponent.html en production afin que votre application de production puisse charger le fragment de modèle via une seconde demande réseau pour l'obtenir, OU cela signifie que vous devrez ajouter une construction. étape qui trouvera toutes les instances de templateUrl et amènera ces modèles dans AngularJS templateCache. Ces deux solutions ont des problèmes.

Les problèmes avec le premier sont évidents: si tous vos modèles en production nécessitaient une requête réseau distincte pour les obtenir, le chargement d’une vue unique nécessiterait alors N requêtes réseau pour obtenir toutes les vues, N étant le nombre de composants / directives / ngIncludes à votre vue.

Le problème avec le second problème est que les étapes de construction, bien que très pratiques, chargeront tous vos modèles dans votre bundle Webpack principal. Cela signifie que même lorsque vous avez l'intention de charger un composant, ou une section entière de composants, leurs modèles seront toujours chargés avec votre bundle principal. Vous ne pouvez donc pas profiter pleinement des avantages de lazyloading.

Compte tenu des centaines et des centaines de modèles que nous avons dans notre projet, aucun de ceux-ci était réalisable. Nous avions besoin de quelque chose d'autre. Nous avions besoin de quelque chose qui nous permettrait de charger nos modèles efficacement, sans requêtes réseau séparées pour chacun, tout en nous permettant également de charger complètement ces mêmes modèles. Nous avons donc décidé d'utiliser un chargeur Webpack qui nous permettrait d'exiger l'intégration de nos modèles dans nos composants sous forme de chaînes en ligne de modèles HTML / angulaires.

Les avantages

En utilisant le webpack html-loader pour charger tous les fichiers .html, nous avons découvert que nous pouvions charger efficacement nos modèles, tout en nous permettant de tirer pleinement parti du lazyloading. Lorsque vous utilisez la syntaxe template: require ('foo / bar / my.html'), webpack remplace votre instruction require par une fonction appelée et renvoyée avec la chaîne correspondant au modèle. Étant donné que le modèle est maintenant fourni en tant que chaîne html, si vous lazylez chargez le composant, le modèle sera également chargé. C'est exactement ce dont nous avions besoin. Cependant, nous avons découvert plusieurs autres avantages, dont la découverte a incité ce post.

  • Initialisation plus rapide des composants - Lorsque vous utilisez une chaîne en ligne comme modèle, le composant peut être initialisé de manière synchrone. En utilisant templateUrl, AngularJS demandera le modèle à templateCache. Puisque templateCache peut déjà avoir le modèle dans son cache, ou peut-être avoir besoin de se rendre sur le réseau pour l'obtenir, demander un modèle à partir du cache est un processus qui se déroule de manière asynchrone. Même si le modèle est déjà dans le cache, templateCache renverra le modèle déjà mis en cache via un appel basé sur une promesse. Cela signifie que le composant ne peut pas s'initialiser dans la même boucle d'événement. La demande à templateCache sera toujours placée sur la prochaine boucle d'événements, même dans le meilleur des scénarios. Cela signifie que le composant peut commencer à s’initialiser, demander son modèle, puis terminer son initialisation dans la boucle d’événement suivante. Mais lorsque vous utilisez une chaîne en ligne, le composant a déjà son modèle prêt, de sorte qu’il puisse commencer et terminer son initialisation dans la même boucle d’événements. Cela peut ne pas sembler significatif, mais il a eu plusieurs résultats inattendus que nous avons dû compenser.
    - Les composants s’initialisent plus rapidement - ce qui semble génial, AIR? C'est génial. Toutefois, cela signifie que certains de vos composants dont les valeurs d'entrée ont toujours été définies lorsque leur initialisation risque de se rompre risquent de ne pas être présents. Nous avons eu plusieurs ruptures de composants, dues à des valeurs de liaison d'entrée non définies. Nous avons dû modifier ces composants pour utiliser $ watch ou $ onChanges afin de détecter la mise à jour des valeurs d'entrée.
    - Les tests unitaires se dérouleront différemment - Etant donné que les tests d'écriture changent lorsque vous effectuez un test synchrone par rapport à un test asynchrone, le test de ces composants peut définitivement changer. Par exemple, dans Mocha, si votre test est asynchrone, vous injectez la méthode done dans votre test et vous l'appelez une fois le test terminé. Nous avons constaté que les tests fonctionnaient maintenant de manière synchrone, ce qui signifiait que la nécessité d'une injection effectuée n'était plus nécessaire. De plus, et il est embarrassant d’admettre cela, mais nous avions des tests écrits de manière synchrone, cependant, les modèles étant asynchrones, ces tests n’étaient JAMAIS complétés avec succès. Ainsi, lorsque j'ai validé les modifications apportées aux modèles en ligne, ces tests ont commencé à s'exécuter avec succès et au lieu de réussir, ils ont échoué! Au début, je pensais avoir passé tous ces tests. Ce n’est qu’après 5 heures de fouilles que je me suis rendu compte que ces tests ne passaient jamais. Nous avons donc réellement augmenté la couverture de test maintenant que nous utilisons des modèles en ligne.
  • html-loader utilise un minifier html - Ce petit fait a instantanément réduit la taille de nos modèles de 19% dans l'ensemble de l'application. C’est tellement remarquable et c’est vraiment quelque chose que nous aurions dû faire depuis longtemps. Il analyse également les modèles et nous aide à trouver quelques dizaines de modèles contenant du code HTML non valide. Des choses comme: classe "blah", où le = était manquant. Ou attribut = {{quelque chose}}, qui manque les guillemets autour de tout. Une fois que j'ai corrigé ceux-ci, la construction a encore fonctionné.
  • Les paquets ng-include étaient toujours en panne - Alors que les modèles de composants fonctionnaient maintenant, les fichiers ng-include étaient maintenant en panne. Nous devions trouver quelque chose pour eux. Nous avons donc créé un petit chargeur personnalisé, qui importera le modèle dans templateCache. Nos pratiques internes nous disent de ne pas utiliser ng-include, mais nous avons toujours beaucoup de code vieux de plus de 3 ans qui les contient. Donc, plutôt que de refactoriser tout cela dans ce commit, j'ai utilisé ce nouveau chargeur, et je suis allé dans chaque section de l'application qui a un ng-include et chargé le modèle pour cette section, comme je l'ai montré ci-dessous. Cela signifie que les ng-include sont également pris en charge dans ce nouveau processus.

JSCodeShift utilisé

Je recommande vivement d'utiliser webpack pour les applications AngularJS et d'utiliser html-loader pour intégrer vos modèles en ligne, ce qui signifie que vous devrez modifier vos instances templateUrl en instances de modèles. Comme ils ont tous une apparence très différente, j’ai décidé que c’était un très bon scénario pour JSCodeShift, un projet de Facebook qui vous permet d’analyser l’AST et de remplacer par programmation toutes les instances pour vous. Vous pouvez le considérer comme Trouver et remplacer sur des stéroïdes injectés avec plus de stéroïdes. C'était très simple d'écrire le script qui a trouvé et mis à jour toutes les utilisations de templateUrl: ‘some / url / to.html avec template: require (). J'ai pu modifier 90% des utilisations par programme (environ 700 fichiers) et j'ai dû terminer les 70 derniers à la main. J'aurais pu écrire le code pour finir les 70 autres, mais je me suis dit que je pourrais les faire plus facilement à la main qu'en essayant de les coder individuellement. Une note rapide, l'AST Explorer est un must absolu lorsque vous utilisez JSCodeShift. Sans cela, je n’aurais pas pu progresser.

Conclusion

Obtenez vos applications AngularJS sur une version de WebPack, et prenez le temps de les utiliser pour charger html-loader. Utilisez template au lieu de templateUrl, et si ce n’est déjà fait, arrêtez d’utiliser ng-include. Et puis, lazyload, lazyload, lazyload! Parfois, les gens font la distinction entre chargement différé et chargement paresseux. Je fais référence à la fois au chargement différé et au chargement paresseux quand je dis «lazyload». C’est votre meilleur changement pour réduire le temps consacré à la première peinture utile, et pour disposer d’une application avec laquelle l’utilisateur peut interagir. Bonne chance. Retour sur tes têtes!