Introduction au WebAssembly

Salut !

Si on discutait un peu de WebAssembly aujourd’hui ? J’ai très envie d’en parler parce que le concept a un potentiel monstrueux et qu’en plus c’est déjà une norme du web depuis 2019.

Posons-nous un peu. Enormément de programmes (avec un big up pour les jeux vidéos) sont codés en C/C++. Le Javascript est écrit en C++. Sublime Text est en C++. Les moteurs graphiques sont en C++. Pourquoi ? Parce qu’il faut bien se dire que même si le langage est complexe et très bas niveau, c’est tout de même une bombe de puissance niveau performances (tant que c’est correctement écrit mais ça s’applique à tous les langages…). Le Rust fait également doucement son trou, certains et certaines le considérant comme étant le digne héritier de C/C++. De l’autre côté, les applications web s’imposent comme étant aujourd’hui un incontournable avec toutes les technologies géniales qui émergent autant en front que en back, avec l’explosion du Javascript, du NodeJS, etc.

Le souci, c’est que le C/C++ ou le Rust ne sont pas des langages web, et que Javascript ne les dépassera pas niveau performance ou sécurité sur les developpements (typage fort etc). Et si..et si on arrivait à combiner les deux ? Mesdames, mesdemoiselles, messieurs, chers lecteurs, je vous présente le WebAssembly.

Présentation du WebAssembly

WebAssembly c’est avant tout un langage pouvant être éxecuté dans les navigateurs modernes (IE non compris on s’entend). Il correspond à un bytecode (fichier binaire) obtenu après compilation en WebAssembly d’un langage tier, principalement C/C++ ou Rust mais il en existe d’autres. Comme le bytecode est particulièrement bas niveau, cela permet aux navigateurs d’accéder à des performances très élevées.

Plus concrètement, les applications web vont encapsuler via Javascript un fichier .wasm contenant le bytecode qui sera compilé en langage machine en temps réel au fur et à mesure de sa lecture. Rien que ça, ça en jette. Ils appellent cela la Streaming Compilation. Le module WebAssembly va exposer une fonction que Javascript pourra alors utiliser. Ce qui est génial c’est l’avancée que cela représente par rapport à l’ancien existant, je m’explique :

Sans parler de WebAssembly, on sait compiler un fichier C ou Rust en JS mais le JS lui est compilé en just-in-time pour obtenir le langage machine (dans le cas des navigateurs modernes). Cette compilation just-in-time ne peut donc pas commencer tant que les fichiers externes n’ont pas été entièrement téléchargés. C’est le drame niveau performance et on perd tout l’interêt du JIT.

La Streaming Compilation, elle, permet de compiler le bytecode WebAssembly en langage machine au fur et à mesure de la lecture des octets. C’est donc du temps réel et une amélioration très importante des performances et des temps de démarrages. De cette façon, il est possible de faire tourner dans le navigateur du code autre que Javascript à une vitesse proche de celle du processeur.

Schéma

Il y a plusieurs façon d’utiliser le WebAssembly. Comme je le disais plus haut, il s’agit avant tout d’un langage et comme tout langage il est simplement possible de l’écrire. Je vous redirige vers la documentation de Mozilla si vous souhaitez l’apprendre.

La manière la plus courante pour le moment reste l’utilisation de l’outil Emscripten. Il s’agit d’une chaine de compilation basée sur LLVM (Low Level Virtual Machine). Cette dernière est une infrastructure de compilateurs permettant de créer une machine virtuelle, un générateur de code et des optimiseurs de compilation. On notera que la LLVM propose entre autre des compilateurs à la volée (le fameux Just-In-Time). Emscripten va donc compiler le code source C/C++ en WebAssembly (dans un module .wasm) et générer le code Javascript encapsulant le module. Enfin il est capable de créer de l’HTML pour le rendu visuel.

Emscripten a donc 3 rôles principaux :

  • ● Envoyer le code C/C++ à LLVM
  • ● Compiler ce code en WebAssembly binaire (là nous obtenons notre .wasm)
  • ● Générer du HTML avec le code Javascript appelant le module WebAssembly.
  •  

En quelque sorte à ce moment là, le Javascript sert d’intermédiaire car le WebAssembly ne sait pas appeler l’API Web. Il a donc besoin de cet intermédiaire pour intéragir avec le navigateur.

Je vous fais le schéma :

 

Enfin il y a la manière toute simple d’utiliser un outil en ligne qui compilera comme un grand votre code en WebAssembly textuel et/ou binaire. Il ne reste plus qu’à utiliser le fichier .wasm téléchargé à partir du résultat. Vous pouvez puncher ce lien si vous souhaitez un exemple.

Cas d’utilisation

Nous pourrions nous dire: vu que la Streaming Compilation est aussi cool, pourquoi ne pas l’utiliser tout le temps ? Je répondrais que c’est une problématique déjà connue dans bien des technologies: ce n’est pas parce qu’on peut l’utiliser qu’il faut l’utiliser. Dans mon second article sur les choix techniques, je disais que chaque technologie correspond à un besoin avec ses spécificités, ses forces et ses faiblesses. Cela vaut également pour le WebAssembly.

Dans beaucoup de cas, il sera plus simple d’écrire directement du JS au lieu de devoir par exemple embarquer des libs standards de C, de devoir gérer les allocations et fuites mémoire et de devoir redéfinir un protocole ou la classe Array alors que le Javascript gère tout cela nativement. C’est donc une solution viable pour gérer des soucis de performances ou répondre à un besoin que le C/C++ sait mieux gérer que le Javascript.

On peut donc lister non exhaustivement quelques cas privilégiés d’utilisation du WebAssembly:

  • ● Traitement d’image
  • ● Reconnaissance d’image
  • ● Deep learning
  • ● Jeux vidéos : Que ce soit de petits jeux occasionels, des jeux à gros budgets classés AAA ou des moteurs de jeux en ligne…
  • ● 3D de manière générale (Réalité augmentée, simulation, réalité virtuelle)
  • ● Applications collaboratives
  • ● Outils de développement informatique

 

Et il existe un cas qui je trouve particulièrement intéressant: La réutilisation de code critique lors d’une migration. De nos jours, les applications vieillissantes ont de plus en plus tendances à être migrées sur des clients web légers. Cependant, le code peut parfois être critique, certains algorithmes particulièrement complexes et difficile à réécrire, ou demandant de la puissance. On peut également parler des codes sources non documentés dont le reverse engineering serait abominable. Bref cela pourrait clairement baisser significativement des coûts de migrations. Intéressant non ?

Les grands principes du langage

Le WebAssembly est conçut pour répondre a certains principes.

Pour commencer il doit être un langage rapide et portable. Nous l’avons vu, il peut être éxecuté une vitesse proche de celle du CPU et s’inscrit sur plusieurs plateformes embarquant l’API WASM pour Javascript. Il faut tout de même noter que certains environnements d’éxecution n’offrant pas les caractéristiques requises selon la documentation officielle peuvent dans certains cas avoir à émuler un environnement d’éxecution afin que les modules puissent s’éxécuter comme si l’hôte ou le système d’exploitation les prenait correctement en charge. Dans ce genre de cas, cela peut entraîner de mauvaises performances.

