Gestione del front-end con Webpack su Symfony

Symfony logo e webpack logo

In questo esempio ti mostrerò come installare e configurare Webpack per la gestione del front-end di un applicazione Symfony.

PREREQUISITI: In questo articolo darò per scontato che sia stato creato un nuovo progetto utilizzando la Symfony CLI. Se non sai come fare puoi seguire questo tutorial.


Cosa è Webpack?

A bundler for javascript and friends. Packs many modules into a few bundled assets. Code Splitting allows for loading parts of the application on demand.

– pagina GitHub di Webpack

Webpack è un assmblatore di bundle. Ciò significa che il compito principale di Webpack è quello di elaborare un insieme di asset composto da file JS/CSS/Immagini combinandoli in dei pacchetti chiamati bundle.
Ad esempio può essere usato per creare due bundle a partire dai file Javascript di un’applicazione: uno per i file specifici della webapp e uno contenente i file delle librerie installate.

Webpack prende in ingresso un set di file JS/CSS/JPG e li combina in bundle
Webpack prende in ingresso un set di file JS/CSS/JPG e li combina in bundle

Webpack non si limita a impacchettare le risorse front-end del progetto ma può essere utilizzato per ottimizzare al massimo le performance di un’applicazione. Questa possibilità però viene fornita ad un costo: Webpack è molto complesso. Nonostante gli sviluppatori abbiano cercato di semplificare sempre più le impostazioni versione dopo versione, ad oggi rimane abbastanza difficile impostare Webpack al meglio e capire cosa stia accadendo dietro le quinte. Proprio per questo motivo gli sviluppatori di Symfony hanno pensato di semplificarne l’utilizzo creando un wrapper chiamato webpack-encore.

Aggiungere una nuova pagina

La prima cosa da fare per iniziare la tua avventura nel mondo frontend è creare una nuova pagina web. Per farlo è necessario seguire due passaggi:

  • Creare una rotta (route): una rotta rappresenta l’URL della pagina (es /about) che verrà mappato su di un’azione del controller,
  • Creare un controller: un controller è una classe PHP che prende in ingresso la richiesta del browser e restituisce in uscita la risposta alla richiesta.

Ai fini di questo tutorial immaginiamo di creare l’home page del sito che risponderà alla rotta /. Per farlo la prima cosa da fare è creare una nuova classe PHP chiamata IndexController.php all’interno della cartella della cartella src/Controller/.

<?php
declare(strict_types=1);

namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;

class IndexController extends AbstractController
{
    /**
     * @Route(path="/")
     */
    public function index(): Response
    {
        return $this->render(
            'index.html.twig'
        );
    }
}

Il metodo index() appena creato non fa altro che accettare una richiesta GET alla rotta / (definita tramite l’annotazione @Route) e rispondere renderizzando il file index.html.twig che andrai a definire ora.

NOTA: Twig è il template engine predefinito usato da Symfony.

All’interno della cartella src\templates crea un nuovo file e chiamalo index.html.twig ed inserisci il seguente contenuto:

{% extends 'base.html.twig' %}

{% block body %}
  <h1>Hello world!</h1>
{% endblock %}

A questo punto, se tutto ha funzionato correttamente, navigando sul sito all’indirizzo http://127.0.0.1:8000 dovresti essere visualizzare la nuova pagina appena creata con il messaggio “Hello world”.

La prima pagina web creata con Symfony e Twig
La prima pagina web creata con Symfony e Twig

Installare Webpack

Arrivato a questo punto hai realizzato la prima pagina web dell’applicazione utilizzando esclusivamente l’HTML ma come ogni sviluppatore web sa, per creare una pagina accattivante è necessario adornarla con del buon stile CSS e renderla interattiva aggiungendo del Javascript.
Ed è qui che entra in gioco Webpack per la creazione dei bundle CSS e JS con il codice dell’applicazione.

