Webpack ou Browserify & Gulp : quel est le meilleur ?
Publié: 2022-03-11À mesure que les applications Web deviennent de plus en plus complexes, rendre votre application Web évolutive devient de la plus haute importance. Alors que par le passé, écrire du JavaScript et du jQuery ad hoc suffisait, aujourd'hui, la création d'une application Web nécessite un degré beaucoup plus élevé de discipline et de pratiques formelles de développement de logiciels, telles que :
- Tests unitaires pour s'assurer que les modifications apportées à votre code ne cassent pas les fonctionnalités existantes
- Lunt pour assurer un style de codage cohérent sans erreurs
- Versions de production différentes des versions de développement
Le Web présente également certains de ses propres défis de développement uniques. Par exemple, étant donné que les pages Web effectuent de nombreuses requêtes asynchrones, les performances de votre application Web peuvent être considérablement dégradées si vous devez demander des centaines de fichiers JS et CSS, chacun avec sa propre surcharge (en-têtes, poignées de main, etc.). Ce problème particulier peut souvent être résolu en regroupant les fichiers, de sorte que vous ne demandez qu'un seul fichier JS et CSS groupé plutôt que des centaines de fichiers individuels.
Il est également assez courant d'utiliser des préprocesseurs de langage tels que SASS et JSX qui compilent en JS et CSS natifs, ainsi que des transpileurs JS tels que Babel, pour bénéficier du code ES6 tout en conservant la compatibilité ES5.
Cela représente un nombre important de tâches qui n'ont rien à voir avec l'écriture de la logique de l'application Web elle-même. C'est là qu'interviennent les exécuteurs de tâches. Le but d'un exécuteur de tâches est d'automatiser toutes ces tâches afin que vous puissiez bénéficier d'un environnement de développement amélioré tout en vous concentrant sur l'écriture de votre application. Une fois l'exécuteur de tâches configuré, il vous suffit d'invoquer une seule commande dans un terminal.
J'utiliserai Gulp comme exécuteur de tâches car il est très convivial pour les développeurs, facile à apprendre et facilement compréhensible.
Une introduction rapide à Gulp
L'API de Gulp se compose de quatre fonctions :
-
gulp.src
-
gulp.dest
-
gulp.task
-
gulp.watch
Voici, par exemple, un exemple de tâche qui utilise trois de ces quatre fonctions :
gulp.task('my-first-task', function() { gulp.src('/public/js/**/*.js') .pipe(concat()) .pipe(minify()) .pipe(gulp.dest('build')) });
Lorsque my-first-task
est exécutée, tous les fichiers correspondant au modèle glob /public/js/**/*.js
sont minifiés puis transférés dans un dossier de build
.
La beauté de ceci réside dans le chaînage .pipe()
. Vous prenez un ensemble de fichiers d'entrée, les dirigez vers une série de transformations, puis renvoyez les fichiers de sortie. Pour rendre les choses encore plus pratiques, les transformations de tuyauterie réelles, telles que minify()
, sont souvent effectuées par des bibliothèques NPM. Par conséquent, il est très rare en pratique que vous ayez besoin d'écrire vos propres transformations au-delà du renommage des fichiers dans le tube.
La prochaine étape pour comprendre Gulp consiste à comprendre le tableau des dépendances des tâches.
gulp.task('my-second-task', ['lint', 'bundle'], function() { ... });
Ici, my-second-task
n'exécute la fonction de rappel qu'une fois les tâches lint
et bundle
terminées. Cela permet de séparer les préoccupations : vous créez une série de petites tâches avec une seule responsabilité, comme la conversion de LESS
en CSS
, et créez une sorte de tâche principale qui appelle simplement toutes les autres tâches via le tableau des dépendances de tâches.
Enfin, nous avons gulp.watch
, qui surveille un modèle de fichier glob pour les changements, et lorsqu'un changement est détecté, exécute une série de tâches.
gulp.task('my-third-task', function() { gulp.watch('/public/js/**/*.js', ['lint', 'reload']) })
Dans l'exemple ci-dessus, toute modification apportée à un fichier correspondant à /public/js/**/*.js
déclencherait la tâche de lint
et de reload
. Une utilisation courante de gulp.watch
est de déclencher des rechargements en direct dans le navigateur, une fonctionnalité si utile pour le développement que vous ne pourrez plus vous en passer une fois que vous l'aurez expérimentée.
Et juste comme ça, vous comprenez tout ce que vous devez vraiment savoir sur gulp
.
Où Webpack s'intègre-t-il ?
Lorsque vous utilisez le modèle CommonJS, regrouper des fichiers JavaScript n'est pas aussi simple que de les concaténer. Au lieu de cela, vous avez un point d'entrée (généralement appelé index.js
ou app.js
) avec une série d'instructions require
ou import
en haut du fichier :
ES5
var Component1 = require('./components/Component1'); var Component2 = require('./components/Component2');
ES6
import Component1 from './components/Component1'; import Component2 from './components/Component2';
Les dépendances doivent être résolues avant le code restant dans app.js
, et ces dépendances peuvent elles-mêmes avoir d'autres dépendances à résoudre. En outre, vous pouvez avoir require
la même dépendance à plusieurs endroits de votre application, mais vous ne souhaitez résoudre cette dépendance qu'une seule fois. Comme vous pouvez l'imaginer, une fois que vous avez une arborescence de dépendances à quelques niveaux de profondeur, le processus de regroupement de votre JavaScript devient plutôt complexe. C'est là qu'interviennent les bundlers tels que Browserify et Webpack.
Pourquoi les développeurs utilisent-ils Webpack au lieu de Gulp ?
Webpack est un bundler alors que Gulp est un exécuteur de tâches, vous vous attendez donc à voir ces deux outils couramment utilisés ensemble. Au lieu de cela, il y a une tendance croissante, en particulier au sein de la communauté React, à utiliser Webpack au lieu de Gulp. Pourquoi est-ce?
En termes simples, Webpack est un outil si puissant qu'il peut déjà effectuer la grande majorité des tâches que vous feriez autrement via un exécuteur de tâches. Par exemple, Webpack fournit déjà des options de minification et de cartes source pour votre bundle. De plus, Webpack peut être exécuté en tant que middleware via un serveur personnalisé appelé webpack-dev-server
, qui prend en charge à la fois le rechargement en direct et le rechargement à chaud (nous parlerons de ces fonctionnalités plus tard). En utilisant des chargeurs, vous pouvez également ajouter une transpilation ES6 à ES5, ainsi que des pré- et post-processeurs CSS. Cela laisse vraiment les tests unitaires et les peluches comme des tâches majeures que Webpack ne peut pas gérer indépendamment. Étant donné que nous avons réduit au moins une demi-douzaine de tâches potentielles de gulp à deux, de nombreux développeurs choisissent plutôt d'utiliser directement les scripts NPM, car cela évite les frais généraux liés à l'ajout de Gulp au projet (dont nous parlerons également plus tard). .
L'inconvénient majeur de l'utilisation de Webpack est qu'il est plutôt difficile à configurer, ce qui n'est pas attrayant si vous essayez de mettre rapidement en place un projet.
Nos 3 configurations de coureur de tâches
Je vais mettre en place un projet avec trois configurations différentes de coureur de tâches. Chaque configuration effectuera les tâches suivantes :
- Configurer un serveur de développement avec rechargement en direct sur les modifications de fichiers surveillés
- Regroupez nos fichiers JS et CSS (y compris la transpilation ES6 vers ES5, la conversion SASS vers CSS et les cartes source) de manière évolutive sur les modifications de fichiers surveillées
- Exécutez des tests unitaires en tant que tâche autonome ou en mode veille
- Exécutez le linting en tant que tâche autonome ou en mode montre
- Fournir la possibilité d'exécuter tout ce qui précède via une seule commande dans le terminal
- Avoir une autre commande pour créer un bundle de production avec minification et autres optimisations
Nos trois configurations seront :
- Gulp + Naviguer
- Gulp + Webpack
- Webpack + scripts NPM
L'application utilisera React pour le front-end. À l'origine, je voulais utiliser une approche indépendante du framework, mais l'utilisation de React simplifie en fait les responsabilités de l'exécuteur de tâches, car un seul fichier HTML est nécessaire, et React fonctionne très bien avec le modèle CommonJS.
Nous couvrirons les avantages et les inconvénients de chaque configuration afin que vous puissiez prendre une décision éclairée sur le type de configuration qui convient le mieux aux besoins de votre projet.
J'ai configuré un référentiel Git avec trois branches, une pour chaque approche (lien). Tester chaque configuration est aussi simple que :
git checkout <branch name> npm prune (optional) npm install gulp (or npm start, depending on the setup)
Examinons en détail le code de chaque branche…
Code commun
Structure des dossiers
- app - components - fonts - styles - index.html - index.js - index.test.js - routes.js
index.html
Un fichier HTML simple. L'application React est chargée dans <div></div>
et nous n'utilisons qu'un seul fichier JS et CSS groupé. En fait, dans notre configuration de développement Webpack, nous n'aurons même pas besoin bundle.css
.
index.js
Cela agit comme le point d'entrée JS de notre application. Essentiellement, nous chargeons simplement React Router dans l' app
div
with id que nous avons mentionnée précédemment.
routes.js
Ce fichier définit nos itinéraires. Les URL /
, /about
et /contact
sont mappées aux composants HomePage
, AboutPage
et ContactPage
, respectivement.
index.test.js
Il s'agit d'une série de tests unitaires qui testent le comportement JavaScript natif. Dans une application de qualité de production réelle, vous écririez un test unitaire par composant React (au moins ceux qui manipulent l'état), testant le comportement spécifique à React. Cependant, pour les besoins de cet article, il suffit simplement d'avoir un test unitaire fonctionnel qui peut s'exécuter en mode montre.
composants/App.js
Cela peut être considéré comme le conteneur de toutes nos pages vues. Chaque page contient un composant <Header/>
ainsi que this.props.children
, qui évalue la page vue elle-même (ex/ ContactPage
si à /contact
dans le navigateur).
composants/home/HomePage.js
C'est notre point de vue d'accueil. J'ai choisi d'utiliser react-bootstrap
car le système de grille de bootstrap est excellent pour créer des pages réactives. Avec une utilisation appropriée du bootstrap, le nombre de requêtes média que vous devez écrire pour des fenêtres plus petites est considérablement réduit.
Les composants restants ( Header
, AboutPage
, ContactPage
) sont structurés de manière similaire (balisage react react-bootstrap
, pas de manipulation d'état).
Parlons maintenant plus du style.
Approche de style CSS
Mon approche préférée pour styliser les composants React consiste à avoir une feuille de style par composant, dont les styles sont conçus pour s'appliquer uniquement à ce composant spécifique. Vous remarquerez que dans chacun de mes composants React, la div
de niveau supérieur a un nom de classe correspondant au nom du composant. Ainsi, par exemple, HomePage.js
a son balisage enveloppé par :
<div className="HomePage"> ... </div>
Il existe également un fichier HomePage.scss
associé qui est structuré comme suit :
@import '../../styles/variables'; .HomePage { // Content here }
Pourquoi cette approche est-elle si utile ? Il en résulte un CSS hautement modulaire, éliminant en grande partie le problème des comportements en cascade indésirables.
Supposons que nous ayons deux composants React, Component1
et Component2
. Dans les deux cas, nous voulons remplacer la taille de police h2
.
/* Component1.scss */ .Component1 { h2 { font-size: 30px; } } /* Component2.scss */ .Component2 { h2 { font-size: 60px; } }
La taille de police h2
de Component1
et Component2
est indépendante du fait que les composants sont adjacents ou qu'un composant est imbriqué dans l'autre. Idéalement, cela signifie que le style d'un composant est complètement autonome, ce qui signifie que le composant aura exactement le même aspect, peu importe où il est placé dans votre balisage. En réalité, ce n'est pas toujours aussi simple, mais c'est certainement un grand pas dans la bonne direction.
En plus des styles par composant, j'aime avoir un dossier de styles
contenant une feuille de style globale global.scss
, ainsi que des partiels SASS qui gèrent une responsabilité spécifique (dans ce cas, _fonts.scss
et _variables.scss
pour les polices et les variables, respectivement ). La feuille de style globale nous permet de définir l'apparence générale de l'ensemble de l'application, tandis que les partiels d'assistance peuvent être importés par les feuilles de style par composant selon les besoins.
Maintenant que le code commun de chaque branche a été exploré en profondeur, concentrons-nous sur la première approche de l'exécuteur de tâches/du bundler.
Configuration de Gulp + Browserify
gulpfile.js
Cela se traduit par un fichier gulp étonnamment volumineux, avec 22 importations et 150 lignes de code. Donc, par souci de brièveté, je ne passerai en revue que les tâches js
, css
, server
, watch
et default
en détail.
Pack 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)); }
Cette approche est plutôt laide pour un certain nombre de raisons. D'une part, la tâche est divisée en trois parties distinctes. Tout d'abord, vous créez votre objet bundle Browserify b
, en passant certaines options et en définissant certains gestionnaires d'événements. Ensuite, vous avez la tâche Gulp elle-même, qui doit transmettre une fonction nommée comme rappel au lieu de l'intégrer (puisque b.on('update')
utilise ce même rappel). Cela n'a guère l'élégance d'une tâche Gulp où vous passez simplement un gulp.src
et faites quelques changements.
Un autre problème est que cela nous oblige à avoir différentes approches pour recharger html
, css
et js
dans le navigateur. En regardant notre tâche 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'); }); });
Lorsqu'un fichier HTML est modifié, la tâche html
est réexécutée.
gulp.task('html', () => { return gulp.src(config.paths.html) .pipe(gulp.dest(config.paths.baseDir)) .pipe(cond(!PROD, livereload())); });
Le dernier tuyau appelle livereload()
si le NODE_ENV
n'est pas production
, ce qui déclenche un rafraîchissement dans le navigateur.
La même logique est utilisée pour la veille CSS. Lorsqu'un fichier CSS est modifié, la tâche css
est réexécutée et le dernier canal de la tâche css
déclenche livereload()
et actualise le navigateur.
Cependant, la montre js
n'appelle pas du tout la tâche js
. Au lieu de cela, le gestionnaire d'événements de b.on('update', bundle)
gère le rechargement en utilisant une approche complètement différente (à savoir, le remplacement de module à chaud). L'incohérence de cette approche est irritante, mais malheureusement nécessaire pour avoir des builds incrémentiels . Si nous appelions naïvement livereload()
à la fin de la fonction bundle
, cela reconstruirait l' ensemble du bundle JS sur n'importe quel changement de fichier JS individuel. Une telle approche n'est évidemment pas à l'échelle. Plus vous avez de fichiers JS, plus chaque regroupement prend du temps. Soudainement, vos regroupements de 500 ms commencent à prendre 30 secondes, ce qui inhibe vraiment le développement agile.
Pack 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())); });
Le premier problème ici est l'inclusion CSS lourde du fournisseur. Chaque fois qu'un nouveau fichier CSS fournisseur est ajouté au projet, nous devons nous rappeler de modifier notre fichier gulp pour ajouter un élément au tableau gulp.src
, plutôt que d'ajouter l'importation à un endroit pertinent dans notre code source réel.
L'autre problème principal est la logique alambiquée dans chaque tuyau. J'ai dû ajouter une bibliothèque NPM appelée gulp-cond
juste pour configurer la logique conditionnelle dans mes tuyaux, et le résultat final n'est pas trop lisible (triples crochets partout !).
Tâche serveur
gulp.task('server', () => { nodemon({ script: 'server.js' }); });
Cette tâche est très simple. Il s'agit essentiellement d'un wrapper autour de l'appel de ligne de commande nodemon server.js
, qui exécute server.js
dans un environnement de nœud. nodemon
est utilisé à la place de node
afin que toute modification apportée au fichier provoque son redémarrage. Par défaut, nodemon
redémarrerait le processus en cours d'exécution sur tout changement de fichier JS, c'est pourquoi il est important d'inclure un fichier nodemon.json
pour limiter sa portée :
{ "watch": "server.js" }
Passons en revue notre code de serveur.
serveur.js
const baseDir = process.env.NODE_ENV === 'production' ? 'build' : 'dist'; const port = process.env.NODE_ENV === 'production' ? 8080: 3000; const app = express();
Cela définit le répertoire de base du serveur et le port en fonction de l'environnement du nœud, et crée une instance de express.
app.use(require('connect-livereload')({port: 35729})); app.use(express.static(path.join(__dirname, baseDir)));
Cela ajoute le middleware connect-livereload
(nécessaire pour notre configuration de rechargement en direct) et le middleware statique (nécessaire pour gérer nos actifs statiques).

