Vai al contenuto

Webpack, creare un bundle con il file di configurazione

Per quanto possa sembrare immediato utilizzare la riga di comando per utilizzare webpack, per dei progetti importanti potrebbe essere più comodo utilizzare un file di configurazione, dove potremo specificare meglio qualsiasi tipo di opzione e sarà sicuramente più leggibile della riga di comando. Il file di configurazione è scritto in Javascript e di default deve essere chiamato webpack.config.js. In alternativa, si può passare l’opzione –config [filename] se vogliamo dare un nome diverso al file di configurazione o posizionarlo in una directory diversa. L’esempio più semplice che si possa creare è il seguente:

const path = require('path');

module.exports = { 
    entry: './src/main.js', 
    output: { 
        path: path.resolve(__dirname, 'dist'),
        filename: 'bundle.js' 
    }
};

Stiamo specificando il file di input (entry) e il file di output (bundle.js) nonchè la cartella di destinazione usando la libreria path. Rispetto alla riga di comando, abbiamo aggiunto il nome di destinazione. Essendo un file JavaScript è necessario esportare l’oggetto di configurazione con la direttiva module.exports. Fatto ciò, possiamo modificare il file package.json (che abbiamo imparato a gestire nell’articolo precedente), togliendo l’istruzione:

"build": "webpack --mode=development ./src/main.js",

in

"build": "webpack",

Il file Json dovrebbe assomigliare a questo:

"scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "build": "webpack",
    "execute": "node dist/bundle.js", 
    "start": "npm run build -s && npm run execute -s"
  },

A questo punto basterà lanciare il comando npm start per avviare il processo completo e l’esecuzione.

Utilizziamo le potenzialità di ES6

Facciamo adesso un altro passo avanti. Ritorniamo al file main.js e modifichiamolo in modo tale da sfruttare le potenzialità di ES6. Le poche righe di codice presenti nel file si possono ridurre a solo due righe in questo modo:

import { map } from 'lodash'; 
console.log(map([1,2,3,4,5,6], n => n*n));

Abbiamo usato la parola chiave import per importare la funzione map dalla libreria lodash (l’estensione .js è sottintesa in lodash). Inoltre abbiamo eliminato la funzione quadrato utilizzando una funzione arrow con la stessa logica. Il tutto si riduce ad una semplice riga di codice.

In uno script cosi semplice, è chiaro che caricare l’intera libreria sarebbe controproducente. In questo particolare caso, il codice si può ulteriormente ottimizzare, affinchè la direttiva import importi nel nostro codice solo ciò che realmente ci serve dalla “libreria” lodash. Scegliendo di importare una parte di essa, il bundle sia di dimensioni inferiori. Ecco come fare:

import map from 'lodash/map'; 
console.log(map([1,2,3,4,5,6], n => n*n));

In questo caso abbiamo eliminato le parentesi {} da map. Ovviamente l’import di una porzione di libreria si è potuto fare perchè gli sviluppatori di lodash lo hanno reso disponibile. Non sempre infatti è possibile importare una parte di libreria.

In ogni caso, se decidiamo di importare per intero la libreria lodash, il file bundle.js sarà circa 70kb. Se invece decidiamo di importare solo una parte, il file bundle sarà di circa 20kb. Un risparmio considerevole che lo sviluppatore deve tenere assolutamente conto in progetti più estesi di questo. Non è fantastico tutto ciò?

Utilizzare i loaders

Abbiamo due modi principali per aggiungere funzionalità di webpack: loaders e plugin. Dei plugin ne parleremo dopo. I loaders vengono utilizzati per applicare trasformazioni o eseguire operazioni su file di un determinato tipo.  I caricatori possono trasformare i file da un linguaggio diverso (come TypeScript) in JavaScript o caricare immagini inline come URL di dati.

Ad esempio, possiamo utilizzare i loaders per dire a webpack di caricare un file CSS o di convertire TypeScript in JavaScript. Per fare ciò, dovremmo innanzitutto installare i loaders di cui abbiamo bisogno