NOTA: Come accennavo nell’introduzione, gli sviluppatori di Symfony hanno realizzato un wrapper per Webpack che ne semplifica di gran lunga la configurazione chiamato webpack-encore. In questo tutorial farò sempre riferimento a quello ma è possibile adattare tutti i concetti riportati anche alla configurazione di Webpack “standard”.

La prima cosa da fare è ovviamente quella di installare sia il bundle PHP (tramite composer) che la libreria Javascript (tramite yarn) utilizzando i seguenti comandi:

 composer require symfony/webpack-encore-bundle
 yarn add @symfony/webpack-encore --dev

Al termine dell’installazione verrà creato automaticamente il file di configurazione di Webpack chiamato webpack.config.js e una cartella assets che andrà a contenere tutti i file CSS e JS del progetto.

La configurazione di default di Webpack

Analizziamo ora la configurazione di default webpack.config.js appena generata:

var Encore = require('@symfony/webpack-encore');

// Manually configure the runtime environment if not already configured yet by the "encore" command.
// It's useful when you use tools that rely on webpack.config.js file.
if (!Encore.isRuntimeEnvironmentConfigured()) {
    Encore.configureRuntimeEnvironment(process.env.NODE_ENV || 'dev');
}

Encore
    // directory where compiled assets will be stored
    .setOutputPath('public/build/')
    // public path used by the web server to access the output path
    .setPublicPath('/build')
    // only needed for CDN's or sub-directory deploy
    //.setManifestKeyPrefix('build/')

    /*
     * ENTRY CONFIG
     *
     * Add 1 entry for each "page" of your app
     * (including one that's included on every page - e.g. "app")
     *
     * Each entry will result in one JavaScript file (e.g. app.js)
     * and one CSS file (e.g. app.css) if your JavaScript imports CSS.
     */
    .addEntry('app', './assets/js/app.js')
    //.addEntry('page1', './assets/js/page1.js')
    //.addEntry('page2', './assets/js/page2.js')

    // When enabled, Webpack "splits" your files into smaller pieces for greater optimization.
    .splitEntryChunks()

    // will require an extra script tag for runtime.js
    // but, you probably want this, unless you're building a single-page app
    .enableSingleRuntimeChunk()

    /*
     * FEATURE CONFIG
     *
     * Enable & configure other features below. For a full
     * list of features, see:
     * https://symfony.com/doc/current/frontend.html#adding-more-features
     */
    .cleanupOutputBeforeBuild()
    .enableBuildNotifications()
    .enableSourceMaps(!Encore.isProduction())
    // enables hashed filenames (e.g. app.abc123.css)
    .enableVersioning(Encore.isProduction())

    // enables @babel/preset-env polyfills
    .configureBabel(() => {}, {
        useBuiltIns: 'usage',
        corejs: 3
    })

    // enables Sass/SCSS support
    //.enableSassLoader()

    // uncomment if you use TypeScript
    //.enableTypeScriptLoader()

    // uncomment to get integrity="..." attributes on your script & link tags
    // requires WebpackEncoreBundle 1.4 or higher
    //.enableIntegrityHashes(Encore.isProduction())

    // uncomment if you're having problems with a jQuery plugin
    //.autoProvidejQuery()

    // uncomment if you use API Platform Admin (composer req api-admin)
    //.enableReactPreset()
    //.addEntry('admin', './assets/js/admin.js')
;

module.exports = Encore.getWebpackConfig();

