Pourquoi j’ai abandonné Angular pour mon site

Frustration et sentiment d’échec. Voilà les sentiments qui m’habitent alors que j’entame l’écriture de ce billet. Suite aux trop nombreuses difficultés rencontrées en tentant de migrer ce site sur Angular, j’ai décidé de renoncer à l’utiliser. Quelle ironie pour un site consacré à l’apprentissage d’Angular…

Motivations pour migrer ce site vers Angular

Pourquoi j’ai entamé la migration de ce site vers Angular il y a quelques mois ?

Pour m’améliorer sur Angular

J’ai beau faire des formations et du consulting Angular, rien ne remplace le fait de mettre soi-même les mains dans le code pour bien apprendre une techno. Réaliser mon propre site avec Angular, c’était la garantie de rencontrer (et de devoir solutionner) des problématiques fréquentes de développement Angular. De plus, en créant un site web publique plutôt qu’une application, j’avais des contraintes de SEO et compatibilité qui sont des problématiques chères aux clients, mais peu présentes en formation.

Pour avoir une belle vitrine

En tant que formateur Angular, ça semblait cohérent que mon propre site web soit réalisé avec Angular. Cela aurait été une vitrine de mes compétences, et cela montrerait que je suis prêt à l’utiliser pour mes propres projets, et pas seulement comme un sujet de formation.

Pour avoir un site à l’UI hyper-réactive

J’ai utilisé le CMS Drupal pendant de nombreuses années. Si cet outil était parfaitement adapté à de la gestion de contenu, il ne l’était pas vraiment pour la création d’applications en ligne ou d’interfaces très “dynamiques”, avec des rafraîchissements partiels d’interface. (J’ai arrêté Drupal en 2012 et à l’époque, il n’intégrait que sporadiquement les rafraîchissements partiels.) J’étais donc séduit par l’idée de faire un site web avec une interface très réactive et dynamique.

Parce que ça ne semblait pas bien compliqué

Au moment de la migration, AngularChef ne comptait qu’une vingtaine de pages, essentiellement des tutos Angular, et tournait sur le générateur de site statique Hugo. J’imaginais que ça ne serait pas bien compliqué de reproduire le même niveau de fonctionnalité avec Angular, et même d’aller plus loin ensuite (en développant des applis support pour la formation, en créant une partie “pro” avec des contenus premium pour les utilisateurs payants du site, etc.).

Je me suis donc lancé dans la refonte d’AngularChef sous Angular avec confiance et motivation, sans aucune idée des déconvenues que j’allais rencontrer…

Difficultés rencontrées avec Angular (et quelques solutions)

Afficher du HTML qui vient du backend

Si afficher du contenu HTML venant de la base de données est une tâche triviale pour un CMS comme Drupal, c’est un chouia plus compliqué pour Angular.

Dans un composant, si on essaie d’interpoler une propriété contenant du HTML, les balises ne sont pas interprétées et affichées telles quelles à l’écran. On doit donc binder à la propriété innerHTML du DOM pour que le HTML soit interprété :

@Component({
  template: `
    {{ contenuHTML }}                      <!-- NON, les balises HTML sont échappées -->

    <div [innerHTML]="contenuHTML"></div>  <!-- OK, les balises HTML sont interprétées -->
  `
})
export class MyComponent {
  // Dans un vrai projet, ce contenu viendrait de la base de données.
  contenuHTML = '<p>Je suis du contenu <em>dynamique</em>.</p>';
}

Mais cette solution présente plusieurs limitations :

  • Le HTML bindé est “nettoyé” par Angular pour des raisons de sécurité.
  • Le binding à [innerHTML] force parfois à introduire une balise bidon (ici, une <div>...</div>).
  • Les syntaxes Angular comme *ngIf, *ngFor… ne seraient pas interprétées si la chaîne en contenait.

Voir #77 Afficher une chaîne contenant des balises HTML pour plus de détails sur ces points et quelques solutions de contournement.

Créer un composant dynamiquement

Pour garantir que toutes les balises HTML soient autorisées ET que les syntaxes Angular soient interprétées, j’ai donc voulu créer des composants dynamiquement.

L’idée est d’assembler à la volée un template contenant un mélange de texte, de HTML et de syntaxes Angular, et une classe contenant les données et les méthodes éventuellement requises par le template, et enfin de compiler le tout comme un composant Angular statique.

Gardez en tête qu’un simple lien vers une autre page nécessite d’utiliser la directive routerLink :

<a routerLink="/lien/vers/page">Lien</a>

Ce n’est donc pas du luxe que de pouvoir utiliser les syntaxes Angular directement dans son contenu. Dans mon cas, puisque je cherchais à utiliser Angular pour afficher des contenus de type CMS (articles, tutos…), il était certain que j’aurais besoin de ce genre de fonctionnalité. (Et faire des liens HTML classiques — <a href="/lien/vers/page">Lien</a> — n’était pas une option car cela déclencherait un rafraîchissement complet de page. Pas très SPA…)

