Una guida alla gestione delle dipendenze di Webpack
Pubblicato: 2022-03-11Il concetto di modularizzazione è parte integrante della maggior parte dei linguaggi di programmazione moderni. JavaScript, tuttavia, non ha alcun approccio formale alla modularizzazione fino all'arrivo dell'ultima versione di ECMAScript ES6.
In Node.js, uno dei framework JavaScript più popolari di oggi, i bundler di moduli consentono di caricare moduli NPM nei browser Web e le librerie orientate ai componenti (come React) incoraggiano e facilitano la modularizzazione del codice JavaScript.
Webpack è uno dei bundler di moduli disponibili che elabora il codice JavaScript, così come tutte le risorse statiche, come fogli di stile, immagini e caratteri, in un file in bundle. L'elaborazione può includere tutte le attività necessarie per la gestione e l'ottimizzazione delle dipendenze del codice, come compilazione, concatenazione, minimizzazione e compressione.
Tuttavia, la configurazione di Webpack e delle sue dipendenze può essere stressante e non è sempre un processo semplice, soprattutto per i principianti.
Questo post del blog fornisce linee guida, con esempi, su come configurare Webpack per diversi scenari e sottolinea le insidie più comuni relative al raggruppamento di dipendenze di progetto tramite Webpack.
La prima parte di questo post sul blog spiega come semplificare la definizione delle dipendenze in un progetto. Successivamente, discutiamo e dimostriamo la configurazione per la suddivisione del codice di applicazioni a pagina singola e multipla. Infine, discutiamo come configurare Webpack, se vogliamo includere librerie di terze parti nel nostro progetto.
Configurazione di alias e percorsi relativi
I percorsi relativi non sono direttamente correlati alle dipendenze, ma li usiamo quando definiamo le dipendenze. Se la struttura di un file di progetto è complessa, può essere difficile risolvere i percorsi dei moduli rilevanti. Uno dei vantaggi più fondamentali della configurazione di Webpack è che aiuta a semplificare la definizione di percorsi relativi in un progetto.
Supponiamo di avere la seguente struttura di progetto:
- Project - node_modules - bower_modules - src - script - components - Modal.js - Navigation.js - containers - Home.js - Admin.js
Possiamo fare riferimento alle dipendenze tramite percorsi relativi ai file di cui abbiamo bisogno e, se vogliamo importare componenti in contenitori nel nostro codice sorgente, si presenta come segue:
Home.js
Import Modal from '../components/Modal'; Import Navigation from '../components/Navigation';
Modal.js
import {datepicker} from '../../../../bower_modules/datepicker/dist/js/datepicker';
Ogni volta che vogliamo importare uno script o un modulo, dobbiamo conoscere la posizione della directory corrente e trovare il percorso relativo a ciò che vogliamo importare. Possiamo immaginare come questo problema possa aumentare in complessità se abbiamo un grande progetto con una struttura di file nidificata o se vogliamo refactoring di alcune parti di una struttura di progetto complessa.
Possiamo facilmente gestire questo problema con l'opzione resolve.alias
di Webpack. Possiamo dichiarare i cosiddetti alias: il nome di una directory o di un modulo con la sua posizione e non ci basiamo su percorsi relativi nel codice sorgente del progetto.
webpack.config.js
resolve: { alias: { 'node_modules': path.join(__dirname, 'node_modules'), 'bower_modules': path.join(__dirname, 'bower_modules'), } }
Nel file Modal.js
, ora possiamo importare datepicker in modo molto più semplice:
import {datepicker} from 'bower_modules/datepicker/dist/js/datepicker';
Divisione del codice
Possiamo avere scenari in cui è necessario aggiungere uno script al bundle finale o dividere il bundle finale, oppure si desidera caricare bundle separati su richiesta. L'impostazione del nostro progetto e della configurazione del Webpack per questi scenari potrebbe non essere semplice.
Nella configurazione di Webpack, l'opzione Entry
indica a Webpack dove si trova il punto di partenza per il bundle finale. Un punto di ingresso può avere tre diversi tipi di dati: String, Array o Object.
Se abbiamo un unico punto di partenza, possiamo utilizzare uno qualsiasi di questi formati e ottenere lo stesso risultato.
Se vogliamo aggiungere più file e non dipendono l'uno dall'altro, possiamo utilizzare un formato Array. Ad esempio, possiamo aggiungere analytics.js
alla fine di bundle.js
:
webpack.config.js
module.exports = { // creates a bundle out of index.js and then append analytics.js entry: ['./src/script/index.jsx', './src/script/analytics.js'], output: { path: './build', filename: bundle.js ' } };
Gestione di più punti di ingresso
Supponiamo di avere un'applicazione multipagina con più file HTML, come index.html
e admin.html
. Possiamo generare più bundle utilizzando il punto di ingresso come tipo di oggetto. La configurazione seguente genera due bundle JavaScript:
webpack.config.js
module.exports = { entry: { index: './src/script/index.jsx', admin: './src/script/admin.jsx' }, output: { path: './build', filename: '[name].js' // template based on keys in entry above (index.js & admin.js) } };
index.html
<script src=”build/index.js”></script>
admin.html
<script src=”build/admin.js”></script>
Entrambi i bundle JavaScript possono condividere librerie e componenti comuni. Per questo, possiamo usare CommonsChunkPlugin
, che trova i moduli che si trovano in più blocchi di voci e crea un pacchetto condiviso che può essere memorizzato nella cache tra più pagine.
webpack.config.js
var commonsPlugin = new webpack.optimize.CommonsChunkPlugin('common.js'); module.exports = { entry: { index: './src/script/index.jsx', admin: './src/script/admin.jsx' }, output: { path: './build', filename: '[name].js' // template based on keys in entry above (index.js & admin.js) }, plugins: [commonsPlugin] };
Ora, non dobbiamo dimenticare di aggiungere <script src="build/common.js"></script>
prima degli script raggruppati.
Abilitazione del caricamento lento
Webpack può suddividere le risorse statiche in blocchi più piccoli e questo approccio è più flessibile della concatenazione standard. Se abbiamo una grande applicazione a pagina singola (SPA), la semplice concatenazione in un pacchetto non è un buon approccio perché il caricamento di un pacchetto enorme può essere lento e gli utenti di solito non hanno bisogno di tutte le dipendenze su ciascuna vista.
Abbiamo spiegato in precedenza come suddividere un'applicazione in più bundle, concatenare le dipendenze comuni e trarre vantaggio dal comportamento di memorizzazione nella cache del browser. Questo approccio funziona molto bene per le applicazioni multipagina, ma non per le applicazioni a pagina singola.
Per la SPA, dovremmo fornire solo le risorse statiche necessarie per eseguire il rendering della vista corrente. Il router lato client nell'architettura SPA è un luogo perfetto per gestire la divisione del codice. Quando l'utente entra in un percorso, possiamo caricare solo le dipendenze necessarie per la vista risultante. In alternativa, possiamo caricare le dipendenze mentre l'utente scorre una pagina verso il basso.
A tale scopo, possiamo utilizzare le funzioni require.ensure
o System.import
, che Webpack può rilevare staticamente. Webpack può generare un pacchetto separato basato su questo punto di divisione e chiamarlo su richiesta.
In questo esempio, abbiamo due contenitori React; una vista amministratore e una vista dashboard.
admin.jsx
import React, {Component} from 'react'; export default class Admin extends Component { render() { return <div > Admin < /div>; } }
dashboard.jsx
import React, {Component} from 'react'; export default class Dashboard extends Component { render() { return <div > Dashboard < /div>; } }
Se l'utente immette l'URL /dashboard
o /admin
, viene caricato solo il bundle JavaScript richiesto corrispondente. Di seguito possiamo vedere esempi con e senza il router lato client.
index.jsx
if (window.location.pathname === '/dashboard') { require.ensure([], function() { require('./containers/dashboard').default; }); } else if (window.location.pathname === '/admin') { require.ensure([], function() { require('./containers/admin').default; }); }
index.jsx
ReactDOM.render( <Router> <Route path="/" component={props => <div>{props.children}</div>}> <IndexRoute component={Home} /> <Route path="dashboard" getComponent={(nextState, cb) => { require.ensure([], function (require) { cb(null, require('./containers/dashboard').default) }, "dashboard")}} /> <Route path="admin" getComponent={(nextState, cb) => { require.ensure([], function (require) { cb(null, require('./containers/admin').default) }, "admin")}} /> </Route> </Router> , document.getElementById('content') );
Estrazione di stili in pacchetti separati
In Webpack, i caricatori, come style-loader
e css-loader
, pre-elaborano i fogli di stile e li incorporano nel bundle JavaScript di output, ma in alcuni casi possono causare il Flash of unstyled content (FOUC).

Possiamo evitare il FOUC con ExtractTextWebpackPlugin
che consente di generare tutti gli stili in bundle CSS separati invece di averli incorporati nel bundle JavaScript finale.
webpack.config.js
var ExtractTextPlugin = require('extract-text-webpack-plugin'); module.exports = { module: { loaders: [{ test: /\.css/, loader: ExtractTextPlugin.extract('style', 'css')' }], }, plugins: [ // output extracted CSS to a file new ExtractTextPlugin('[name].[chunkhash].css') ] }
Gestione di librerie e plugin di terze parti
Molte volte, abbiamo bisogno di utilizzare librerie di terze parti, vari plugin o script aggiuntivi, perché non vogliamo perdere tempo a sviluppare gli stessi componenti da zero. Sono disponibili molte librerie e plug-in legacy che non vengono mantenuti attivamente, non comprendono i moduli JavaScript e presuppongono la presenza di dipendenze a livello globale con nomi predefiniti.
Di seguito sono riportati alcuni esempi con i plugin jQuery, con una spiegazione di come configurare correttamente Webpack per poter generare il bundle finale.
Fornire Plugin
La maggior parte dei plugin di terze parti si basa sulla presenza di specifiche dipendenze globali. Nel caso di jQuery, i plugin si basano sulla definizione della variabile $
o jQuery
e possiamo usare i plugin jQuery chiamando $('div.content').pluginFunc()
nel nostro codice.
Possiamo usare il plugin Webpack ProvidePlugin
per anteporre var $ = require("jquery")
ogni volta che incontra l'identificatore globale $
.
webpack.config.js
webpack.ProvidePlugin({ '$': 'jquery', })
Quando Webpack elabora il codice, cerca la presenza $
e fornisce un riferimento alle dipendenze globali senza importare il modulo specificato dalla funzione require
.
Importazioni-caricatore
Alcuni plugin jQuery assumono $
nello spazio dei nomi globale o si basano sul fatto che this
sia l'oggetto window
. A questo scopo, possiamo usare imports-loader
che inietta variabili globali nei moduli.
example.js
$('div.content').pluginFunc();
Quindi, possiamo iniettare la variabile $
nel modulo configurando imports-loader
:
require("imports?$=jquery!./example.js");
Questo semplicemente antepone var $ = require("jquery");
ad example.js
.
Nel secondo caso d'uso:
webpack.config.js
module: { loaders: [{ test: /jquery-plugin/, loader: 'imports?jQuery=jquery,$=jquery,this=>window' }] }
Utilizzando il simbolo =>
(da non confondere con le funzioni ES6 Arrow), possiamo impostare variabili arbitrarie. L'ultimo valore ridefinisce la variabile globale this
in modo che punti all'oggetto window
. È lo stesso che avvolgere l'intero contenuto del file con (function () { ... }).call(window);
e chiamando this
funzione con window
come argomento.
Possiamo anche richiedere librerie utilizzando il formato del modulo CommonJS o AMD:
// CommonJS var $ = require("jquery"); // jquery is available // AMD define(['jquery'], function($) { // jquery is available });
Alcune librerie e moduli possono supportare diversi formati di modulo.
Nel prossimo esempio, abbiamo un plugin jQuery che utilizza il formato del modulo AMD e CommonJS e ha una dipendenza jQuery:
jquery-plugin.js
(function(factory) { if (typeof define === 'function' && define.amd) { // AMD format is used define(['jquery'], factory); } else if (typeof exports === 'object') { // CommonJS format is used module.exports = factory(require('jquery')); } else { // Neither AMD nor CommonJS used. Use global variables. } });
webpack.config.js
module: { loaders: [{ test: /jquery-plugin/, loader: "imports?define=>false,exports=>false" }] }
Possiamo scegliere quale formato di modulo vogliamo utilizzare per la libreria specifica. Se dichiariamo define
to equal false
, Webpack non analizza il modulo nel formato del modulo AMD e se dichiariamo le exports
delle variabili su equal false
, Webpack non analizza il modulo nel formato del modulo CommonJS.
Caricatore di esposizione
Se abbiamo bisogno di esporre un modulo al contesto globale, possiamo usare expose-loader
. Questo può essere utile, ad esempio, se disponiamo di script esterni che non fanno parte della configurazione di Webpack e si basano sul simbolo nello spazio dei nomi globale, oppure utilizziamo plug-in del browser che devono accedere a un simbolo nella console del browser.
webpack.config.js
module: { loaders: [ test: require.resolve('jquery'), loader: 'expose-loader?jQuery!expose-loader?$' ] }
La libreria jQuery è ora disponibile nello spazio dei nomi globale per altri script nella pagina web.
window.$ window.jQuery
Configurazione delle dipendenze esterne
Se vogliamo includere moduli da script ospitati esternamente, dobbiamo definirli nella configurazione. In caso contrario, Webpack non può generare il bundle finale.
Possiamo configurare script esterni utilizzando l'opzione externals
nella configurazione di Webpack. Ad esempio, possiamo utilizzare una libreria da una CDN tramite un tag <script>
separato, pur dichiarandola esplicitamente come dipendenza del modulo nel nostro progetto.
webpack.config.js
externals: { react: 'React', 'react-dom': 'ReactDOM' }
Supporto di più istanze di una libreria
È fantastico utilizzare il gestore di pacchetti NPM nello sviluppo front-end per la gestione di librerie e dipendenze di terze parti. Tuttavia, a volte possiamo avere più istanze della stessa libreria con versioni diverse e non funzionano bene insieme in un unico ambiente.
Questo potrebbe accadere, ad esempio, con la libreria React, dove possiamo installare React da NPM e in seguito può diventare disponibile una versione diversa di React con qualche pacchetto o plugin aggiuntivo. La struttura del nostro progetto può essere simile alla seguente:
project | |-- node_modules | |-- react |-- react-plugin | |--node_modules | |--react
I componenti provenienti dal react-plugin
hanno un'istanza React diversa rispetto al resto dei componenti del progetto. Ora abbiamo due copie separate di React e possono essere versioni diverse. Nella nostra applicazione, questo scenario può rovinare il nostro DOM mutevole globale e possiamo vedere messaggi di errore nel registro della console web. La soluzione a questo problema è avere la stessa versione di React durante l'intero progetto. Possiamo risolverlo tramite alias Webpack.
webpack.config.js
module.exports = { resolve: { alias: { 'react': path.join(__dirname, './node_modules/react'), 'react/addons': path.join(__dirname, '/node_modules/react/addons'), } } }
Quando react-plugin
tenta di richiedere React, utilizza la versione in node_modules
del progetto. Se vogliamo scoprire quale versione di React utilizziamo, possiamo aggiungere console.log(React.version)
nel codice sorgente.
Concentrati sullo sviluppo, non sulla configurazione del Webpack
Questo post graffia solo la superficie della potenza e dell'utilità di Webpack.
Esistono molti altri caricatori e plug-in Webpack che ti aiuteranno a ottimizzare e semplificare il raggruppamento di JavaScript.
Anche se sei un principiante, questa guida ti offre una solida base per iniziare a utilizzare Webpack, che ti consentirà di concentrarti maggiormente sullo sviluppo e meno sulla configurazione in bundle.