Le WebAssembly doit également être un langage lisible et débuggable. Il est en effet possible de l’écrire de manière textuelle ce qui permet de le débugguer normalement et d’offir une relecture aisée.

Ensuite, le langage doit être sécurisé. Dans ce domaine, le WebAssembly est un langage éxecuté en sandbox. Il s’agit d’un mécanisme de sécurité basé sur l’isolation de composants logiciels par rapport à leur hôte. De ce fait, il lui est impossible de faire directement des appels système ou de lire dans la mémoire du navigateur. Les règles d’autorisations des navigateurs lui sont également appliqué au même titre que le Javascript.

Pour finir le WebAssembly n’est pas un remplaçant de Javascript. Son but est de se coupler aux langages web pour les assister et répondre à des problématiques particulières. Du coup, on retrouve bien le fait qu’il n’y a pas d’interêt de vouloir l’utiliser partout.

Un peu de code

Je ne vous aurais pas écrit la conclusion sans vous montrer à quoi ça ressemble. C’est plutôt joli pour le moment, bien que ce ne soit pas aussi simple qu’un import javascript. Mais… c’est sur le feu cette histoire d’import, donc nous devrions bientôt pouvoir l’utiliser bien plus facilement. On peut également penser aux frameworks Javascript qui peut être un jour l’embarquerons nativement lorsque la technologie aura un peu maturé.

Voilà le bout de code qui nous servira d’exemple : un simple algo en C++ de calcul factoriel.

double fact(int i) {
  long long n = 1;
  for (;i > 0; i--) {
    n *= i;
  }
  return (double)n;
}

On le compile en .wasm et on peut utiliser le bytecode obtenu.

<html>
    <head>
        <meta charset="utf-8">
        <title>WASM test</title>
    </head>
    <body>
        <script>
            WebAssembly.instantiateStreaming(
                fetch("fact.wasm")
            )
            .then(obj => obj.instance.exports._Z4facti)
            .then(fact => {
                document.getElementById("fact").textContent = fact(5)
            });
        </script>
        <h1>La méthode C++ retourne :</h1>
        <p id="fact"></p>
        </body>
</html>

Donc l’API Javascript de WebAssembly nous donne la méthode instantiateStreaming(...) pour lancer la Streaming Compilation du fichier .wasm. Elle retourne une promesse sur laquelle on va aller chercher le nom de la fonction. Vu que j’ai utilisé le générateur dont je vous ai parlé plus haut, la mienne a un nom tout moche : _Z4facti. On va ensuite chercher l’élément DOM dans lequel je veux afficher le résultat et j’y attribut ma fonction avec son petit argument. Logiquement le résultat sera 120 (Ouais j’ai fais S au lycée t’as vu ?).

Bon donc là vous avez pris le code, compilé votre C++ pour obtenir votre bytecode, vous vous disiez waw c’est cool ça va marcher. Tenez vous bien on arrive sur la partie pas cool. Non seulement vous vous prenez l’erreur de CORS dans la tête, mais en plus même en réglant ce souci en lançant un live server via VSCode (le vénéré), vous avez cette erreur :

Uncaught (in promise) TypeError: Failed to execute 'compile' on 'WebAssembly': Incorrect response MIME type. Expected 'application/wasm'.

Sérieusement ? Yes. Bon bah faut configurer le serveur… Est-ce que un petit express vous tenterait ? Aller.

mkdir wasmTest
cd wasmTest
npm init
npm install express
touch index.js
mkdir public
cd public
touch index.html
cd ..

Voilà, ça c’est fait.

Dans index.js je vous propose de coller cela :

const express = require('express')
express.static.mime.types["wasm"] = "application/wasm";
var app = express();

app.use('/', express.static('public'));

app.listen(3000, function () {
  console.log('Example wasm app listening on port 3000 !')
})

…et dans votre répertoire /public vous allez placer le fichier .wasm et le code HTML donné plus haut dans le fichier index.html.

On exécute tout ça:

node index.js

 

Plus qu’à aller sur http://127.0.0.1:3000/ et vous trouverez votre HTML avec le résultat de la fonction.

Et voilà vous venez d’éxecuter du C++ sur un navigateur. Plutôt sympa, non ?

Conclusion

C’est fou comme cette technologie à l’air prometteuse. Elle peut répondre à énormément de problématiques que bien des entreprises peuvent rencontrer et donner une nouvelle dimension au développement web. Cependant, c’est une technologie très jeune qui a vraiment besoin de grandir, de faire ses preuves concrètement sur des projets complexes avant d’être considérée comme étant une révolution. Il y a eu des jeux vidéos créés avec le WebAssembly mais qu’en est-il des applications évoluant dans des systèmes d’informations aussi massifs que complexes ? Quelle est sa réelle compatibilité avec au sein d’un projet de grande envergure ?

Je souhaiterais terminer cet article avec une ouverture sur une réflexion. Si le WebAssembly prend beaucoup d’ampleur, est-ce qu’un développeur web devra également impérativement connaître les langages systèmes ? Est-ce qu’un développeur C/C++ devra également savoir faire du web ? Pour le moment, il me semble que ces deux mondes soient encore pas mal séparés formant au final deux métier assez différents.

J’espère que cet article vous aura plu et je vous donne rendez-vous dans le prochain.

Happy coding et prenez soin de vous !

Bien à vous,

Pophip

Rémi Perreira

Consultant en développement

NodeJS : Parlons Worker Threads

Salut !

Aller pour le moment je vais vous laisser respirer loin des tutoriels sur Firebase. Changeons de sujet (mais pas trop non plus). Le 6 Juin 2018 est arrivée la version 10.5.0 de NodeJS et avec un module plutôt interessant : les Worker Threads. Maintenant que le module est stable (depuis la v12 LTS de la fin d’année 2019), je souhaiterais revenir dessus afin d’expliquer un peu le pourquoi du comment et éclairer sur cette bizarrerie les personnes n’ayant pas encore eu l’occasion ou le temps de s’y interesser.

Javascript et son thread "forever alone"

Avant d’entamer les Worker Threads, revenons tranquillement quelques années en arrière lors de la conception et l’avènement du Javascript. Nous sommes en décembre 1995 lorsque Sun Microsystem et Netscape annoncent le langage officiellement. D’ailleurs juste pour la culture personnelle, Javascript s’appelait à la base LiveScript mais comme Sun Microsystem et Netscape étaient partenaires et que La JVM de Sun Microsystem était de plus en plus populaire, les deux gaillards ont décidé de changer le nom en Javascript.

Bref, le Javascript a été conçu pour rendre à l’époque les pages web du navigateur dynamiques et faire de la validation de formulaires. On clique sur un bouton, il se passe des trucs. On scroll, une animation se déclenche et j’en passe. En fait, rien de suffisament méchant pour qu’il y ait besoin de concevoir un langage multithreadé avec toute la complexité que cela implique. Donc le JS fût développé comme un langage de programmation mono-threadé. Cela veut dire qu’une instruction à la fois s’éxécute sur le même processus.

