Retour d’expérience sur Electron

Il y a deux ans, j’ai eu l’occasion de découvrir le framework Electron, permettant de développer une application de bureau tout en utilisant des langages web pour en créer l’interface.

Pour mettre un peu de contexte, dans le cadre de ma dernière année d’école, en équipe de six, nous avons développé une application de billettique pour un client extérieur à l’école. Un des critères de cette application était le fait que ce devait être une application de bureau (devant tourner sur Windows). Historiquement, l’application qu’ils souhaitaient remplacer était développée en C++ et utilisait la bibliothèque Qt. Or, nous n’avions pour ce projet que quatre semaines de développement et aucun de nous n'était à l’aise avec ce langage. Nous avons alors cherché une solution nous permettant d'utiliser des technologies que nous connaissions mieux.

Et nous avons trouvé Electron !

Nous avons alors pu développer l’interface de notre application de bureau en Angular, et développer notre back-end en Java. Puisque Electron n’encapsule que le front, le choix du back n’a aucun impact. Notons qu’ici, dû aux besoins du projet, nous avions notre backend en local, mais il est tout à fait possible d’appeler une API externe. Nous avons fait le choix d’utiliser Angular, mais Electron permet d’utiliser d’autres frameworks JS tels que React ou Vue. Vous pouvez avoir un aperçu des différentes possibilités dans cet article :
https://exhesham.wordpress.com/2018/02/05/what-language-should-you-select-for-electron-framework/

Notre application se présentait donc de cette façon :

Initialiser le projet

Installations pré-requises :
Il faut installer Node.js si ce n’est pas déjà fait.

npm install -g @angular/cli
npm install electron-packager --save-dev

La première commande sert à installer Angular CLI, pour créer et construire des applications Angular.
La seconde sert à installer electron-packager, qui permet de regrouper le code source de l'application Electron dans un exécutable.

Pour Initialiser le projet :

ng new projet-angular
cd projet-angular
npm install electron --save-dev

Ces commandes servent à créer le projet Angular et installer Electron dans l’environnement de développement.

Pour configurer le projet Angular avec Electron, il faut ajouter un fichier main.js tel que celui-ci à la racine du projet (le contenu du fichier sera détaillé un peu plus bas) :

const { app, BrowserWindow, screen, ipcRenderer, ipcMain } = require('electron');

let primary;
let secondary;

app.on('ready', () => {
  let displays = screen.getAllDisplays();
  let externalDisplay = displays.find((display) => {
    return display.bounds.x !== 0 || display.bounds.y !== 0
  });

  if (externalDisplay) {
    secondary = new BrowserWindow({
      fullscreen : true,
      x: externalDisplay.bounds.x,
      y: externalDisplay.bounds.y,
      webPreferences: {
        nodeIntegration: true,
        nodeIntegrationInWorker: true
      }
    });

    ipcMain.on('fromOne', (event, arg)  => {
      secondary.webContents.send('toTwo', arg);
    });

    secondary.setMenuBarVisibility(false);
    secondary.loadURL(`file://${__dirname}/dist/frontend/index.html#/welcome-second-screen`);
    secondary.on('closed', function () {
      secondary = null
    })
  }

  primary = new BrowserWindow({
    fullscreen : true,
    webPreferences: {
      nodeIntegration: true,
      nodeIntegrationInWorker: true
    }
  });
  primary.setMenuBarVisibility(false);

  primary.loadURL(`file://${__dirname}/dist/frontend/index.html#/auth`);

  primary.on('closed', function () {
    primary = null
  });
});

Ce fichier est le point d'entrée pour Electron et définit la façon dont l’application de bureau réagira aux divers événements de la fenêtre. Il faut également modifier le fichier package.json à la racine du projet pour ajouter les lignes ci-dessous :

[...]
"main": "main.js",
"description": "description de l'application",
"scripts": {
  [...]
  "electron": "electron .",
  "electron-build": "ng build && electron .",
  "electron-build-prod": "ng build --prod && electron .",
  "create-win-installer": "ng build --prod && electron-packager . --platform=win32 --overwrite --asar=true && node build.js"
}
[...]

Attention, s’il n y a pas de description, le packaging de l’application ne fonctionnera pas.
Les différents scripts ajoutés sont des exemples. Le premier sert à exécuter l’application encapsulée dans Electron sans la builder. Les deux suivants servent à compiler l’application (avec les propriétés de l’environnement de dev (par défaut) ou avec celles de l’environnement de prod). Le quatrième sert à packager l’application et créer un support d’installation pour Windows.

On peut aussi exécuter l’application dans un navigateur web (sans Electron), ce qui permet le hot reload (pas besoin de re-builder ou même d’actualiser la page lorsqu’une modification est faite) :

npm run start

Gestion d’un second écran

Electron permet de gérer plus d’une fenêtre, et plus d’un écran. Pour cela, Electron permet de récupérer les écrans connectés à un ordinateur grâce au module screen de la manière suivante :