Vediamo nel dettaglio cosa vanno ad impostare le istruzioni contenute in questo file di configurazione:

  • La prima cosa che Webpack va a controllare (righe 5-6) è l’ambiente da utilizzare per costruire i bundle. Nel caso in cui nessun ambiente sia stato impostato viene assunto di default che la build sia stata lanciata in modalità dev. Questo controllo viene fatto perché a seconda dell’ambiente utilizzato per lanciare la costruzione dei bundle vengono eseguite delle ottimizzazioni diverse (ad esempio il codice Javascript dell’ambiente di produzione sarà minificato mentre quello in sviluppo no).
  • Alle righe 11 e 13 viene indicato a Webpack quali sono le cartelle da utilizzare come output del suo processo di pacchettizzazione.
  • Alla riga 26 inizia il vero utilizzo di Webpack. In particolare con il comando addEntry() viene indicato a Webpack di generare un nuovo pacchetto a partire dal file indicato come argomento della funzione.
  • Alla riga 31 abbiamo la prima ottimizzazione che Webpack andrà ad eseguire sul nostro codice: splitEntryChunks() (vedi sezione dedicata)
  • Alla riga 35 abbiamo ancora un’altra ottimizzazione: enableSingleRuntimeChunk() (vedi sezione dedicata).
  • Alle righe 44 e 45 abbiamo ancora delle configurazioni di Webpack per le quali chiediamo esplicitamente di pulire la cartella di output ogni volta prima della build (cleanupOutputBeforeBuild())e di utilizzare le notifiche del sistema per informarci sullo stato di avanzamento della build (enableBuildNotifications()).
  • Le righe 46 e 48 indicano a Webpack di creare e servire i file map quando l’ambiente è in development e di aggiungere un numero di versione ai bundle generati quando invece l’ambiente è in produzione.
  • Infine alla riga 51 viene aggiunta una configurazione specifica per Babel. In particolare viene indicato a Babel di importare esclusivamente le polyfill che sono necessarie per il corretto funzionamento del codice Javascript scritto (useBuiltIns: 'usage‘) e che la versione di corejs installata è la 3 (corejs: 3).

All’interno della configurazione di default di Webpack sono abilitate le opzioni che comunemente vengono utilizzate nello sviluppo di un applicazione web con Symfony. Tuttavia già all’interno del file sono presenti altre righe di codice commentate che illustrano come sia possibile espandere il file di configurazione di default per aggiungere funzionalità extra (come la traspilazione Typescript o dei file Sass)

Ottimizzazione: SingleRuntimeChunk

Di default i moduli importati dagli script sono inizializzati una volta per ogni entry-point.

Questo vuol dire che se all’interno della stessa pagina vengono importati i due file scriptA.js e scriptB.js ed entrambi importano jQuery allora di quest’ultimo verranno inizializzate due istanze separate per i due script. Di conseguenza, se il primo script emette un evento all’interno della propria istanza jQuery allora questo non verrà intercettato dal secondo script (e viceversa).

Abilitando il SingleRuntimeChunk con l’istruzione enableSingleRuntimeChunk() viene creato un asset aggiuntivo chiamato runtime.js che, come suggerisce il nome, contiene il codice necessario ad inizializzare il runtime di Webpack.
Poiché in questo modo tutti i moduli vengono inizializzati da questo script allora tutti gli script importati all’interno della stessa pagina web condivideranno le stesse istanze dei moduli.
Quindi, tornando all’esempio con scriptA.js e scriptB.js all’interno della stessa pagina questa volta entrambi gli script si riferiranno alla medesima istanza di jQuery.

ATTENZIONE: abilitare o disabilitare il SingleRuntimeChunk è una scelta progettuale molto importante. In generale può essere una buna idea lasciarlo abilitato in modo che se in una pagina sono presenti più script questi condividano le stesse istanze dei moduli in comune. Tuttavia se stai realizzando una single-page application allora avere un runtime condiviso può essere un overhead non necessario.

NOTA PERSONALE: se nella tua applicazione utilizzi dei modali il cui contenuto viene caricato tramite AJAX e che al loro interno importano degli script allora potrebbe venire istanziato un nuovo runtime! Questo potrebbe portare all’impossibilità di comunicare tra il modale e il contenuto della pagina sottostante.

Per saperne di più sul single runtime chunk e per approfondire le opzioni di configurazione puoi consultare la pagina con la documentazione ufficiale.

Ottimizzazione: SplitEntryChunks

Abilitando splitEntryChunks() Webpack si occuperà di frammentare gli script in chunk di dimensioni minori ottimizzando le risorse necessarie al browser per valutare ed interpretare gli script.
Di default Webpack applicherà le seguenti regole per spezzare gli script in chunk:

  • I nuovi chunk devono essere condivisi tra più script oppure provengono dalla cartella node_modules;
  • Il nuovo chunk ha una dimensione maggiore di 30Kb (prima della compressione);
  • Il numero di richieste parallele da fare per scaricare i chunk on-demand è minore o uguale di 5;
  • Il numero di richieste parallele da fare per scaricare tutti i chunk necessari al rendering iniziale della pagina è minore o uguale di 3.

Per saperne di più sullo splitting dei chunks puoi consultare la pagina della documentazione ufficiale.

Creazione dei bundle

Una volta che Webpack è installato puoi iniziare a creare i bundle a partire dai tuoi script con il seguente comando da terminale:

yarn encore dev

Una volta che il comando sarà terminato, all’interno della cartella /public/build troverai i bundle appena realizzati.

Aggiunta di uno script

Arrivati a questo punto dovresti avere Webpack configurato e pronto per l’uso. Per verificare che tutto funzioni correttamente ti mostrerò come aggiungere uno script Javascript che al click di un bottone apra un alert del browser.

Come prima cosa aggiungi un bottone all’interno della pagina creata allo step precedente:

{% extends 'base.html.twig' %}

{% block body %}
  <h1>Hello world!</h1>
  <div>
    <button id="clickMeButton">Click me</button>
  </div>
{% endblock %}

Vai ora ad aggiungere un nuovo file Javascript chiamato homepage.js all’interno della cartella assets/js come mostrato di seguito:

const clickMeButton = document.getElementById('clickMeButton');

clickMeButton.addEventListener('click', function () {
  alert('Bottone clickato!');
});

Aggiungiamo il file appena creato a quelli che Webpack andrà ad analizzare per l’impacchettamento all’interno del bundle

.addEntry('homepage', './assets/js/homepage.js')

Aggiungi il nuovo script alla pagina appena creata

{% block javascripts %}
    {{ encore_entry_script_tags('homepage') }}
{% endblock %}

A questo punto ricompilando gli asset con Webpack (comando: yarn encore dev) e ricaricando la pagina, al click del bottone verrà mostrato l’alert appena creato.

Aggiunta di Bootstrap

Per dare un po’ di stile alla pagina aggiungi un framework CSS come Bootstrap.

 yarn add bootstrap

Aggiungi a Webpack una nuova entry:

.addStyleEntry('bootstrap', './node_modules/bootstrap/dist/css/bootstrap.css')

importa nella pagina il nuovo asset e aggiungi un po’ di stile

{% extends 'base.html.twig' %}

{% block body %}
  <div class="container">
    <h1>Hello world!</h1>
    <div class="row">
      <div class="col">
        <button id="clickMeButton" class="btn btn-primary">Click me</button>
      </div>
    </div>
  </div>

{% endblock %}

{% block stylesheets %}
  {{ encore_entry_link_tags('bootstrap') }}
{% endblock %}


{% block javascripts %}
  {{ encore_entry_script_tags('homepage') }}
{% endblock %}

Conclusioni

Sviluppare un applicazione web moderna e accattivante richiede l’integrazione di diverse conoscenze e di diversi strumenti di sviluppo che permettano di trarre il meglio dal mondo back-end e da quello front-end. Proprio per questo gli sviluppatori di Symfony hanno realizzato uno strumento come webpack-encore in grado di semplificare al massimo la gestione degli asset javascript e CSS.

Puoi scaricare il codice sorgente del progetto direttamente da GitHub:

https://github.com/lmillucci/symfony-webpack-example


Se questo post ti è stato utile puoi farmelo sapere con un commento qui sotto oppure scrivendomi direttamente a t.me/lorenzomillucci.
Inoltre ti invito ad iscriverti al mio canale Telegram o a seguirmi su Twitter per non perderti nemmeno un post del mio blog.