Jusque là, le Javascript était tout de même peu populaire et vu comme un langage brouillon, un peu batard, sans avenir, sous la pluie, seul. Pour cause, niveau performance ce n’était vraiment pas la folie. Cela a duré jusqu’en 2008 quand Google a sorti le moteur V8 (non cher lecteur, pas comme celui de Clément Ader en 1903. Celui là n’avait que 24ch) directement câblé dans Google Chrome, tout nouveau à l’époque.

Pour expliquer de manière brève, le V8 utilise la compilation Just-In-Time en compilant le Javascript en Assembleur au moment de l’éxécution. Cela a fait littéralement exploser les performances du Javascript et par conséquent permis l’apparition des navigateurs web modernes, bien plus puissants qu’à l’origine. Grâce à cela, non seulement la popularité du Javascript est montée de manière exponentielle mais on a vu apparaître des technologies comme NodeJS.

D’ailleurs le but de Ryan Dahl (créateur de NodeJS) était d’implémenter une plateforme basée sur l’asynchrone. Par conséquent, il n’y avait pas besoin de plusieurs threads. Le souci du mono-threading étant le partage mémoire pouvant causer de gros problèmes liés aux accès concurrentiels.

NodeJS étant la plateforme évenementielle Javascript, il est basé sur une architecture non bloquante. Node va donc invoquer les fonctions sans bloquer l’exécution du reste du code. Via la callback, Node est averti que le job est terminé en revoie le résultat. Comme Javascript est rapide et que les tâches ne sont jamais très importantes et en général bien découpées, nous ne remarquons pas la différence.

…sauf quand il s’agit d’une tâche intensive synchrone. Là Node commence à ramer et tout le code se bloque. Pour l’exemple extrême, essayez donc de lancer un while(true) {} dans un script JS. Tout le navigateur tire la tronche. En fait non, n’essayez pas ça à la maison…

On pourrait plus concrètement imaginer un retour de requêtage en base de données qu’il y a besoin de hautement chiffrer/déchiffrer avant de l’utiliser dans l’application. Donc dans le .then({ /**...**/ }) de la promesse, on écrit la série d’instructions permettant de chiffrer/déchiffrer ces données. Nous sommes alors face à un code procédural lourd à la place des bouts de code en général suffisament léger et rapide à exécuter. Dans tous les cas, voilà un exemple qui pourrait occuper le mono-thread de Javascript pour un certain temps, empêchant toute autre instruction d’être prise en charge.

Bref, la popularité de Node ne cesse de grandir, beaucoup d’entreprises commencent à migrer vers cette plateforme et évidemment peuvent rencontrer des problèmes avec ce satané mono-thread.

Et là paf ! Les Worker Threads, on fait du multi-threading en NodeJS c’est cool non ?

Ça n'existait pas déjà ?

Si, mais c’était pas aussi bien. Chapitre suivant.

Ce n’est pas un argument ? Bon, bien sûr qu’il existait déjà différentes solutions afin de palier à ce problème. Notez cependant qu’il s’agit en général de solutions pas entièrement satisfaisantes.

Utiliser par exemple setImmediate(callback) afin de spécifier à Node qu’il peut continuer directement à prendre en charge ce qui est présent de sa queue d’exécution, cela complique très vite les algorithmes et rend le code de moins en moins maintenable au fil de l’écriture et des évolutions.

De la même façon, il est possible de fork le processus. Cependant un fork est particulièrement gourmand en ressources. Ce n’est pas une solution viable à long terme, sans parler de la perte massive en performances.

Enfin il y a le module cluster. Le concept peut encore une fois paraître similaire. En réalité ce module crée plusieurs instances de Node (donc plusieurs processus) avec un processus principal à leur tête servant un peu de routeur pour les requêtes entrantes. Cela permet d’améliorer les performances en effet et de soulager le serveur. Cependant, il ne s’agit pas de multi-threading, vu que chaque processus possède un unique thread dans ce cas-ci. De plus, encore une fois, il n’y a pas de partage mémoire.

Ces solutions (pour ne pas toutes les citer) ne se rapprochent pas de l’ensemble des avantages du multi-threading. Typiquement, les Workers Threads restent plus léger à l’utilisation, partagent le même processus et leur mémoire. Bref du vrai multi-threading.

Les Worker Threads

Si je parle de tâches intensives depuis tout à l’heure, c’est parce que la documentation de NodeJS précise que

Workers (threads) are useful for performing CPU-intensive JavaScript operations.

Documentation officielle

Du coup quelle est la différence ?

A la base l’utilisation de NodeJS s’effectue via un seul processus disposant d’un thread forever alone, on l’a vu plus haut. Ensuite tout est mono instancié. Une instance du moteur V8 (avec libuv pour gérer l’asynchrone) est pris en charge par une unique event loop sur une unique instance de NodeJS.

Okay. Imaginons maintenant que les Worker Threads permettent d’accèder à plusieurs threads. Bien évidemment, tous ces threads sont sur le même processus. Chaque thread possède alors son instance de V8 couplé à libuv et sa propre event loop. Le tout fonctionnant sur une instance de NodeJS. Donc on aura une instance de NodeJS par threads.

Cependant, les-dits threads ne sont pas cloisonnés pour autant puisque le module permet de gérer les accès concurrentiels du code sur les threads.

Aller un schéma vaut mille mots :

Source : The NodeSourceBlog

Et cela donne quoi concrètement ?

Concrètement lorsque l’on déclare un nouveau thread dans le code, on l’associe avec un script JS “isolé” censé exécuter le traitement lourd. Vu comme ça, on pourrait presque comparer avec le fonctionnement d’une Cloud Function.

Mais niveau code voilà ce que ça donne :

index.js

const { Worker } = require('worker_threads')

/** 
* Crée un worker
* @param file On donne à l'objet Worker le chemin d'accès au fichier contenant la tâche à exécuter en parallèle.
*/
function executeWorker (file) {
  return new Promise((resolve, reject) => {
    const worker = new Worker(file)

    // Une fois le worker actif
    worker.on('online', () => { 
      console.log('DEBUT : Execution de la tâche intensive en parallèle') 
    })

    // Si un message est reçu du worker
    worker.on('message', workerMessage => {
      console.log(workerMessage)
      return resolve
    })

    worker.on('error', reject)
    worker.on('exit', code => {
      if (code !== 0) {
        reject(new Error(`Worker stopped with exit code ${code}`))
      }
    })
  })
}

/**
 * Le main écrit dans la console
 */
async function prog () {

  // Tâche principale
  setInterval( () => { console.log('Tâche principale: la tâche en parallèle peut s\'exécuter') }, 1000)

  // Tâche en parallèle
  await executeWorker('./worker.js')
}

prog()

 

worker.js

const { parentPort } = require('worker_threads')

let count = 0
for (let i = 0; i < 10000000000; i++) {
  count += 1
}
console.log(`FIN: ${count}`)
const message = `Tâche intensive terminée, total : ${count}`

