Une messagerie instantanée full javascript et 100% dans le cloud

Publié depuis il y a 17 jours

  1. Blog
  2. Une messagerie instantanée full javascript et 100% dans le cloud

Sommaire

Voilà un défi intéressant permettant d’exploiter pleinement les nouvelles technologies que nous apporte Javascript et les nombreuses solutions cloud qui pullulent sur internet.

Pour rester simple, l’application visée sera une énième messagerie instantanée dialoguant grâce aux web sockets, pour le reste no limits 😀.

Voici donc les technologies que nous allons voir:

C’est parti !

⚠️ Ce tutoriel demande des notions de ES6, Angular 2 et Nodejs

L’application

Les développeurs d’Angular 2 conseillent de développer en Typescript, ce que je ne ferais pas : Typescript a des avantages indéniables et il peut être une force pour un projet d’équipe. Mais pour une utilisation personnelle je trouve le ratio : apports / contraintes inintéressant. On veut souvent pouvoir avancer vite et si au début on se prend au jeu du typage fort, par la suite on se laisse facilement aller aux any (comprenez, type fourre tout) par-ci par-là.

Nous allons donc rester sur Javascript, avec ES6 tout de même ! Bootstraper un projet Angular 2 en ES6 n’est pas très compliqué :

npm init
npm install --save @angular/core @angular/common @angular/http @angular/platform-browser @angular/platform-browser-dynamic @angular/router @angular/compiler
npm install --save es6-shim reflect-metadata rxjs zone.js

Avec l’arrivé de Angular 2 RC.1, l’équipe a fait le choix de découper leur framework en plusieurs modules (@angular/core, @angular/common etc …).

Après avoir installé angular et ses dépendances il va nous falloir transpiler notre code en ES5 majoritairement reconnu par tous les navigateurs. Pour cela notre loyal Gulp fera l’affaire.

npm install -g gulp
npm install --save-dev gulp browserify vinyl-source-stream babelify

Quelques dépendances installées plus tard, voici le fichier gulpfile.js :

//gulp.js

var gulp = require('gulp');
var browserify = require('browserify');
var source = require('vinyl-source-stream');

gulp.task('copy', function() {
    gulp.src([
        'node_modules/es6-shim/es6-shim.min.js',
        'node_modules/reflect-metadata/Reflect.js',
        'node_modules/zone.js/dist/zone.js'
    ])
    .pipe(gulp.dest('dist/lib'));
});

gulp.task('build', ['copy'], function() {
    browserify('src/bootstrap.js', { debug: true })
        .transform("babelify")
        .bundle()
        .on('error', function (error) { console.error(error.toString()); })
        .pipe(source('app.js'))
        .pipe(gulp.dest('dist'));
});

gulp.task('default', ['build']);

Notre fichier Gulp possède 2 tasks :copy permet de copier les librairies dont nous allons avoir besoin dans notre fichier HTML pour faire fonctionner notre application Angular 2. build pour transpiler notre application grâce à babelify et browserify.

Depuis la version 6 de Babel, toute la configuration peut être déportée dans un fichier .babelrc, de plus Babel fournit tout un tas de preset (regroupement de tags ES6) permettant de choisir quelles innovations de ES6 on veut utiliser ou non (encore plus fort, on peut même avoir accès à des concepts encore à l’état de draft). Enfin Babel nous fournit également des plugins ; pour profiter des annotations par exemple, qu’on retrouve dans Angular 2 avec Typscript.

Voici donc notre fichier .babelrc (preset + plugins pour angular) :

{
  "plugins": [
    "angular2-annotations",
    "transform-decorators-legacy",
    "transform-class-properties",
    "transform-flow-strip-types"
  ],
  "presets": [
    "es2015"
  ]
}

N’oublions pas d’ajouter ces dépendances à notre projet

npm install --save-dev babel-preset-es2015 babel-plugin-angular2-annotations babel-plugin-transform-decorators-legacy babel-plugin-transform-class-properties babel-plugin-transform-flow-strip-types

Nous sommes fin prêt à coder notre application. Pour rappel l’objectif est une messagerie instantanée, nous allons donc découper notre application comme tel :

