Application en temps réel avec Angular et FireBase
Par le passé, je me suis fortement intéressé aux applications en temps réel. Je m’étais attaqué aux différentes façons que les technologies de l’époque nous offraient pour y parvenir, comme le periodic polling, le long polling et les websockets. Ça restait relativement complexe et coûteux à mettre en place dans une application traditionnelle, donc ces technos étaient plus réservées à des cas d’utilisation bien précis, comme les réseaux sociaux, les applications collaboratives, etc..
Aujourd’hui, je vous présente comme faire une application en temps réel avec Angular et la base de données temps réel de Firebase.
Attends, Sylvain… une application sans backend ?
Yup.
Qu’est qu’une BD Temps Réel ?
La base de données en temps réel (RTDB) de Firebase est une BD no sql documentaire autogérée et hébergée dans le cloud. Elle permet de syncroniser les données stockées au format JSON avec tous les clients en temps réel via une des méthodes énumérées en introduction.
Un autre gros avantage de la RTDB vient du fait qu’elle fonctionne même si le client est hors ligne. Toute transaction faite pendant qu’un client est hors ligne sera écrite dans un stockage local, puis synchronisée dès que la connexion se rétablie.
De plus, RTDB nous offre un système de règles définies au format JSON pour permettre d’appliquer des validations au niveau de l’accès aux données. Par validations, j’entends des validations standard de données, mais aussi des validations au niveau du jeton d’accès de l’utilisateur.
Pourquoi il n’y a pas de backend ?
Comme expliqué juste avant, Firebase gère déjà le processus d’authentification des utilisateurs. En étant authentifié auprès de Firebase, un jeton est émit à l’utilisateur qui doit être envoyé lors de chaque accès à un élément dans la BD. Vous savez aussi, comme mentionné au paragraphe ci-dessus, que RTBD permet d’appliquer des validations sur les données et sur le jeton d’accès pour permettre ou non l’accès à certaines données.
Il y a un cas bien précis que nous avons eu besoin d’ajouter une cloud function à notre projet. Je vous en parle dans la section sécurité, plus bas.
Création d’une application ToDo
Parce que j’adore toujours refaire le même projet dans des technos différentes, faisons une application de gestion de tâches en collaboration en temps réel.
Création du projet FireBase
Rendez-vous sur https://console.firebase.google.com/ pour créer votre projet Firebase. Vous devrez probablement activer une facturation auprès de Google si ce n’est pas déjà fait via Google Cloud. Par contre, l’utilisation de la BD en temps réel est gratuite pour une utilisation en mode preuve de concept. Vous serez facturer uniquement après un certain nombre d’accès, de stockage, ou de transfert réseau.
Puis, lorsque vous voyez :
On est presque prêt à partir. Il nous reste simplement à récupérer notre configuration Firebase pour le client Angular. Depuis votre tableau de bord Firebase, créer une nouvelle application web en cliquant sur l’icône, comme ci-dessous :
Firebase devrait vous poser quelques questions concernant votre application. Répondez au meilleur de vos connaissances 😛 On devrait vous remettre quelque chose qui ressemble à ceci :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
<!-- The core Firebase JS SDK is always required and must be listed first --> <script src="https://www.gstatic.com/firebasejs/8.7.1/firebase-app.js"></script> <!-- TODO: Add SDKs for Firebase products that you want to use https://firebase.google.com/docs/web/setup#available-libraries --> <script> // Your web app's Firebase configuration var firebaseConfig = { apiKey: "AIzaSyAg4fK7KA1zCN_svVq5tI91FcWvCrOrdPs", authDomain: "blog-213414.firebaseapp.com", projectId: "blog-213414", storageBucket: "blog-213414.appspot.com", messagingSenderId: "224957507650", appId: "1:274957205650:web:0a32c4d92f523670b5eeb" }; // Initialize Firebase firebase.initializeApp(firebaseConfig); </script> |
N’utilisez pas ce code, j’ai altéré les clés. Il ne fonctionnera pas, utilisez ce que Firebase vous donne.
Création de l’application Angular
Depuis un dossier sur votre poste :
1 |
ng new todo-application |
Puis, ajoutons Angular Firebase au projet pour pouvoir se connecter facilement à notre base de données :
1 |
ng add @angular/fire |
Afin de configurer Angular Firebase, ajoutons les propriétés ci-dessus au environment.ts d’Angular, dans notre projet :
1 2 3 4 5 6 7 8 9 10 11 |
export const environment = { production: false, firebase: { apiKey: "AIzaSyAg4fK7KA1zCN_svVq5tI91FcWvCrOrdPs", authDomain: "blog-213414.firebaseapp.com", projectId: "blog-213414", storageBucket: "blog-213414.appspot.com", messagingSenderId: "224957507650", appId: "1:274957205650:web:0a32c4d92f523670b5eeb" } }; |
De cette façon, nous pourrions avoir une configuration Firebase pour la dev et la prod, par exemple. Dernier truc pour la configuration d’Angular Firebase, c’est l’importation dans le module principal du module AngularFirebase :
1 2 3 4 5 6 7 8 9 10 11 12 13 |
@NgModule({ declarations: [ AppComponent ], imports: [ BrowserModule, AngularFireModule.initializeApp(environment.firebase), ReactiveFormsModule ], providers: [], bootstrap: [AppComponent] }) export class AppModule { } |
Et on est prêt à coder 🙂 Commençons simplement par la vue :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
<form #todoForm="ngForm" [formGroup]="todoFormGroup" (submit)="createTodo()"> <div> <input type="text" formControlName="description"> <button>Ajouter</button> </div> </form> <table> <thead> <tr> <th>Description</th> <th>Fait</th> </tr> </thead> <tbody> <tr *ngFor="let todo of allTodos"> <td>{{todo.description}}</td> <td>{{todo.done ? 'Oui' : 'Non'}}</td> </tr> </tbody> </table> |
Ensuite, il nous faut un objet pour représenter la tâche à faire. Gardons-là simple, mais remarquez la propriété key de la classe. Cette propriété nous servira à stocker la clé d’identification unique de l’objet dans la BD temps réel.
1 2 3 4 5 |
export interface Todo { key: string; description: string; done: boolean; } |
Puis, créons le code du composant. Remarquez l’utilisation du TodoService qui n’existe pas encore, nous y viendrons à la suite!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
@Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.scss'] }) export class AppComponent implements OnInit, OnDestroy { todoFormGroup: FormGroup; allTodos: Todo[] = []; @ViewChild('todoForm') todoForm?: NgForm; private sub = new Subscription(); constructor( private todoService: TodoService, private fb: FormBuilder) { this.todoFormGroup = this.fb.group({ description: ['', Validators.required], done: [false, Validators.required] }); } ngOnInit() { const todoListSub = this.todoService .allTodos() .subscribe((todos: Todo[]) => this.allTodos = todos); this.sub.add(todoListSub); } ngOnDestroy() { this.sub.unsubscribe(); } createTodo() { if (this.todoFormGroup.invalid) return; const todo: Todo = this.todoFormGroup.value; this.todoService.create(todo); this.resetForm(); } private resetForm() { this.todoForm?.resetForm(); this.todoFormGroup.setValue({ description: '', done: false }); } } |
Jusqu’ici, tout devrait vous sembler du code Angular normal. Nous souscrivons sur le service de todos afin d’être notifié par la base de données via l’observable retourné. Pour la création, on ne fait qu’appeler le service de création d’un todo. Remarquez qu’on ne traite pas le nouveau todo pour l’ajouter à la liste de todos, puisque la BD temps réel va nous avertir de l’ajout, il s’ajoutera par lui-même à la liste via la souscription. Voyons le service :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
@Injectable({ providedIn: 'root' }) export class TodoService { private allTodosRef: AngularFireList<Todo>; constructor( private db: AngularFireDatabase ) { this.allTodosRef = this.db.list("todos"); } allTodos(): Observable<Todo[]> { return this.allTodosRef .snapshotChanges() .pipe(map(changes => changes.map(c => ({ key: c.payload.key, ...c.payload.val() } as Todo)))); } create(todo: Todo): void { this.allTodosRef.push(todo); } } |
Ici, on identifie les objets JSON stockés en BD via un chemin d’accès, comme une URL, soit « todos ». Un exemple plus complexe pourrait ressembler à « settings/calendar-settings/latest », qui pourrait contenir un objet de configuration d’un calendrier. Donc, le principe est d’utiliser une référence à la collection d’objets dans la BD temps réel et cette dernière va nous avertir de changement via l’observable retourné par la méthode allTodos().
À ce moment, si vous exécutez l’application, vous devriez être en mesure d’ajouter des todo à votre BD. Vous pouvez aussi tester via la console Firebase de mettre à jour un objet en BD, votre liste de todos devrait se rafraichir automatiquement.
La sécurité
Bon, là je vous entends me crier après : Mais, Sylvain, on peut écrire en BD sans authentification ? Oui, et non 🙂 Je le permets ici pour des fins de simplicité, mais il est possible d’utiliser l’authentification Firebase pour forcer l’utilisateur à s’authentifier, puis à ajouter des règles dans la BD temps réel pour bloquer la lecture ou l’écriture. Dans la console Firebase, dans votre BD temps réel, choisissez l’onglet « Rules ». La mienne ressemble à :
1 2 3 4 5 6 |
{ "rules": { ".read": "now < 1628913600000", // 2021-8-14 ".write": "now < 1628913600000", // 2021-8-14 } } |
Ceci signifie que, pour tous les objets de la BD, la lecture et l’écriture sera activée sans authentification jusqu’au 14 août 2021. Voyons une règle un peu plus complèxe :
1 2 3 4 5 6 |
{ "rules": { ".read": "auth.token.admin === true", ".write": "auth.token.admin === true" } } |
Ici, auth.token est une variable founie par Firebase qui nous permet d’obtenir les claims associées au jeton. On s’attend donc à ce que l’utilisateur authentifié aie le rôle admin. Il peut y avoir plusieurs façon de gérer les claims d’un utilisateur, mais un bon exemple est de créer une Cloud Function dans le projet Firebase qui sera appellée à la création d’un nouvel utilisateur. À la création, vous pourriez valider, par exemple, que son adresse courriel se trouve dans un groupe précis et lui ajouter la claim admin si tel est le cas.
Si, dans un autre cas, nous voudrions avoir des données accessible uniquement à un utilisateur, nous pourrions stocker les todos sous « todos/public » pour les todos partagés et sous « todos/[userIdFromToken] » pour les todos privés. En ajoutant une règle comme celle-ci, seulement l’utilisateur propriétaire pourrait voir ses todos :
1 2 3 4 5 6 7 8 9 10 |
{ "rules": { "todos": { "$uid": { ".write": "$uid === auth.uid", ".read": "$uid === auth.uid" } } } } |
Conclusion
Firebase et AngularFirebase nous offre des outils puissants, vous l’avez lu, pour créer des applications en temps réel. Je vous lance donc la question : Qu’est qui justifierait de ne pas utiliser une base de données en temps réel ? J’aimerais bien avoir votre opinion en commentaires 🙂
Merci pour votre lecture! Cheers!
Commentaires
Laisser un commentaire