let displays = screen.getAllDisplays();
let externalDisplay = displays.find((display) => {
  return display.bounds.x !== 0 || display.bounds.y !== 0
});

if (externalDisplay) {
... }

À partir de là, on peut récupérer le deuxième écran connecté et charger l’application.

Comme vu plus haut dans le fichier main.js, pour créer une fenêtre, on utilise le module BrowserWindow. On peut spécifier des propriétés liées à la dimension, le positionnement et le mode plein écran.

secondary = new BrowserWindow({
  width :800,
  height : 480,
  x: externalDisplay.bounds.x ,
  y: externalDisplay.bounds.y ,
  fullscreen : true
});

On spécifie également la ressource que doit charger la fenêtre grâce à la fonction loadUrl sur l’objet BrowserWindow :

secondary.loadURL(`file://${__dirname}/dist/angular-electron/index.html#/second-route`)

Il est possible que nous ayons besoin de faire communiquer les deux écrans. Pour cela on peut utiliser le module ngx-electron  via cette commande npm install ngx-electron --save.

On commence par ajouter des propriétés webPreferences sur les fenêtres instanciées par BrowserWindow :

secondary = new BrowserWindow({
  width :800,
  height : 480,
  x: externalDisplay.bounds.x ,
  y: externalDisplay.bounds.y ,
  fullscreen : true,
  webPreferences: {
    nodeIntegration: true,
    nodeIntegrationInWorker: true
  }
}); 

On réalise l’échange de messages grâce aux modules ipcMain et ipcRenderer d’Electron. On considère les écrans primary et secondary. On ajoute la fonction ci-dessous dans notre fichier main.js :

ipcMain.on('fromOne', (event, arg)  => {
  secondary.webContents.send('toTwo', arg);
});

Imaginons que nous avons une application ressemblant à ceci :
Cette fenêtre apparaît sur le premier écran :


Et celle-ci apparaît sur le deuxième écran :

À noter que j’ai laissé ici l’application en fenêtrée et j’ai laissé la barre de menu visible, mais il est possible d’initialiser les fenêtres en plein écran avec les propriétés fullscreen et setMenuBarVisibility de BrowserWindow dans le fichier main.js.

À ce stade, la configuration est mise en place mais il n’y a pas encore d’interaction entre les fenêtres. Pour cela, dans le component utilisé par l’écran primary, on ajoute l’import du service ngx-electron  qu’on initialise dans le constructeur :

import {ElectronService} from 'ngx-electron';
...
constructor(private _electronService: ElectronService) { }

Pour envoyer des messages depuis la fenêtre principale on définit la fonction suivante, qui sera appelée au moment du clic sur le bouton send to second screen :

toSend(){
  this._electronService.ipcRenderer.send("fromOne",this.data);
}

Dans le component utilisé par l’écran secondary, on ajoute comme pour l’écran primary l’import et le constructeur pour utiliser le service ngx-electron :

import {ElectronService} from 'ngx-electron';
...
constructor(private _electronService: ElectronService) { }

On définit la fonction suivante pour recevoir les messages :

ngOnInit() {
  this._electronService.ipcRenderer.on("toTwo", (event, args) => {
    this.zone.run(() => {
      this.message = args;
    });
  });
}

Suite à ces ajouts, lorsque l’on clique sur le bouton, le message est envoyé à l’autre écran, qui l’affiche.

Points de vigilance

Le développement via Electron enjoint à être plus prudent d’un point de vue sécurité, car Electron n’est pas un navigateur web. Ici, on peut accéder au système de fichiers, au shell utilisateur, etc. Il est donc important de veiller à ce que votre application n’exécute pas du code d’une source externe non fiable. Pour plus d’informations et de recommandations concernant la sécurité avec Electron, je vous renvoie vers la documentation d’Electron : https://www.electronjs.org/fr/docs/latest/tutorial/security.

Un reproche souvent fait à Electron est le fait qu’il soit lourd. En effet, il charge une grande partie de Chromium donc il n’y a pas de moyen de rendre une application Electron réellement légère. Donc si une de vos problématiques est d’utiliser très peu de mémoire, Electron n’est peut-être pas la meilleure solution.

Pour conclure

Si vous ne connaissez pas déjà Electron, vous connaissez très probablement une ou plusieurs applications de bureau qui utilisent ce framework, comme Discord, Atom, Visual Studio Code ou Twitch.

Dans cet article, on a vu comment initialiser un projet Angular encapsulé dans Electron. Ce framework permet de faire toute sorte de choses, comme on l’a vu avec la gestion d’un second écran. C’est une très bonne solution pour développer une application de bureau tout en continuant à utiliser des langages web. Toutefois, comme toute technologie, il faut bien considérer les problématiques auxquelles on est confronté (l’utilisation de la mémoire par exemple) avant de la choisir.