Il 19 febbraio 2014 il repository webpack/webpack su GitHub ha taggato la versione 1.0.0, licenza MIT. Lo strumento, che Tobias Koppers porta avanti dal primo commit del 10 marzo 2012, costruisce il bundle JavaScript a partire da un grafo di dipendenze invece che da una lista ordinata di file.
Contesto
Nel 2013 il JavaScript lato browser si carica ancora quasi sempre con una sequenza di tag <script> in cui l’ordine ha un significato, oppure con un loader AMD (Asynchronous Module Definition) come RequireJS che risolve le dipendenze a runtime. Tutte e due le strade hanno un costo. La prima fa dell’ordine di caricamento un pezzo della logica applicativa, senza poi verificarlo. La seconda sposta la risoluzione nel browser: in sviluppo una richiesta HTTP per ogni modulo, in produzione un passo di ottimizzazione a parte (r.js).
Browserify, uscito nel 2011, prende un’altra direzione. Porta nel browser la convenzione require() di CommonJS, che fino ad allora viveva solo in Node.js: risolve il grafo dei require in modo statico e produce un file unico. Il modello è lineare e prevedibile, ma a questa data webpack copre due cose che browserify non affronta direttamente: spezzare il bundle in più frammenti caricabili a richiesta, e tenere nello stesso grafo anche i file che non sono JavaScript.
Architettura
Webpack parte da uno o più entry point. Da ognuno segue tutti i require (CommonJS), define (AMD) e import (la bozza degli ES Modules) che riesce a raggiungere, costruisce il grafo completo delle dipendenze e da lì ricava l’output. La configurazione sta in un file webpack.config.js che esporta un oggetto con i campi entry, output, module e plugins.
A distinguere il modello sono tre meccanismi.
Loader. Un loader è una funzione che riceve il contenuto di un file e restituisce JavaScript valido. La pipeline si dichiara per estensione: module.loaders lega un’espressione regolare (test) a una catena di loader. Un require('./style.css') non è un errore, è una richiesta che passa per css-loader e diventa un modulo. Lo stesso accade con coffee-loader, json-loader, jade-loader o con i loader che trasformano un’immagine in data URI. Il risultato è che il grafo non tiene solo JavaScript: tiene tutto quello che un loader sa tradurre in JavaScript.
Code splitting. Una chiamata require.ensure([...], callback) dice a webpack che le dipendenze elencate vanno in un frammento a parte (un chunk) e si caricano solo quando parte il callback. Webpack emette quel chunk come file distinto e mette nel bundle principale il codice che lo carica a richiesta. Il taglio del grafo sta scritto nel sorgente, non lo si deduce da una configurazione esterna.
Plugin. Dove il loader trasforma il singolo file, il plugin lavora sull’intera build, agganciandosi agli eventi del compilatore. CommonsChunkPlugin, che arriva insieme a webpack, trova i moduli condivisi tra più entry point e li estrae in un chunk comune, così non finiscono duplicati in ogni bundle.
A tutto questo si aggiunge l’Hot Module Replacement: in sviluppo webpack-dev-server rimpiazza nella pagina già caricata un modulo modificato senza ricaricarla, e propaga l’aggiornamento lungo il grafo fino al confine che dichiara di saperlo gestire.
Il punto critico
La scelta progettuale che orienta tutto il resto è trattare ogni asset come un modulo. In browserify il confine del grafo coincide con quello di CommonJS: ciò che non si può richiedere con un require come modulo JavaScript resta fuori e lo gestisce un altro strumento — di solito un task runner come Grunt o Gulp, che copia file, concatena fogli di stile e ottimizza immagini in pipeline parallele al bundling del JavaScript.
Webpack tira dentro al grafo anche quelle pipeline. Il foglio di stile che un componente importa è una dipendenza di quel componente; se il componente finisce in un chunk caricato a richiesta, lo stile lo segue. Sapere quando serve un asset non vive più in una configurazione separata del task runner: lo si ricava dalla struttura dei require nel codice. Così si toglie la duplicazione tra la dichiarazione delle dipendenze e la configurazione della build, e si sposta il costo altrove — sul file di configurazione di webpack e sulla catena di loader, dove ora si decide cosa succede a ogni tipo di file.
Implicazioni
Spostare i require dal codice applicativo a webpack apre analisi che la concatenazione di file non regge. Eliminare il codice morto a partire dal grafo, estrarre i moduli comuni, tagliare in chunk per route: sono tutte cose che danno per noto il grafo completo e i confini espliciti tra le sue parti. Su un’applicazione organizzata in viste, ognuna isolata dietro un require.ensure, all’avvio il browser scarica solo il codice della vista iniziale più il chunk comune, e il resto arriva quando navighi.
Sul lato sviluppo, l’HMR cambia il giro di iterazione: la pagina si tiene il proprio stato mentre il modulo cambia. Per interfacce con stato profondo — un form a più passi, una vista con dati già caricati — non rifare il reload completo accorcia il tempo tra la modifica e la sua verifica.
Limiti
Il prezzo è la configurazione. webpack.config.js diventa la rappresentazione di come ogni tipo di file entra nel grafo, e man mano che si accumulano loader e plugin diventa a sua volta un artefatto da mantenere e da capire. La documentazione, alla versione 1.0, descrive le API ma resta lacunosa sulle ricette d’uso, e diverse risposte pratiche stanno ancora nelle issue del repository.
Il grafo, poi, rende implicite alcune dipendenze che con i tag <script> stavano davanti agli occhi: un require dentro un loader, una catena di trasformazioni, e per capire da dove arriva davvero un modulo nel bundle tocca leggere la configurazione e ricostruirlo. È uno spostamento di complessità, non la sua scomparsa: dall’ordine dei file alla descrizione del grafo. Su progetti piccoli, dove la sequenza di <script> si legge ancora a colpo d’occhio, quello spostamento può non ripagare.
https://github.com/webpack/webpack https://github.com/substack/node-browserify https://requirejs.org/ https://www.noze.it/insights/webpack-1-0/
Immagine di copertina: Diagramma di un grafo di dipendenze: quattro riquadri etichettati GTK, GLIB, ATK e OpenGL, collegati da frecce direzionate che… — diagramma di Aleksi Nurmi, pubblico dominio — https://commons.wikimedia.org/wiki/File:DependencyGraph.svg