// On renvoie un message depuis le worker récupérer lors du worker.on('message'...)
parentPort.postMessage(message)

 

Et voilà ! Nous avons bien ici un unique processus mais deux threads bien séparés.

Conclusion

Ce nouveau module de l’API de Node semble vraiment pratique et plutôt puissant mais il est peut être un peu tôt pour dire si oui ou non cela sera une feature incontournable de Node.

Les Worker Threads permettent d’installer du multi-threading assez facilement sous Node et ainsi de largement améliorer les performances CPU de la plateforme. Nous pouvons à priori imaginer une ouverture prochaine de NodeJS dans des domaines demandant de la performance CPU comme l’intelligence artificielle par exemple.

Pour donner mon avis personnel, je trouverais super que la plateforme s’ouvre à de nouvelles utilisations mais je souhaite contraster un peu cela. Il faut noter qu’ils ne font pas de miracles particuliers. Le blog NodeSource précise lui même que :

  • Don’t think Workers make everything magically faster, in some cases is better to use Worker pool
  • Don’t use Workers for parallelizing I/O operations.
  • Don’t think spawning Workers is cheap

Il n’y a donc pas de magie. Il reste à considérer l’arrivée (éventuelle) de Deno qui, si il continue sur sa lancée, pourrait peut être compléter voir remplacer NodeJS. Le débat est ouvert, à creuser.

Sur ce il est déjà temps de vous laisser. J’espère que cet article vous aura plu !

Happy coding !

Bien à vous,

Pophip

Rémi Perreira

Consultant en développement

Environnements et intégration continue : NuxtJS & Firebase (Partie 3)

Bien le bonjour !

Justement je pensais à vous en me disant : mais je ne vais quand même pas les laisser avec le gâteau prêt à être mangé sans ajouter les fruits et la crème. Il est midi, j’ai faim.

Vous l’aurez compris, si vous êtes là c’est que vous avez suivi (et je vous en remercie) au moins mon tutoriel vous expliquant comment installer une application SSR Nuxt sous Firebase. Et si vous avez été attentifs j’ai, lors de ce tutoriel, supprimé le répertoire .git afin de nous débarrasser de la trace des versions de fichiers.

Je vais maintenant vous proposer de remettre en place la gestion des sources ainsi que de l’intégration continue sous Gitlab et ce avec deux environnements : développement et production.

Si vous n’êtes pas encore familiarisés, je vous redirige vers cet article qui explique de manière simple et concise l’intérêt de mettre en place plusieurs environnements, leurs rôles et leurs spécificités.

Cela étant dit, allons-y !

 

Pré-requis

Ce tutoriel est la continuité du dernier. Je me baserai donc sur l’application obtenue à la fin de la partie 2.

Vous allez avoir besoin de Git.

Il vous faudra un compte Gitlab. Bien évidemment il est possible d’utiliser le CI/CD de votre choix mais il vous faudra adapter les instructions que je fournirai au système pour lequel vous opterez.

Il vous faudra avoir créé deux projets Firebase et récupérer leur ID pour les écrire dans le fichier .firebaserc. L’un sera dédié au developpement, l’autre à la production.

 

Création du projet Gitlab

Juste créer un projet Gitlab portant le nom de votre application.

 

Initialisation

Je vous invite de vous rendre à la racine du projet nuxt-on-firebase/ et à lancer la commande suivante qui initialisera le traçage git :

git init

Sous VS Code vous devriez voir apparaitre la surbrillance verte propre à la detection de modifications dans le projet.

Vous êtes à présent sous la branche principale master.

Décidément pour le moment c’est pas la folie. On y vient on y vient.

Maintenant on va envoyer tout ce beau monde sous votre projet gitlab :

## Association au répertoire distant
git remote add origin <lien de votre projet gitlab>
## Ajout de tout le répertoire local
git add .
## Création du commit
git commit -m "Commit initial, le SSR est en place"
## Et du coup on envoie tout sur le répertoire distant
git push -u origin master

Et si vous allez sur votre projet, pouf il y a vos sources.

 

Récupération du token firebase

Vous avez du remarquer que pour déployer une application sous Firehost il nous faut l’associer à un projet sous la console. Souvenez vous :) Pour cela nous nous connections à Firebase via un firebase login qui nous ouvrait un onglet de navigateur avec une interface bien pratique. Une fois la partie interface passée, hop on était loggués.

La question se pose maintenant : comment faire pour de l’intégration continue ?

Une fois le déploiement lancé dans le pipeline, l’ouverture d’une page web est impossible. Firebase a pensé à tout, nous allons utiliser un token de connexion généré ainsi :

firebase login:ci

Cette commande va vous donner un token de connexion que vous irez appliquer dans gitlab dans les variables d’environnement du repository.

Settings > CI/CD > Variables.

 

Le Fichier .gitlab-ci.yml

Pour commencer je vous invite à regarde un peu la documentation de Gitlab si vous n’êtes pas familiés de ce type de fichier.

Maintenant on va écrire le script de déploiement. Le fichier Yaml par défaut lu par gitlab lors d’un commit est le fichier .gitlab-ci.yml.

Créons le à la racine :

touch .gitlab-ci.yml

 

Et voici ce que l’on trouvera dedans :

image: andreysenov/firebase-tools

stages:
  - build
  - deploy

build:
  environment: test
  stage: build
  script:
    - npm run setup
    - npm run build
  except:
    - master

deploy-dev:
  environment: test
  stage: deploy
  only: 
    - master
  script:
    - npm run setup
    - firebase use develop --token $FIREBASE_TOKEN
    - firebase deploy -m "Deploying to test environment" --token $FIREBASE_TOKEN

deploy-prod:
  environment: production
  stage: deploy
  only: 
    - tags
  script:
    - npm run setup
    - firebase use production --token $FIREBASE_TOKEN
    - firebase deploy -m "Deploying to production environment" --token $FIREBASE_TOKEN

Nous utilisons déjà une image firebase afin d’avoir les commandes Firebase et Node de base.

  • Nous avons donc un job build qui s’éxécutera à chaque fois que vous pusherez quelque chose sur le repository Gitlab tant qu’il ne s’agit pas de la branche master.

  • Le job deploy-dev s’éxécutera lorsque vous mergerez vos travaux sur la branche master. Cela déclenchera donc un déploiement en environnement de développement.

  • Enfin, le job deploy-prod s’éxécutera au tag de la branche master. Cela vous permettra de gérer vos releases de production efficacement.

Dans chacun des scripts je dis à firebase d’utiliser l’ID du projet Firebase indiqué pour le développement ou la production. Ainsi il vous suffira de créer un autre projet Firebase via la Firebase Console et de modifier le fichier .firebaserc avec les ID des projets vous servant d’environnement de dev ou de prod. De cette manière, il est possible d’ajouter une multitude d’environnements.

L’authentification firebase s’effectue via l’option --token à laquelle on attribue la variable d’environnement Gitlab $FIREBASE_TOKEN mise en place plus haut.

Allez, on commit tout ça :

