Webpack ECMAScript 6 – Parte I

maxresdefault

Le odierne Applicazioni Web (2.0) ed in particolare l’enorme successo riscosso da NodeJS, hanno catapultato agli onori delle cronache un linguaggio a cui era stata predetta una fine a breve termine, il Javascript! Si, proprio lui, quel linguaggio che il GWT (Google Web Toolkit), ne aveva quasi decretato la fine, oggi appare (e lo è :-)) uno degli attori principali nel panorama delle SPA (Single Page Application) e non solo.  AngularJS, React, MongoDB, ExpressJS, e non ultimi Ember.js ed Electron, ne fanno parte e non necessitano di alcuna presentazione.

Quelle persone che a lungo lo hanno bistrattato, per una forte inclinzione a linguaggi strutturati o semi-strutturati, strong typed, deve oggi ricredersi, grazie anche alla connotazione Object-Oriented  che questo ha ricevuto con le Specifiche definite nello Standard ECMAScript 6.

Ma l’obiettivo di questo post non è il linguaggio Javascript in se, ma uno dei tool più interessanti degli ultimi anni, webpack!! In questa prima parte del tutorial, andremo assieme a conoscere la teoria dei Module Loader, Module Bundler, ed alcuni module pattern, introduzione dovuta, al fine di comprendere appieno le peculiarità del tool oggetto di studio.

Module Loader:  CommonJS, AMD, RequireJS

…….Good authors divide their books into chapters and sections; good programmers divide their programs into modules……..

Negli ultimi dieci anni l’evoluzione del Javacript ha regalato la vita al concetto di “modulo di codice”, dovendo rispondere ad un notevole cambiamento di direzione della logica applicativa delle Web Application, che si è diretta in maniera convulsa, e repentina dal back-end al front-end.

Il concetto di modulo di codice ad oggi ha una fondamentale importanza, basti pensare che solo fino a pochi anni fa, era difficile gestire efficientemente molti file, a causa delle “dipendenze” fra di essi,  in quanto il linguaggio non era in possesso di meccanismi nativi  atti a gestirle (import ed export), e la conseguente  mancanza di un alberatura delle dipendenze, delegava su di noi l’ordinamento dei vari moduli.

Questa mancanza ci obbligava ad utilizzare combinazioni di closures e anonymous function (IIFEImmediately-invoked Function Expression). Vediamo un esempio in old school,  conosciuto anche come Module Pattern:

 

var cnjModulePattern = (function () {
    
  // Keep this variable private inside this closure scope (lexical scope)
  var cnjMagicNumber = [93, 95, 88, 0, 55, 91];

  // Expose these functions via an interface while hiding
  // the implementation of the module within the function() block

  return {

     elevateAndSum: function() {

      var sum = cnjMagicNumber.reduce(function(accumulator, number) {
        return accumulator + number * number;
        }, 0);
        
      return 'The sum of any duplicate numbers is ' + sum ;
    },

    filtered: function(extremeSup) {
    	
      var filteredNumber = cnjMagicNumber.filter(function(number) {
          return number < extremeSup;
        });

      return 'Number < of ' + extremeSup + ' are: ' + filteredNumber;
    }
  }
})();

cnjModulePattern.elevateAndSum(); // "The sum of any duplicate numbers is 36724"
cnjModulePattern.filtered(); // "Number < of 70: 0,55"

L’ambiente condiviso appena creato, come è facile notare, è costruito all’interno di un anonymous function, la quale contiene una variabile privata, cnjMagicNumber, a cui non è possibile accedere direttamente al di fuori del suo body (closure scope) , mentre, grazie al Javascript Lexical Scope, possiamo accedere ad i metodi pubblici, cnjModulePattern.elevateAndSum() e cnjModulePattern.filtered();

Con questo tipo di approccio (Self-Contained Object Interface), siamo in grado di decidere quale variabile o metodo rendere privato e quale variabile o metodo esporre, rendere pubblico, attraverso lo statement return, come avviene per le API.

Sicuramente il concetto di modularizzazione risolve diversi problemi ben noti in tutti gli ambienti ingegneristici. Rendere un qualsiasi processo/prodotto una composizione di moduli ha degli importanti vantaggi che vedremo nel seguito del post.

CommonJS

Il 2009 è un anno di rinascita per il Javascript, NodeJS implementa il module pattern CommonJS, nato dal comitato spontaneo con nome omonimo, eliminando definitivamente i problemi legati al corretto ordinamento delle inclusioni.

