Primi passi con Electron

Qualche tempo fa ho avuto il privilegio di poter tenere una lezione all’università di Camerino per il corso di Programmazione Web. La lezione intitolata “Modern Web Development” ha toccato vari argomenti tra cui JavaScript. La prima slide riguardante il famoso linguaggio di scripting recitava:

Javascript is the new Java

A parte l’apertura ad effetto, il messaggio che ho voluto passare ai ragazzi è che ad oggi Javascript è un linguaggio non più limitato alle applicazioni web. Abbiamo già parlato di Cordova/Ionic e Node.js che permettono, rispettivamente, di creare applicazioni mobile e backend. Riuscendo dove Java ha in parte fallito, non solo girare su tutte le piattaforme ma anche su ogni possibile ambiente di sviluppo.

Cos’è Electron?

C’è un’altra tipologia di applicazioni che possiamo creare con Javascript: le applicazioni desktop. La tecnologia che ce lo permette è Electron, tool creato dal team di GitHub. In pratica Electron genera, a partire da una web application, un eseguibile per windows, macOS e Linux.

Github Electron

Github Electron

Tra le applicazioni che utilizzano Electron troviamo Slack, il notissimo sistema di chat, ma anche gli editor Atom e Visual Studio Code. Per avere un elenco completo delle applicazioni che sfruttano questo tool potete visitare la pagina dedicata sul sito ufficiale.

Sotto il cofano

Partiamo subito con un primo esempio, ogni progetto Electron ha un unico punto di ingresso: il file main.js che rappresenta il main process della nostra applicazione.

'use strict';

const electron = require('electron');

const app = electron.app;

app.on('window-all-closed', function() {
    app.quit();
});

app.on('ready', function() {

  let mainWindow = new electron.BrowserWindow({width: 800, height: 600});

  mainWindow.loadURL('file://' + __dirname + '/index.html');

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

Avrete notato che lo script che avete appena letto ha la forma di uno script node. Questo ci fa intuire cosa c’è sotto il cofano di Electron. In pratica abbiamo a disposizione un’istanza di node e una di Chromium e quindi possiamo utilizzare tutte le API messe a disposizione da entrambi gli ambienti. Al ready dell’applicazione creiamo una nuova electron.BrowserWindow e gli associamo il file index.html. Alla chiusura dell’unica finestra l’applicazione stessa viene chiusa. Per lanciare la nostra applicazione basta utilizzare il seguente comando:

electron .

Electron cercherà il file main.js nella cartella corrente e farà partire la nostra applicazione. Se invece vogliamo creare un pacchetto distribuibile abbiamo bisogno del pacchetto electron-packager e lanciare il comando:

electron-packager . myAppName --all --out=dist

dove myAppName è il nome della nostra applicazione, --all indica che vogliamo creare il pacchetto per tutte le piattaforme disponibili e --out è semplicemente la cartella di uscita.

Let’s Code

Occupiamoci ora di creare la nostra applicazione, alla fine del post avremo creato un piccolo editor di file che ci permetterà di effettuare operazioni CRUD su dei file di testo sulla nostra macchina. Iniziamo con la parte di view: una piccola applicazione React basata su MaterialUI.

import ...
import Files from '../model/Files';

export default class App extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            files:[],
            selectedFile:null
        };
    }

    componentDidMount() {
        Files.list().then((files) => {
            this.setState({
                files:files
            })
        });
    }

    onDeleteItem(file){
        Files.remove(file).then((files) => {
            this.setState({
                files:files,
                selectedFile: null
            })
        });
    };

    onItemClick(filename){
        Files.getContent(filename).then((content) => {
            this.setState(Object.assign(this.state,{
                selectedFile:{
                    name:filename,
                    content:content
                }
            }));
        });
    };

    onNewItem(){
        this.setState(Object.assign(this.state,{
            selectedFile:{
                name:"",
                content:""
            }
        }));
    }

    onSaveItem(file){
        Files.save(file.name,file.content).then((files) => {
            this.setState({
                files:files,
                selectedFile: null
            })
        });
    }

    render() {

        let currentPage;

        if(!this.state.selectedFile){
            currentPage = (
                <FileList
                    onNewItemClick={this.onNewItem.bind(this)}
                    onClick={this.onItemClick.bind(this)}
                    files={this.state.files}/>
            );
        }else{
            currentPage = (
                <Editor
                    file={this.state.selectedFile}
                    onDeleteItem={this.onDeleteItem.bind(this)}
                    onSaveItem={this.onSaveItem.bind(this)}/>
            );
        }

        return (
            <div>
                <AppBar
                    title="electron-file-editor"
                    showMenuIconButton={false}/>
                {currentPage}
            </div>
        );
    }
}

Il nostro componente App fa semplicemente da passacarte tra un oggetto di model chiamato Files e due componenti che rappresentano l’elenco dei file e la form di dettaglio del singolo file.

Il nostro file editor con Electron

Il nostro file editor con Electron

L’oggetto Files si occupa dell’effettiva gestione CRUD dei file. Abbiamo già accennato che Electron ci mette a disposizione tutte le API del mondo node, all’interno di un container web. Il nostro Files quindi sfrutta il modulo fs per leggere/scrivere contenuto dal disco.

'use strict';

const path = require('path');
const fs = require('fs');
const os = require('os');
const workPath = path.join(os.homedir(),'extrategy-electron');

const init = () => {
    return new Promise((resolve, reject) => {
        fs.mkdir(workPath, () => {
            resolve();
        });
    });
};

const list = () => {
    return new Promise((resolve, reject) => {
        fs.readdir(workPath,(err,files)=>{
            if(err){
                reject(err)
            }else{
                resolve(files);
            }
        });
    });
};

const remove = (filename) => {
    return new Promise((resolve, reject) => {
        fs.unlink(path.join(workPath,filename), (err) => {
            if(err){
                reject(err)
            }else{
                resolve(list());
            }
        });
    });
};

const getContent = (filename) => {
    return new Promise((resolve, reject) => {
        fs.readFile(path.join(workPath,filename),'utf-8', (err,data) => {
            if(err){
                reject(err)
            }else{
                resolve(data);
            }
        });
    });
};

const save = (filename,content) => {
    return new Promise((resolve, reject) => {
        fs.writeFile(path.join(workPath,filename), content, 'utf-8', (err) => {
            if(err){
                reject(err)
            }else{
                resolve(list());
            }
        });
    });
};

module.exports = {
    init:init,
    list:list,
    remove:remove,
    getContent:getContent,
    save:save
};

Conclusioni

L’ultimo esempio di codice fa capire la vera potenza di Electron: fare convivere nella stessa applicazione codice node e classico codice da web application. Secondo me Electron ha grosse potenzialità non solo dal punto di vista meramente tecnico, ma dal punto di vista commerciale. Ci sono ancora molti ambiti in cui gli utenti ancora non si fidano del software web ma preferiscono la cara vecchia icona sul desktop. Electron può quindi aiutare a sfruttare figure legate al mondo del web development per lo sviluppo di applicazione desktop. Per i più curiosi potete trovare il codice dell’esempio su questo repository GitHub. Alla prossima!

Francesco Strazzullo

Faccio il Front-end developer per e-xtrategy dove mi occupo di applicazioni AngularJS e mobile. In passato ho lavorato principalmente con applicazioni con stack Spring+Hibernate+JSF 2.X+Primefaces. Sono tra i collaboratori del progetto Primefaces Extensions: suite di componenti aggiuntivi ufficialmente riconosciuta da Primefaces. Sono anche uno dei fondatori del progetto MaterialPrime: una libreria JSF che segue le direttive del Material Design di Google.