J’ai donc trouvé une solution pour créer un composant dynamiquement, dont voici les grandes étapes :

  • Créer un composant programmatiquement.
  • Créer un NgModule programmatiquement, en y déclarant le composant créé à l’étape précédente.
  • Compiler le NgModule avec le compilateur Angular et récupérer la factory du composant.
  • Utiliser la factory du composant ainsi généré pour l’insérer dans le DOM.

Les détails d’implémentation sont présentés dans la recette #66 Créer un composant dynamiquement, et comme vous pourrez le voir, la “solution” possède de sérieuses limitations :

  • Pas compatible avec la compilation AOT, car il faut le compilateur Angular pour compiler les templates dynamiquement (et en AOT le compilateur n’est pas inclus).
  • Pas très performante, car il faut rester en compilation JIT ou trouver un moyen de ré-inclure le code du compilateur dans le build ; il faut aussi recompiler les composants dynamiques à chaque affichage.
  • Pas très robuste, car on utilise des API Angular de bas niveau et on ne gère ni les erreurs ni la sécurité.

De plus, je n’ai pas réussi à faire fonctionner cette implémentation de manière stable dans mon projet. Cela voulait donc dire que je n’aurais pas droit aux syntaxes Angular dans mes contenus, et donc que je n’aurais pas la possibilité de faire des liens routerLink entre les pages !

À ce stade, je commençais sérieusement à douter du choix d’Angular pour la refonte de mon site. Ce qui allait venir allait confirmer mes doutes…

Compatibilité avec les bots Facebook, Twitter…

Une problématique centrale de tout site web est d’être facilement crawlable par tous les bots et moteurs de recherche. Aujourd’hui, les moteurs de recherche comme Google sont capables d’exécuter le JavaScript et donc d’accéder (et d’indexer) au contenu d’un site qui tourne sur Angular. C’est un bon point.

En revanche, c’est une autre histoire avec les bots des réseaux sociaux. Quand vous partagez un lien sur Facebook ou Twitter (par exemple), un bot visite la page correspondante et tente d’en extraire des métadonnées (titre, description, image…). Malheureusement, contrairement au Googlebot, ces bots-là n’exécutent pas le JavaScript. Quand ils tombent sur un site propulsé par Angular, ils ne voient donc rien… (Ou plus exactement, ils voient le contenu du fichier index.html renvoyé par le serveur, qui ne contient qu’une balise <app-root> vide puisque JavaScript/Angular ne s’exécute pas.)

C’est assez pénalisant…

Des solutions de contournement existent. Elles consistent en gros à pré-générer la page côté serveur, de sorte qu’on soit capable de renvoyer une page HTML complète (pas seulement le “shell” vide de l’application) soit lors de l’accès initial au site, soit si on détecte que l’utilisateur est un bot.

Une première solution consiste à exécuter l’application Angular directement sur le serveur et à capturer le HTML généré pour le renvoyer au client (ce que fait Angular Universal). Une autre solution consiste à simuler un accès à l’application Angular via un navigateur virtuel et à enregistrer le HTML affiché dans ce navigateur pour le renvoyer au client (ce que fait Rendertron).

J’ai exploré ces deux solutions, mais je les ai trouvées pleines de limitations :

  • Pour exécuter une appli Angular côté serveur avec Universal, il faut respecter certaines manières de coder, éviter d’utiliser certaines APIs disponibles côté navigateur uniquement.
  • Certaines librairies JavaScript tierce-partie ne sont pas compatibles avec une exécution côté serveur. Exemple : Impossible d’utiliser AngularFire avec Universal…
  • Cela complexifie beaucoup le stack technique. D’une architecture serverless, on se retrouve à devoir installer/configurer un serveur avec Node.js (pour Universal) ou avec une image Docker (pour Rendertron). Dire que le choix initial d’Angular avait en partie pour but de simplifier le stack…
  • Les technologies comme Universal et Rendertron sont encore assez immatures, et on sent qu’il y a peu de documentation et encore pas mal de problèmes/bugs à régler.

Deuxième grosse déconvenue, donc. On dirait que j’allais devoir faire une croix sur un site optimisé pour les réseaux sociaux…

Compatibilité avec le plus grand nombre de navigateurs

“Est-ce que mon code tournera sur le plus grand nombre de navigateurs ?”

Cette question me revenait tout le temps pendant le développement. Puisqu’Angular s’exécute côté client, le spectre de compatibilité navigateurs est beaucoup moins large qu’un site généré côté serveur. C’est moins un problème quand on développe une appli interne (où le type de navigateur est davantage connu à l’avance), mais pas quand on développe un site web publique.

À chaque fois que j’utilisais une syntaxe JavaScript un peu moderne comme Array.prototype.find, je me disais “Je devrais vérifier si c’est supporté par Internet Explorer”. En l’occurrence, pour Array.prototype.find, la réponse est non :

Compatibilité navigateur de Array.prototype.find