/src
    /components
        app.js
        login.js
        chat.js
    /services
        auth.js
        chat.js
    bootstrap.js
index.html

Concentrons nous sur les deux services : Authentication (auth.js) est un service pour gérer une authentification toute relative à nos clients (on ne va pas faire dans le compliqué et stocker les informations du client dans le localStorage).

//auth.js

'use strict';

import { Injectable } from '@angular/core';

@Injectable()
export class Authentication {

  constructor () {
    this.user = JSON.parse(localStorage.getItem('chat-auth'));
  }

  isAuthenticated () {
    return !!this.user;
  }

  login (username) {
    this.user = { username }; //ES6
    localStorage.setItem('chat-auth',  JSON.stringify(this.user));
  }

  logout () {
    this.user = null;
    localStorage.removeItem('chat-auth');
  }
}

Rien de bien compliqué ici, à noter toute fois l’utilisation de l’annotation Injectable permettant d’injecter le service dans nos composants angular.

Notre deuxième service, Chat (chat.js) va permettre le dialogue entre l’application et le serveur, il servira à envoyer des messages et de recevoir ceux des autres utilisateurs.

//chat.js

'use strict';

import io from 'socket.io-client';

export class Chat {

  constructor (url, params) {
    this.messages = [];
    this.socket = io.connect(url, params);
    this.socket.on('chat.receive_message', message => this.receiveMessage(message));
    this.socket.on('chat.connected', messages => this.receiveMessages(messages));
  }

  sendMessage (message) {
    this.socket.emit('chat.send_message', message);
  }

  receiveMessage (message) {
    this.messages.push(message);
  }

  receiveMessages (messages) {
    messages.forEach(message => this.messages.push(message));
  }
}

Nous voilà confronté à l’utilisation des sockets pour dialoguer avec le serveur, pour ce faire nous allons utiliser la librairie Socket.io. La première chose à faire est de se connecter via io.connect(url, params) (nous pouvons également passer des paramètres à la connexion, typiquement notre objet user contenant le pseudo de l’utilisateur).

Ensuite on peut entamer le dialogue :

socket.on('mon-event', callback(data)); // Le client écoute un événement venant du serveur
socket.emit('mon-event', data); // Le client envoie un événement au serveur

N’oublions pas d’ajouter Socket.io à notre projet (version client) :

npm install --save socket.io-client

Voilà qui est fait, nous pouvons maintenant nous concentrer sur nos composants : un premier gérant l’authentification (login.js), un deuxième l’affichage de la messagerie (chat.js) et enfin notre troisième qui fera le lien (app.js, le composant root).

//app.js

'use strict';

import { Component } from '@angular/core';
import { LoginComponent } from './login';
import { ChatComponent } from './chat';
import { Authentication } from './../services/auth';

@Component({
  selector: 'chat-application',
  directives: [LoginComponent, ChatComponent],
  template: `
  <h1>Ma messagerie instantanée</h1>
  <login-component *ngIf="!auth.isAuthenticated()"></login-component>
  <chat-component *ngIf="auth.isAuthenticated()"></chat-component>
  `
})
export class AppComponent {
  constructor (auth: Authentication) {
    this.auth = auth;
  }
}
//chat.js

'use strict';

import { Component } from '@angular/core';
import { Authentication } from './../services/auth';
import { Chat } from './../services/chat';

@Component({
  selector: 'chat-component',
  template: `
  <ul>
    <li *ngFor="#message of messages">{{ message.username }} : {{ message.content }}</li>
  </ul>
  <form (submit)="sendMessage()">
    <input type="text" [(ngModel)]="message" />
    <button type="submit">Envoyer</button>
  </form>
  `
})
export class ChatComponent {
  constructor (auth: Authentication) {
    this.chat = new Chat('http://localhost:8081', { query: `username=${this.auth.user.username}`});
    this.messages = this.chat.messages;
  }

  sendMessage () {
    this.chat.sendMessage(this.message);
    this.message = null;
  }
}
//login.js

'use strict';

import { Component, Input } from '@angular/core';
import { Authentication } from './../services/auth';

