Jan 08, 2020 Javascript

Maîtriser Javascript : webpack

Quand on découvre webpack pour la première fois on est un peu perdu parce que l’outil est vraiment riche et complexe. Mais il s’agit sans doute du meilleur bundler actuellement et ça vaut vraiment le coup de se l’approprier. J’ai déjà abordé webpack dans un article précédent mais de manière rapide parmi d’autres outils. Je vais à présent lui consacrer plus de place.

Mais d’abord c’est quoi webpack ? Je pense que l’illustration sur leur site parle mieux qu’un long discours :

En gros webpack traite toutes les ressources, même imbriquées en modules, sous n’importe quelle forme en les modifiant et compactant pour les rendre parfaites pour le déploiement. A l’origine webpack ne s’intéressait qu’au Javascript mais avec ses loaders il peut traiter tout type de fichier.

Les concepts

Pour comprendre webpack il faut digérer quelques concepts :

  • point d’entrée (Entry) : souvent les modules dépendent d’autres modules qui dépendent d’autres modules… pour gérer ça webpack crée un graphe de dépendances. Mais il faut bien démarrer quelque part ! C’est là que se situe le point d’entrée qui est par défaut ./src/index.js. Si on a ce point d’entrée pas besoin de faire de configuration, ça fonctionnera ! On peut aussi définir plusieurs points d’entrée.
  • sortie (Ouput) : on sait comment entrer mais il faut bien aussi savoir comment sortir. Autrement dit quel nom et quel endroit ? Là aussi on a une valeur par défaut qui est ./dist/main.js pour le bundle principal et ./dist pour tout le reste (par exemple les images).
  • les chargeurs (loaders) : j’ai dit plus haut qu’à la base webpack ne connait que le Javascript et son copain le JSON. Pour tout autre type de fichier il faut utiliser un loader. En gros le loader va faire une première transformation pour que webpack puisse gérer le fichier.
  • les plugins : les plugins vont plus loin que les loaders, par exemple ils vont permettre d’optimiser du code.
  • le mode (Mode) : le mode permet de définir si on est en développement ou en déploiement.
  • configuration : webpack a beaucoup évolué pour se simplifier et il peut être utilisé désormais sans configuration. Mais ça correspond à des cas très simples et on est rapidement amené à créer un fichier de configuration webpack.config.js à la racine que webpack va lire automatiquement s’il existe.

Mais assez discuté des principes passons à la pratique…

On se lance !

On ouvre la console (ou ce qui y ressemble ) :

mkdir webpack
cd webpack
npm init -y
npm i webpack webpack-cli -D

Au moment où j’écris cet article on a les versions suivantes :

"devDependencies": {
  "webpack": "^4.41.5",
  "webpack-cli": "^3.3.10"
}

On va créer un fichier src/index.js :

Avec un code minimaliste :

console.log('Coucou !')

Et on lance webpack pour voir ce que ça donne :

webpack
Hash: e3145dba7bb769305021
Version: webpack 4.41.5
Time: 283ms
Built at: 2020-01-07 13:55:06
  Asset       Size  Chunks             Chunk Names
main.js  953 bytes       0  [emitted]  main
Entrypoint main = main.js
[0] ./src/index.js 23 bytes {0} [built]

WARNING in configuration
The 'mode' option has not been set, webpack will fallback to 'production' for this value. Set 'mode' option to 'development' or 'production' to enable defaults for each environment.
You can also set it to 'none' to disable any default behavior. Learn more: https://webpack.js.org/configuration/mode/

On a bien création d’un fichier comme prévu :

On a aussi une alerte qui nous dit qu’on a pas précisé l’option mode. Du coup webpack ne sait pas si on est en développement ou déploiement. On va arranger ça tout de suite en créant deux script dans package.json :

"scripts": {
  "test": "echo \"Error: no test specified\" && exit 1",
  "dev": "webpack --mode development",
  "build": "webpack --mode production"
},

On va ajouter un fichier HTML pour utiliser le script :

Avec ce code :

<!doctype html>
<html lang="fr">
<head>
  <meta charset="utf-8">
  <title>Essai de Webpack</title>  
</head>
<body>
  <p>Ma page à moi !</p>
  <script src="main.js"></script>
</body>
</html>

Si on l’ouvre on va normalement avoir le message dans la console :

Vous me direz c’est bien du boulot pour pas grand chose ! Mais au moins la base est en place.

Les dépendances