npm install --save-dev css-loader ts-loader

E poi dobbiamo “istruire” il nostro file di configurazione con le apposite righe per le estensioni CSS e TS:

module.exports = {
  module: {
    rules: [
      { test: /\.css$/, use: 'css-loader' },
      { test: /\.ts$/, use: 'ts-loader' },
    ],
  },
};

Se garantire la compatibilità del nostro codice ES6 anche nei vecchi browser, possiamo ad esempio installare Babel e utilizzarlo come loader. In tal caso, dobbiamo prima installare le librerie  che ci servono:

npm i -D babel-core babel-loader babel-preset-es2015 babel-plugin-transform-runtime babel-polyfill

e poi dovremmo modificare il webpack.config.js in questo modo:

const path = require('path');

module.exports = { 
    entry: './src/main.js', 
    output: { 
        path: path.resolve(__dirname, 'dist'),
        filename: 'bundle.js' 
    },
    module: { 
        rules: [ 
            { test: /\.jsx?$/, loader: 'babel-loader' }
         ] 
    } 
};

Abbiamo aggiunto una proprietà denominata module, e all’interno c’è la proprietà rules, che è un array contenete la configurazione per ogni caricatore usato. Per ogni caricatore, dobbiamo impostare almeno due opzioni: testloadertest è in genere un’espressione regolare che viene verificata rispetto al percorso assoluto di ogni file. Di solito verifica solo l’estensione del file; ad esempio, /\.js$/ verifica se il nome del file termina con .js. Ad esempio, nel caso si voglia utilizzare React, bisognerà utilizzare l’espressione regolare  /\.jsx?$/, che corrisponderà a .jse .jsx.

Se dobbiamo specificare anche delle opzioni per il loader, allora basterà aggiungere al blocco rules anche l’oggetto options, che sarà semplicemente una mappa di coppie key->value.

const path = require('path');

module.exports = { 
    entry: './src/main.js', 
    output: { 
        path: path.resolve(__dirname, 'dist'),
        filename: 'bundle.js' 
    },
    module: { 
        rules: [ 
            { 
                test: /\.jsx?$/, 
                loader: 'babel-loader',
                options: { 
                    plugins: ['transform-runtime'], 
                    presets: ['es2015'] 
                }
            }
         ] 
    } 
};

Impostiamo il preset in modo che tutte le funzionalità di ES2015 vengano trasformate in ES5 utilizzando il plug-in transform-runtime che abbiamo installato, anche se non sarebbe necessario. Infine dobbiamo dire al loader di non elaborare i file nella cartella node_modules, in modo che l’elaborazione avvenga in maniera più veloce.

...
{ 
    test: /\.jsx?$/, 
    loader: 'babel-loader',
    exclude: /node_modules/,
    options: { 
        plugins: ['transform-runtime'], 
        presets: ['es2015'] 
    }
}
...

In alternativa alla proprietà exclude, avremmo potuto usare la proprietà include e specificare solo la cartella src.

Installiamo adesso un altro loader che  ci permetterà di formattare i risultati in formato HTML leggibile dal browser. Il loader in questione è un motore di template e si chiama HandlebarsJS. Installiamo quindi il motore e il suo rispettivo loader per webpack.

npm install -D handlebars handlebars-loader

Ovviamente affinchè il loader venga utilizzato, dobbiamo dichiararlo all’interno di webpack.config.js, aggiungendo questa riga all’array rules, senza ovviamente togliere quanto già esistente.

...
rules: [
    ...
    { test: /\.handlebars$/, loader: 'handlebars-loader' }
    ...
]
...

In questo modo, abbiamo detto a webpack di intercettare i file con estensione .handlebars, che non essendo JS andrebbe in errore non sapendo come gestire tale estensione, e passarli al loader handlebars. Creiamo adesso il file di template che ci servirà per formattare i risultati in HTML. Creiamo un nuovo file in src e lo chiamiamo  lista.handlebars,