app.get('/api/sample-route', (req, res) => { res.send({ website: 'Toptal', blogPost: true }); });
Il ne s'agit que d'une simple route d'API. Si vous accédez à localhost:3000/api/sample-route
dans le navigateur, vous verrez :
{ website: "Toptal", blogPost: true }
Dans un véritable backend, vous auriez un dossier entier dédié aux routes d'API, des fichiers séparés pour établir des connexions à la base de données, etc. Cet exemple de route a simplement été inclus pour montrer que nous pouvons facilement créer un backend au-dessus du frontend que nous avons mis en place.
app.get('*', (req, res) => { res.sendFile(path.join(__dirname, './', baseDir ,'/index.html')); });
Il s'agit d'un itinéraire fourre-tout, ce qui signifie que quelle que soit l'URL que vous tapez dans le navigateur, le serveur renverra notre seule page index.html
. Il est alors de la responsabilité du React Router de résoudre nos routes côté client.
app.listen(port, () => { open(`http://localhost:${port}`); });
Cela indique à notre instance express d'écouter le port que nous avons spécifié et d'ouvrir le navigateur dans un nouvel onglet à l'URL spécifiée.
Jusqu'à présent, la seule chose que je n'aime pas dans la configuration du serveur est :
app.use(require('connect-livereload')({port: 35729}));
Étant donné que nous utilisons déjà gulp-livereload
dans notre fichier gulp, cela crée deux endroits distincts où livereload doit être utilisé.
Maintenant, last but not least :
Tâche par défaut
gulp.task('default', (cb) => { runSequence('clean', 'lint', 'test', 'html', 'css', 'js', 'fonts', 'server', 'watch', cb); });
C'est la tâche qui s'exécute lorsque vous tapez simplement gulp
dans le terminal. Une bizarrerie est la nécessité d'utiliser runSequence
pour que les tâches s'exécutent de manière séquentielle. Normalement, un tableau de tâches est exécuté en parallèle, mais ce n'est pas toujours le comportement souhaité. Par exemple, nous devons exécuter la tâche de clean
avant html
pour nous assurer que nos dossiers de destination sont vides avant d'y déplacer des fichiers. Lorsque gulp 4 sera publié, il prendra en charge nativement les méthodes gulp.series
et gulp.parallel
, mais pour l'instant, nous devons partir avec cette légère bizarrerie dans notre configuration.
Au-delà de cela, c'est en fait assez élégant. L'ensemble de la création et de l'hébergement de notre application est effectué en une seule commande, et comprendre n'importe quelle partie du flux de travail est aussi simple que d'examiner une tâche individuelle dans la séquence d'exécution. De plus, nous pouvons diviser toute la séquence en plus petits morceaux pour une approche plus granulaire de la création et de l'hébergement de l'application. Par exemple, nous pourrions configurer une tâche distincte appelée validate
qui exécute les tâches lint
et test
. Ou nous pourrions avoir une tâche host
qui exécute server
et watch
. Cette capacité à orchestrer les tâches est très puissante, d'autant plus que votre application évolue et nécessite davantage de tâches automatisées.
Développement vs constructions de production
if (argv.prod) { process.env.NODE_ENV = 'production'; } let PROD = process.env.NODE_ENV === 'production';
En utilisant la bibliothèque yargs
NPM, nous pouvons fournir des drapeaux de ligne de commande à Gulp. Ici, j'ordonne au fichier gulp de définir l'environnement du nœud sur production si --prod
est passé à gulp
dans le terminal. Notre variable PROD
est ensuite utilisée comme condition pour différencier le comportement de développement et de production dans notre gulpfile. Par exemple, l'une des options que nous passons à notre configuration browserify
est :
plugin: PROD ? [] : [hmr, watchify]
Cela indique à browserify
de ne pas utiliser de plugins en mode production et d'utiliser les plugins hmr
et watchify
dans d'autres environnements.
Ce conditionnel PROD
est très utile car il nous évite d'avoir à écrire un fichier gulp séparé pour la production et le développement, qui contiendrait finalement beaucoup de répétitions de code. Au lieu de cela, nous pouvons faire des choses comme gulp --prod
pour exécuter la tâche par défaut en production, ou gulp html --prod
pour exécuter uniquement la tâche html
en production. D'un autre côté, nous avons vu précédemment que joncher nos pipelines Gulp avec des instructions telles que .pipe(cond(!PROD, livereload()))
n'est pas la plus lisible. En fin de compte, c'est une question de préférence si vous souhaitez utiliser l'approche variable booléenne ou configurer deux fichiers gulp distincts.
Voyons maintenant ce qui se passe lorsque nous continuons à utiliser Gulp comme exécuteur de tâches mais que nous remplaçons Browserify par Webpack.
Configuration de Gulp + Webpack
Soudain, notre fichier gulp ne fait que 99 lignes avec 12 importations, une réduction par rapport à notre configuration précédente ! Si nous vérifions la tâche par défaut :
gulp.task('default', (cb) => { runSequence('lint', 'test', 'build', 'server', 'watch', cb); });
Désormais, la configuration complète de notre application Web ne nécessite que cinq tâches au lieu de neuf, une amélioration spectaculaire.
De plus, nous avons éliminé le besoin de livereload
. Notre tâche de watch
est maintenant simplement :
gulp.task('watch', () => { gulp.watch(config.paths.js, () => { runSequence('lint', 'test'); }); });
Cela signifie que notre observateur de gulp ne déclenche aucun type de comportement de regroupement. En prime, nous n'avons plus besoin de transférer index.html
de l' app
à la dist
ou à build
.
Pour revenir à la réduction des tâches, nos tâches html
, css
, js
et fonts
ont toutes été remplacées par une seule tâche 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)); });
Assez simple. Exécutez les tâches de clean
et html
dans l'ordre. Une fois ceux-ci terminés, saisissez notre point d'entrée, dirigez-le via Webpack, en passant un fichier webpack.config.js
pour le configurer, et envoyez le bundle résultant à notre baseDir
( dist
ou build
, selon le nœud env).
Jetons un coup d'œil au fichier de configuration Webpack :
webpack.config.js
Il s'agit d'un fichier de configuration assez volumineux et intimidant, alors expliquons certaines des propriétés importantes définies sur notre objet module.exports
.
devtool: PROD ? 'source-map' : 'eval-source-map',
Cela définit le type de cartes source que Webpack utilisera. Non seulement Webpack prend en charge les cartes source prêtes à l'emploi, mais il prend également en charge un large éventail d'options de carte source. Chaque option fournit un équilibre différent entre les détails de la carte source et la vitesse de reconstruction (le temps nécessaire pour regrouper les modifications). Cela signifie que nous pouvons utiliser une option de carte source "bon marché" pour le développement afin d'obtenir des rechargements rapides, et une option de carte source plus coûteuse en production.
entry: PROD ? './app/index' : [ 'webpack-hot-middleware/client?reload=true', // reloads the page if hot module reloading fails. './app/index' ]
C'est notre point d'entrée de bundle. Notez qu'un tableau est passé, ce qui signifie qu'il est possible d'avoir plusieurs points d'entrée. Dans ce cas, nous avons notre point d'entrée attendu app/index.js
ainsi que le point d'entrée webpack-hot-middleware
qui est utilisé dans le cadre de notre configuration de rechargement de module à chaud.
output: { path: PROD ? __dirname + '/build' : __dirname + '/dist', publicPath: '/', filename: 'bundle.js' },
C'est là que le bundle compilé sera sorti. L'option la plus déroutante est publicPath
. Il définit l'URL de base de l'endroit où votre bundle sera hébergé sur le serveur. Ainsi, par exemple, si votre publicPath
est /public/assets
, le bundle apparaîtra sous /public/assets/bundle.js
sur le serveur.
devServer: { contentBase: PROD ? './build' : './app' }
Cela indique au serveur quel dossier de votre projet utiliser comme répertoire racine du serveur.
Si jamais vous ne comprenez pas comment Webpack mappe le bundle créé dans votre projet au bundle sur le serveur, souvenez-vous simplement de ce qui suit :
-
path
+filename
: L'emplacement exact du bundle dans le code source de votre projet -
contentBase
(en tant que root,/
) +publicPath
: L'emplacement du bundle sur le serveur
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() ],
Ce sont des plugins qui améliorent d'une manière ou d'une autre les fonctionnalités de Webpack. Par exemple, webpack.optimize.UglifyJsPlugin
est responsable de la minification.
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]'} ]
Ce sont des chargeurs. Essentiellement, ils pré-traitent les fichiers qui sont chargés via les instructions require()
. Ils sont quelque peu similaires aux tuyaux Gulp en ce sens que vous pouvez enchaîner les chargeurs ensemble.
Examinons l'un de nos objets de chargement :
{test: /\.scss$/, loader: 'style!css?sourceMap!resolve-url!sass?sourceMap'}
La propriété test
indique à Webpack que le chargeur donné s'applique si un fichier correspond au modèle regex fourni, dans ce cas /\.scss$/
. La propriété loader
correspond à l'action effectuée par le chargeur. Ici, nous enchaînons les chargeurs style
, css
, resolve-url
et sass
, qui sont exécutés dans l'ordre inverse.
Je dois admettre que je ne trouve pas la loader3!loader2!loader1
très élégante. Après tout, quand devez-vous lire quoi que ce soit dans un programme de droite à gauche ? Malgré cela, les chargeurs sont une fonctionnalité très puissante de Webpack. En fait, le chargeur dont je viens de parler nous permet d'importer des fichiers SASS directement dans notre JavaScript ! Par exemple, nous pouvons importer nos feuilles de style fournisseur et globales dans notre fichier de point d'entrée :
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'));
De même, dans notre composant Header, nous pouvons ajouter import './Header.scss'
pour importer la feuille de style associée au composant. Ceci s'applique également à tous nos autres composants.
À mon avis, cela peut presque être considéré comme un changement révolutionnaire dans le monde du développement JavaScript. Il n'y a pas besoin de s'inquiéter du regroupement CSS, de la minification ou des cartes source puisque notre chargeur gère tout cela pour nous. Même le rechargement de modules à chaud fonctionne pour nos fichiers CSS. Ensuite, être capable de gérer les importations JS et CSS dans le même fichier rend le développement plus simple sur le plan conceptuel : plus de cohérence, moins de changement de contexte et plus facile à raisonner.
Pour donner un bref résumé du fonctionnement de cette fonctionnalité : Webpack intègre le CSS dans notre bundle JS. En fait, Webpack peut également le faire pour les images et les polices :
{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]'}
Le chargeur d'URL demande à Webpack d'intégrer nos images et polices en tant qu'URL de données si elles sont inférieures à 100 Ko, sinon servez-les en tant que fichiers séparés. Bien sûr, nous pouvons également configurer la taille de coupure sur une valeur différente telle que 10 Ko.
Et c'est la configuration de Webpack en un mot. J'admets qu'il y a une bonne quantité de configuration, mais les avantages de son utilisation sont tout simplement phénoménaux. Bien que Browserify ait des plugins et des transformations, ils ne peuvent tout simplement pas se comparer aux chargeurs Webpack en termes de fonctionnalités supplémentaires.
Configuration des scripts Webpack + NPM
Dans cette configuration, nous utilisons directement les scripts npm au lieu de nous fier à un fichier gulp pour automatiser nos tâches.
package.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" }
Pour exécuter des versions de développement et de production, entrez respectivement npm start
et npm run start:prod
.
C'est certainement plus compact que notre fichier gulp, étant donné que nous avons réduit de 99 à 150 lignes de code à 19 scripts NPM, ou 12 si nous excluons les scripts de production (dont la plupart ne font que refléter les scripts de développement avec l'environnement de nœud défini sur production ). L'inconvénient est que ces commandes sont quelque peu cryptées par rapport à nos homologues de la tâche Gulp, et pas aussi expressives. Par exemple, il n'y a aucun moyen (du moins à ma connaissance) d'avoir un seul script npm pour exécuter certaines commandes en série et d'autres en parallèle. C'est soit l'un soit l'autre.
Cependant, il y a un énorme avantage à cette approche. En utilisant des bibliothèques NPM telles que mocha
directement à partir de la ligne de commande, vous n'avez pas besoin d'installer un wrapper Gulp équivalent pour chacune (dans ce cas, gulp-mocha
).
Au lieu d'installer NPM
- gulp-eslint
- gorgée de moka
- gulp-nodemon
- etc
Nous installons les packages suivants :
- eslin
- moka
- Nodemon
- etc
Citant le post de Cory House, Why I Left Gulp and Grunt for NPM Scripts :
J'étais un grand fan de Gulp. Mais sur mon dernier projet, je me suis retrouvé avec des centaines de lignes dans mon fichier gulp et une douzaine de plugins Gulp. J'avais du mal à intégrer Webpack, Browsersync, le rechargement à chaud, Mocha et bien plus encore avec Gulp. Pourquoi? Eh bien, certains plugins avaient une documentation insuffisante pour mon cas d'utilisation. Certains plugins n'exposaient qu'une partie de l'API dont j'avais besoin. L'un avait un bogue étrange où il ne regardait qu'un petit nombre de fichiers. Un autre a supprimé les couleurs lors de la sortie sur la ligne de commande.
Il précise trois problèmes principaux avec Gulp :
- Dépendance aux auteurs de plugins
- Frustrant à déboguer
- Documentation décousue
J'aurais tendance à être d'accord avec tout cela.
1. Dépendance vis-à-vis des auteurs de plugins
Chaque fois qu'une bibliothèque telle que eslint
est mise à jour, la bibliothèque gulp-eslint
associée nécessite une mise à jour correspondante. Si le responsable de la bibliothèque perd tout intérêt, la version gulp de la bibliothèque se désynchronise avec la version native. Il en va de même lorsqu'une nouvelle bibliothèque est créée. Si quelqu'un crée une bibliothèque xyz
et qu'elle se propage, vous avez soudainement besoin d'une bibliothèque gulp-xyz
correspondante pour l'utiliser dans vos tâches gulp.
Dans un sens, cette approche n'est tout simplement pas à l'échelle. Idéalement, nous voudrions une approche comme Gulp qui puisse utiliser les bibliothèques natives.
2. Frustrant de déboguer
Bien que des bibliothèques telles que gulp-plumber
aident à atténuer considérablement ce problème, il est néanmoins vrai que le rapport d'erreurs dans gulp
n'est tout simplement pas très utile. Si même un canal lève une exception non gérée, vous obtenez une trace de pile pour un problème qui semble complètement indépendant de la cause du problème dans votre code source. Cela peut faire du débogage un cauchemar dans certains cas. Aucune recherche sur Google ou Stack Overflow ne peut vraiment vous aider si l'erreur est suffisamment cryptée ou trompeuse.
3. Documentation décousue
Souvent, je trouve que les petites bibliothèques gulp
ont tendance à avoir une documentation très limitée. Je soupçonne que c'est parce que l'auteur fait généralement la bibliothèque principalement pour son propre usage. De plus, il est courant de devoir consulter la documentation du plugin Gulp et de la bibliothèque native elle-même, ce qui signifie beaucoup de changement de contexte et deux fois plus de lecture à faire.
Conclusion
Il me semble assez clair que Webpack est préférable à Browserify et que les scripts NPM sont préférables à Gulp, bien que chaque option ait ses avantages et ses inconvénients. Gulp est certainement plus expressif et pratique à utiliser que les scripts NPM, mais vous en payez le prix dans toute l'abstraction ajoutée.
Toutes les combinaisons ne sont peut-être pas parfaites pour votre application, mais si vous souhaitez éviter un nombre écrasant de dépendances de développement et une expérience de débogage frustrante, Webpack avec des scripts NPM est la solution. J'espère que vous trouverez cet article utile pour choisir les bons outils pour votre prochain projet.
- Gardez le contrôle : un guide pour Webpack et React, Pt. 1
- Gulp Under the Hood : création d'un outil d'automatisation des tâches basé sur les flux