@Component({
  selector: 'login-component',
  template: `
  <form (ngSubmit)="login()">
    <label for="username">Speudo</label>
    <input id="username" type="text" [(ngModel)]="username" required="required" />
    <button type="submit">Connexion</button>
  </form>
  `
})
export class LoginComponent {
  constructor (auth: Authentication) {
    this.auth = auth;
    this.username = null;
  }

  login () {
    this.auth.login(this.username);
  }
}

Pas de pièges, ce sont 3 composants basiques (cf : créer un composant), on injecte notre service Authentication au trois composants :

⚠️ Attention l’url du serveur est actuellement en dur dans le code, idéalement il faudrait créer un service de configuration, ou encore inclure un fichier parameters.json avec notre config.

ℹ️ On aurait pu utiliser le routage d’Angular 2 pour définir quoi afficher dans notre composant root (AppComponent). Cela aurait été plus propre dans la mesure où l’on peut utiliser l’annotation CanActivate avec notre service Authentication.

Enfin on bootstrap notre application dans un fichier bootstrap.js contenant :

'use strict';

import { bootstrap } from ‘@angular/platform-browser-dynamic’;
import { Authentication } from ‘./services/auth’;
import { AppComponent } from ‘./components/app’;

bootstrap(AppComponent, [Authentication]).catch(err => console.error);

Une bonne chose de faite, on peut maintenant ajouter tout ceci dans un fichier HTML pour tester notre application.

<!DOCTYPE html>
<html lang="fr">
    <head>
        <meta charset="utf-8"/>
        <title>Messagerie instantanée</title>
    </head>
    <body>
        <chat-application></chat-application>
        <script src="dist/lib/es6-shim.min.js"></script>
        <script src="dist/lib/Reflect.js"></script>
        <script src="dist/lib/zone.js"></script>
        <script src="dist/app.js"></script>
    </body>
</html>

On build et on lance notre serveur de test, puis on se rend sur http://localhost:8080 and tadaa 🎉.

gulp build
http-server .

Le serveur

Bon soyons honnête, notre application ne fait pas grand chose sans serveur. Pour ce faire je vous invite à créer un nouveau projet avec les dépendances que voici :

npm init
npm install --save socket.io mongoose

On va faire un premier jet en créant un serveur sans base de données, il n’y aura donc aucune persistance lorsqu’on arrêtera celui-ci.

//simple-server.js

'use strict';

import socket from 'socket.io';

const PORT = process.env.NODE_PORT || 8080;

let io = socket(PORT);
console.log(`🌏  Server start on port ${PORT}`);

let messages = [];
io.on('connection', socket => {
    // On stock les données du client dans la socket
    socket._user = { username: socket.handshake.query.username };

    // Lorsqu'un utilisateur se connecte, on lui envoie tous les messages
    socket.emit('chat.connected', messages);

    socket.on('chat.send_message', content => {
        let message = { username: socket._user.username, content };
        messages.push(message);
        io.emit('chat.receive_message', message);
    });
});

Encore une fois pas de piège, on retrouve Socket.io (version serveur) qu’on à déjà vu dans le front. Petites subtilités toutefois :

ℹ️ Il faut bien faire la distinction entre (dans l’exemple) socket qui fait référence au client connecté et io (via des méthodes comme emit) qui permet de faire référence à tous les autres clients connectés.

Pour tester ce code on doit, tout comme le front, passer par Babel (on pourrait bien entendu utiliser nativement Node, mais actuellement le support de ES6 n’est pas totalement finie), nous allons donc installer babel-cli ainsi que le preset que l’on va ajouter dans un nouveau fichier de configuration .babelrc :

{
  "presets": [
    "es2015"
  ]
}

Et …

npm install --save babel-cli babel-preset-es2015
babel-node simple-server.js

⚠️ Attention aux ports entre l’application et le serveur. Dans notre code nous avons opté pour que le serveur en localhost soit sur le port 8081. Si vous voulez changer le port du serveur il faut passer par la variable d’environnement NODE_PORT :

NODE_PORT=8081 babel-node simple-server.js

chat

La base de données

