On passe au front ?
Maintenant qu’on a une super API REST qui tient la charge, si on passait à un front auquel on tenterait d’appliquer les même contraintes ?
Ici on va tenter de créer un projet Angular classique, le faire héberger par Firebase Hosting et comme on aimerait que le site soit indexé correctement par les bots on va utiliser Firebase Cloud Functions pour faire du server-side-rendering.
Les sources du projet sont disponibles sur Gitlab : https://gitlab.com/gautier-levert/firebase-universal
Création du projet
Pour créer le projet Angular, sans surprise on va utiliser Angular CLI
|
|
Mon objectif n’est pas de vous montrer tout le fonctionnement d’Angular, du coup si vous avez besoin d’un tuto sur le sujet je vous renvoie vers le tutoriel officiel.
Ce qui nous intéresse ici c’est l’hébergement et on va avoir besoin de créer un projet sur la console Firebase.
La création d’un projet est assez guidée et ne demande que quelques clics, je pense que vous saurez vous débrouiller pour arriver à cet écran :
Configuration du déploiement
Les choses sérieuses commencent : on a notre application Angular et on a un projet Firebase pour la déployer.
Pour cette étape vous aurez besoin du CLI de Firebase.
Pour le connecter à son compte Google on utilise la commande :
|
|
Une fois le CLI bien installé et configuré on se place à la racine du projet Angular et on lance la commande :
|
|
Et là tout est guidé, on sélectionne donc Hosting
puis Use an existing project
et enfin on sélectionne le nom de notre projet Firebase.
Vient maintenant le moment de la configuration du dossier public, il va falloir changer la configuration par défaut par votre dossier de build.
Par défaut il s’agit de dist/$nom-du-projet
(remplacer $nom-du-projet
par le nom de votre projet Angular).
On peut retrouver ce nom de dossier dans le fichier angular.json
à l’arborescence projects > architect > build > options > outputPath
.
À la question Configure as a single-page app (rewrite all urls to /index.html)?
il faut répondre oui, c’est important pour que le routing Angular fonctionne correctement.
Et enfin ce n’est pas la peine de remplacer le fichier index.html
.
Voilà ce que ça donne sur mon projet :
Bon bah plus qu’à déployer :
|
|
et voilà c’est en ligne : https://angular-universal-f04ce.web.app/ simple, non ?
L’indexation Google
Le vrai problème des applications web dont le rendu est fait par JavaScript (comme Angular, React, Vue, etc) c’est leur indexation par Google (et les autres bots).
Le bot est capable d’exécuter un peu de JS mais les scripts seront interrompus au bout de quelques secondes et il y a très peu de chances que votre site fonctionne correctement dans ces conditions.
Heureusement, ces frameworks ont la solution : faire le rendu des pages côté serveur (ou server side rendering avec votre plus bel accent anglais) ! Dans le cas d’Angular, cette technique porte le nom de Angular Universal.
Pour ajouter Angular Universal dans notre projet on se place dans la dossier racine et on tape la ligne suivante (attention de bien remplacer firebase-universal
par le nom de votre projet, celui de ng new
)
|
|
Cette feature rajoute pas mal de fichiers, il s’agit du code de configuration du serveur express pour le rendu des pages.
Essayons maintenant de tester tout ça avec yarn
|
|
ou si vous utilisez npm :
|
|
Maintenant vous pouvez lancer votre navigateur préféré, vous rendre sur l’URL donnée : par défaut http://localhost:4000/ Vous pouvez même essayer de vous rendre sur la page en désactivant complètement JavaScript et le rendu serait le même.
Faire tourner sur Cloud Functions
Si vou possédez votre propre serveur équipé de node, vous pouvez donc vous contentez de faire tourner cette application et votre problème est déjà résolu.
Mais on veut aller plus loin, on ne veut pas avoir à gérer de serveur. On va donc configurer Cloud Functions pour faire tourner tout ça.
|
|
On sélectionne Cloud Functions et on répond aux questions du wizard. J’ai choisi ici de coder la fonction en TypeScript mais ce n’est pas du tout obligatoire.
On se retrouve avec un nouveau dossier functions
c’est un nouveau module node dans lequel on va écrire le code qui
sera déployé sur Firebase.
Si comme moi vous utilisez yarn
, il est probable que yarn install
ne fonctionne pas pour des histoires de version de
node incompatible (CLoud Functions tourne actuellement avec node 10 tandis que la dernière version stable en date est la 14).
Pour régler ce problème on peut soit exécuter yarn avec une version de node 10 ou alors forcer l’installation yarn install --ignore-engines
.
Je n’ai jamais rencontré de problème de compatibilité mais on ne sait jamais.
Maintenant que tout ça est en place on va pouvoir commencer à coder la fonction à proprement parler.
Changer la configuration Angular
Je vais faire en sorte que la compilation des bundles Angular génère les fichiers dans un dossier à l’intérieur du module functions
Je change donc les valeurs projects > firebase-universal > architect > build > options > outputPath
par "functions/lib/dist/browser"
et projects > firebase-universal > architect > server > options > outputPath
par "functions/lib/dist/server"
.
Attention, ici firebase-universal
correspond au nom de mon projet et sera certainement différent pour le votre.
Donc à partir de maintenant, quand one génère les bundles à l’aide de la task build:client-and-server-bundles
on vient
peupler un dossier lib
à l’intérieur de functions
. Ce qui permet d’accéder à ces bundle depuis notre fonction de rendering.
Changer la configuration Firebase
Il faut également mettre à jour le fichier firebase.json
et changer le dossier à uploader pour la partie Hosting hosting > public
pour y mettre le dossier du bundle browser "functions/lib/dist/browser"
et on vient enfin modifier la partie hosting > rewrites
pour demander l’exécution de la fonction ssr
. Et enfin changer le script de déploiement pour être sûr de générer les bundles.
Avec toutes ces modifications, on se retrouve avec quelque chose comme ça :
|
|
Coder la fonction ssr
Pour cette partie on doit gérer les dépendances du module functions
. Pour ça, la seule manière que j’ai trouvé pour
obtenir quelque chose de fonctionnel est de copier les dépendances depuis le fichier package.json
du projet racine et
de les coller dans le fichier package.json
du module functions
.
Enfin on code la fonction :
|
|
On voit que ce fichier reprend presque entièrement le fichier server.ts
qui se trouve à la racine du projet. On a
simplement mis à jour les chemins des dossiers et remplacé le fait d’écouter sur un port de la machine locale par
la définition d’un handler de Cloud Functions.
Bonus : rendu du chemin racine
Par défaut, quand on demande un fichier existant dans la partie Hosting de Firebase il nous est renvoyé directement.
C’est pratique, ça nous évite de payer une utilisation de notre Cloud Function pour juste servir des fichiers statiques.
Ça devient par contre plus embêtant quand un lien pointe sur la racine du site : Firebase va alors fournir le fichier index.html
sans le rendu côté serveur !
Pas de panique, j’ai la solution et elle est plutôt simple. Il suffit de demander à firebase d’ignorer le fichier index.html
lors de l’upload de la partie hosting (tout en le conservant dans la partie Functions).
Dans le fichier firebase.json
|
|
Bonus : ne faire le rendu que pour les bots
Une astuce pour éviter de consommer des ressources Cloud Functions inutilement, c’est d’éviter d’effectuer le rendu côté serveur quand c’est inutile (donc quand le client n’est pas un bot).
Il suffit de modifier un peu la fonction functions/src/index.ts
:
|
|
On voit que le code n’est pas très compliqué, si on considère que le client est un bot alors on fait le prérendu du template,
sinon on sert le fichier index.html
directement en s’épargnant le processus de rendu.
Et voici la méthode isBot
qui est sans doute améliorable mais remplit plutôt bien sa fonction :
|
|
Déploiement
Si tout va bien, vous n’avez plus qu’à taper la commande firebase deploy
à la racine du projet pour tout mettre en ligne
et laisser la magie opérer.