Webpack sau Browserify & Gulp: care este mai bun?
Publicat: 2022-03-11Pe măsură ce aplicațiile web devin din ce în ce mai complexe, a face aplicația dvs. web scalabilă devine de cea mai mare importanță. În timp ce în trecut, scrierea ad-hoc JavaScript și jQuery era suficientă, în zilele noastre construirea unei aplicații web necesită un grad mult mai mare de disciplină și practici formale de dezvoltare software, cum ar fi:
- Teste unitare pentru a se asigura că modificările aduse codului dvs. nu încalcă funcționalitatea existentă
- Litting pentru a asigura un stil de codare consecvent fără erori
- Build-uri de producție care diferă de build-urile de dezvoltare
Web-ul oferă, de asemenea, unele dintre propriile provocări unice de dezvoltare. De exemplu, deoarece paginile web fac o mulțime de solicitări asincrone, performanța aplicației dvs. web poate fi semnificativ degradată de la necesitatea de a solicita sute de fișiere JS și CSS, fiecare cu propriile lor supraîncărcări (anteturi, strângeri de mână și așa mai departe). Această problemă specială poate fi rezolvată adesea prin gruparea fișierelor împreună, astfel încât solicitați doar un singur fișier JS și CSS grupat, mai degrabă decât sute de fișiere individuale.
Este, de asemenea, destul de obișnuit să folosiți preprocesoare de limbaj, cum ar fi SASS și JSX, care se compilează în JS și CSS nativ, precum și transpilere JS, cum ar fi Babel, pentru a beneficia de codul ES6, păstrând în același timp compatibilitatea ES5.
Aceasta înseamnă un număr semnificativ de sarcini care nu au nimic de-a face cu scrierea logicii aplicației web în sine. Aici intervin cei care rulează sarcinile. Scopul unui alergător de sarcini este de a automatiza toate aceste sarcini, astfel încât să puteți beneficia de un mediu de dezvoltare îmbunătățit, concentrându-vă în același timp pe scrierea aplicației. Odată ce rulerul de sarcini este configurat, tot ce trebuie să faceți este să invocați o singură comandă într-un terminal.
Voi folosi Gulp ca un task runner, deoarece este foarte prietenos cu dezvoltatorii, ușor de învățat și ușor de înțeles.
O introducere rapidă în Gulp
API-ul Gulp constă din patru funcții:
-
gulp.src
-
gulp.dest
-
gulp.task
-
gulp.watch
Iată, de exemplu, un exemplu de sarcină care utilizează trei dintre aceste patru funcții:
gulp.task('my-first-task', function() { gulp.src('/public/js/**/*.js') .pipe(concat()) .pipe(minify()) .pipe(gulp.dest('build')) });
Când este efectuată my-first-task
, toate fișierele care se potrivesc cu modelul glob /public/js/**/*.js
sunt reduse și apoi transferate într build
un folder de compilare.
Frumusețea acestui lucru este în .pipe()
. Luați un set de fișiere de intrare, le treceți printr-o serie de transformări, apoi returnați fișierele de ieșire. Pentru a face lucrurile și mai convenabile, transformările actuale ale conductelor, cum ar fi minify()
, sunt adesea efectuate de bibliotecile NPM. Ca rezultat, este foarte rar în practică să aveți nevoie să scrieți propriile transformări dincolo de redenumirea fișierelor în conductă.
Următorul pas pentru a înțelege Gulp este înțelegerea gamei de dependențe de sarcini.
gulp.task('my-second-task', ['lint', 'bundle'], function() { ... });
Aici, my-second-task
rulează funcția de apel invers numai după ce sarcinile de lint
și bundle
sunt finalizate. Acest lucru permite separarea preocupărilor: creați o serie de sarcini mici cu o singură responsabilitate, cum ar fi conversia LESS
în CSS
și creați un fel de sarcină principală care apelează pur și simplu toate celelalte sarcini prin intermediul seriei de dependențe de sarcini.
În cele din urmă, avem gulp.watch
, care urmărește un model de fișier global pentru modificări și, atunci când este detectată o modificare, rulează o serie de sarcini.
gulp.task('my-third-task', function() { gulp.watch('/public/js/**/*.js', ['lint', 'reload']) })
În exemplul de mai sus, orice modificare a unui fișier care se potrivește cu /public/js/**/*.js
ar declanșa sarcina de lint
și reload
. O utilizare obișnuită a gulp.watch
este de a declanșa reîncărcările live în browser, o caracteristică atât de utilă pentru dezvoltare încât nu vei mai putea trăi fără ea odată ce ai experimentat-o.
Și chiar așa, înțelegi tot ce trebuie să știi despre gulp
.
Unde se încadrează Webpack?
Când utilizați modelul CommonJS, gruparea fișierelor JavaScript nu este la fel de simplă precum concatenarea lor. Mai degrabă, aveți un punct de intrare (numit de obicei index.js
sau app.js
) cu o serie de instrucțiuni require
sau import
în partea de sus a fișierului:
ES5
var Component1 = require('./components/Component1'); var Component2 = require('./components/Component2');
ES6
import Component1 from './components/Component1'; import Component2 from './components/Component2';
Dependențele trebuie rezolvate înainte de codul rămas în app.js
, iar aceste dependențe pot avea ele însele alte dependențe de rezolvat. În plus, este posibil să aveți require
de aceeași dependență în mai multe locuri din aplicația dvs., dar doriți să rezolvați această dependență o singură dată. După cum vă puteți imagina, odată ce aveți un arbore de dependență la câteva niveluri adânc, procesul de grupare JavaScript devine destul de complex. Aici intervin pachetele precum Browserify și Webpack.
De ce folosesc dezvoltatorii Webpack în loc de Gulp?
Webpack este un bundler, în timp ce Gulp este un rulant de sarcini, așa că v-ați aștepta să vedeți aceste două instrumente utilizate în mod obișnuit împreună. În schimb, există o tendință în creștere, în special în rândul comunității React, de a folosi Webpack în loc de Gulp. De ce asta?
Simplu spus, Webpack este un instrument atât de puternic încât poate deja să îndeplinească marea majoritate a sarcinilor pe care altfel le-ați face printr-un task runner. De exemplu, Webpack oferă deja opțiuni pentru minificare și hărți sursă pentru pachetul dvs. În plus, Webpack poate fi rulat ca middleware printr-un server personalizat numit webpack-dev-server
, care acceptă atât reîncărcarea live, cât și reîncărcarea la cald (vom vorbi despre aceste caracteristici mai târziu). Folosind încărcătoare, puteți adăuga, de asemenea, transpilarea ES6 la ES5 și pre- și post-procesoare CSS. Asta lasă doar testele unitare și lining-ul ca sarcini majore pe care Webpack nu le poate gestiona independent. Având în vedere că am redus cel puțin o jumătate de duzină de sarcini potențiale gulp la două, mulți dezvoltatori optează să folosească în schimb scripturile NPM direct, deoarece acest lucru evită suprasolicitarea adăugării Gulp la proiect (despre care vom vorbi și mai târziu) .
Dezavantajul major al utilizării Webpack este că este destul de dificil de configurat, ceea ce este neatractiv dacă încercați să puneți rapid un proiect în funcțiune.
Cele 3 configurații ale noastre Task Runner
Voi înființa un proiect cu trei setări diferite de rulare de sarcini. Fiecare configurare va îndeplini următoarele sarcini:
- Configurați un server de dezvoltare cu reîncărcare live pe modificările fișierelor urmărite
- Grupați fișierele noastre JS și CSS (inclusiv transpilarea ES6 la ES5, conversia SASS în CSS și hărți sursă) într-o manieră scalabilă în funcție de modificările fișierelor urmărite
- Rulați teste unitare fie ca sarcină independentă, fie în modul ceas
- Rulați lining fie ca sarcină independentă, fie în modul ceas
- Oferiți capacitatea de a executa toate cele de mai sus printr-o singură comandă în terminal
- Aveți o altă comandă pentru a crea un pachet de producție cu minificare și alte optimizări
Cele trei configurații ale noastre vor fi:
- Gulp + Browserify
- Gulp + Webpack
- Webpack + Scripturi NPM
Aplicația va folosi React pentru front-end. Inițial, am vrut să folosesc o abordare agnostică a cadrului, dar utilizarea React simplifică de fapt responsabilitățile care rulează sarcinile, deoarece este nevoie de un singur fișier HTML, iar React funcționează foarte bine cu modelul CommonJS.
Vom acoperi beneficiile și dezavantajele fiecărei configurații, astfel încât să puteți lua o decizie informată cu privire la tipul de configurație care se potrivește cel mai bine nevoilor proiectului dumneavoastră.
Am configurat un depozit Git cu trei ramuri, câte una pentru fiecare abordare (link). Testarea fiecărei configurații este la fel de simplă ca:
git checkout <branch name> npm prune (optional) npm install gulp (or npm start, depending on the setup)
Să examinăm în detaliu codul din fiecare ramură...
Cod comun
Structura folderului
- app - components - fonts - styles - index.html - index.js - index.test.js - routes.js
index.html
Un fișier HTML simplu. Aplicația React este încărcată în <div></div>
și folosim doar un singur fișier JS și CSS. De fapt, în configurația noastră de dezvoltare Webpack, nici măcar nu vom avea nevoie de bundle.css
.
index.js
Acesta acționează ca punct de intrare JS al aplicației noastre. În esență, încărcăm React Router în app
div
cu id pe care am menționat-o mai devreme.
rute.js
Acest fișier definește rutele noastre. Adresele URL /
, /about
și /contact
sunt mapate la componentele HomePage
, AboutPage
și, respectiv, ContactPage
.
index.test.js
Aceasta este o serie de teste unitare care testează comportamentul JavaScript nativ. Într-o aplicație de calitate reală a producției, ați scrie un test unitar pentru fiecare componentă React (cel puțin cele care manipulează starea), testând comportamentul specific React. Cu toate acestea, în scopul acestei postări, este suficient să aveți pur și simplu un test unitar funcțional care poate rula în modul ceas.
componente/App.js
Acesta poate fi considerat ca fiind containerul pentru toate vizualizările paginii noastre. Fiecare pagină conține o componentă <Header/>
, precum și this.props.children
, care evaluează la vizualizarea paginii în sine (ex/ ContactPage
dacă la /contact
în browser).
componente/home/HomePage.js
Aceasta este punctul nostru de vedere acasă. Am ales să folosesc react-bootstrap
deoarece sistemul de grilă al bootstrap este excelent pentru a crea pagini receptive. Cu utilizarea corectă a bootstrap, numărul de interogări media pe care trebuie să le scrieți pentru ferestre de vizualizare mai mici este redus dramatic.
Componentele rămase ( Header
, AboutPage
, ContactPage
) sunt structurate în mod similar ( react-bootstrap
markup, fără manipulare de stare).
Acum să vorbim mai multe despre styling.
Abordarea stilului CSS
Abordarea mea preferată pentru stilarea componentelor React este să aibă o foaie de stil pe componentă, ale cărei stiluri sunt aplicate doar pentru acea componentă specifică. Veți observa că în fiecare dintre componentele mele React, div
-ul de nivel superior are un nume de clasă care se potrivește cu numele componentei. Deci, de exemplu, HomePage.js
are marcajul împachetat de:
<div className="HomePage"> ... </div>
Există, de asemenea, un fișier HomePage.scss
asociat, care este structurat după cum urmează:
@import '../../styles/variables'; .HomePage { // Content here }
De ce este această abordare atât de utilă? Rezultă un CSS extrem de modular, eliminând în mare măsură problema comportamentului în cascadă nedorit.
Să presupunem că avem două componente React, Component1
și Component2
. În ambele cazuri, dorim să suprascriem dimensiunea fontului h2
.
/* Component1.scss */ .Component1 { h2 { font-size: 30px; } } /* Component2.scss */ .Component2 { h2 { font-size: 60px; } }
Dimensiunea fontului h2
pentru Component1
și Component2
este independentă dacă componentele sunt adiacente sau dacă o componentă este imbricată în cealaltă. În mod ideal, aceasta înseamnă că stilul unei componente este complet autonom, ceea ce înseamnă că componenta va arăta exact la fel, indiferent de locul în care este plasată în marcaj. În realitate, nu este întotdeauna atât de simplu, dar cu siguranță este un pas uriaș în direcția corectă.
Pe lângă stilurile pe componentă, îmi place să am un folder de styles
care să conțină o foaie de stil globală global.scss
, împreună cu parțiale SASS care se ocupă de o anumită responsabilitate (în acest caz, _fonts.scss
și, respectiv, _variables.scss
pentru fonturi și variabile). ). Foaia de stil globală ne permite să definim aspectul general al întregii aplicații, în timp ce părțile de ajutor pot fi importate de foile de stil pentru fiecare componentă, după cum este necesar.
Acum că codul comun din fiecare ramură a fost explorat în profunzime, haideți să ne concentrăm asupra primei abordări de rulare de sarcini / grupare.
Configurare Gulp + Browserify
gulpfile.js
Aceasta rezultă într-un fișier gulp surprinzător de mare, cu 22 de importuri și 150 de linii de cod. Deci, de dragul conciziei, voi revizui în detaliu doar sarcinile js
, css
, server
, watch
și default
.
Pachetul 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)); }
Această abordare este destul de urâtă din mai multe motive. În primul rând, sarcina este împărțită în trei părți separate. În primul rând, creați obiectul pachetului Browserify b
, introducând unele opțiuni și definind niște handlere de evenimente. Apoi aveți însăși sarcina Gulp, care trebuie să treacă o funcție numită ca apel invers, în loc să o introducă (deoarece b.on('update')
folosește același callback). Acest lucru nu are eleganța unei sarcini Gulp în care doar treceți într-un gulp.src
și efectuați câteva modificări.
O altă problemă este că acest lucru ne obligă să avem abordări diferite pentru reîncărcarea html
, css
și js
în browser. Privind sarcina noastră de 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'); }); });
Când un fișier HTML este modificat, sarcina html
este reluată.
gulp.task('html', () => { return gulp.src(config.paths.html) .pipe(gulp.dest(config.paths.baseDir)) .pipe(cond(!PROD, livereload())); });
Ultima conductă apelează livereload()
dacă NODE_ENV
nu este production
, ceea ce declanșează o reîmprospătare în browser.
Aceeași logică este folosită pentru ceasul CSS. Când un fișier CSS este modificat, sarcina css
este reluată, iar ultima conductă din sarcina css
declanșează livereload()
și reîmprospătează browserul.
Cu toate acestea, ceasul js
nu apelează deloc sarcina js
. În schimb, handlerul de evenimente al b.on('update', bundle)
se ocupă de reîncărcare folosind o abordare complet diferită (și anume, înlocuirea la cald a modulului). Inconsecvența în această abordare este iritante, dar, din păcate, necesară pentru a avea versiuni incrementale . Dacă am apelat naiv doar livereload()
la sfârșitul funcției bundle
, aceasta ar reconstrui întregul pachet JS la orice modificare individuală a fișierului JS. O astfel de abordare, evident, nu se extinde. Cu cât aveți mai multe fișiere JS, cu atât durează mai mult fiecare regrupare. Brusc, regrupările de 500 ms încep să dureze 30 de secunde, ceea ce inhibă într-adevăr dezvoltarea agilă.
Pachet 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())); });
Prima problemă aici este includerea greoaie a CSS a furnizorului. Ori de câte ori un nou fișier CSS de furnizor este adăugat la proiect, trebuie să ne amintim să ne schimbăm fișierul gulp pentru a adăuga un element la matricea gulp.src
, mai degrabă decât să adăugăm importul într-un loc relevant în codul nostru sursă real.
Cealaltă problemă principală este logica complicată din fiecare conductă. A trebuit să adaug o bibliotecă NPM numită gulp-cond
doar pentru a configura logica condiționată în conductele mele, iar rezultatul final nu este prea ușor de citit (paranteze triple peste tot!).
Sarcina serverului
gulp.task('server', () => { nodemon({ script: 'server.js' }); });
Această sarcină este foarte simplă. Este, în esență, un înveliș în jurul invocării în linia de comandă nodemon server.js
, care rulează server.js
într-un mediu nod. nodemon
este folosit în loc de node
, astfel încât orice modificări aduse fișierului să-l facă să repornească. În mod implicit, nodemon
ar reporni procesul de rulare la orice modificare a fișierului JS, motiv pentru care este important să includeți un fișier nodemon.json
pentru a limita domeniul de aplicare al acestuia:
{ "watch": "server.js" }
Să revizuim codul serverului nostru.
server.js
const baseDir = process.env.NODE_ENV === 'production' ? 'build' : 'dist'; const port = process.env.NODE_ENV === 'production' ? 8080: 3000; const app = express();
Aceasta setează directorul de bază al serverului și portul pe baza mediului nod și creează o instanță de expres.
app.use(require('connect-livereload')({port: 35729})); app.use(express.static(path.join(__dirname, baseDir)));
Aceasta adaugă middleware connect-livereload
(necesar pentru configurarea noastră de reîncărcare live) și middleware static (necesar pentru gestionarea activelor noastre statice).
app.get('/api/sample-route', (req, res) => { res.send({ website: 'Toptal', blogPost: true }); });
Aceasta este doar o rută API simplă. Dacă navigați la localhost:3000/api/sample-route
în browser, veți vedea:

{ website: "Toptal", blogPost: true }
Într-un backend real, ai avea un folder întreg dedicat rutelor API, fișiere separate pentru stabilirea conexiunilor DB și așa mai departe. Acest exemplu de traseu a fost inclus doar pentru a arăta că putem construi cu ușurință un backend peste front-end-ul pe care l-am configurat.
app.get('*', (req, res) => { res.sendFile(path.join(__dirname, './', baseDir ,'/index.html')); });
Aceasta este o rută universală, ceea ce înseamnă că indiferent de adresa URL introdusă în browser, serverul va returna singura noastră pagină index.html
. Apoi, este responsabilitatea React Router să rezolve rutele noastre din partea clientului.
app.listen(port, () => { open(`http://localhost:${port}`); });
Aceasta îi spune instanței noastre expres să asculte portul pe care l-am specificat și să deschidă browserul într-o filă nouă la adresa URL specificată.
Până acum, singurul lucru care nu îmi place la configurarea serverului este:
app.use(require('connect-livereload')({port: 35729}));
Având în vedere că folosim deja gulp-livereload
în fișierul nostru gulp, acest lucru face două locuri separate în care trebuie utilizat livereload.
Acum, nu în ultimul rând:
Sarcină implicită
gulp.task('default', (cb) => { runSequence('clean', 'lint', 'test', 'html', 'css', 'js', 'fonts', 'server', 'watch', cb); });
Aceasta este sarcina care rulează atunci când tastați pur și simplu gulp
în terminal. O ciudățenie este necesitatea de a utiliza runSequence
pentru a face ca sarcinile să ruleze secvenţial. În mod normal, o serie de sarcini sunt executate în paralel, dar acesta nu este întotdeauna comportamentul dorit. De exemplu, trebuie să rulăm sarcina de clean
înainte de html
pentru a ne asigura că folderele noastre de destinație sunt goale înainte de a muta fișierele în ele. Când gulp 4 este lansat, va suporta metodele gulp.series
și gulp.parallel
în mod nativ, dar deocamdată trebuie să plecăm cu această ușoară ciudație în configurarea noastră.
Dincolo de asta, acesta este de fapt destul de elegant. Întreaga creare și găzduire a aplicației noastre sunt efectuate într-o singură comandă, iar înțelegerea oricărei părți a fluxului de lucru este la fel de simplă ca examinarea unei sarcini individuale în secvența de rulare. În plus, putem împărți întreaga secvență în bucăți mai mici pentru o abordare mai granulară a creării și găzduirii aplicației. De exemplu, am putea configura o sarcină separată numită validate
care rulează sarcinile de lint
și test
. Sau am putea avea o sarcină host
care rulează server
și watch
. Această capacitate de a orchestra sarcini este foarte puternică, mai ales că aplicația dvs. se scalează și necesită sarcini mai automate.
Dezvoltare vs Construcții de producție
if (argv.prod) { process.env.NODE_ENV = 'production'; } let PROD = process.env.NODE_ENV === 'production';
Folosind biblioteca yargs
NPM, putem furniza steaguri de linie de comandă lui Gulp. Aici instruiesc fișierul gulp să seteze mediul nodului la producție dacă --prod
este transmis la gulp
în terminal. Variabila noastră PROD
este apoi folosită ca o condiție pentru a diferenția comportamentul de dezvoltare și producție în fișierul nostru gulp. De exemplu, una dintre opțiunile pe care le transmitem configurației noastre browserify
este:
plugin: PROD ? [] : [hmr, watchify]
Acest lucru îi spune browserify
să nu folosească niciun plugin în modul de producție și să folosească pluginuri hmr
și watchify
în alte medii.
Această condițională PROD
este foarte utilă, deoarece ne scutește de a trebui să scriem un fișier gulp separat pentru producție și dezvoltare, care ar conține în cele din urmă multă repetare a codului. În schimb, putem face lucruri precum gulp --prod
pentru a rula sarcina implicită în producție sau gulp html --prod
pentru a rula doar sarcina html
în producție. Pe de altă parte, am văzut mai devreme că împrăștierea conductelor noastre Gulp cu instrucțiuni precum .pipe(cond(!PROD, livereload()))
nu este cea mai lizibilă. În cele din urmă, este o chestiune de preferință dacă doriți să utilizați abordarea variabilă booleană sau să configurați două fișiere gulpfile separate.
Acum haideți să vedem ce se întâmplă când continuăm să folosim Gulp ca director de sarcini, dar înlocuim Browserify cu Webpack.
Configurare Gulp + Webpack
Dintr-o dată, fișierul nostru gulp are doar 99 de linii cu 12 importuri, o reducere semnificativă față de configurația noastră anterioară! Dacă verificăm sarcina implicită:
gulp.task('default', (cb) => { runSequence('lint', 'test', 'build', 'server', 'watch', cb); });
Acum, configurarea noastră completă a aplicației web necesită doar cinci sarcini în loc de nouă, o îmbunătățire dramatică.
În plus, am eliminat nevoia de livereload
. Sarcina noastră de watch
este acum pur și simplu:
gulp.task('watch', () => { gulp.watch(config.paths.js, () => { runSequence('lint', 'test'); }); });
Aceasta înseamnă că observatorul nostru nu declanșează niciun tip de comportament de regrupare. Ca bonus suplimentar, nu mai trebuie să transferăm index.html
din app
în dist
sau build
.
Întorcându-ne atenția asupra reducerii sarcinilor, sarcinile noastre html
, css
, js
și fonts
au fost toate înlocuite cu o singură sarcină de build
:
gulp.task('build', () => { runSequence('clean', 'html'); return gulp.src(config.paths.entry) .pipe(webpack(require('./webpack.config'))) .pipe(gulp.dest(config.paths.baseDir)); });
Destul de simplu. Rulați sarcinile de clean
și html
în secvență. Odată ce acestea sunt finalizate, luați punctul nostru de intrare, treceți-l prin Webpack, trecând un fișier webpack.config.js
pentru a-l configura și trimiteți pachetul rezultat la baseDir
-ul nostru (fie dist
, fie build
, în funcție de nodul env).
Să aruncăm o privire la fișierul de configurare Webpack:
webpack.config.js
Acesta este un fișier de configurare destul de mare și intimidant, așa că haideți să explicăm unele dintre proprietățile importante care sunt setate pe obiectul nostru module.exports
.
devtool: PROD ? 'source-map' : 'eval-source-map',
Aceasta setează tipul de hărți sursă pe care le va folosi Webpack. Nu numai că Webpack acceptă hărți sursă din cutie, ci și o gamă largă de opțiuni pentru hărți sursă. Fiecare opțiune oferă un echilibru diferit între detaliile hărții sursă și viteza de reconstrucție (timpul necesar pentru regruparea la modificări). Aceasta înseamnă că putem folosi o opțiune de hartă sursă „ieftină” pentru dezvoltare pentru a obține reîncărcări rapide și o opțiune de hartă sursă mai scumpă în producție.
entry: PROD ? './app/index' : [ 'webpack-hot-middleware/client?reload=true', // reloads the page if hot module reloading fails. './app/index' ]
Acesta este punctul nostru de intrare pentru pachet. Observați că este transmisă o matrice, ceea ce înseamnă că este posibil să aveți mai multe puncte de intrare. În acest caz, avem punctul nostru de intrare așteptat app/index.js
, precum și punctul de intrare webpack-hot-middleware
care este utilizat ca parte a configurației noastre de reîncărcare a modulului fierbinte.
output: { path: PROD ? __dirname + '/build' : __dirname + '/dist', publicPath: '/', filename: 'bundle.js' },
Aici va fi scos pachetul compilat. Cea mai confuză opțiune este publicPath
. Setează adresa URL de bază pentru unde va fi găzduit pachetul dvs. pe server. Deci, de exemplu, dacă publicPath
este /public/assets
, atunci pachetul va apărea sub /public/assets/bundle.js
pe server.
devServer: { contentBase: PROD ? './build' : './app' }
Aceasta îi spune serverului ce folder din proiectul tău să folosească ca director rădăcină al serverului.
Dacă vă confundați vreodată cu privire la modul în care Webpack mapează pachetul creat în proiectul dvs. cu pachetul de pe server, pur și simplu amintiți-vă următoarele:
-
path
+filename
: locația exactă a pachetului în codul sursă al proiectului -
contentBase
(ca rădăcină,/
) +publicPath
: locația pachetului pe 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() ],
Acestea sunt plugin-uri care îmbunătățesc într-un fel funcționalitatea Webpack-ului. De exemplu, webpack.optimize.UglifyJsPlugin
este responsabil pentru minificare.
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]'} ]
Acestea sunt încărcătoare. În esență, preprocesează fișierele care sunt încărcate prin instrucțiunile require()
. Sunt oarecum asemănătoare cu țevile Gulp prin faptul că puteți lega încărcătoarele împreună.
Să examinăm unul dintre obiectele noastre de încărcare:
{test: /\.scss$/, loader: 'style!css?sourceMap!resolve-url!sass?sourceMap'}
Proprietatea test
îi spune lui Webpack că încărcătorul dat se aplică dacă un fișier se potrivește cu modelul regex furnizat, în acest caz /\.scss$/
. Proprietatea loader
corespunde acțiunii pe care o efectuează. Aici înlănțuim încărcătoarele style
, css
, resolve-url
și sass
, care sunt executate în ordine inversă.
Trebuie să recunosc că nu mi se pare foarte elegantă sintaxa loader3!loader2!loader1
. La urma urmei, când trebuie să citești vreodată ceva într-un program de la dreapta la stânga? În ciuda acestui fapt, încărcătoarele sunt o caracteristică foarte puternică a webpack-ului. De fapt, încărcătorul pe care tocmai l-am menționat ne permite să importăm fișiere SASS direct în JavaScript! De exemplu, putem importa foile de stil ale furnizorului și globale în fișierul nostru de intrare:
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'));
În mod similar, în componenta noastră Header putem adăuga import './Header.scss'
pentru a importa foaia de stil asociată componentei. Acest lucru este valabil și pentru toate celelalte componente ale noastre.
În opinia mea, aceasta poate fi considerată aproape o schimbare revoluționară în lumea dezvoltării JavaScript. Nu este nevoie să vă faceți griji cu privire la gruparea CSS, la minimizarea sau la hărțile sursă, deoarece încărcătorul nostru se ocupă de toate acestea pentru noi. Chiar și reîncărcarea la cald a modulelor funcționează pentru fișierele noastre CSS. Apoi, capacitatea de a gestiona importurile JS și CSS în același fișier face dezvoltarea conceptuală mai simplă: mai multă consistență, mai puțină schimbare de context și mai ușor de raționat.
Pentru a oferi un scurt rezumat al modului în care funcționează această caracteristică: Webpack include CSS-ul în pachetul nostru JS. De fapt, Webpack poate face acest lucru și pentru imagini și fonturi:
{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]'}
Încărcătorul de adrese URL îi indică pe Webpack să alinieze imaginile și fonturile noastre ca adrese URL de date dacă acestea sunt sub 100 KB, altfel le servește ca fișiere separate. Desigur, putem configura și dimensiunea limită la o valoare diferită, cum ar fi 10 KB.
Și aceasta este configurația Webpack pe scurt. Voi recunoaște că există o cantitate destul de mare de configurare, dar beneficiile utilizării lui sunt pur și simplu fenomenale. Deși Browserify are pluginuri și transformări, pur și simplu nu se pot compara cu încărcătoarele Webpack în ceea ce privește funcționalitatea adăugată.
Webpack + Configurare scripturi NPM
În această configurare, folosim direct scripturi npm, în loc să ne bazăm pe un fișier gulp pentru automatizarea sarcinilor noastre.
pachet.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" }
Pentru a rula versiuni de dezvoltare și producție, introduceți npm start
și, respectiv, npm run start:prod
.
Acesta este cu siguranță mai compact decât fișierul nostru gulp, având în vedere că am redus 99 până la 150 de linii de cod la 19 scripturi NPM, sau 12 dacă excludem scripturile de producție (majoritatea dintre care doar oglindește scripturile de dezvoltare cu mediul nod setat la producție). ). Dezavantajul este că aceste comenzi sunt oarecum criptice în comparație cu omologii noștri de activitate Gulp și nu la fel de expresive. De exemplu, nu există nicio modalitate (cel puțin despre care știu eu) ca un singur script npm să ruleze anumite comenzi în serie și altele în paralel. Este fie una, fie alta.
Cu toate acestea, această abordare are un avantaj imens. Folosind biblioteci NPM, cum ar fi mocha
direct din linia de comandă, nu este nevoie să instalați un wrapper Gulp echivalent pentru fiecare (în acest caz, gulp-mocha
).
În loc de instalarea NPM
- gup-eslint
- gup-mocha
- gulp-nodemon
- etc
Instalăm următoarele pachete:
- eslint
- moca
- nodemon
- etc
Citând postarea lui Cory House, De ce am lăsat Gulp și Grunt pentru scripturile NPM :
Eram un mare fan al lui Gulp. Dar la ultimul meu proiect, am ajuns să am sute de linii în fișierul meu gulp și aproximativ o duzină de plugin-uri Gulp. M-am chinuit să integrez Webpack, Browsersync, reîncărcare la cald, Mocha și multe altele folosind Gulp. De ce? Ei bine, unele plugin-uri aveau documentație insuficientă pentru cazul meu de utilizare. Unele plugin-uri au expus doar o parte din API-ul de care aveam nevoie. Unul a avut o eroare ciudată în care ar urmări doar un număr mic de fișiere. O altă culoare a dezbrăcat la ieșire în linia de comandă.
El specifică trei probleme de bază cu Gulp:
- Dependență de autorii pluginurilor
- Frustant de depanat
- Documentație dezarticulată
Aș avea tendința să fiu de acord cu toate acestea.
1. Dependența de autorii pluginurilor
Ori de câte ori o bibliotecă precum eslint
este actualizată, biblioteca asociată gulp-eslint
are nevoie de o actualizare corespunzătoare. Dacă întreținătorul bibliotecii își pierde interesul, versiunea gulp a bibliotecii nu se sincronizează cu cea nativă. Același lucru este valabil și atunci când este creată o nouă bibliotecă. Dacă cineva creează o bibliotecă xyz
și prinde bine, atunci dintr-o dată aveți nevoie de o bibliotecă gulp-xyz
corespunzătoare pentru a o folosi în sarcinile dvs. gulp.
Într-un fel, această abordare pur și simplu nu se extinde. În mod ideal, am dori o abordare precum Gulp care să poată folosi bibliotecile native.
2. Frustrant pentru depanare
Deși bibliotecile precum gulp-plumber
ajută la atenuarea considerabil a acestei probleme, este totuși adevărat că raportarea erorilor în gulp
pur și simplu nu este de mare ajutor. Dacă chiar și o țeavă generează o excepție netratată, obțineți o urmărire a stivei pentru o problemă care pare complet fără legătură cu ceea ce cauzează problema în codul sursă. Acest lucru poate face depanarea un coșmar în unele cazuri. Nicio cantitate de căutare pe Google sau Stack Overflow nu vă poate ajuta cu adevărat dacă eroarea este criptică sau suficient de înșelătoare.
3. Documentație disjunsă
De multe ori constat că bibliotecile mici gulp
tind să aibă o documentație foarte limitată. Bănuiesc că acest lucru se datorează faptului că, de obicei, autorul face biblioteca în primul rând pentru uzul său. În plus, este obișnuit să trebuiască să te uiți la documentația atât pentru plugin-ul Gulp, cât și pentru biblioteca nativă în sine, ceea ce înseamnă multă schimbare de context și de două ori mai mult de citit.
Concluzie
Mi se pare destul de clar că Webpack este de preferat în locul Browserify, iar scripturile NPM sunt preferabile față de Gulp, deși fiecare opțiune are beneficiile și dezavantajele ei. Gulp este cu siguranță mai expresiv și mai convenabil de utilizat decât scripturile NPM, dar plătiți prețul în toată abstracția adăugată.
Nu orice combinație poate fi perfectă pentru aplicația dvs., dar dacă doriți să evitați un număr copleșitor de dependențe de dezvoltare și o experiență frustrantă de depanare, Webpack cu scripturi NPM este calea de urmat. Sper că veți găsi acest articol util în alegerea instrumentelor potrivite pentru următorul dvs. proiect.
- Menține controlul: un ghid pentru Webpack și React, Pt. 1
- Gulp Under the Hood: Construirea unui instrument de automatizare a sarcinilor bazat pe flux