C’est maintenant qu’on commence à jouer avec les services cloud, pour notre base de données MongoDB ; mLab (https://mlab.com/) fournit 500MB gratuitement, largement suffisant pour notre messagerie.

Il suffit donc de créer un compte, puis de créer une nouvelle base de données (single core > sandbox), n’oubliez pas de créer un utilisateur pour votre base.

Enfin nous allons faire évoluer notre serveur pour y intégrer notre base de données :

//server.js

'use strict';

import socket from 'socket.io';
import mongoose from 'mongoose';
import Message from './models/message';

const PORT = process.env.NODE_PORT || 8080;

mongoose.connection.on('connected', () => {

    let io = socket(PORT);
    console.log(`🌏  Server start on port ${PORT}`);
    console.log('📦  Mongoose connection open');

    io.on('connection', socket => {
        // On stock les données du client dans la socket
        socket._user = { username: socket.handshake.query.username };

        Message.find().exec().then(messages => {
            // Lorsqu'un utilisateur se connecte, on lui envoie tous les messages
            // depuis la base de données
            socket.emit('chat.connected', messages);
        });

        socket.on('chat.send_message', content => {
            let message = new Message();
            message.username = socket._user.username;
            message.content = content;
            message.save(); // On persiste notre message

            io.emit('chat.receive_message', message);
        });
    });
});

mongoose.connection.on('error', console.error);
mongoose.connect('mongodb://<dbuser>:<dbpassword>@ds021691.mlab.com:21691/<dbname>');
// Permet d'utiliser les Promise native avec mongoose
mongoose.Promise = global.Promise;

On importe donc Mongoose et notre modèle Message (voir plus bas). On initie la connexion et seulement lorsque celle-ci est ouverte on ouvre notre socket.

Pas de surprises ici non plus, soulignons toute fois qu’on peut utiliser les Promise avec Mongoose grâce à mongoose.Promise = global.Promise.

Enfin notre modèle (à mettre dans un dossier models):

//message.js

'use strict';

import mongoose from 'mongoose';

let messageSchema = new mongoose.Schema({
    content: String,
    username: String,
    created_at: { type: Date, default: Date.now }
});

export default mongoose.model('Message', messageSchema);

Déploiement du serveur

Pour déployer dans le cloud notre serveur nous avons besoin d’un service équipé de Node : Now (https://zeit.co/now). Pour utiliser Now rien de plus simple :

npm install -g now
now

Lors de votre première utilisation, Now va vous demander de créer un compte en invite de commande. Puis il suffira de remplir notre fichier package.json pour ajouter le script start. En effet Now après avoir publier le code sur sa plateforme joue deux lignes de commande : npm install et npm start.

Il faut donc ajouter cette ligne dans notre fichier package.json :

"scripts": {
    [...]    
    "start": "babel-node server.js"
}

Une fois déployé, Now copie dans le presse papier le lien pour accéder au serveur, c’est ce lien qu’il faudra changer dans notre application.

Déploiement de l’application

Concernant notre front on va utiliser Surge.sh (https://surge.sh/), à l’instar de Now, Surge permet de déployer des application HTML / JS : parfait pour notre application Angular.

Surge s’utilise exactement comme Now, un fois installé il suffit de lancer la commande et de s’inscrire :

npm install -g surge
surge

Une fois déployé, Surge vous donne le lien vers votre application 👍.

⚠️ N’oubliez pas de lance la commande gulp build avant de déployer pour prendre en compte les dernières modifications. De plus Now et Surge propose un service gratuit mais limité en nombre de déploiement.

Voir la démonstration

Conclusion

Voici un exemple de ce qu’on peut faire avec javascript et les nombreux services disponibles sur internet. Ceci est bien entendu qu’une petite partie du potentiel de javascript. On pourrait également parler de Electron qui permet de faire des applications bureau ou encore Ionic pour les applications mobile…

Bref javascript n’a pas fini de nous étonner ! Vous pouvez retrouver mon code sur github :

Messagerie instantanée

Bonus

Déployer ses projets dans le cloud est un bon moyen de tester ou faire tester “à la volée” son code sans avoir besoin d’installer un environnement local, et cette étape peut facilement s’industrialiser dans du déploiement continue. C’est ce que propose cet article de Julien Bianchi avec gitlab CI :

Preview your website with Gitlab CI and Surge