Supposons maintenant que notre script comporte une dépendance. Par exemple Moment.js pour gérer des dates. On charge cette librairie dans notre projet :

npm i -D moment

On modifie notre fichier src/index.js pour utiliser la librairie :

var moment = require('moment')
console.log(moment().format())

Et on lance à nouveau webpack en utilisant cette fois un des deux scripts qu’on a créés :

npm run dev

> webpacktest@1.0.0 dev E:\webpacktest
> webpack --mode development

Hash: b39b80a27d3104280167
Version: webpack 4.41.5
Time: 524ms
Built at: 2020-01-07 14:18:35
  Asset     Size  Chunks             Chunk Names
main.js  636 KiB    main  [emitted]  main
Entrypoint main = main.js
[./node_modules/moment/locale sync recursive ^\.\/.*$] ./node_modules/moment/locale sync ^\.\/.*$ 3 KiB {main} [optional] [built]
[./node_modules/webpack/buildin/module.js] (webpack)/buildin/module.js 497 bytes {main} [built]
[./src/index.js] 64 bytes {main} [built]
    + 128 hidden modules

On voit que Webpack explore notre fichier à la recherche de librairies et qu’il trouve bien Moment.

Si on recharge la page HTML on obtient maintenant :

Donc Webpack a bien ajouté Moment à notre code de sortie.

Ça serait la même chose si on crée des modules. On va séparer notre code en deux fichiers :

Pour index.js :

import date from "./date"
console.log(date())

Et pour date.js :

var moment = require('moment')
export default () => moment().format()

On relance Webpack :

λ npm run dev                                                                                                                               
                                                                                                                                            
> webpacktest@1.0.0 dev E:\webpacktest                                                                                                      
> webpack --mode development                                                                                                                
                                                                                                                                            
Hash: 8cea4d466017745925c1                                                                                                                  
Version: webpack 4.41.5                                                                                                                     
Time: 518ms                                                                                                                                 
Built at: 2020-01-07 14:26:46                                                                                                               
  Asset     Size  Chunks             Chunk Names                                                                                            
main.js  637 KiB    main  [emitted]  main                                                                                                   
Entrypoint main = main.js                                                                                                                   
[./node_modules/moment/locale sync recursive ^\.\/.*$] ./node_modules/moment/locale sync ^\.\/.*$ 3 KiB {main} [optional] [built]           
[./node_modules/webpack/buildin/module.js] (webpack)/buildin/module.js 497 bytes {main} [built]                                             
[./src/date.js] 70 bytes {main} [built]                                                                                                     
[./src/index.js] 48 bytes {main} [built]                                                                                                    
    + 128 hidden modules

On voit que Webpack a bien trouvé le module et on peut vérifier le résultat en rechargeant la page HTML.

Un loader : babel

Une transformation classique pour le Javascript est de passer d’ES6 (ou plus) à ES5 pour tenir compte des navigateurs qui ne digèrent pas les nouveautés. J’ai déjà évoqué Babel dans cet article. On va voir comment le gérer avec Webpack.

Il existe un loader pour Babel. On va donc voir à présent comment utiliser un loader dans Webpack. Mais il faut commencer par installer Babel dans le projet :

npm i -D babel-loader @babel/core @babel/preset-env

Il faut ensuite créer un fichier de configuration webpack.config.js pour Webpack :

module.exports = {
  module: {
    rules: [
      {
        test: /\.m?js$/,
        exclude: /(node_modules|bower_components)/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: ['@babel/preset-env']
          }
        }
      }
    ],
  }
}

On a 3 parties :

  • test : filtre les fichiers qui doivent subir la transformation,
  • exclude : c’est l’inverse du précédent, donc les fichiers qui ne doivent pas être transformés,
  • use : définit le loader à utiliser avec ses options éventuelles.

On peutchanger index.js ainsi :

const SumElements = (arr) => {
  let sum = 0
  for (let element of arr) {
      sum += element
  }
  console.log(sum) // 220
}
SumElements([10, 20, 40, 60, 90])

Et lancer Webpack, par exemple en mode dev. Voilà notre code transformé :

eval("var SumElements = function SumElements(arr) {\n  var sum = 0;\n  var _iteratorNormalCompletion = true;\n  var _didIteratorError = false;\n  var _iteratorError = undefined;\n\n  try {\n    for (var _iterator = arr[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) {\n      var element = _step.value;\n      sum += element;\n    }\n  } catch (err) {\n    _didIteratorError = true;\n    _iteratorError = err;\n  } finally {\n    try {\n      if (!_iteratorNormalCompletion && _iterator[\"return\"] != null) {\n        _iterator[\"return\"]();\n      }\n    } finally {\n      if (_didIteratorError) {\n        throw _iteratorError;\n      }\n    }\n  }\n\n  console.log(sum); // 220\n};\n\nSumElements([10, 20, 40, 60, 90]);\n\n//# sourceURL=webpack:///./src/index.js?");

Un plugin : HtmlWebpackPlugin

Après avoir vu un loader voyons un plugin. Dans mes précédents exemples j’ai mis le fichier index.html dans le dossier dist. Ça serait quand même plus pertinent d’avoir un template présent dans la racine ou les sources et que le fichier index.html soit automatiquement créé dans le dossier dist avec les liens qui vont bien. Ce désir devient réalité avec le plugin HtmlWebpackPlugin.

On commence par l’installer :

npm i -D html-webpack-plugin

Il faut déclarer ce plugin dans webpack.config.js :

const HtmlWebpackPlugin = require('html-webpack-plugin')

module.exports = {
  module: {
    ...
  },
  plugins: [
    new HtmlWebpackPlugin()
  ]
}

Supprimez le dossier dist si vous en avez un. Prévoyez un fichier src/index.js :

On lance Webpack et on regarde :

On a bien la création d’un fichier index.html avec ce code :

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>Webpack App</title>
  </head>
  <body>
  <script type="text/javascript" src="main.js"></script></body>
</html>

On voit que le fichier main.js est bien injecté dans une structure HTML très sommaire. Ce qui serait mieux c’est d’avoir un template dans lequel on injecte les liens. Par défaut le plugin cherche un fichier nommé src/index.ejs mais on va plutôt utiliser l’option template dans la configuration :

plugins: [
  new HtmlWebpackPlugin({
    template: './src/index.html'
  })
]

On crée un fichier src/index.html à la racine avec ce code :

<!doctype html>
<html lang="fr">
<head>
  <meta charset="utf-8">
  <title>Essai de Webpack</title>  
</head>
<body>
  <p>Ma page à moi !</p>
</body>
</html>

On lance Webpack et on se retrouve avec ce fichier dist/index.html :

<!doctype html>
<html lang="fr">
<head>
  <meta charset="utf-8">
  <title>Essai de Webpack</title>  
</head>
<body>
  <p>Ma page à moi !</p>
<script type="text/javascript" src="main.js"></script></body>
</html>

On voit que notre code a été utilisé et qu’il y a injection du chargement du fichier Javascript.

On peut utiliser plusiuers types de template : pug, ejs, underscore… Mais évidemment il faudra prévoir un loader adapté pour chaque cas. Il y a une documentation sur ce sujet.

Un autre plugin : CleanWebpackPlugin

Vous avez sans doute remarqué que le dossier de distribution n’est pas nettoyé avant chaque construction. Ca peut se révéler un peu gênant alors on va ajouter ce plugin :

npm i -D clean-webpack-plugin

Il suffit ensuite de le déclarer dans la configuration :

const { CleanWebpackPlugin } = require('clean-webpack-plugin')

...

plugins: [
  ...
  new CleanWebpackPlugin()
]

Ajoutez un fichier dans dist pour vérifier que ça fonctionne.

Le style

Il n’y a pas que du Javascript dans nos pages mais aussi du style. Webpack peut aussi très bien le gérer avec quelques loaders. Imaginons que nous utilisons Sass pour notre style. On va devoir installer encore quelques packages :

npm i sass-loader node-sass css-loader style-loader -D

Le css-loader transforme le CSS en Javascript. Le style-loader inclut le style dans les bonnes balises. Et le sass-loader évidemment transforme le Sass en CSS.

On déclare tout ça dans la configuration :

rules: [
  ...
  {
    test: /\.s[ac]ss$/i,
    use: [
      'style-loader',
      'css-loader',
      'sass-loader',
    ],
  },
],

On crée un fichier src/styles/index.scss :

Avec un code basique :

$color: green;
body { color: $color; }

On importe ce fichier dans index.js :

import './styles/index.scss'

Et on lance Webpack ! Si vous fouiller dans le fichier main.js vous allez trouver quelque part la déclaration de la couleur et dans la page que vous ouvrez la bonne règle de style :