L’idea alla base è quella di ottenere un applicazione modulare, un insieme di funzionalità altamente disaccoppiate (loose coupling), facilmente manutenibili, riusabili, organizzate e strutturate in moduli di codice. Passiamo subito ad un semplice esempio pratico del pattern specificato:

// cnj/core is a dependency we require
var core = require( "cnj/core" );

// behaviour for our module
function foo(){
    core.log( "hello world from Coding-Jam!" );
}

// export (expose) foo to other modules
exports.foo = foo;

Molto semplicemente CommonJS, anziché eseguire il codice Javascript all interno del global scope, lo esegue all interno dello scope del modulo ( “incapsulato” in una closure). In questo scope nascono due nuove variabili (oltre a module), con cui importare ed esportare i moduli:  exports e require(id).

Il primo viene usato per esporre variabili o metodi ad altre librerie, il secondo per effettuarne l’import o leggere il contenuto del file indicato.

L’id della funzione require() è l’id del modulo, concepito originariamente per fornire di metadati quest’ultimo. Tuttavia, per superare un inconveniente con l’export pattern, Node.JS  ha esteso la variabile module, esponendo l’ oggetto exports come proprietà. Da qui nasce il famoso e tanto caro statement , module.exports.

AMD

Purtroppo l’esecuzione delle funzione require() è una computazione sincrona, per cui non si adatta e neanche sposa pienamente il contesto dei browser, dove il caricamento dei singoli file javascript avviene in modalità asincrona.

Per questi motivi, CommonJS definisce una variante asincrona del pattern, chiamata AMD (Asynchronous Modules Definition, nata dall’esecuzione di un fork di CommonJS), variante molto più congeniale al contesto dei browser. Il caricamento asincrono di AMD ci permette di scaricare progressivamente solo i file strettamente necessari all’esecuzione della nostra applicazione.

I due concetti chiave che è necessario conoscere sono i seguenti:

  • funzione define(): utilizzata per facilitare la definizione dei moduli;
  • funzione require(): utilizzata per la gestione e caricamento (import) dei moduli;

Ma entriamo nello specifico attraverso un semplice caso d’uso per espletare queste informazioni finora fornite:

 
define('cnjModule', 
    ['happy', 'coding'], 
    // module definition function
    // dependencies (happy and coding) are mapped to function parameters
    function ( happy, coding ) {
        // return a value that defines the module export
        // (i.e the functionality we want to expose for consumption)
    
        //  our module 
        var cnjModule = {
            happyCoding:function(){
                console.log('Happy Coding from CNJ');
            }
        }
 
        return cnjModule;
});

Come precedentemente asserito, per mezzo della funzione define(), vengono definiti i moduli, a cui gli viene assegnato un nome (e.g cnjModule), e le eventuali dipendenze, sotto forma di array. Il pattern in oggetto carica questa ultime in modalità asincrona, le quali vengono passate come parametri alla funzione di callback (anonymous function), nello stesso esatto ordine con cui sono state specificate nella definizione iniziale, nel nostro caso le dipendenze  sono happy,coding, e sono passate alla callback in accordo all’ordine specificato nell’array.

RequireJS ed r.js:

Una delle implementazioni più note delle Specifiche AMD è RequireJS. Come i suoi “parenti”  module loader, anch’esso ci permette di scrivere codice modulare, scalabile,  gestendo per noi le dipendenze.

Con esso abbiamo una novità, che nel seguito sarà mantenuta anche dagli altri tool di “bundling“, e consiste nel concentrare in un unico file di configurazione, tutte le informazioni necessarie all’applicazione, informazioni che saranno servite a run-time.

Sul fondo del body del markup HTML, sarà inserito un tag script con la libreria RequireJS, contraddistinto dall’attributo data HTML5, data-main, entry-point dell’applicazione,  punto da cui si dirama la chain di import asincroni, come da manuale AMD; per cui in una tipica Web Application con RequireJS, il nostro entry-point sarà dato dalla seguente istruzione:

<script data-main="js/main" src="js/libs/require/require.js"></script>

Vediamo un semplice esempio di file di configurazione ed analizziamo i singoli parametri:

requirejs.config({ 
    "baseUrl": "js/lib", 
    "paths": { 
        "app": "../app"
    },
    "shim": {
        "coding": ["jquery"]
    } 
});
  • baseUrl: RequireJS utilizza questo elemento per la ricerca dei moduli javascript da caricare attraverso la funzione define(), se omettiamo questo parametro, verrà considerato con percorso di default quello della pagine HTML con lo script data-main sopra citato.
  • paths: questo parametro serve a configurare i percorsi dei moduli di codice che non risiedono nel path specificato da baseUrl;
  • shim: in presenza di funzioni asincrone come dipendenze, risulta molto utile utilizzare questa proprietà, per definire queste direttamente nel file di configurazione, inoltre questo parametro indica a RequireJs l’ordine dei caricamento dei vari moduli;

In genere la naming convention per la costruzione dei riferimenti è costituita concatenando il baseUrl con il path, mentre il suffisso .js è aggiunto automaticamente, quando omesso.

Il file di configurazione utilizzato risulta molto semplice, RequireJS mette a disposizione un set di opzioni che non andremo a percorrere, ma che sono disponibili nella Documentazione Ufficiale.

Require Optimizer r.js

La Latenza di Networking sul download asincrono di ogni file javascript, ha  un impatto negativo sulle performance dell applicazione, motivo per il quale RequireJS consiglia di utilizzare il suo tool da riga di comando ,RequireJS Optimizer, aka r.js, il quale crea un “bundle” per ogni modulo di codice, seguendo una procedura piuttosto intuitiva, scorriamola insieme:

  • dopo aver effettuato il parsing dei vari file a partire dall’entry-point, viene ricostruita l’intera alberatura delle dipendenze, a questo punto i moduli vengono ordinati e concatenati in un unico file, che può avere un nome qualsiasi, ma è pratica comune chiamarlo bundle.js.

Quest’ultimo andrà a sostituire il nostro main, visto precedentemente, con la seguente istruzione:

<script data-main="js/bundle" src="js/libs/require/require.js"></script>

Questo passaggio dovrebbe ottimizzare le performance, difatti l’ordinamento con cui vengono concatenati i moduli da parte del tool, permette a RequireJS di effettuare un operazione di “caching” dei moduli, a run-time, con lo scopo di eliminare o diminuire drasticamente eventuali richieste di download, prima di invocare la funzione requirejs(), con cui viene inizializzata la libreria.

Se desiderate avere maggiori dettagli su RequireJS, vi consiglio il post di David Zambon TDD in Javascript con RequireJS.

Module Bundler e Webpack !!!!

Eccoci giunti al vero obiettivo di questo tutorial, webpack,  uno  dei principali attori nel folto ecosistema dei module bundler. Ma cos’è un Module Bundler?

Ad alto livello, un module bundler è semplicemente un processo che unisce un insieme di moduli di codice (e le loro dipendenze), in un singolo file (o gruppi di file), in ordine corretto.

webpack prende i moduli e le loro dipendenze e genera risorse statiche che rappresentano quei moduli, per cui nel complesso grafico delle dipendenze saranno incluse anche immagini, fogli di stile (CSS), font e javascript file. Questa peculiarità è molto vantaggiosa in applicazioni dove le risorse statiche sono in numero alto, meno in applicazioni di piccola taglia.

L'”artefatto” dei module bundler come webpack è un bundle, letteralmente “fagotto” dall’inglese, prodotto finale del processo di build.

Oltre a gestire le dipendenze per noi, webpack enfatizza il concetto di Lazy Loading (caricamento pigro), traducibile in download di script on demand, ovvero solamente quando essi sono effettivamente necessari per il prosieguo dell’applicazione, concetto molto importante in un contesto, il Web 2.0, dove gran parte dell elaborazione è a carico del client, con pagine web che sono sempre più complesse.

La feature che si occupa di questo aspetto, ovvero della gestione del caricamento dinamico dei moduli, è il Code Splitting. Webpack “splitta“, suddivide i moduli di codice in “chunks” (pezzi, parti, traduzione dall’inglese, ma che fornisce un idea molto chiara del concetto), caricati on demand. Altri module bundler, chiamano questa feature “layers“, “rollups” o “fragments“.

L’obiettivo di questo meccanismo è mantenere ad un livello molto basso il tempo iniziale di download delle risorse, che incide notevolmente sulle performance di un applicazione, per caricarle successivamente ad ogni richiesta da parte di quest’ultima.

Lazy Loading di webpack

dynamic

Come si nota dal grafo,  a partire da un entry-point, webpack , suddivide in piccole parti il bundle, ottenendo un meccanismo molto più flessibile della standard concatenazione in serie. Anche se la concatenazione ottiene buoni risultati, non è sempre un buon approccio.

Questo è particolarmente vero quando la dimensione della nostra applicazione comincia a crescere, ed è qui che il meccanismo del Code Splitting comincia a dare i suoi frutti e fare la differenza.

Anche CommonJS e AMD hanno i loro meccanismi di Lazy Loading, ottenuti rispettivamente dalle funzioni require.ensure([],callback), per il primo e require() per il secondo, ma la trattazione di questi non è oggetto di questi post per motivi di spazio.

Continuiamo il nostro percorso nei meandri di webpack. Il grafico sottostante mostra il work-flow del processo di build:

how-it-works

La logica che traspare è molto chiara e semplice. A partire da un entry-point (app.js) webpack analizza le dipendenze (cat.js), dipendenze di dipendenze e crea un bundle in un unico file, app.bundle.js.

Ma veniamo alla parte più pratica e che più ci attira, il codice!!!!!

Prerequisiti

Per iniziare, dobbiamo soddisfare alcuni semplici requisì, racchiusi in questi due punti (per il momento):

  • aver installato sulla propria macchina  NodeJS, con il suo Package Manager, npm.
  • terminale con la shell testuale Bash per sistemi Unix-like, o una variante per sistemi Windows, Cygwin o Git-Bash;

La visitazione di questo tool, sarà effettuata in accordo alla nuova sintassi definita dalle Specifiche ECMAScript 6, a tal proposito il post di Andrea Como su AngularJS in ES6, oltre ad essere un interessante post, appare propedeutico per comprendere nella totalità questo tutorial, che utilizzerà nella seconda parte AngularJS, come applicazione di esempio. Qui potete trovare il sorgente di questa esercitazione.

Installazione

Detto questo apriamo una finestra del terminale ed installiamo webpack, previa inizializzazione del progetto via npm (inserite a vostro piacimento le informazioni  richieste):

$ mkdir webpack-tutorial
$ cd webpack-tutorial
$ npm init
$ npm install webpack --save-dev

Creiamo ora il nostro primo bundle webpack, a partire da un semplice script (index.js è l entry-point):

//index.js
let codingJam = {
    slackUrl: 'https://coding-jam.slack.com',
    viewSlackUrl: function() {
        setTimeout(() => console.log('Coding-Jam Slack Url is ' + this.slackUrl));
    }
}
codingJam.viewSlackUrl();
//bundle.js
webpack index.js bundle.js

Come si vede dall esempio, il processo di build del nostro bundle è davvero semplice, e non richiede un bagaglio tecnico di elevata caratura. Da notare l’utilizzo della keyword this, all’interno della funzione viewSlackUrl(), di fatti senza il lexical this, this.slackUrl sarebbe stato 'undefined'. Possiamo ottenere un minimale “live-reload” , impostando webpack in modalità changes listening:

$ webpack index.js bundle.js -w   oppure
$ webpack index.js bundle.js --watch

L’opzione -w o --watch attiva questa modalità, ed  ogni modifica al vostro file, anche un semplice inserimento di uno spazio o la cancellazione di un singolo carattere, “scatena” un evento, che culmina nuovamente con  la compilazione.

wepack.config.js

Utilizzare WebPack nelle modalità di cui sopra non è in linea con la best practice del buon developer. Questa richiede la creazione di un file di testo contenente tutte le impostazioni in modo da evitare di doverle scrivere ripetutamente.

Diamo il benvenuto al file webpack.config.js, il file di configurazione principale di webpack. Questo permette di organizzare e strutturare in modo migliore tutte le impostazioni di progetto.

La forma sintattica di questo file può cambiare in base alla modalità con cui si utilizza il tool, nella pratica questo può essere delucidato come segue:

  • utilizzo di webpack tramite CLI (Command Line Interface): in questo caso  i parametri di configurazione saranno incapsulati all’interno dello statement module.exports: con questa modalità è possibile anche specificare un nome di file diverso, con l’opzione --config;
  • utilizzo di webpack tramite NodeJS API: in questo caso i parametri di configurazione saranno passati come parametro;

In entrambi i casi, può essere utilizzato un array di configurazioni (come vedremo nel secondo post), le quali condivideranno la stessa cache del filesystem, caratteristica che rende  il processo di build  molto performante, in quanto le multiple richieste al module bundler saranno processate in parallelo.

Queste sono le due casistiche:

//CLI
module.exports = {
    // configuration
};