## Ici on est sous le master
git checkout -b continuous-integration
git add .gitlab-ci.yml
git commit -m "ajout du fichier de déploiement"
git push --set-upstream origin continuous-integration

Il ne nous reste plus qu’à merge cette branche dans le master depuis gitlab.

Suite à cela, Gitlab lance un déploiement du master. Vous pouvez alors tagger le master. Le déploiement se fera sous le projet firebase lié à l’environnement de production.

Tout va bien dans le meilleur des mondes ! Vous avez mis en place de l’intégration continue ! Bravo.

Maintenant d’autres petites questions vont se poser.

 

Gestion des environnements

Ne vous rejouissez pas trop vite, Nuxt a tout de même ses petites manignances derrière notre dos.

Reprenons, nous sommes d’accord que si vous souhaitez une application tournant en dev et une en production il va vous falloir deux projets Firebase.

Il est à noter qu’il est tout de même possible sous Firebase d’avoir plusieurs applications sous un même projet. Elle partageront les ressources et auront chacunes leurs statistiques propres. Ce cas peut vous interesser mais ce n’est pas celui que je choisis ici car si on souhaite par la suite avoir une base de données Firestore par environnement, on aura besoin de deux projets distincts.

Bref qui dit deux projets dit deux configurations d’API différentes. Il nous faut donc tester si nous sommes en developpement ou en production.

Toi, au fond je t’ai entendu avec ton : "Nan mais il suffit d’utiliser NODE_ENV et voilà"

Eh non… Nuxt est, comme je vous le disais, un petit malin. La commande nuxt build lancée lors de notre npm run build écrase automatiquement NODE_ENV par “production”.

Donc il faut ruser. Et là je retrouve mon nuxtichou d’amour avec la propriété env dans le fichier nuxt.config.js.

Tout simplement on va définir des variables d’environnement au build que l’on pourra par la suite réutiliser à notre guise.

Il va donc nous falloir créer une propriété indiquant l’environnement courant qui nous servira pour gérer des comportements différents de l’application en fonction de son environnement d’éxécution. Nous allons également créer les variables nécessaires à la configuration de l’API Firebase.

Et pour les récupérer on attribue donc la fameuse propriété env dans le fichier nuxt.config.js :

env: {
    app_env: process.env.APP_ENV,
    FIREBASE_API_KEY: process.env.FIREBASE_API_KEY,
    FIREBASE_DATABASE_NAME: process.env.FIREBASE_DATABASE_NAME,
    FIREBASE_PROJECT_ID: process.env.FIREBASE_PROJECT_ID,
    FIREBASE_SENDER_ID: process.env.FIREBASE_SENDER_ID
},

La question qui se pose maintenant c’est comment récupérer tout cela au build dépendament de l’environnement sachant que l’on doit considérer le démarrage en local ?

Je répondrai par : Dotenv + Gitlab !

Dotenv

Afin de gérer des variables d’environnement en local nous allons utiliser dotenv et plus spécifiquement le module fait pour Nuxt. Il s’agit d’un module permettant de charger les variables d’environnement du fichier .env directement dans l’application. Cette attribution s’effectue au build de Nuxt.

## Ici nous sommes sous /src dans notre application. 
## En effet on souhaite installer cette dépendance pour l'application Nuxt uniquement.
npm install @nuxtjs/dotenv

Il nous faudra ensuite aller dans le fichier nuxt.config.js et y rajouter le module dotenv :

buildModules: [
    '@nuxtjs/dotenv'
  ],

Parlons maintenant de ce fameux fichier .env. Avant de créer le fichier, bien s’assurer qu’il est présent dans votre .gitignore. On n’en veut pas dans la gestion de sources.

## Sous src toujours
touch .env

Dans le fichier vous y mettrez vos variables d’environnement comme suis:

APP_ENV=develop
FIREBASE_API_KEY=votre_api_key_Firebase
FIREBASE_DATABASE_NAME=votre_database_name_Firebase
FIREBASE_PROJECT_ID=votre_project_id_Firebase
FIREBASE_SENDER_ID=votre_sender_id_Firebase

J’ai jamais autant écrit Firebase de ma vie. Firebase.

Bien pour le local c’est bon. Vos variables d’environnment sont attribuées. Le fichier .env n’est pas tracé et donc il faut trouver un autre moyen de les attribuer lorsque l’on déploie l’application.

Gitlab

Ce qu’il y a de bien avec le Gitlab CI c’est que l’on peut lui spécifier dans ses jobs des constantes utilisables au moment du build de l’application. Donc ce code FIREBASE_API_KEY: process.env.FIREBASE_API_KEY (voir plus haut) fonctionnera autant en local qu’au déploiement en dev ou prod.

Donc déjà il faut ajouter les variables sous Settings > CI/CD > Variables.

On ajoute donc ces 8 variables :

  • FIREBASE_API_KEY_DEV
  • FIREBASE_DATABASE_NAME_DEV
  • FIREBASE_PROJECT_ID_DEV
  • FIREBASE_SENDER_ID_DEV
  • FIREBASE_API_KEY_PROD
  • FIREBASE_DATABASE_NAME_PROD
  • FIREBASE_PROJECT_ID_PROD
  • FIREBASE_SENDER_ID_PROD

Je vous laisse attribuer les bonnes valeurs avec celles de vos projets Firebase.

Vous pouvez modifier votre fichier .gitlab-ci.yml qui ressemblera à ceci :

image: andreysenov/firebase-tools

stages:
  - build
  - deploy

build:
  environment: test
  stage: build
  script:
    - npm run setup
    - npm run build:dev
  except:
    - master

deploy-dev:
  environment: test
  stage: deploy
  only: 
    - master
  variables:
    APP_ENV: $APP_ENV_DEV
    FIREBASE_API_KEY: $FIREBASE_API_KEY_DEV
    FIREBASE_DATABASE_NAME: $FIREBASE_DATABASE_NAME_DEV
    FIREBASE_PROJECT_ID: $FIREBASE_PROJECT_ID_DEV
    FIREBASE_SENDER_ID: $FIREBASE_SENDER_ID_DEV
  script:
    - npm run setup
    - firebase use develop --token $FIREBASE_TOKEN
    - firebase deploy -m "Deploying to test environment" --token $FIREBASE_TOKEN

deploy-prod:
  environment: production
  stage: deploy
  only: 
    - tags
  variables: 
    APP_ENV: $APP_ENV_PROD
    FIREBASE_API_KEY: $FIREBASE_API_KEY_PROD
    FIREBASE_DATABASE_NAME: $FIREBASE_DATABASE_NAME_PROD
    FIREBASE_PROJECT_ID: $FIREBASE_PROJECT_ID_PROD
    FIREBASE_SENDER_ID: $FIREBASE_SENDER_ID_PROD
  script:
    - npm run setup
    - firebase use production --token $FIREBASE_TOKEN
    - firebase deploy -m "Deploying to production environment" --token $FIREBASE_TOKEN

On notera bien les propriétés variables qui ont été rajoutées avec les clefs précédemment configurées sous Gitlab.

