Webpack o Browserify & Gulp: quale è meglio?
Pubblicato: 2022-03-11Man mano che le applicazioni Web diventano sempre più complesse, rendere scalabile la tua app Web diventa della massima importanza. Mentre in passato sarebbe stato sufficiente scrivere JavaScript e jQuery ad-hoc, oggi la creazione di un'app Web richiede un grado molto maggiore di disciplina e pratiche formali di sviluppo software, come ad esempio:
- Unit test per garantire che le modifiche al codice non interrompano le funzionalità esistenti
- Linting per garantire uno stile di codifica coerente e privo di errori
- Build di produzione che differiscono da build di sviluppo
Il web offre anche alcune delle sue sfide di sviluppo uniche. Ad esempio, poiché le pagine Web effettuano molte richieste asincrone, le prestazioni della tua app Web possono essere notevolmente ridotte dalla necessità di richiedere centinaia di file JS e CSS, ciascuno con il proprio piccolo sovraccarico (intestazioni, handshake e così via). Questo particolare problema può essere spesso risolto raggruppando i file insieme, quindi stai richiedendo solo un singolo file JS e CSS in bundle anziché centinaia di singoli file.
È anche abbastanza comune utilizzare preprocessori di linguaggio come SASS e JSX che compilano in JS e CSS nativi, nonché transpiler JS come Babel, per beneficiare del codice ES6 mantenendo la compatibilità ES5.
Ciò equivale a un numero significativo di attività che non hanno nulla a che fare con la scrittura della logica dell'app Web stessa. È qui che entrano in gioco i task runner. Lo scopo di un task runner è automatizzare tutte queste attività in modo che tu possa beneficiare di un ambiente di sviluppo avanzato mentre ti concentri sulla scrittura della tua app. Una volta configurato il task runner, tutto ciò che devi fare è invocare un singolo comando in un terminale.
Userò Gulp come task runner perché è molto intuitivo per gli sviluppatori, facile da imparare e facilmente comprensibile.
Una rapida introduzione a Gulp
L'API di Gulp è composta da quattro funzioni:
-
gulp.src
-
gulp.dest
-
gulp.task
-
gulp.watch
Ecco, ad esempio, un'attività di esempio che utilizza tre di queste quattro funzioni:
gulp.task('my-first-task', function() { gulp.src('/public/js/**/*.js') .pipe(concat()) .pipe(minify()) .pipe(gulp.dest('build')) });
Quando viene eseguita la my-first-task
, tutti i file che corrispondono al pattern glob /public/js/**/*.js
vengono minimizzati e quindi trasferiti in una cartella di build
.
La bellezza di questo è nel concatenamento .pipe()
. Prendi una serie di file di input, li reindirizza attraverso una serie di trasformazioni, quindi restituisci i file di output. Per rendere le cose ancora più convenienti, le effettive trasformazioni delle tubazioni, come minify()
, vengono spesso eseguite dalle librerie NPM. Di conseguenza, è molto raro in pratica che sia necessario scrivere le proprie trasformazioni oltre a rinominare i file nella pipe.
Il passaggio successivo per comprendere Gulp è comprendere la matrice delle dipendenze delle attività.
gulp.task('my-second-task', ['lint', 'bundle'], function() { ... });
Qui, my-second-task
esegue la funzione di callback solo dopo che le attività di lint
e bundle
sono state completate. Ciò consente la separazione delle preoccupazioni: crei una serie di piccole attività con un'unica responsabilità, come la conversione di LESS
in CSS
, e crei una sorta di attività principale che chiama semplicemente tutte le altre attività tramite l'array di dipendenze delle attività.
Infine, abbiamo gulp.watch
, che controlla un pattern di file glob per le modifiche e, quando viene rilevata una modifica, esegue una serie di attività.
gulp.task('my-third-task', function() { gulp.watch('/public/js/**/*.js', ['lint', 'reload']) })
Nell'esempio precedente, qualsiasi modifica a un file corrispondente a /public/js/**/*.js
l'attività lint
e reload
. Un uso comune di gulp.watch
è attivare ricariche live nel browser, una funzionalità così utile per lo sviluppo che non potrai più farne a meno una volta che l'avrai sperimentata.
E proprio così, capisci tutto ciò che devi davvero sapere su gulp
.
Dove si inserisce Webpack?
Quando si utilizza il modello CommonJS, il raggruppamento di file JavaScript non è semplice come concatenarli. Piuttosto, hai un punto di ingresso (di solito chiamato index.js
o app.js
) con una serie di istruzioni require
o import
nella parte superiore del file:
ES5
var Component1 = require('./components/Component1'); var Component2 = require('./components/Component2');
ES6
import Component1 from './components/Component1'; import Component2 from './components/Component2';
Le dipendenze devono essere risolte prima del codice rimanente in app.js
e tali dipendenze potrebbero avere ulteriori dipendenze da risolvere. Inoltre, potresti require
la stessa dipendenza in più punti dell'applicazione, ma desideri risolvere tale dipendenza solo una volta. Come puoi immaginare, una volta che hai un albero delle dipendenze profondo alcuni livelli, il processo di raggruppamento del tuo JavaScript diventa piuttosto complesso. È qui che entrano in gioco bundler come Browserify e Webpack.
Perché gli sviluppatori utilizzano Webpack invece di Gulp?
Webpack è un bundler mentre Gulp è un task runner, quindi ti aspetteresti di vedere questi due strumenti comunemente usati insieme. Invece, c'è una tendenza crescente, specialmente nella comunità di React, a usare Webpack invece di Gulp. Perchè è questo?
In poche parole, Webpack è uno strumento così potente che può già eseguire la stragrande maggioranza delle attività che altrimenti faresti tramite un task runner. Ad esempio, Webpack fornisce già opzioni per la minimizzazione e le mappe dei sorgenti per il tuo pacchetto. Inoltre, Webpack può essere eseguito come middleware tramite un server personalizzato chiamato webpack-dev-server
, che supporta sia il ricaricamento live che il ricaricamento a caldo (ne parleremo più avanti). Utilizzando i caricatori, puoi anche aggiungere la transpilazione da ES6 a ES5 e pre e post-processori CSS. Ciò lascia davvero solo unit test e linting come attività principali che Webpack non può gestire in modo indipendente. Dato che abbiamo ridotto a due almeno una mezza dozzina di potenziali attività di gulp, molti sviluppatori scelgono invece di utilizzare direttamente gli script NPM, in quanto ciò evita il sovraccarico dell'aggiunta di Gulp al progetto (di cui parleremo anche più avanti) .
Il principale svantaggio dell'utilizzo di Webpack è che è piuttosto difficile da configurare, il che non è interessante se stai cercando di far funzionare rapidamente un progetto.
Le nostre 3 configurazioni di Task Runner
Creerò un progetto con tre diverse configurazioni di task runner. Ogni configurazione eseguirà le seguenti attività:
- Configura un server di sviluppo con ricarica in tempo reale sulle modifiche ai file osservati
- Raggruppa i nostri file JS e CSS (inclusa la transpilazione da ES6 a ES5, la conversione da SASS a CSS e le mappe dei sorgenti) in modo scalabile sulle modifiche ai file osservati
- Esegui unit test come attività autonoma o in modalità di controllo
- Esegui linting come attività autonoma o in modalità orologio
- Fornire la possibilità di eseguire tutto quanto sopra tramite un singolo comando nel terminale
- Avere un altro comando per creare un pacchetto di produzione con minimizzazione e altre ottimizzazioni
I nostri tre allestimenti saranno:
- Gulp + Browserify
- Gulp + Webpack
- Webpack + script NPM
L'applicazione utilizzerà React per il front-end. Inizialmente, volevo utilizzare un approccio indipendente dal framework, ma l'uso di React semplifica effettivamente le responsabilità del task runner, poiché è necessario un solo file HTML e React funziona molto bene con il modello CommonJS.
Tratteremo i vantaggi e gli svantaggi di ogni configurazione in modo che tu possa prendere una decisione informata su quale tipo di configurazione si adatta meglio alle esigenze del tuo progetto.
Ho impostato un repository Git con tre rami, uno per ogni approccio (link). Testare ogni configurazione è semplice come:
git checkout <branch name> npm prune (optional) npm install gulp (or npm start, depending on the setup)
Esaminiamo nel dettaglio il codice in ogni ramo...
Codice comune
Struttura delle cartelle
- app - components - fonts - styles - index.html - index.js - index.test.js - routes.js
indice.html
Un semplice file HTML. L'applicazione React viene caricata in <div></div>
e utilizziamo solo un singolo file JS e CSS in bundle. In effetti, nella nostra configurazione di sviluppo di Webpack, non avremo nemmeno bisogno bundle.css
.
index.js
Questo funge da punto di ingresso JS della nostra app. In sostanza, stiamo solo caricando React Router app
div
con id di cui abbiamo parlato in precedenza.
rotte.js
Questo file definisce i nostri percorsi. Gli URL /
, /about
e /contact
sono mappati rispettivamente ai componenti HomePage
, AboutPage
e ContactPage
.
index.test.js
Questa è una serie di unit test che testano il comportamento JavaScript nativo. In una vera app per la qualità della produzione, scriveresti uno unit test per componente React (almeno quelli che manipolano lo stato), testando il comportamento specifico di React. Tuttavia, ai fini di questo post, è sufficiente avere semplicemente un test dell'unità funzionale che può essere eseguito in modalità orologio.
componenti/App.js
Questo può essere considerato il contenitore per tutte le nostre visualizzazioni di pagina. Ogni pagina contiene un componente <Header/>
oltre a this.props.children
, che restituisce la visualizzazione della pagina stessa (ex/ ContactPage
se in /contact
nel browser).
componenti/home/HomePage.js
Questa è la nostra vista di casa. Ho scelto di utilizzare react-bootstrap
poiché il sistema di griglia di bootstrap è eccellente per la creazione di pagine reattive. Con un uso corretto del bootstrap, il numero di media query che devi scrivere per viewport più piccoli viene drasticamente ridotto.
I restanti componenti ( Header
, AboutPage
, ContactPage
) sono strutturati in modo simile ( react-bootstrap
markup, nessuna manipolazione dello stato).
Ora parliamo di più di styling.
Approccio di stile CSS
Il mio approccio preferito allo styling dei componenti di React consiste nell'avere un foglio di stile per componente, i cui stili hanno lo scopo di applicarsi solo a quel componente specifico. Noterai che in ciascuno dei miei componenti React, il div
di primo livello ha un nome di classe che corrisponde al nome del componente. Quindi, ad esempio, HomePage.js
ha il suo markup racchiuso da:
<div className="HomePage"> ... </div>
C'è anche un file HomePage.scss
associato strutturato come segue:
@import '../../styles/variables'; .HomePage { // Content here }
Perché questo approccio è così utile? Si traduce in CSS altamente modulari, eliminando in gran parte il problema del comportamento a cascata indesiderato.
Supponiamo di avere due componenti React, Component1
e Component2
. In entrambi i casi, vogliamo sovrascrivere la dimensione del carattere h2
.
/* Component1.scss */ .Component1 { h2 { font-size: 30px; } } /* Component2.scss */ .Component2 { h2 { font-size: 60px; } }
La dimensione del carattere h2
di Component1
e Component2
è indipendente dal fatto che i componenti siano adiacenti o che un componente sia nidificato all'interno dell'altro. Idealmente, ciò significa che lo stile di un componente è completamente autonomo, il che significa che il componente avrà esattamente lo stesso aspetto, indipendentemente da dove è posizionato nel markup. In realtà, non è sempre così semplice, ma è sicuramente un grande passo nella giusta direzione.
Oltre agli stili per componente, mi piace avere una cartella styles
contenente un foglio di stile globale global.scss
, insieme a parziali SASS che gestiscono una responsabilità specifica (in questo caso, _fonts.scss
e _variables.scss
per font e variabili ). Il foglio di stile globale ci consente di definire l'aspetto generale dell'intera app, mentre i parziali dell'helper possono essere importati dai fogli di stile per componente secondo necessità.
Ora che il codice comune in ogni ramo è stato esplorato in profondità, spostiamo la nostra attenzione sul primo approccio task runner/bundle.
Configurazione Gulp + Browserify
gulpfile.js
Questo risulta in un gulpfile sorprendentemente grande, con 22 importazioni e 150 righe di codice. Quindi, per brevità, esaminerò in dettaglio solo le attività js
, css
, server
, watch
e default
.
Pacchetto JS
// Browserify specific configuration const b = browserify({ entries: [config.paths.entry], debug: true, plugin: PROD ? [] : [hmr, watchify], cache: {}, packageCache: {} }) .transform('babelify'); b.on('update', bundle); b.on('log', gutil.log); (...) gulp.task('js', bundle); (...) // Bundles our JS using Browserify. Sourcemaps are used in development, while minification is used in production. function bundle() { return b.bundle() .on('error', gutil.log.bind(gutil, 'Browserify Error')) .pipe(source('bundle.js')) .pipe(buffer()) .pipe(cond(PROD, minifyJS())) .pipe(cond(!PROD, sourcemaps.init({loadMaps: true}))) .pipe(cond(!PROD, sourcemaps.write())) .pipe(gulp.dest(config.paths.baseDir)); }
Questo approccio è piuttosto brutto per una serie di motivi. Per prima cosa, l'attività è divisa in tre parti separate. Innanzitutto, crei il tuo oggetto bundle Browserify b
, passando alcune opzioni e definendo alcuni gestori di eventi. Quindi hai l'attività Gulp stessa, che deve passare una funzione denominata come callback invece di incorporarla (poiché b.on('update')
usa lo stesso callback). Questo non ha l'eleganza di un compito di Gulp in cui si passa semplicemente in un gulp.src
e si inviano alcune modifiche.
Un altro problema è che questo ci costringe ad avere approcci diversi per ricaricare html
, css
e js
nel browser. Guardando il nostro compito di watch
Gulp:
gulp.task('watch', () => { livereload.listen({basePath: 'dist'}); gulp.watch(config.paths.html, ['html']); gulp.watch(config.paths.css, ['css']); gulp.watch(config.paths.js, () => { runSequence('lint', 'test'); }); });
Quando un file HTML viene modificato, l'attività html
viene rieseguita.
gulp.task('html', () => { return gulp.src(config.paths.html) .pipe(gulp.dest(config.paths.baseDir)) .pipe(cond(!PROD, livereload())); });
L'ultima pipe chiama livereload()
se NODE_ENV
non è production
, che attiva un aggiornamento nel browser.
La stessa logica viene utilizzata per l'orologio CSS. Quando un file CSS viene modificato, l'attività css
viene rieseguita e l'ultima pipe nell'attività css
attiva livereload()
e aggiorna il browser.
Tuttavia, l'orologio js
non chiama affatto l'attività js
. Invece, il gestore di eventi di b.on('update', bundle)
gestisce il ricaricamento utilizzando un approccio completamente diverso (vale a dire, sostituzione a caldo del modulo). L'incoerenza in questo approccio è irritante, ma sfortunatamente necessaria per avere build incrementali . Se ingenuamente chiamassimo livereload()
alla fine della funzione bundle
, ciò ricostruirebbe l' intero bundle JS su ogni singola modifica del file JS. Un tale approccio ovviamente non è scalabile. Più file JS hai, più tempo richiederà ogni riraggruppamento. Improvvisamente, i tuoi rebundle da 500 ms iniziano a richiedere 30 secondi, il che inibisce davvero lo sviluppo agile.
Pacchetto CSS
gulp.task('css', () => { return gulp.src( [ 'node_modules/bootstrap/dist/css/bootstrap.css', 'node_modules/font-awesome/css/font-awesome.css', config.paths.css ] ) .pipe(cond(!PROD, sourcemaps.init())) .pipe(sass().on('error', sass.logError)) .pipe(concat('bundle.css')) .pipe(cond(PROD, minifyCSS())) .pipe(cond(!PROD, sourcemaps.write())) .pipe(gulp.dest(config.paths.baseDir)) .pipe(cond(!PROD, livereload())); });
Il primo problema qui è l'ingombrante inclusione del CSS del fornitore. Ogni volta che un nuovo file CSS del fornitore viene aggiunto al progetto, dobbiamo ricordarci di cambiare il nostro gulpfile per aggiungere un elemento all'array gulp.src
, piuttosto che aggiungere l'importazione in una posizione rilevante nel nostro codice sorgente effettivo.
L'altro problema principale è la logica contorta in ogni pipe. Ho dovuto aggiungere una libreria NPM chiamata gulp-cond
solo per impostare la logica condizionale nelle mie pipe e il risultato finale non è troppo leggibile (tre parentesi quadre ovunque!).
Attività del server
gulp.task('server', () => { nodemon({ script: 'server.js' }); });
Questo compito è molto semplice. È essenzialmente un wrapper attorno alla chiamata della riga di comando nodemon server.js
, che esegue server.js
in un ambiente nodo. nodemon
viene utilizzato al posto di node
in modo che qualsiasi modifica al file ne provochi il riavvio. Per impostazione predefinita, nodemon
riavvia il processo in esecuzione su qualsiasi modifica del file JS, motivo per cui è importante includere un file nodemon.json
per limitarne l'ambito:
{ "watch": "server.js" }
Esaminiamo il nostro codice del server.
server.js
const baseDir = process.env.NODE_ENV === 'production' ? 'build' : 'dist'; const port = process.env.NODE_ENV === 'production' ? 8080: 3000; const app = express();
Questo imposta la directory di base del server e la porta in base all'ambiente del nodo e crea un'istanza di express.
app.use(require('connect-livereload')({port: 35729})); app.use(express.static(path.join(__dirname, baseDir)));
Ciò aggiunge il middleware connect-livereload
(necessario per la nostra configurazione di ricarica in tempo reale) e il middleware statico (necessario per la gestione delle nostre risorse statiche).
app.get('/api/sample-route', (req, res) => { res.send({ website: 'Toptal', blogPost: true }); });
Questo è solo un semplice percorso API. Se accedi a localhost:3000/api/sample-route
nel browser, vedrai:

{ website: "Toptal", blogPost: true }
In un vero back-end, avresti un'intera cartella dedicata ai percorsi API, file separati per stabilire connessioni DB e così via. Questo percorso di esempio è stato semplicemente incluso per mostrare che possiamo facilmente creare un backend sopra il frontend che abbiamo impostato.
app.get('*', (req, res) => { res.sendFile(path.join(__dirname, './', baseDir ,'/index.html')); });
Questo è un percorso generico, il che significa che, indipendentemente dall'URL digitato nel browser, il server restituirà la nostra pagina index.html
solitaria. È quindi responsabilità del router React risolvere i nostri percorsi sul lato client.
app.listen(port, () => { open(`http://localhost:${port}`); });
Questo dice alla nostra istanza express di ascoltare la porta che abbiamo specificato e di aprire il browser in una nuova scheda all'URL specificato.
Finora l'unica cosa che non mi piace della configurazione del server è:
app.use(require('connect-livereload')({port: 35729}));
Dato che stiamo già usando gulp-livereload
nel nostro gulpfile, questo crea due posti separati in cui deve essere usato livereload.
Ora, ultimo ma non meno importante:
Attività predefinita
gulp.task('default', (cb) => { runSequence('clean', 'lint', 'test', 'html', 'css', 'js', 'fonts', 'server', 'watch', cb); });
Questa è l'attività che viene eseguita quando si digita semplicemente gulp
nel terminale. Una stranezza è la necessità di utilizzare runSequence
per far eseguire le attività in sequenza. Normalmente, una serie di attività viene eseguita in parallelo, ma questo non è sempre il comportamento desiderato. Ad esempio, è necessario eseguire l'attività di clean
prima di html
per garantire che le cartelle di destinazione siano vuote prima di spostarvi i file. Quando gulp 4 verrà rilasciato, supporterà nativamente i metodi gulp.series
e gulp.parallel
, ma per ora dobbiamo lasciare questa piccola stranezza nella nostra configurazione.
Oltre a ciò, questo è in realtà piuttosto elegante. L'intera creazione e hosting della nostra app viene eseguita in un unico comando e comprendere qualsiasi parte del flusso di lavoro è semplice come esaminare una singola attività nella sequenza di esecuzione. Inoltre, possiamo suddividere l'intera sequenza in blocchi più piccoli per un approccio più granulare alla creazione e all'hosting dell'app. Ad esempio, potremmo impostare un'attività separata chiamata validate
che esegue le attività di lint
e test
. Oppure potremmo avere un'attività host
che esegue server
e watch
. Questa capacità di orchestrare le attività è molto potente, soprattutto perché l'applicazione è scalabile e richiede attività più automatizzate.
Sviluppo vs build di produzione
if (argv.prod) { process.env.NODE_ENV = 'production'; } let PROD = process.env.NODE_ENV === 'production';
Usando la libreria yargs
NPM, possiamo fornire i flag della riga di comando a Gulp. Qui dico a gulpfile di impostare l'ambiente del nodo in produzione se --prod
viene passato a gulp
nel terminale. La nostra variabile PROD
viene quindi utilizzata come condizionale per differenziare il comportamento di sviluppo e produzione nel nostro gulpfile. Ad esempio, una delle opzioni che passiamo alla nostra configurazione di browserify
è:
plugin: PROD ? [] : [hmr, watchify]
Questo dice a browserify
di non utilizzare alcun plug-in in modalità di produzione e di utilizzare i plug-in hmr
e watchify
in altri ambienti.
Questo condizionale PROD
è molto utile perché ci evita di dover scrivere un gulpfile separato per la produzione e lo sviluppo, che alla fine conterrebbe molte ripetizioni del codice. Invece, possiamo fare cose come gulp --prod
per eseguire l'attività predefinita in produzione, o gulp html --prod
per eseguire solo l'attività html
in produzione. D'altra parte, abbiamo visto in precedenza che disseminare le nostre pipeline Gulp con istruzioni come .pipe(cond(!PROD, livereload()))
non è il più leggibile. Alla fine, è una questione di preferenza se si desidera utilizzare l'approccio variabile booleano o impostare due gulpfile separati.
Ora vediamo cosa succede quando continuiamo a utilizzare Gulp come nostro task runner ma sostituiamo Browserify con Webpack.
Configurazione Gulp + Webpack
Improvvisamente il nostro gulpfile è lungo solo 99 righe con 12 importazioni, una bella riduzione rispetto alla nostra configurazione precedente! Se controlliamo l'attività predefinita:
gulp.task('default', (cb) => { runSequence('lint', 'test', 'build', 'server', 'watch', cb); });
Ora la nostra configurazione completa dell'app Web richiede solo cinque attività anziché nove, un notevole miglioramento.
Inoltre, abbiamo eliminato la necessità di livereload
. Il nostro compito watch
ora è semplicemente:
gulp.task('watch', () => { gulp.watch(config.paths.js, () => { runSequence('lint', 'test'); }); });
Ciò significa che il nostro Gulp Watcher non sta attivando alcun tipo di comportamento di ricomposizione. Come bonus aggiuntivo, non abbiamo più bisogno di trasferire index.html
app
a dist
o build
.
Riportando il nostro focus sulla riduzione delle attività, le nostre attività html
, css
, js
e fonts
sono state tutte sostituite da un'unica attività di build
:
gulp.task('build', () => { runSequence('clean', 'html'); return gulp.src(config.paths.entry) .pipe(webpack(require('./webpack.config'))) .pipe(gulp.dest(config.paths.baseDir)); });
Abbastanza semplice. Esegui le attività clean
e html
in sequenza. Una volta completati, prendi il nostro punto di ingresso, invialo tramite Webpack, passando un file webpack.config.js
per configurarlo e invia il bundle risultante alla nostra baseDir
( dist
o build
, a seconda dell'env del nodo).
Diamo un'occhiata al file di configurazione di Webpack:
webpack.config.js
Questo è un file di configurazione piuttosto grande e intimidatorio, quindi spieghiamo alcune delle proprietà importanti impostate sul nostro oggetto module.exports
.
devtool: PROD ? 'source-map' : 'eval-source-map',
Questo imposta il tipo di mappe dei sorgenti che Webpack utilizzerà. Non solo Webpack supporta le mappe dei sorgenti immediatamente, in realtà supporta un'ampia gamma di opzioni per le mappe dei sorgenti. Ciascuna opzione fornisce un diverso equilibrio tra i dettagli della mappa sorgente e la velocità di ricostruzione (il tempo impiegato per ricomporre le modifiche). Ciò significa che possiamo utilizzare un'opzione sourcemap "economica" per lo sviluppo per ottenere ricaricamenti veloci e un'opzione sourcemap più costosa in produzione.
entry: PROD ? './app/index' : [ 'webpack-hot-middleware/client?reload=true', // reloads the page if hot module reloading fails. './app/index' ]
Questo è il nostro punto di ingresso del pacchetto. Si noti che viene passato un array, il che significa che è possibile avere più punti di ingresso. In questo caso, abbiamo il nostro punto di ingresso previsto app/index.js
, nonché il punto di ingresso webpack-hot-middleware
che viene utilizzato come parte della nostra configurazione di ricaricamento del modulo hot.
output: { path: PROD ? __dirname + '/build' : __dirname + '/dist', publicPath: '/', filename: 'bundle.js' },
Qui è dove verrà prodotto il pacchetto compilato. L'opzione più confusa è publicPath
. Imposta l'URL di base per la posizione in cui il tuo pacchetto sarà ospitato sul server. Quindi, ad esempio, se il tuo publicPath
è /public/assets
, il pacchetto apparirà in /public/assets/bundle.js
sul server.
devServer: { contentBase: PROD ? './build' : './app' }
Questo dice al server quale cartella nel tuo progetto usare come directory principale del server.
Se ti confondi sul modo in cui Webpack sta mappando il pacchetto creato nel tuo progetto al pacchetto sul server, ricorda semplicemente quanto segue:
-
path
+filename
: la posizione esatta del pacchetto nel codice sorgente del progetto -
contentBase
(come root,/
) +publicPath
: la posizione del bundle sul server
plugins: PROD ? [ new webpack.optimize.OccurenceOrderPlugin(), new webpack.DefinePlugin(GLOBALS), new ExtractTextPlugin('bundle.css'), new webpack.optimize.DedupePlugin(), new webpack.optimize.UglifyJsPlugin({compress: {warnings: false}}) ] : [ new webpack.HotModuleReplacementPlugin(), new webpack.NoErrorsPlugin() ],
Questi sono plugin che migliorano in qualche modo la funzionalità di Webpack. Ad esempio, webpack.optimize.UglifyJsPlugin
è responsabile della minimizzazione.
loaders: [ {test: /\.js$/, include: path.join(__dirname, 'app'), loaders: ['babel']}, { test: /\.css$/, loader: PROD ? ExtractTextPlugin.extract('style', 'css?sourceMap'): 'style!css?sourceMap' }, { test: /\.scss$/, loader: PROD ? ExtractTextPlugin.extract('style', 'css?sourceMap!resolve-url!sass?sourceMap') : 'style!css?sourceMap!resolve-url!sass?sourceMap' }, {test: /\.(svg|png|jpe?g|gif)(\?\S*)?$/, loader: 'url?limit=100000&name=img/[name].[ext]'}, {test: /\.(eot|woff|woff2|ttf)(\?\S*)?$/, loader: 'url?limit=100000&name=fonts/[name].[ext]'} ]
Questi sono caricatori. Essenzialmente, pre-elaborano i file che vengono caricati tramite le istruzioni require()
. Sono in qualche modo simili ai tubi Gulp in quanto puoi concatenare i caricatori insieme.
Esaminiamo uno dei nostri oggetti caricatore:
{test: /\.scss$/, loader: 'style!css?sourceMap!resolve-url!sass?sourceMap'}
La proprietà test
indica a Webpack che il caricatore specificato si applica se un file corrisponde al modello regex fornito, in questo caso /\.scss$/
. La proprietà del loader
corrisponde all'azione eseguita dal caricatore. Qui stiamo concatenando i caricatori style
, css
, resolve-url
e sass
, che vengono eseguiti in ordine inverso.
Devo ammettere che non trovo la loader3!loader2!loader1
molto elegante. Dopotutto, quando mai devi leggere qualcosa in un programma da destra a sinistra? Nonostante ciò, i caricatori sono una funzionalità molto potente di webpack. In effetti, il caricatore che ho appena menzionato ci consente di importare file SASS direttamente nel nostro JavaScript! Ad esempio, possiamo importare il nostro fornitore e i fogli di stile globali nel nostro file entrypoint:
index.js
import React from 'react'; import {render} from 'react-dom'; import {Router, browserHistory} from 'react-router'; import routes from './routes'; // CSS imports import '../node_modules/bootstrap/dist/css/bootstrap.css'; import '../node_modules/font-awesome/css/font-awesome.css'; import './styles/global.scss'; render(<Router history={browserHistory} routes={routes} />, document.getElementById('app'));
Allo stesso modo, nel nostro componente Header possiamo aggiungere import './Header.scss'
per importare il foglio di stile associato al componente. Questo vale anche per tutti gli altri nostri componenti.
A mio parere, questo può quasi essere considerato un cambiamento rivoluzionario nel mondo dello sviluppo JavaScript. Non è necessario preoccuparsi del raggruppamento, della minimizzazione o delle mappe dei sorgenti CSS poiché il nostro caricatore gestisce tutto questo per noi. Anche il ricaricamento dei moduli a caldo funziona per i nostri file CSS. Quindi essere in grado di gestire le importazioni JS e CSS nello stesso file rende lo sviluppo concettualmente più semplice: più coerenza, meno cambio di contesto e più facile ragionare.
Per fornire un breve riassunto di come funziona questa funzione: Webpack integra il CSS nel nostro bundle JS. In effetti, Webpack può farlo anche per immagini e caratteri:
{test: /\.(svg|png|jpe?g|gif)(\?\S*)?$/, loader: 'url?limit=100000&name=img/[name].[ext]'}, {test: /\.(eot|woff|woff2|ttf)(\?\S*)?$/, loader: 'url?limit=100000&name=fonts/[name].[ext]'}
Il caricatore di URL indica a Webpack di incorporare le nostre immagini e i nostri caratteri come URL di dati se sono inferiori a 100 KB, altrimenti servirli come file separati. Naturalmente, possiamo anche configurare la dimensione del taglio su un valore diverso, ad esempio 10 KB.
E questa è la configurazione di Webpack in poche parole. Devo ammettere che c'è una discreta quantità di configurazione, ma i vantaggi dell'utilizzo sono semplicemente fenomenali. Sebbene Browserify abbia plug-in e trasformazioni, semplicemente non possono essere paragonati ai caricatori Webpack in termini di funzionalità aggiuntive.
Configurazione degli script Webpack + NPM
In questa configurazione, utilizziamo direttamente gli script npm invece di fare affidamento su un file gulp per automatizzare le nostre attività.
pacchetto.json
"scripts": { "start": "npm-run-all --parallel lint:watch test:watch build", "start:prod": "npm-run-all --parallel lint test build:prod", "clean-dist": "rimraf ./dist && mkdir dist", "clean-build": "rimraf ./build && mkdir build", "clean": "npm-run-all clean-dist clean-build", "test": "mocha ./app/**/*.test.js --compilers js:babel-core/register", "test:watch": "npm run test -- --watch", "lint": "esw ./app/**/*.js", "lint:watch": "npm run lint -- --watch", "server": "nodemon server.js", "server:prod": "cross-env NODE_ENV=production nodemon server.js", "build-html": "node tools/buildHtml.js", "build-html:prod": "cross-env NODE_ENV=production node tools/buildHtml.js", "prebuild": "npm-run-all clean-dist build-html", "build": "webpack", "postbuild": "npm run server", "prebuild:prod": "npm-run-all clean-build build-html:prod", "build:prod": "cross-env NODE_ENV=production webpack", "postbuild:prod": "npm run server:prod" }
Per eseguire build di sviluppo e produzione, immettere npm start
e npm run start:prod
.
Questo è sicuramente più compatto del nostro gulpfile, dato che abbiamo ridotto da 99 a 150 righe di codice fino a 19 script NPM, o 12 se escludiamo gli script di produzione (la maggior parte dei quali rispecchia semplicemente gli script di sviluppo con l'ambiente del nodo impostato su produzione ). Lo svantaggio è che questi comandi sono alquanto criptici rispetto alle nostre controparti di attività Gulp e non altrettanto espressivi. Ad esempio, non c'è modo (almeno che io sappia) per fare in modo che un singolo script npm esegua determinati comandi in serie e altri in parallelo. O è l'uno o l'altro.
Tuttavia, c'è un enorme vantaggio in questo approccio. Utilizzando le librerie NPM come mocha
direttamente dalla riga di comando, non è necessario installare un wrapper Gulp equivalente per ciascuna (in questo caso, gulp-mocha
).
Invece dell'installazione di NPM
- gulp-eslint
- sorso-moka
- Gulp-nodemon
- eccetera
Installiamo i seguenti pacchetti:
- eslint
- moka
- nodomon
- eccetera
Citando il post di Cory House, Perché ho lasciato Gulp e Grunt per gli script NPM :
Ero un grande fan di Gulp. Ma nel mio ultimo progetto, ho finito con centinaia di righe nel mio gulpfile e una dozzina di plugin Gulp. Stavo lottando per integrare Webpack, Browsersync, hot reloading, Mocha e molto altro usando Gulp. Come mai? Bene, alcuni plugin avevano una documentazione insufficiente per il mio caso d'uso. Alcuni plugin hanno esposto solo una parte dell'API di cui avevo bisogno. Uno aveva uno strano bug in cui guardava solo un piccolo numero di file. Un altro ha spogliato i colori durante l'output sulla riga di comando.
Specifica tre problemi principali con Gulp:
- Dipendenza dagli autori dei plugin
- Frustrante per il debug
- Documentazione disgiunta
Tenderei ad essere d'accordo con tutti questi.
1. Dipendenza dagli autori dei plugin
Ogni volta che una libreria come eslint
viene aggiornata, la libreria associata gulp-eslint
necessita di un aggiornamento corrispondente. Se il manutentore della libreria perde interesse, la versione gulp della libreria perde la sincronizzazione con quella nativa. Lo stesso vale quando viene creata una nuova libreria. Se qualcuno crea una libreria xyz
e prende piede, all'improvviso hai bisogno di una libreria gulp-xyz
corrispondente per usarla nelle tue attività di gulp.
In un certo senso, questo approccio semplicemente non è scalabile. Idealmente, vorremmo un approccio come Gulp che può utilizzare le librerie native.
2. Frustrante per il debug
Sebbene librerie come gulp-plumber
aiutino ad alleviare considerevolmente questo problema, è comunque vero che la segnalazione degli errori in gulp
non è molto utile. Se anche una pipe genera un'eccezione non gestita, ottieni una traccia dello stack per un problema che sembra completamente estraneo alla causa del problema nel codice sorgente. Questo può rendere il debug un incubo in alcuni casi. Nessuna quantità di ricerca su Google o Stack Overflow può davvero aiutarti se l'errore è criptico o abbastanza fuorviante.
3. Documentazione disgiunta
Spesso trovo che le piccole librerie di gulp
tendano ad avere una documentazione molto limitata. Sospetto che ciò sia dovuto al fatto che l'autore di solito crea la libreria principalmente per il proprio uso. Inoltre, è comune dover guardare la documentazione sia per il plug-in Gulp che per la libreria nativa stessa, il che significa molti cambi di contesto e il doppio delle letture da fare.
Conclusione
Mi sembra abbastanza chiaro che Webpack è preferibile a Browserify e gli script NPM sono preferibili a Gulp, sebbene ogni opzione abbia i suoi vantaggi e svantaggi. Gulp è sicuramente più espressivo e comodo da usare rispetto agli script NPM, ma ne paghi il prezzo in tutta l'astrazione aggiunta.
Non tutte le combinazioni potrebbero essere perfette per la tua app, ma se vuoi evitare un numero schiacciante di dipendenze di sviluppo e un'esperienza di debug frustrante, Webpack con script NPM è la strada da percorrere. Spero che troverai questo articolo utile nella scelta degli strumenti giusti per il tuo prossimo progetto.
- Mantenere il controllo: una guida a Webpack e reagire, pt. 1
- Gulp Under the Hood: creazione di uno strumento di automazione delle attività basato sul flusso