Comment configurer Webpack
Webpack est un bundler, c’est à dire qu’il prend les différents modules NodeJS ainsi que les plugins JavaScript utilisés dans votre projet et crée un fichier ( ou plusieurs ) en sortie. Il permet ainsi d’utiliser l’ES6, la nouvelle version du langage JavaScript, mais également ReactJS et d’autres frameworks JavaScript. Il peut être utilisé comme compilateur et est même obligatoire pour certains frameworks JavaScript comme je le disais dans mon précédent article.
Par ailleurs, il existe un très bon chapitre dans le livre dédié à Webpack de la série SurviveJS et accessible gratuitement en ligne qui le compare à d’autres outils.
- Pour la mise en production :
- Compiler une version optimisée pour le déploiement en production.
- Pendant le développement :
- Compiler l’ensemble des fichiers et avoir une version lui permettant de déboguer facilement.
- Compiler l’ensemble des fichiers et avoir une version lui permettant de déboguer facilement et de recompiler à chaque modification faite sur les fichiers.
- Lancer le serveur de développement de Webpack qui fait la même chose que le précédent point mais charge les fichiers compilés à partir de la mémoire et les distribuent à travers un serveur. Il recharge la page automatiquement également.
Installation de Webpack
Évidemment, Il faut installer au préalable NodeJS et NPM. Ensuite, il suffit de se placer dans le dossier du projet contenant le package.json. Enfin, dans un Terminal :
1 |
npm i --save-dev webpack |
Vous pouvez rajouter « -g » pour l’installer de manière globale mais je préfère avoir une bonne séparation des modules NodeJS pour chaque projet.
Ensuite, Il faut créer le fichier webpack.dev.config.js et le fichier webpack.prod.config.js qui vont contenir les configurations pour chaque environnement.
Remarque : Par défaut Webpack va chercher un fichier « webpack.config.js » mais nous allons mettre en place un fichier pour l’environnement de développement et un autre pour créer les fichiers de production.
Pour le moment on va remplir uniquement webpack.dev.config.js :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
const config = { entry : { // les fichiers d’entrés }, output : { // les fichiers de sorties }, plugins : [ // la liste des plugins qui vont permettre // d’ajouter des fonctionnalités ], module : { // la configuration des plugins rules: [ // les différentes configurations qui vont // permettre d’interpréter chaque fichier. ] } } module.exports = config; |
Comme Webpack n’est pas installé de manière globale, il faut le lancer à partir du dossier node_modules :
1 |
./node_modules/.bin/webpack --config webpack.dev.config.js --progress --debug |
Néanmoins, pour faciliter le lancement des commandes on va renseigner le fichier package.json pour pouvoir lancer les commandes avec « npm run« . En somme, voici les commandes pour chaque situation :
1 2 3 4 5 6 7 8 |
... "scripts": { "prod-build" : "./node_modules/.bin/webpack --config webpack.prod.config.js --progress --debug", "dev-build" : "./node_modules/.bin/webpack --config webpack.dev.config.js --progress --debug", "dev-watch" : "./node_modules/.bin/webpack --config webpack.dev.config.js --progress --colors --watch --debug", "dev-server" : "./node_modules/.bin/webpack-dev-server --config webpack.dev.config.js --progress --colors --inline --debug" }, ... |
Les fichiers d’entrées et de sorties
Commençons d’abord par définir les fichiers d’entrées et de sorties. Considérons cet exemple :
1 2 3 4 5 6 |
const elements = [ 'foo', 'bar', 'baz' ] for(var i = 0; i < elements.length; i++){ console.log(elements[i]) } |
1 2 3 4 5 6 7 8 9 10 |
const config = { entry : { superApp : './app/main.js' }, output : { path: './public', filename: '[name].js' } } module.exports = config |
Ainsi , l’objet « entry » contient les chunks qui vont être analysés et traités. Dans l’exemple ci-dessus, Webpack va lire le fichier main.js, transformer tous les appels utilisant la syntaxe ES6 dans un format ES5 et construire un fichier concaténé contenant l’ensemble des éléments requis.
De plus, la valeur « [name].js » de « output.filename » indique que la clé de l’objet « entry » va être utilisée pour le nom du fichier généré. Ainsi, le fichier concatené va s’appeler superApp.js.
Vous pouvez forcer un nom fixe évidemment, mais je trouve que cette configuration est plus flexible.
Si vous voulez servir les fichiers à partir d’un CDN, il faut rajouter publicPath à l’objet « output » :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
const exludedFolder = [ path.join(__dirname, 'node_modules'), path.join(__dirname, 'vendor') ] const config = { entry : { superApp : './app/main.js' }, output : { path: './public', filename: '[name].js', publicPath: 'https://my.super.cdn.com/' } } module.exports = config |
En effet, cette valeur va être utilisée pour savoir où sont stockés les fichiers compilés. Par ailleurs, il est également utilisé par les loaders (comme file-loader ou url-loader) afin de remplacer le début des URL. C’est pourquoi, dans l’exemple ci-dessus, « https://my.super.cdn.com/ » va être rajouté au début de tous les attributs src et href , mais aussi dans les appels url() dans les CSS.
Les plugins et les modules
La section module contient principalement la clé « loaders » avec une liste d’objets. Ces objets contiennent :
- Le test qui permet de filtrer les fichiers.
- Les fichiers et dossiers exclus.
- Le loader qui va transformer le fichier.
Les loaders doivent être ajoutés avec NPM, ainsi pour babel :
1 |
npm install --save-dev babel-core babel-loader babel-preset-env babel-plugin-transform-runtime babel-runtime |
Le loader BabelJS va transpiler le code ES6 vers du code ES5. Considérons donc un fichier contenant le code suivant :
1 2 3 4 5 6 7 8 9 10 11 12 13 |
import _ from 'lodash'; export default class { constructor() { this.elements = [ 'foo', 'bar', 'baz' ] } logElement() { _.each(this.elements, (elem) => { console.log(elem) }) } } |
Il va être importé dans le fichier main.js de la manière suivante :
1 2 3 |
import MyFeature from './MyFeature'; const feature = new MyFeature(); feature.logElement(); |
On ajoute la configuration de BabelJS dans notre fichier de configuration de Webpack :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
const path = require('path') const exludedFolder = [ path.join(__dirname, 'node_modules'), path.join(__dirname, 'vendor') ] const config = { entry : { superApp : './app/main.js', }, output : { path: './public', filename: '[name].js' }, module: { loaders: [ { test: /\.js$/, exclude: exludedFolder, use: [ { loader: 'babel-loader', options: { presets: [ 'env' ], plugins: [ ['transform-runtime'] ] } } ] } ] } } module.exports = config |
La clé « query » contient des paramètres pour BabelJS. Les « presets » permettent de donner des indications sur la syntaxe du fichier source afin de configurer BabelJS pour la transformation du code ES6 vers ES5. Le plugin pour BabelJS, « transform-runtime« , permet d’accélérer la compilation en optimisant les appels des modules.
Les plugins que je conseils
CommonChunkPlugin
Ce plugin peut être utilisé de deux façons :
- En indiquant le chunck contenant les modules communs.
- En indiquant le nom du fichier qui va contenir l’ensemble des modules communs à tous les chunks de votre application.
On va donc avoir des fichiers de chunk plus légers et permettre de mettre en cache chez l’utilisateur, le fichier JavaScript contenant les modules communs à toute l’application.
Pour que ces modules communs soient mis automatiquement dans un fichier à part, vous n’avez qu’a rajouter le plugin dans la liste des plugins de la manière suivante :
1 2 |
// le nom du fichier des modules communs sera "common.js" new webpack.optimize.CommonsChunkPlugin('common.js') |
ou en choisissant explicitement les chunks ayant les modules communs:
1 2 3 4 5 6 |
// le nom du fichier aura le nom du chunk, ici "common.js"... new webpack.optimize.CommonsChunkPlugin({ name: 'common' // ... mais si vous voulez donner un autre nom, il suffit de rajouter ceci // filename: "vendor.js" }) |
Il faut bien faire attention à charger le fichier généré par le plugin avant les chunks. Pour de plus amples informations concernant la configuration de ce plugin très puissant, c’est par ici.
ExtractTextPlugin
Il requiert une installation :
1 |
npm i --save-dev extract-text-webpack-plugin |
Il permet de mettre dans un fichier css l’ensemble des styles écrits en SCSS ou SASS ou avec n’importe quelle autre langue de stylisation compilée.
D’abord, il faut l’ajouter dans la liste des plugins :
1 2 3 4 5 6 |
... plugins: [ // le '[name]' fonctionne comme avec output.filename, il va faire en sorte qu'il crée un css par chunk new ExtractTextPlugin('[name].css') ] ... |
puis dans les modules, il faut le spécifier en tant que loader :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
... { test: /\.scss$/, exclude: exludedFolder, use: styleExtractor.extract({ fallback: "style-loader", use: [ { loader : 'css-loader' }, { loader : 'resolve-url-loader' } ] }) }, ... |
Vous remarquerez que j’utilise plusieurs loader. Il faut évidement les installer avant de pouvoir les utiliser. Ainsi, resolve-url, par exemple, est très utile pour réécrire les chemins d’accès des fichiers appelé avec url() dans les styles écrits en SASS .
ProvidePlugin
Il permet de spécifier un module pour une variable. En effet, lors de la compilation, la variable en clé va être détectée et le compilateur va rajouter le module spécifié en valeur. Il faut rajouter l’instantiation dans la liste des plugins. Exemple :
1 2 3 4 |
new webpack.ProvidePlugin({ 'window.$' : 'jquery', 'window.jQuery' : 'jquery' }) |
UglifyJsPlugin
Il sera utilisé en production principalement. En effet, il va permettre de minifier et d’obfusquer les chunks, exemple :
1 2 3 4 5 6 7 |
new webpack.optimize.UglifyJsPlugin({ compress: { warnings: false }, comments: false, sourceMap: false }) |
DefinePlugin
Il permet de définir des constantes qui seront utilisées pendant la compilation. Exemple :
1 2 3 |
new webpack.DefinePlugin({ 'process.env': { NODE_ENV: '"production"' } }) |
NoErrorsPlugins
Il permet d’arrêter la compilation lorsqu’il y a une erreur.
1 |
new webpack.NoErrorsPlugin() |
La configuration pour le développement
Commençons par installer le serveur de développement :
1 |
npm i webpack-dev-server --save-dev |
Il faut penser à rajouter la clé « devtool » pour avoir les source-maps des fichiers compilés et un publicPath pointant sur une IP local. Donc, si on prend le fichier de configuration ci-dessus, il faut rajouter les indications suivantes pour avoir le serveur correctement configuré:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
const path = require('path') const webpack = require('webpack') const exludedFolder = [ path.join(__dirname, 'node_modules'), path.join(__dirname, 'vendor') ] const currentHost = require('./app/lib/tools.js').getCurrentHost('dev'); const config = { entry : { superApp : './app/main.js' }, output : { path: './public', filename: '[name].js', publicPath: currentHost.name + ':' + currentHost.port + '/' }, plugins:[ new webpack.NoErrorsPlugin(), new webpack.optimize.CommonsChunkPlugin('commons.js') ], module: { loaders: [ { test: /\.js$/, exclude: /node_modules/, use: [ { loader: 'babel-loader', options: { presets: [ 'env' ] } } ] } ] }, watchOptions: { poll: true }, devtool: 'source-map', devServer: { contentBase: './public', host: currentHost.name, port: currentHost.port, headers: { 'Access-Control-Allow-Origin': '*' } } } module.exports = config |
Pour commencer, la clé contentBase de l’objet devServer correspond au dossier où se trouve vos fichiers statiques (comme les images). Ensuite, en ce qui concerne les fichiers compilés, ils sont chargés en mémoire et servies par le chemin indiqué par « output.publicPath ». Ainsi, à chaque modification de fichier, les fichiers en mémoire sont remplacés et le navigateur est rechargé.
Enfin, J’ai rajouté quelques configurations :
- Quelque plugins à titre d’exemple ( si vous utilisez CommonsChunkPlugin, il faut faire attention à charger le fichier commons.js avant les autres chunks ).
- La clé headers pour éviter l’erreur CORS lors de l’appel au serveur.
- La clé watchOption doit être mis en place lorsque vous utilisez Webpack dans une VM.
Remarque:
La configuration du serveur de développement doit avoir le host et le port identique au output.publicPath. En effet cela permet au fichier static de correctement charger.
Exemple d’une configuration pour l’environnement de production
La configuration de production ressemble évidemment à celle pour le développement. Il faut néanmoins rajouter quelque plugins et enlever les clés concernant les outils de développement :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
const path = require('path') const webpack = require('webpack') const exludedFolder = [ path.join(__dirname, 'node_modules'), path.join(__dirname, 'vendor') ] const currentHost = require('./app/lib/tools.js').getCurrentHost('prod') const config = { entry : { superApp : './app/main.js' }, output : { path: './public', filename: '[name].js', publicPath: currentHost.name + ':' + currentHost.port + '/' }, plugins:[ new webpack.NoErrorsPlugin(), new webpack.optimize.CommonsChunkPlugin('commons.js'), new webpack.optimize.UglifyJsPlugin({ compress: { warnings: false }, comments: false, sourceMap: false }), new webpack.DefinePlugin({ 'process.env': { NODE_ENV: '"production"' } }) ], module: { rules: [ { test: /\.js$/, exclude: /node_modules/, use: [ { loader: 'babel-loader', options: { presets: [ 'env' ] } } ] } ] } } module.exports = config |
Vous pouvez encore rajouter d’autre plugins comme OccurenceOrderPlugin ou DedupePlugin. Mais comme indiqué dans le titre, la configuration ci-dessus n’est qu’un exemple, il n’est pas prévu pour un véritable environnement de production. Mais je pense qu’il vous donne déjà une bonne direction. De plus, je vous invite à tester les différents plugins disponible sur la documentation officielle pour trouver la configuration qui convient le mieux à votre application.
Dans quel projet utiliser Webpack ?
Cet outil devrait s’intégrer dans n’importe quel type de projet qui utilise du JavaScript et du CSS. Il a un écosystème de plugin et de loader tellement large que vous allez forcément trouver chaussure à votre pied. En plus il va vous permettre d’utiliser les dernières innovations tant au niveau du langage JavaScript avec l’ECMAScript 6 aka ES6 ou le TypeScript qu’au niveau du langage CSS avec SASS ou LESS.
Il s’intègre naturellement dans des projets NodeJS mais sa flexibilité est telle qu’il est aussi l’aise dans des projets PHP avec Zend ou Symfony. En effet, pour se dernier, vous pouvez le mettre en place pour l’utiliser avec ou en remplacement d’Assetic.
En somme, Webpack vous permet d’utiliser les dernières technologies disponibles en JavaScript et en CSS, tout en vous permettant d’industrialiser vos déploiements avec facilité et flexibilité.