C’est encore une fois assez pénalisant. Certes, cela peut être solutionné en utilisant une petite librairie comme Lodash qui ajoute une fine couche d’abstraction au-dessus des API inconsistantes des navigateurs. Mais cela rend le développement moins intuitif et moins agile.

Packager son code sous forme de librairie réutilisable

Ce dernier point était plus une “cerise sur le gâteau” qui ne faisait pas partie de mon cahier des charges initial.

Après quelques semaines de développement, je me suis retrouvé avec deux catégories de code :

  • Des fonctionnalités très génériques que je pensais pouvoir réutiliser dans d’autres projets Angular.
  • Des fonctionnalités spécifiques au site que j’étais en train de développer.

J’ai donc voulu packager le code générique dans une librairie Angular réutilisable. Cela me permettrait de mieux organiser le code du projet, et cela me donnerait un socle de code prêt à réutiliser pour les futurs projets Angular que je ne manquerais pas de faire. ;-)

Il se trouve que la création d’une librairie Angular est une tâche bien compliquée. On ne peut pas juste utiliser un projet CLI classique (ng new PROJECT) car la librairie est destinée à être consommée comme un “bout de code” dans un autre projet Angular, pas à exister en tant qu’appli exécutable autonome. La librairie ne doit pas embarquer le code d’Angular. La librairie doit être buildée dans les principaux formats de module (UMD, ESM2015…). La librairie doit être buildée en AOT. Etc.

Bref, il faut tenir compte de nombreux paramètres. Plus j’explorais le sujet, plus j’avais l’impression d’avoir mis le doigt dans un engrenage infernal. Pour compliquer le tout, de nombreux tutos trouvés sur le net ne marchaient pas (ou plus) et donnaient des conseils contradicatoires. J’ai donc perdu pas mal de temps… jusqu’à ce que je tombe sur ng-packagr.

ng-packagr permet de convertir un simple NgModule en librairie Angular. Il builde la librairie dans les principaux formats de module (UMD, ESM2015…). Et il respecte la recommandation officielle Angular Package Format.

Pour voir comment l’utiliser, consultez la recette #58 Créer une librairie Angular réutilisable (avec composants et services).

Si vous voulez créer une librairie Angular, je vous conseille vivement de partir d’un socle préconfiguré comme ng-packagr plutôt que de tout paramétrer vous-même. À partir du moment où j’ai passé mon code sur ng-packagr, j’ai eu une librairie prête fonctionnelle en quelques heures. Mes réserves sur ng-packagr sont la jeunesse du code (version 1.x à l’époque où je l’ai utilisé) et la documentation éparpillée dans les issues Github du projet. Mais c’est une librairie prometteuse, en développement actif.

Sur le point “librairie réutilisable”, j’ai donc obtenu une solution satisfaisante, même si j’ai perdu énormément de temps à trouver la solution et ensuite à la mettre en oeuvre (dans la pratique, il y a eu des tas de petits bugs à résoudre, et il a fallu refactoriser tout mon code pour tenir compte de la nouvelle architecture).

Conclusion : Faut-il abandonner Angular complétement ?

Vous l’avez sûrement compris, j’ai rencontré trop de points bloquants au cours de la migration de ce site sous Angular pour persister dans cette voie.

J’ai donc abandonné Angular (et je suis revenu sur Hugo), notamment pour les raisons suivantes :

  • L’impossibilité d’utiliser les syntaxes Angular dans le contenu (et notamment la directive routerLink pour faire des liens entre les pages du site).
  • La complexité pour rendre le site indexable par les bots Facebook et autres.
  • Une compatibilité non garantie avec certains navigateurs si on a la malchance d’utiliser une API JavaScript non supportée.

Notez que ces limitations sont assez spécifiques à mon cahier des charges, qui était de créer un site web de contenu avec Angular. Ce n’est pas le cas d’usage le plus répandu, Angular étant plus adapté à la création d’applications web. Dans ce cas certaines contraintes disparaissent complétement (comme la nécessité de rendre le contenu indexable par les bots des réseaux sociaux).

Au final, je tire deux grandes conclusions de cette expérience :

1) J’ai bien approfondi mes connaissances sur Angular, et notamment sur l’architecture d’une application de taille moyenne à importante. J’ai compris bien plus en profondeur comment les NgModules servent à organiser le code d’une application Angular et comment le fait de packager une partie de son code dans une librairie réutilisable peut être judicieux.

2) Certaines parties de l’écosystème Angular manquent encore de maturité. Si le framework lui-même est aujourd’hui bien stable (et nettement plus carré que son prédécesseur AngularJS), ce n’est pas le cas de tout l’outillage qui gravite autour. Cela dit, si l’on regarde la direction prise par Angular CLI, qui a commencé comme un petit utilitaire sur le côté et qui est devenu aujourd’hui un incontournable du développement Angular, il faut espérer que les autres briques de l’écosystème suivront la même direction, notamment Angular Universal, et toutes les technologies permettant de builder et déployer plus facilement ses applications Angular.

comments powered by Disqus