<ul>
  {{#each arr as |number i|}}
    <li>{{number}}</li>
  {{/each}}
</ul>

e lo importiamo nel nostro script main.js, che a questo punto diventerà:

import map from 'lodash/map'; 
import template from './lista.handlebars';

let arr = map([1,2,3,4,5,6], n => n*n);
console.log(template({arr}));

L’import del template (che ricordiamo ha un’estensione diversa da JS) andrà a buon fine solo se abbiamo correttamente definito il nostro file di configurazione di webpack, altrimenti durante la compilazione verrà restituito un errore. Se proviamo ad avviare la nostra mini app con il comando npm start da terminale, il risultato sarà il seguente:

Tuttavia vogliamo che il risultato venga visualizzato nella pagina del browser. Come fare? Utilizziamo dei plugin!

Utilizzo Dei Plugin

I plugin sono la spina dorsale di Webpack. Lo stesso Webpack è costruito sullo stesso sistema di plugin che usi nella configurazione del Webpack. I plugin sono il modo, oltre ai caricatori, per installare funzionalità personalizzate nel webpack. Abbiamo molta più libertà di aggiungerli al flusso di lavoro del webpack perché non sono limitati all’uso solo durante il caricamento di tipi di file specifici; possono essere “iniettati” praticamente ovunque e sono, quindi, in grado di fare molto di più.

Utilizzeremo un plugin che genera semplicemente un file HTML, in modo da formattare per benino il nostro array. Il plugin si chiama  HTML Webpack. Prima di utilizzare il plugin, aggiorniamo i nostri script in modo da poter eseguire un semplice server web per testare la nostra applicazione. Innanzitutto, dobbiamo installare un server e la libreria HTML Webpack

npm i -D http-server
npm i -D html-webpack-plugin

poi, cambiamo il nostro aggiornando il blocco script con la proprietà server e start in questo modo:

...
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "build": "webpack",
    "server": "http-server ./dist",
    "start": "npm run build -s && npm run server -s"
  },
...

In questo modo, eseguendo il comando npm run start, verrà avviato anche un web server e potremmo accedere tramite browser all’indirizzo localhost:8080 per visualizzare la pagina di output.

Il risultato è ancora mostrato nella console del browser, mentre la pagina web è bianca. Adesso dobbiamo creare la classica struttura della pagina HTML che memorizziamo in un file chiamato index.html dentro la cartella src:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>
        <%= htmlWebpackPlugin.options.title %>
    </title>
</head>

<body>
    <h2>Prova template</h2>
    <div id="app-container"></div>
</body>

</html>
Nell’head, abbiamo utilizzato una notazione particolare che permette di recuperare un opzione che andremo a definire nel plugin (options.title). La notazione <%= %> è equivalente a un document.write o un echo in php. Nel body invece, abbiamo inserito un’intestazione H2 e un tag div definito dall’id “app-container” che ci permetterà di inserire al suo interno il codice HTML del punto elenco di cui sopra. Adesso, aggiorniamo il file di configurazione di webpack, aggiungendo il blocco dei plugin (alla fine):
const path = require('path');
const HtmlwebpackPlugin = require('html-webpack-plugin');

module.exports = { 
    entry: './src/main.js', 
    output: { 
        path: path.resolve(__dirname, 'dist'),
        filename: 'bundle.js' 
    },
    module: { 
        rules: [ 
            { test: /\.jsx?$/, loader: 'babel-loader' },
            { test: /\.handlebars$/, loader: 'handlebars-loader' }
         ] 
    }, 
    plugins: [ 
        new HtmlwebpackPlugin({ 
            title: 'Test TEMPLATE', 
            template: 'src/index.html' 
        }) 
    ]
};

Infine, affinchè venga mostrato il punto elenco nella pagina, dobbiamo modificare una riga di main.js, togliendo l’istruzione console.log e aggiungendo questa:

document.getElementById("app-container").innerHTML = template({arr});

Riavviando la nostra applicazione (ctrl+c per terminare il server in esecuzione e npm start per ricompilare e riavviare il tutto), il risultato dovrebbe essere simile  a questo: