Webpack ECMAScript 6 – Parte II

Nel primo post introduttivo abbiamo affrontato i concetti base di module loader, module bundler ed introdotto webpack. In questa seconda parte ci concentreremo su quello che più ci attira: il codice!! Andremo a sviluppare un progetto assieme e vedremo come configurare webpack, senza utilizzare “seed” sparsi in rete, ma con un nuovo progetto in sintassi ECMSCript 6/7,  al fine di capire con più semplicità e naturalezza  le feature ed i vantaggi che questo strumento ci mette a disposizione.

 

webpack v2.2

Nel mentre di questi post, webpack ha rilasciato la release 2, in questo post potete trovare l announcement ufficiale, ed i prerequisiti d’ installazione li potete trovare nel sito ufficiale, disponibile al seguente indirizzo.

 

Setup

Andiamo a creare ora,  le directory ed i file necessari per il nostro progetto, inizializziamolo  con  npm, ed installiamo l’ ultima release di webpack.

mkdir webpack-tutorial
cd webpack-tutorial
npm init -y
npm install webpack@beta --save-dev
mkdir src
touch index.html src/app.js webpack.config.js

In accordo alle specifiche fornite da webpack, abbiamo creato il webpack.config.js, motore del progetto. Questo non è altro che un oggetto Javascript, un modulo di codice contenente determinate funzionalità, che necessita di essere “esportato” osservando la sintassi NodeJS, module.exports, e richiamato attraverso la funzione require() in altri files.

Introduciamo per prima cosa tre concetti basici, context, entry ed output, alcuni di essi già introdotti nella prima parte del tutorial:

const webpack = require('webpack')
const path = require('path')

const config = () => {
    return {
        context: path.resolve(__dirname, 'src'),
        entry: './app.js',
        output: {
           path: path.resolve(__dirname, 'build'),
           publicPath: '/build/',
           filename: 'bundle.js'
        }
    }
}

module.exports = config;

La configurazione riportata è un punto di partenza comune a molti progetti: entry, comunica a webpack quale files saranno gli entry point dell’ applicazione. Questi, all’ interno dell’ albero delle dipendenze (dependency tree), saranno situati proprio sulla sommità.

L’ opzione output, fornisce informazioni a webpack, sulle caratteristiche del prodotto di compilazione o processo di bundling, e nel nostro caso abbiamo fornito le seguenti informazioni:

  • path:  è il percorso assoluto dove i file compilati saranno scritti;
  • filename:  i file compilati, saranno nominati secondo questa opzione.
  • publicPath:  questa opzione è utilizzata per specificare l indirizzo pubblico dei file. Viene utilizzata da i loaders quando si rende necessaria la scrittura di tags script o tags link , oppure risulta molto utile in caso si stia utilizzando framework che hanno delle folder ben precise dove salvare gli assets statici. Ad esempio Spring Boot utilizza la cartella resources/static per le risorse statiche, di conseguenza questa dovrà anche essere  il valore da fornire a publicPath;

Il markup del nostro index.html è il seguente:

<!DOCTYPE html>
<html lang="it">
<head>
    <meta charset="utf-8">
    <meta http-equiv="x-ua-compatible" content="ie=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>CodingJam | Webpack Tutorial</title>
</head>
<body>
    <script src="dist/bundle.js"></script>
</body>
</html>

Aggiorniamo la sezione script del package.json, lanciamo npm start, e se non vi sono errori, otterremo il seguente  risultato:

Hash: 58d8e64d2ff0a7d0e061
Version: webpack 2.3.2
Time: 7ms
        Asset   Size  Chunks             Chunk Names
bundle.js  12 kB       0  [emitted]  app
[./app.js] ./app.js 28 bytes {0} [built]
    + 2 hidden modules

Che rispecchia perfettamente quanto definito. Il nostro entry-point, app.js, è contenuto nel file bundle.js prodotto in fase di compilazione.

Per maggiori dettagli su package.json, sue parti ed npm, sarà molto utile la lettura del  post di Andrea Como, I miei primi passi con Node.js ed Express.js.

Bene, ma la nostra intenzione è quella di creare un progetto  in sintassi ES6/7, per cui è necessario arricchire la nostra configurazione, affinché supporti la nuova sintassi e contempli le diverse tipologie di files. Per ottenere questo risultato abbiamo bisogno dei Loaders.

 

Loaders Webpack

 When you encounter this kind of file, do this with it

Un loader è una particolare funzione o processore che elabora il contenuto di una o più risorse statiche, come file Javascript, immagini, fogli di stile, e ci consente di aggiungere funzionalità a webpack.

La Naming Convention per un loader segue la sintassi  xxx-loader, come sass-loader oppure babel-loader.

Come anticipato, vogliamo poter scrivere Javascript moderno fino alla versione ES2017, e per farlo abbiamo proprio bisogno del babel-loader, uno tra i più famosi transpiler , il quale ci garantisce la massima compatibilità di ES6/7 con i maggiori browser, compilando il codice scritto, in Standard Vanilla.

I loaders non sono rilasciati con webpack, motivo per il quale dovranno sempre essere installati prima di essere utilizzati, installazione che avviene mediante npm:

npm install xxx-loader --save.

Procediamo con l istallazione del babel-loader e di alcune librerie aggiuntive, necessarie al corretto funzionamento del famoso transpiler;

npm install babel-core babel-loader babel-preset-latest --save

ed aggiorniamo la nostra configurazione come segue:

//webpack.config.js
const webpack = require('webpack')
const path = require('path')

const config = {
    context: path.resolve(__dirname, 'src'),
    entry: {
        app: './app.js',
    },
    output: {
        path: path.resolve(__dirname, 'build'),
        publicPath: '/build/',
        filename: 'bundle.js'
    },
    module: {
        rules: [{
            test: /\.js$/,
            include: path.resolve(__dirname, 'src'),
            use: [{
                loader: 'babel-loader',
                options: {
                    presets: [
                        ['es2015', { modules: false }]//{module:false} abilita il Three Shaking
                    ]
                }
            }]
        }]
    }
}

Questa struttura, rispecchia le nuove API di webpack, che differenziano dalla release precedente su alcuni punti. Analizziamola:

  • module: oggetto che funge da wrapper, per tutti i loader contenuti nel progetto, definiti all interno dell array di oggetti rules;
  • test: Regular Expression, che indica a webpack, la tipologia di file da includere nella ricerca all interno dell albero delle dipendenze, nel nostro caso file Javascript;
  • include: indica il path dove dovranno essere cercati i file, indicati nella Regular Expression;
  • options: contiene le opzioni per il loader specificato. In questo caso abbiamo definito  l opzione preset, che descrive il tipo di trasformazione che devono subire i nostri file Javascript. Essa sostituisce il file .babelrc.

 

Il codice del nostro progetto è pronto per essere scritto in ECMAScrip6/7, grazie a Babel.

 

Prima di passare all’ introduzione dei plugin, aggiungiamo un loader per la gestione di file Sass, previa installazione:

npm install sass-loader node-sass css-loader

Ed aggiorniamo il nostro file di configurazione come segue;

const config = {
  context: path.resolve(__dirname, 'src'),
  entry: {
    app: './app.js',
  },
  output: {
    path: path.resolve(__dirname, 'build'),
    publicPath: '/build/',
    filename: '[name].bundle.js'
  },
  module: {
    rules: [{
      test: /\.scss$/,
      include: path.resolve(__dirname, 'src'),
      loader: ['css-loader', 'sass-loader']
    }, {
      test: /\.js$/,
      include: path.resolve(__dirname, 'src'),
      use: [{
        loader: 'babel-loader',
        options: {
          presets: [
            ['es2015', { modules: false }]
          ]
        }
      }]
    }]
  },
}

module.exports = config

 

Plugins

Mentre i loaders operano trasformazioni su singoli file, i plugins operano su grandi parti di codice (larger chunks of code). Sono utilizzati per aggiungere  comportamenti addizionali alla normale configurazione di webpack.

Molti sono rilasciati con webpack stesso, come UglifyJsPlugin, per la compressione e contestuale minificazione dei file, CommonChunksPlugin per “innescare” il Code Splitting, che vedremo nei prossimi paragrafi. Ma i plugin aggiuntivi, necessitano di essere  installati come i loaders, attraverso il NodeJS Packager Manager, npm. E’ sempre possibile alterarne il comportamento, ma non è oggetto di questo post la sua delucidazione.

Ne analizzeremo ed utilizzeremo solamente alcuni nel corso del post. Iniziamo da subito con l’ Extract Text Plugin.

 