Un serveur

Webpack nous propose en plus un serveur qui reconstruit notre projet à chaque changement et rafraichit la page !

On l’installe :

npm i -D webpack-dev-server

On va modifier le script dans package.json :

"scripts": {
  "test": "echo \"Error: no test specified\" && exit 1",
  "dev": "webpack-dev-server --mode development",
  "build": "webpack --mode production"
},

Et maintenant quand on utilise npm run dev le serveur se crée à l’adresse http://localhost:8080/. Et si on modifie quelque chose c’est immédiatement répercuté :

i 「wdm」: Compiling...
i 「wdm」: Hash: fbc541b71926df0b906f
Version: webpack 4.41.5
Time: 104ms
Built at: 2020-01-07 18:03:41
     Asset       Size  Chunks             Chunk Names
index.html  219 bytes          [emitted]
   main.js    373 KiB    main  [emitted]  main
Entrypoint main = main.js
[./node_modules/css-loader/dist/cjs.js!./node_modules/sass-loader/dist/cjs.js!./src/styles/index.scss] 260 bytes {main} [built]
    + 36 hidden modules
Child html-webpack-plugin for "index.html":
     1 asset
    Entrypoint undefined = index.html
       4 modules
i 「wdm」: Compiled successfully.

Il existe un dashbord que je n’ai jamais testé mais qui semble intéressant.

Les assets

Dans une page web on a aussi des polices, des images… Webpack peut aussi s’occuper de tout ça. Par exemple pour les images on peut utiliser file-loader :

npm i -D file-loader

Dans la configuration :

module: {
  rules: [
    ...
    {
      test: /\.(png|jpe?g|gif)$/i,
      use: [
        {
          loader: 'file-loader'
        },
      ],
    },     
  ],
},

Et dans le fichier index.js :

import img from './images/img.png'

En production on se retrouve avec l’image recopiée :

Par défaut le nom est le hash et on conserve l’extension d’origine. Il y a une option name qui permet de changer ça :

{
  test: /\.(png|jpe?g|gif)$/i,
  loader: 'file-loader',
  options: {
    name: '[name].[ext]',
  },
},

Évidemment cette image ensuite il faut bien en faire quelque chose, par exemple :

import img from './images/img.png'

const image = document.createElement('img')
image.src = img
document.body.appendChild(image)

Les templates

Quand on démarre un projet on ne s’amuse pas à tout recontruire comme je viens de l’illustrer rapidement dans cet article avec quelques éléments. En général on part d’un template. Vous trouvez plein de starter kits référencés sur cette page. Tout dépend le type de projet que vous voulez faire. Pour quelque chose d’assez simple et généraliste j’aime bien webpack-starter-basic. Il reprend pas mal des choses vues ci-dessus. Il utilise deux fichiers de configuration : une pour le développement (webpack.dev.js) et une autre pour le déploiement (webpack.prod.js). En effet pour le déploiement il y a quelques tâches supplémentaires (les favicons, les optimisations…).

C’est une bonne base pour démarrer un projet, on peut le modifier selon ses besoins mais on gagne pas mal de temps. De base il a :

  • gestion de Sass
  • chargement des assets
  • préfixage CSS
  • serveur de développement
  • les sourcesmaps
  • la génération des favicons
  • des optimisation pour la production

Les ressources

Il existe une page de démos malheureusement un peu ancienne. Et aussi l’incontournable Awesome Page grouillant de références.

On peut aussi s’orienter vers Laravel Mix qui est une couche au-dessus de Webpack pour le rendre plus convivial que les utilisateurs de Laravel connaissent bien mais qui peut très bien être utilisé en dehors de ce contexte. Il existe un bon article pour se lancer.

Une autre alternative pourrait être Neutrino. L’objectif est aussi de simplifier l’utilisation de Webpack. La bonne question à se poser c’est : le temps d’apprentissage de cet outil n’est pas négligeable alors ne vaut-il pas mieux utiliser directement Webpack ?

Dans la même veine on trouve des bundlers concurrents comme Snowpack, Backpack ou Fuse.

Conclusion

Webpack permet encore bien des choses dont je n’ai pas parlé dans cet article. Je vous conseille de lire la documentation qui est plutôt bien faite. Au menu : hot module replacement, dynamic code splitting, lazy loading, cache, postCSS, image optimization…

Notez que la version 5 est déjà en vue et apportera des nouveautés.

 

Laisser un commentaire