Pour le déploiement, tout est en place.

 

Le plugin d’initialisation Firebase

Cet article commence à être vraiment long. C’est le dernier chapitre (promis).

Maintenant que vous disposez de vos projets firebase de dev et de prod et de vos variables d’environnement spécifiques on peut mettre en place ce qu’il faut pour pouvoir utiliser firebase au sein de l’application.

Dans le répertoire plugins de Nuxt, on va créer un fichier nommé fireapp.js

cd src/plugins && touch fireapp.js

## On va avoir besoin du package firebase
## Ne pas oublier de retourner sous src
cd .. && npm install firebase

Dans ce fichier, on trouvera donc :

import firebase from 'firebase'

if(!firebase.apps.length) {

    //On utilise nos fameuses variables d'environnement
    const config = {
        apiKey: process.env.FIREBASE_API_KEY,
        authDomain: `${process.env.FIREBASE_PROJECT_ID}.firebaseapp.com`,
        databaseURL: `https://${process.env.FIREBASE_DATABASE_NAME}.firebaseio.com`,
        projectId: process.env.FIREBASE_PROJECT_ID,
        storageBucket: `${process.env.FIREBASE_PROJECT_ID}.appspot.com`,
        messagingSenderId: process.env.FIREBASE_SENDER_ID
    }
    //Initialisation de firebase
    firebase.initializeApp(config)
}

// On stocke les instances
const firebaseApp = firebase.app()
const fireDb = firebase.firestore()

//Et on les rends disponible à qui en aura besoin dans l'application
export { firebaseApp }
export { fireDb }

N’oubliez pas d’ajouter le fichier aux plugins Nuxt dans le fichier nuxt.config.js.

 

Conclusion

Et voilà !

Nous avons mis en place de l’intégration continue pour notre application ainsi que la disponilité de l’API Firebase (application et base de données en temps réel) pour deux environnements d’éxécution.

Si jamais vous avez besoin d’idées pour la gestion utilisateur FireAuth ou la mise en place d’une base de données Firebase (avec son ORM Firestorm), cela me fera plaisir de vous présenter différentes solutions relatives à ces sujets. N’hésitez donc pas à revenir vers moi.

Je vous remercie d’avoir suivi ce tutoriel et vous dis à très bientot.

Happy coding !

Bien à vous,

Pophip.

Rémi Perreira

Consultant en développement

Se passer d'un backend : NuxtJS & Firebase (Partie 2)

Salut !

Nuxt c’est cool et on va dans ce tutoriel mettre en place un projet Nuxt en combinaison avec Firebase pour faire du SSR via une cloud function.

Ne divergeons pas plus longtemps, et entrons dans le vif du sujet.

Pré-requis

Pour suivre ce tutoriel il vous faudra en prérequis Node JS ainsi que le gestionnaire de packages npm.

Vous trouverez le bundle des deux ici : https://nodejs.org/ . Je vous conseille la version LTS.

Personnellement j’utilise Visual Studio Code avec comme plugins tout ce qu’il faut pour faire du VueJS comme si vous étiez dans vos chaussons.

Aller on y va !

Installation du projet

# On crée notre répertoire racine
mkdir nuxt-on-firebase

cd nuxt-on-firebase

# On crée dedans le répertoire src contenant le projet Nuxt
npx create-nuxt-app src
    -- Nom du projet ? nuxt-on-firebase
    -- Description ? Ce que vous voulez
    -- Nom auteur ? Pophip
    -- Gestionnaire de package ? Npm
    -- Framework interface utilisateur ? Vuetify.js
    -- Framework server ? Aucun
    -- Nuxt.js module ? Axios et PWA
    -- Linter ? Aucun
    -- Framework de test ? Jest
    -- Mode de rendu ? Universel
    -- Outil de développement ? js.config.json (si vous êtes sur visual studio code ce que je vous recommande)

On ne veut pas que git sois initialisé directement dans src/ donc on va supprimer la traçage sous ce répertoire. On pourra relancer un git init à la racine plus tard.

cd src/ && rm -rf .git

Configuration du nuxt.config.js sous src :

Commencez tout d’abord par remplacer le export default { par module.exports = {.

Supprimez ensuite la ligne d’import des couleurs vuetify, on en aura pas besoin et cela risque de nous embêter plus qu’autre chose.

import colors from 'vuetify/es5/util/colors'

Enfin n’oubliez pas de remettre les couleurs hexadécimales en dur sous vuetify {}.

Si vous êtes un bourrin, supprimez littéralement tout ce qu’il y a sous vuetify {} et le framework vous mettra les couleurs Vuetify par défaut.

Au dessus de module.exports, on va importer un json bien connu :

const package = require('./package')
//En fait c'est le package.json sous src/

On va pouvoir mettre à jour la propriéré title sous head: {} :

title: package.name 
//C'est donc le nom du projet indiqué dans le package.json

Petit bonus pour utiliser le material design : rajouter sous link: [] :

{ rel: 'stylesheet', href: 'https://fonts.googleapis.com/css?family=Roboto:300,400,500,700|Material+Icons' }

Et enfin ajouter la configuration du build sous module.exports:

 buildDir: '.nuxt',
  build: {
    publicPath: '/assets/',

    babel: {
      presets({ isServer }) {
        return [
          [ require.resolve('@nuxt/babel-preset-app'), 
            { 
              // targets
              buildTarget: isServer ? 'server' : 'client',
              corejs: { version: 3 }
            } 
          ]
        ]
      },
      'env': {
        'production': {
          'plugins': []
        }
      }
    }
  }

Initialisation du projet racine NPM :

cd ../ && npm init

Script de démarrage de nuxt en local :

cd src/
mkdir server
cd server && touch index.js

On a donc nuxt-on-firebase/src/server/index.js qui se présentera ainsi :

const consola = require('consola')
const express = require('express')
const { Nuxt, Builder } = require('nuxt')
const app = express()

// On importe la configuration de nuxt
let config = require('../nuxt.config.js')
config.dev = !(process.env.NODE_ENV === 'production')

async function start() {
  //Initialisation de Nuxt
  const nuxt = new Nuxt(config)

  const { host, port } = nuxt.options.server

  // Build pour le mode environnement de dev
  if (config.dev) {
    const builder = new Builder(nuxt)
    await builder.build()
  } else {
    await nuxt.ready()
  }

  // On passe le middleware de nuxt à express
  app.use(nuxt.render)

  // Et on écoute le serveur
  app.listen(port, host)
  consola.ready({
    message: `Server is listening on http://${host}:${port}`,
    badge: true
  })
}

//Démarrage du serveur
start()

On retourne dans src/ :

cd ..

Installation des dépendances

Dans package.json sous src/ on va installer ces dépendances. Je vous invite à les copier coller. Nous aurions pu exécuter des npm install à n’en plus finir, j’ai choisi de vous donner toutes les dépendances directement. Normalement la commande d’installation de nuxt vous aura mis nuxt 2.0.0 par défaut en dépendance. Je vous fait cadeau de la dernière version.

"engines": {
    "node": "10"
},
"dependencies": {
    "@nuxtjs/axios": "^5.8.0",
    "@nuxtjs/pwa": "^2.6.0",
    "@nuxtjs/vuetify": "^1.9.0",
    "express": "^4.17.1",
    "firebase-admin": "^8.7.0",
    "firebase-functions": "^3.3.0",
    "node-fetch": "^2.6.0",
    "nuxt": "^2.10.2"
  },
  "devDependencies": {
    "@babel/runtime-corejs3": "^7.7.1",
    "@vue/test-utils": "^1.0.0-beta.29",
    "babel-jest": "^24.9.0",
    "core-js": "^3.3.6",
    "cross-env": "^6.0.3",
    "jest": "^24.9.0",
    "nodemon": "^1.19.4",
    "vue-jest": "^4.0.0-0"
  }

On retourne à la racine du projet et dans package.json on écrit la dépendance cross-env.

 "dependencies": {
    "cross-env": "^6.0.3"
  }

On a donc toutes nos dépendances, il est temps de mettre en place l’environnement Firebase.

Mise en place de Firebase

Tout d’abord vous pouvez créer un nouveau projet Firebase dans la Firebase Console. Une fois le projet créé, dans les Settings vous trouverez l’ID du projet.

Notez le quelque part (du genre sur votre avant-bras ou le front de votre voisin).

Retournez sous VSCode, et installez les outils Firebase.

# Ici vous êtes sous nuxt-on-firebase/
# Outils
npm install --global firebase-tools

# Se connecter à votre compte firebase
firebase login

# Et c'est parti
firebase init hosting
    -- Proceed ? Yep go
    -- Select a project ? Pas de projet par defaut  
# (vous pouvez si vous voulez mais on fera cela après)
    -- public directory ? public
    -- Configure as singles page app ? Non

On supprime ce qui nous sert à rien pour le moment et on crée le répertoire des assets sous public/.

rm .firebaserc && rm public/* && mkdir public/assets

On va créer un fichier qui nous permettra de mettre en place automatiquement le fichier .firebaserc lors de l’installation finale.

touch .setup-firebaserc

Ouvrir .setup-firebaserc et y coller le json suivant :

{
  "projects": {
    "default": "FIREBASE_PROJECT_ID",
    "develop": "FIREBASE_PROJECT_ID",
    "staging": "FIREBASE_PROJECT_ID",
    "production": "FIREBASE_PROJECT_ID"
  }
}

Evidemment je vous laisse remplacer FIREBASE_PROJECT_ID par l’id de votre projet (il est temps de rappeler votre voisin).

Ouvrir maintenant firebase.json. On y écrira le json suivant :

{
  "hosting": {
    "public": "public",
    "rewrites": [
      {
        "source": "**",
        "function": "nuxtapp"
      }
    ]
  },
  "functions": {
    "source": "src",
    "predeploy": "npm run build && npm run pre:deploy",
    "ignore":[
      "static",
      "node_modules",
      "**/**/dist/client"
    ]
  }
}