Extract Text Plugin

Uno dei plugin più utilizzati è l Extract Text Plugin (extract-text-webpack-plugin). Esso ci permette di estrarre in file separati, tutti i moduli di codice contenenti CSS, o particolari preprocessor come il Sass, o Less. Fino a quel momento tutte le caratteristiche di resa sono  inline, contenute nei bundle Javascript.

Per prima cosa installiamo il plugin nella consueta forma npm:

npm install extract-text-webpack-plugin --save

Adesso utilizziamolo all interno del file di configurazione come segue:

const ExtractTextPlugin = require('extract-text-webpack-plugin')
const extractCSS = new ExtractTextPlugin('[name].bundle.css')

const config = {
 ............},
  module: {
    rules: [{
      test: /\.scss$/,
      include: path.resolve(__dirname, 'src'),
      loader: extractCSS.extract(['css-loader', 'sass-loader'])
    }, {
.......
  },
  plugins: [
    extractCSS
  ],
}

module.exports = config

L’ utilizzo del plugin è molto semiplice ed auto-esplicativa. Una volta dichiarato ed importato per mezzo della funzione require(), lo utilizziamo all interno del file di configurazione, nella sezione corrispondente ad i loaders.

L’ unica novità che incontriamo in questa configurazione, è l’ aggiunta della struttura dati array  plugins. Questa struttura serve a contenere tutti i plugins utilizzati in un progetto, sia  rilasciati con webpack, che installati via npm.

L’ Extract Text Plugin, non è legato semplicemente all esternalizzazione in file di CSS, ma risulta molto utile alle performance dell applicazione. Infatti ogni request CSS, avviene in parallelo ed ognuna con cache separata, migliorando le prestazioni generali.

 

CommonChunksPlugin

Il CommonChunksPluigin, è un core-plugin, rilasciato con webpack, utilizzato per implementare il Code Splitting. Questa è un opt-in features, utilizzata per dividere, “splittare” il codice in più chunks, caricati opportunamente on demand.

Viene utilizzata anche per estrarre dal codice della nostra applicazione, le dipendenze da librerie di terze parti (vendor), ma gli autori di webpack, nella documentazione ufficiale, sottolineano  che l importanza di questa tecnica è fornita principalmente dalla possibilità di avere il caricamento dei moduli  on demand e non solo dalla divisione in common chunks delle dipendenze condivise, seppur di grande utilità ed importanza nello sviluppo di un applicazione web.

Ma Procediamo con l’ impementazione.

Fino ad ora, il nostro progetto conteneva un unico entry-point , di conseguenza il prodotto  del processo di build era un singolo bundle, come definito dal oggetto output. Aggiungiamo un nuovo entry-point all’ applicazione: meta.js , ed aggiorniamo la nostra configurazione come segue:

const webpack = require('webpack')
const path = require('path')

const commonChunks = new webpack.optimize.CommonsChunkPlugin({
  name: 'common',
  filename: 'common.bundle.js'
})

const config = {
  context: path.resolve(__dirname, 'src'),
  entry: {
    app: './app.js',
    meta: './meta.js',

  },
  output: {
    path: path.resolve(__dirname, 'build'),
    publicPath: '/build/',
    filename: '[name].bundle.js'
  },
  module: {
    ............
  },
  plugins: [
    commonChunks
  ]
}

module.exports = config

Come si nota facilmente , la proprietà filename dell oggetto output è stata modificata e trasformata in  [name].bundle.js.

Il Placeholder [name], in fase di compilazione verrà rimpiazzato dal nome dell’ entry-point  (app e meta), per cui nel nostro caso verranno generati due file di output distinti : app.bundle.js e meta.bundle.js.

Oltre questi due bundle, il CommonChunksPlugin ne genera un terzo, il common.bundle.js (come specificato nelle sue opzioni), includendo  i moduli di codice condivisi tra entrambi i due entry-points, chiamati anche split-points, nel nostro caso, trattasi solo della libreria Javascript Lodash.

Se provassim ora a lanciare il comando per il processo di build,  questo sarebbe il risultato:

Hash: 539bf2001d458bf97217
Version: webpack 2.3.3
Time: 2465ms
            Asset       Size  Chunks             Chunk Names
   meta.bundle.js  548 bytes       1  [emitted]  meta
    app.bundle.js  689 bytes       2  [emitted]  app
        common.js      73 kB       3  [emitted]  common

Il markup finale della nostra applicazione è il seguente:

<!DOCTYPE html>
<html lang="it">

<head>
  <meta charset="utf-8">
  <meta http-equiv="x-ua-compatible" content="ie=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>CodingJam | Webpack Tutorial</title>
  <link rel="stylesheet" href="build/common.bundle.css">
</head>

<body>
  <script src="build/common.bundle.js"></script>
  <script src="build/app.bundle.js"></script>
</body>

</html>

 

Webpack Dev Server

Come già evidenziato nel primo post, webpack-dev-server è rilasciato con un piccolo server NodeJS (Express Server), utilizzato per servire il bundle prodotto.  Grazie ad esso, gli sviluppatori possono usufruire del live reloading ed altre features, come L Hot Module Replacement.

Sotto il cofano, utilizza il webpack-dev-middleware, che provvede ad un accesso in memoria ( in-memory access) molto veloce, per attingere il più rapidamente possibile agli assets statici prodotti.

Il più semplice impiego di esso è tramite CLI, oppure è possibile configurarlo all interno del file configurazione webpack.config.js.

Anch’esso come i loaders ed i plugins, deve essere installato per poter essere utilizzato, per cui lanciamo da linea di comando : npm install webpack-dev-server --save, ed aggiorniamo lo script start del nostro package.json, come segue:

"start": "webpack-dev-server --inline --open",

L opzione --open aprirà in maniera automatica  il browser all indirizzo http://localhost:8080 (se cosi non fosse provvedete manualmente :-)).  Non rimane che lanciare il comando npm run start et voilà, ogni semplice modifica al sorgente, o qualsiasi altra risorsa presente nel bundle, verrà avvertita dal server di webpack, che lancerà  un nuovo processo di build.

Le modifiche saranno subito visibili senza l utilizzo del refresh, grazie al live reloading.

Qualora si desiderasse utilizzare il file di configurazione al posto delle CLI API,è necessario aggiungere e configurare un nuovo oggetto, il devServer.

Questa è la struttura.

// webpack.config.js
......
devServer: {
    inline: true,
    .....
  }

Le opzioni disponibile per questo oggetto sono svariate e vi è il corrispettivo comando via CLI. Queste sono solo alcune delle più utilizzate:

  • hot: abilità l Hot Module Replacement in concomitanza di plugins specifici;
  • contentBase: webpack servirà le risorse statiche in questo path. In sua mancanza ha la priorità publicPath;
  • compress: se impostato su true abilita la compressione gzip per gli assets prodotti;
  • historyApiFallback: gestisce l accesso al dev-server da url arbitrari, molto utile in caso di SPA con router HTML5;
  • clientLogLevel : controlla il livello di log nella console del browser in presenza dell opzione inline=true. Può assumere i valori  error, warning, info, none ;
  •  proxy: oggetto che definisce le proprietà del  http-proxy-middleware di webpack;

 

Hot Module Replacement

Terminiamo il post con un discorso introduttivo su  l’ HMR,  Hot Module Replacement.

E’ un opt-features, simile al live reloading, ma opera ad un livello di accuratezza molto più sottile. Infatti l’ HMR effettua l’ aggiornamento solamente dei moduli che hanno subito una modifica e non dell intero bundle prodotto. Questa caratteristica rende l’ HMR, uno strumento molto più efficace del live reloading.

Per impiegarlo, anche in questo caso vi sono sempre le stesse identiche due possibilità, come potete immaginare: via CLI o file di configurazione.

Nel primo caso dobbiamo aggiungere l opzione --hot alle opzioni del webpack-dev server, per cui il nostro start script diventerà:

webpack-dev-server --inline --hot --open

In caso di abilitazione via file di configurazione, bisogna aggiungere l HotModuleReplacementPlugin e configurare l oggetto devServer, come segue:

const config = {
  context: path.resolve(__dirname, 'src'),
  entry: {
    app: './app.js',
    meta: './meta.js',

  },
  output: {
    path: path.resolve(__dirname, 'build'),
    publicPath: '/build/',
    filename: '[name].bundle.js'
  },
  module: {
  ........
  },
  plugins: [
    new webpack.HotModuleReplacementPlugin(),
  ],
  devServer: {
    hot: true,
    inline: true
  }
}

module.exports = config

 

Conclusioni

In questo post abbiamo visitato molte parti di questo  tool, nonostante sia stata visitata solamente la superficie per la moltitudine di proprietà e teorie in merito. Ad oggi webpack viene utilizzato nei migliori Framework in commercio ed anche i più recenti, come Ionic2, Angular 2, angular-cliElectron e molti altri.

Questo ultimo  anno ha visto la notevole crescita di Rollup, con supporto built-in del Tree Shaking, e per alcuni il successore di webpack ed in questi mesi invece, ha raccolto enorme approvazione FuseBox, module loader e module bundler. In questo post potete trovare il benchmark inerente la velocità di building messa a confronto con quella di webpack, e se l argomento è di vostro interesse queste sono due Applicazioni di esempio con Angular2 e React.

Nel prossimo post, vedremo inoltre come utilizzare l http-proxy-middleware di webpack (proxy-server), altra features molto interessante. Per il momento potete trovare il codice del post in questo repository GitHub. Alla prossima…

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

  • Alessandro Bx

    Grazie del post mi ha chiarito molto su webpack. Volevo chiedere come modificare il present per abilitare le annotation supportate da esnext

    • Christian

      Ciao, Prima di tutto ti ringrazio e vediamo se riesco o a darti una mano:);
      Devi aggiungere due plugin di babel al file di configurazione come segue:

      options: {

      presets: [

      [‘es2015’, { modules: false }]

      ],

      “plugins”: [

      “transform-decorators-legacy”, // primo plugin

      “transform-class-properties” // secondo plugin

      ],

      }

      //installazione plugin
      npm install –save babel-plugin-transform-decorators-legacy
      npm install –save babel-plugin-transform-class-properties

      Ora puoi usare i Decorator (Annotation) ES7. Ti scrivo un esempietto da provare:

      //creazione annotazioni

      function logger(target, name, descriptor) {

      target.annotated = true;

      //ottieni il valore della funzione di originale

      let fn = descriptor.value;

      // creiamo una nuova funzione che chiama l originale attraverso apply

      let nuovaFunzione = function () {

      fn.apply(target, arguments);

      console.log(‘prametro %s’, name);

      };

      }

      //Utilizzo Decorator

      @logger

      class MyLogger { }

    • Christian

      Ciao, Prima di tutto ti ringrazio e vediamo se riesco o a darti una mano:);
      Devi aggiungere due plugin di babel al file di configurazione come segue:

      ……

      options: {

      presets: [

      [‘es2015’, { modules: false }]

      ],

      “plugins”: [

      “transform-decorators-legacy”,

      “transform-class-properties”

      ],

      }

      ……

      //installazione plugin via npm

      npm install –save babel-plugin-transform-decorators-legacy
      npm install –save babel-plugin-transform-class-properties

      Ora puoi usare i Decorator (Annotation) ES7. Ti scrivo un esempio da provare:

      //creazione annotazioni

      function logger(target, name, descriptor) {

      target.annotated = true;
      //ottieni il valore della funzione di originale

      let fn = descriptor.value;

      // creiamo una nuova funzione che chiama l originale attraverso apply

      let nuovaFunzione = function () {
      fn.apply(target, arguments);
      console.log(‘prametro %s’, name);
      };

      }

      //Utilizzo Decorator

      @logger
      class MyLogger { }

      Se provi ad eseguire questo codice nel progetto del post non dovrebbe dare errori, ma rimango a disposizione. Ciao ed a Presto. Ti aspetto per i prossimi post. Grazie

    • Christian

      Ciao, Prima di tutto ti ringrazio e vediamo se riesco o a darti una mano:);
      Devi aggiungere due plugin di babel al file di configurazione. Ti ho creato un Gist al seguente indirizzo:

      https://gist.github.com/chrchm/7c033965f4a9ba4c84edfddb4895b683

      Ti mostra come modificare il webpack.config.js per l utilizzo dei Decorator ES7 e un piccolo esempio d uso.

      Se provi ad eseguire il codice nel progetto del post non dovrebbe dare errori, ma rimango a disposizione. Ciao ed a Presto. Ti aspetto per i prossimi post. Grazie

  • Claudio Ferrari

    Ciao, ottimo tutorial, complimenti.
    Vorrei sapere come aggiornare la parte start in package.json.
    Grazie