//NodeJS API
webpack({
    // configuration
}, callback

Terminiamo questo post con un esempio di bundle webpack, utilizzando un file di configurazione secondo la prima modalità descritta.

Anche in questo caso l’esempio risulta essere molto semplice, ma rappresenta la struttura di base per configurazioni molto più complesse e strutturate, e nel seguito, quando “sezioneremo” ed andremo ad aggiungere le feature messe a disposizione dal tool, de facto ci sarà molto utile la sua conoscenza.

Vediamo il contenuto del nostro webpack.config.js:

//webpack.config.js
 module.exports = {
     entry: './main.js',
     output: {
         filename: 'bundle.js'
     }
     //all other configuration
 };

Come è facile notare, ci sono solamente  due elementi all interno del corpo dell’istruzione module.exports:

  • entryentry-point del bundle: il  valore assegnato alla entry può essere passato nelle seguenti modalità:
    • come stringa: la stringa è risolta a build-time con un modulo caricato on startup;
    • come array: ogni stringa dell’array viene risolta a build-time in un modulo on startup, ma solo l’ultimo sarà passato alla chiamata di funzione export;
    • come Object Literal: in questo particolare caso viene creato un numero di bundle pari al numeri di entry, identificate univocamente dal loro “chunk name” .
  • output: passato come Object Literal, specifica le opzioni di  configurazione per il file di output, o bundle, queste sono alcune opzioni:
    • filename: il nome del file di output;
    • path: il percorso assoluto;
    • publicPath: percorso relativo (e.g. dist/)

Da evidenziare che le entry sono sempre percorsi relativi. Utilizzare ad esempio ./index.js al posto di index.js.

Utilizziamo infine le classi  ECMAScript 6, per valorizzare il main.jsentry-point dell’esempio:

//main.js
   class CodingJam {
    constructor(name) {
        this.name = name;
    }

    sayHappy() {
        console.log(this.name);
    }
}

let cnj = new CodingJam('Happy Coding');
cnj.sayHappy();

Non ci rimane che lanciare il processo di “bundling“.

A differenza dell’esempio precedente non è necessario specificare il nome del bundle da riga di comando, tutte le informazioni da ora in avanti saranno contenute all’interno del file di configurazione, che di volta in volta sarà sempre più articolato e complesso, a fronte delle  feature illustrate.

Questo è il comando che ci permette di ottenere il bundle:

$ webpack -w

Prima di terminare con le conclusioni finali di questa prima parte, credo valga la pena soffermarsi su un quesito molto spesso poco chiaro, ed oggetto di intere discussioni nel web.

Si può fare a meno di NodeJS?

Si e no, dipende in che fase del progetto siamo. Come vedremo più avanti webpack è rilasciato con un piccolo NodeJs Express server (webpack-dev-server), il quale utilizza il  webpack-dev-middleware per “servire” il bundle prodotto, oltre ad un piccolo “runtime” connesso al server via Sock.js.

Questo presuppone che in ambiente  di sviluppo (dev environment) è necessario , ma come asserito precedentemente, il prodotto finale del processo di bundling sono assets statici, indi per cui nessuno ci vieta di utilizzare il bundle su di un server  differente, una volta passati in produzione (qui troverete un esempio live, AngularJS con webpack su server ngix).

Conclusioni

In questo primo post, ci siamo soffermati più sulla aspetto teorico, fondamenta indispensabile per il proseguo del tutorial. Nella seconda parte, visiteremo quasi tutte le feature messe a disposizione da webpack, Loaders, Plugins, Dev Tools , Hot Module Replacement, Long-term Caching, Shimming modules, Build Performance e molto altro ancora, se siete interessati ed il post vi è interessato non esitate.

Christian Chiama

Software Engineer, appassionato di ogni parte del campo IT. Dal 2006 sino ad oggi sono stato focalizzato principalmente sulla Piattaforma Java EE , utilizzando Struts, Spring, Vaadin e le loro evoluzioni. La mia passione principale è il FullStack JS, e tempo permettendo sviluppo Applicazioni Web in HTML5, Angular , Electron e sviluppo mobile app Ionic 2. Questo è il mio profilo LinkedIn

  • Valerio Radice

    Bella introduzione, mancano le risorse in italiano, sono sempre ben accette 😉

    • Christian Chiama

      Grazie mille, nella prossima parte vedremo più nel profondo i meccanismi più complessi con un applicazione Angular di esempio…Ti aspetto e ti ringrazio per il tuo feedback, sono sempre ben accetti
      😉

  • Bella introduzione, complimenti!

    • Christian Chiama

      Grazie mille, se ti è piaciuto ti aspetto ad i prossimi post:)!