Mise en place de la Cloud Function

On retourne dans src/ et on va mettre en place ce qu’il faut pour la cloud function:

cd src/

touch index.js

Il se présente ainsi :

const functions = require("firebase-functions");
const { Nuxt } = require("nuxt");
const express = require("express");
const app = express();


const config = {
  dev: false,
  buildDir: ".nuxt",
  build: {
    publicPath: "/assets/"
  }
};
const nuxt = new Nuxt(config);

exports.nuxtapp = functions.https.onRequest(async (req, res) => {

  if (config.dev) {
    const builder = new Builder(nuxt)
    await builder.build()
  } else {
    res.set("Cache-Control", "public, max-age=300, s-maxage=600");
    await nuxt.ready()
  }
  app.use(nuxt.render)
  return app(req, res);
});

Création des commandes :

Pour finir il ne nous reste plus qu’à entrer les différentes commandes pour le build, le lancement et le déploiement de l’application.

Dans package.json à la racine nuxt-on-firebase/ :

On va mettre en place un certain nombre de runnables :

.
.
.
"scripts": {
    "setup:client": "cd \"src\" && npm i",
    "setup:firebase": "cp .setup-firebaserc .firebaserc",
    "setup": "npm i && npm run setup:firebase && npm run setup:client",
    "dev": "cd ./src && npm run dev",
    "build": "cd ./src && npm run build",
    "copyassets": "cp -R ./src/.nuxt/dist/client/* ./public/assets",
    "copystatic": "cp -R ./src/static/* ./public",
    "pre:deploy": "npm run copyassets && npm run copystatic",
    "firebase:serve": "npm run build && npm run serve",
    "serve": "cross-env DEBUG=nuxt:*  firebase serve --only hosting,functions",
    "deploy": "firebase deploy && rm -rf .firebase"
  },
.
.
.

Respectivement :

  • Installer l’environnement applicatif
  • Installer l’environnement firebase
  • Installer l’environnement complet (applicatif et firebase)
  • Lancer l’application en local depuis la racine
  • Build l’application depuis la racine
  • Copier les assets depuis le répertoire de build vers public
  • Copier les fichiers statiques vers public
  • Build l’application et lancer firebase en local
  • Uniquement lancer firebase en local
  • Déployer l’application sous firebase

Et dans package.json sous nuxt-on-firebase/src/ :

.
.
.
"scripts": {
    "dev": "cross-env NODE_ENV=development nodemon server/index.js --watch server",
    "build": "nuxt build",
    "start": "cross-env NODE_ENV=production node server/index.js",
    "test": "jest"
  },
.
.
.
  • Démarrer en local en environnement de developpement
  • Builder l’application
  • Démarrer en local en environnement de production
  • Eventuellement lancer les tests sous jest

Let’s build !

Maintenant on va pouvoir lancer l’installation complète !

# Ici vous devez être à la racine du projet
npm run setup

Suite à quoi on va lier le projet à Firebase :

firebase use default
# ou tout profil souhaité inscrit dans le fichier .setup-firebaserc précédemment édité

Let’s run !

Tout est installé correctement ? Testons de lancer Nuxt en local.

npm run dev

Punchez moi ce lien : http://localhost:3000 et vous devriez trouver votre super application.

Et maintenant firebase (en local encore un peu de patience)

npm run firebase:serve

Cliquez sur celui-ci http://localhost:5000 pour la même chose mais cette fois c’est firebase et votre cloud function qui tournent en local !

Let’s deploy !

npm run deploy

Normalement vous devriez avoir une superbe application NuxtJS serverless en SSR tournant sous Firebase avec la Firebase Cloud Function.

Mes félicitations !

Ceci-dit, il est minuit quarante, demain il y a boulot. Merci d’avoir lu, j’espère que le tutoriel vous a plu et je vous donne rendez-vous pour une prochaine publication !

Bien à vous,

Pophip

Rémi Perreira

Consultant en développement

Se passer d'un backend : NuxtJS & Firebase (Partie 1)

Bonjour à tous,

Je vous propose un petit tuto, on aime bien les petits tutos mais avant de commencer je vais le remettre dans son contexte initial.

Dans un précédent article posté sur le site lesveilleursdenuit.fr, je parlais de faire un projet perso pour sortir la tête de l’eau ? Eh bien oui, j’en ai lancé un et m’y suis cassé les dents (dans le sens positif du terme).

Outre la montée en compétence significative que cela m’a offert, je me suis surtout rendu compte d’une chose: il est définitivement essentiel de définir et redéfinir son besoin exact pour pouvoir choisir l’environnement technique. Etre capable de mettre au propre votre concept et le CDC fonctionnel de votre application est déjà un travail assez long et rigoureux. La partie technique elle…demande autre chose : être clairvoyant et prévoyant, voir sur le long terme.

C’est-à-dire Pophip ? C’est à dire que les technos nous réservent souvent des surprises. Elles ont des limites, des spécificités plus ou moins adaptées à vos problématiques. En tant que dev l’envie nous vient souvent d’utiliser une techno par curiosité, par interêt ou parce qu’on nous l’a fortement conseillée. Nous devrions choisir une technologie parce qu’elle correspond à ce dont on a besoin considérant également le contexte et la maintenabilité.

C’est comme ça que je me suis retrouvé avec un environnement technique applicatif trop puissant limite usine à gaz, et un ORM à l’état embryonnaire. Joli combo qui m’a valu quelques mois de : “j’en ai marre de c’te projet”.

De manière plus explicite : j’ai lancé un projet avec Nuxt.js en front et du Sails.js en back. Le tout relié à une base MongoDB et hébergé pour 7 balles le mois sous Heroku.

Nuxt c’est cool. C’est un framework SSR (Server Side Rendering) construit sur VueJS, rapide, pratique, efficace. Je ferai sûrement un article rien que pour ses beaux yeux. Je ne peux que vous conseillez d’au moins y jeter un oeil.

Sails c’est… c’est pas si mal. Si vous ne connaissez pas je vous invite à également voir leur site web.

C’est un framework NodeJS MVC (modèle vue contrôleur) inspiré de Rails (en ruby tout ça…) et qui a adopté le principe de “convention plutôt que configuration” (le fameux convention over configuration). Il permet de créer assez rapidement des APIs REST, des Single Page Applications et des applications en temps réel. Globalement il aide le développeur à écrire moins de code, et je reconnais qu’il est plutôt pratique.

Mais alors, quel est le problème ?

Le problème est que Sails a une documentation lacunaire, une communauté assez restreinte et un ORM suffisament à l’état d’embryon pour ne pas prendre en charge le deep populate. En se baladant sur le web on nous conseille de changer d’ORM, d’importer 36 libs bref on nous conseille de changer de techno.

À cela se rajoute la question de la pertinence de l’introduction de la philosophie Ruby sur du Javascript. Ce ne sont pas les mêmes modes de pensée.

De plus, je ne trouve pas normal qu’un framework back ait 3 failles critiques de sécurité (du genre injection de code en plus) à l’audit de npm sans chercher à les corriger.

Pour finir j’ai trouvé ironique que IBM développant LoopBack (donc concurrent à Sails) ait écrit une meilleur documentation sur Sails que… Sails.

On passe donc plus de temps à chercher de la documentation qu’à l’appliquer, c’est dommage pour un framework au final très bien construit. Pour aller un peu plus loin, je vous redirige vers un article traitant des soucis de sails plus en profondeur. Certes il date de 2015, mais il faut (vraiment) croire que les problématiques du framework restent globalement les mêmes (#balancetonframework).

L’autre problème vient de moi. J’étais curieux de découvrir un Ruby on Rails version JavaScript, je voulais absolument avoir un back pour au final une application qui ne faisait qu’un CRUD assez simple sans intégration particulière de systèmes externes. J’ai ainsi mal étudié mon besoin technique, je me suis mal renseigné sur mon cheval de bataille tout ça pour l’attrait que la technologie avait et le fait qu’il y a quelques temps c’était on the wave. Pophip, la tête de turque qui veut toujours faire du Rock’n Roll hein… On ne m’y reprendra plus.

Revenons à nos moutons

Quoi qu’il en soit, je me suis petit à petit retrouvé freiné à cause de l’ORM Waterline et frustré d’avoir l’impression de passer beaucoup de temps sur Sails pour pas grand chose en plus d’avoir un back à sécuriser.

Tout ça pour dire que suivant ton expérience dans le monde du dev tu apprends petit à petit à non seulement élaborer un concept fonctionnel selon un besoin mais également à choisir correctement un environnement technique. J’entends par “correctement” le terme adapté. C’est à mon sens particulièrement important pour mener à bien un projet avec en tête la pérénisation du produit.

Cela m’amène au sujet du tutoriel qui va suivre. Il n’est plus nécessaire de rappeler l’importance du Search Engine Optimisation (SEO) visant à optimiser la visibilité d’une page web dans les résultats de recherches. De son côté le SSR permet une meilleure indexation SEO. Et ô joie nous disposons grâce aux Chopin brothers de Nuxt.js fait pour ça et restant un framework intuitif et agréable malgré la complexité plus poussée d’un développement en SSR.

Mais qui dit SSR dit il est où le serveur ? Je prends quoi comme back ? Sails ? Nest ? Juste un Express ? Et si… et si il y avait plus simple ? Je fais un CRUD relativement standard donc je pourrais penser serverless. En plus j’aimerais centraliser les web services.

Là, Firebase entre en jeu.

Firebase est une plate-forme de développement d’applications mobiles et Web qui fournit aux développeurs une pléthore d’outils et de services pour les aider à développer des applications de haute qualité, à élargir leur base d’utilisateurs et à générer davantage de profits. Source - M.Chenot - 2018

Je vous propose de cliquer sur les deux liens ci-dessus si vous souhaitez en savoir plus sur Firebase.

Reprenons.

Techniquement il est possible de ne faire que du Vue au lieu de Nuxt mais on a dit que l’on veut du SSR. Donc non seulement je peux utiliser Firebase Hosting pour l’hébergement, Firebase Auth pour la gestion des utilisateurs et Firestore pour la base de données mais je peux également profiter des Cloud Functions for Firebase pour ce qui est du SSR.

On fait les comptes :

Suppression du backend
Centraliser la plupart des web services en un seul ainsi que la BDD
Déploiement au CLI en une ligne de commande
Cloud function pour le SSR
Scale automatique
Serverless
Meilleure indexation (SEO)
Les qualités de Nuxt en matière d’application (SPA, PWA etc…)
Hébergement gratuit
Cela me semble vraiment pas trop mal cette fois comme choix. Maintenant la question c’est: comment va-t-on s’y prendre pour mettre tout cela en place ?

Je vous donne donc maintenant rendez-vous dans la partie 2 pour voir comment on va pouvoir déployer une application SSR Nuxt sous Firebase.

On est d’accord c’est une grosse introduction, mais cela me tient à coeur de montrer un chemin de pensée qui mènerait à revoir son environnement technologique voir son infrastructure.

Bien à vous,

Rémi Perreira

Consultant en développement