Webpack comme des grand·e·s !

Un atelier de Christophe Porteneuve at Paris Web 2017

whoami


const christophe = {
  family: { wife: 'Élodie', son: 'Maxence' },
  city: 'Paris, FR',
  company: 'Delicious Insights',
  trainings: [
    'Web Apps Modernes', 'Node.js', 'Git Total',
    'ES Total', 'Webpack'
  ],
  webSince: 1995,
  claimsToFame: [
    'Prototype.js',
    'Ruby On Rails',
    'Prototype and Script.aculo.us',
    'Paris Web',
    'NodeSchool Paris'
  ],
}
          

Rappelons

les bases

Bundlers vs. Task Runners

Webpack est un bundler, pas un task runner
(De ce point de vue, il se rapproche de Brunch et Broccoli)

Intérêt : moins de config, plus d’optimisations possibles

Points forts particuliers

Vitesse

Hot Module Replacement (HMR)

Approche graphe (on y vient)

Richesse d’optimisations proposées
(en particulier autour du bundle splitting et code splitting)

Le graphe de dépendances

Diagramme illustratif d’un graphe de dépendances Webpack

Ce qu’on ne verra pas

L’intégration du serveur de dev aux nôtres

Les finesses de gestion du HMR

Les parallélisations et mises en cache

L’externalisation et les cibles alternatives de build

Le code splitting

Les passerelles avec React (notamment React-Router et Redux)

Les loaders et plugins personnalisés

Mon premier bundle

Récupérer le support

Suivez les instructions de préparation


node -v
8.6.0
npm -v
5.4.2
git clone https://github.com/deliciousinsights/pw2017-webpack
cd pw2017-webpack
npm install
          

deliciousinsights/pw2017-webpack

Le fichier de configuration

Juste un module CommonJS

On est donc libres de construire l’objet exporté comme on veut !

Peut donc être hyper dynamique, réactif à l’environnement, etc.

A minima :


module.exports = {
  entry: …,        // Chemin(s) absolu(s) de « module »
  output: {
    path: '…',     // Chemin absolu du dossier destinataire
    filename: '…',
  }
}
          

Premier bundling


const path = require('path')

const PATHS = {
  app: path.join(__dirname, 'src'),
  target: path.join(__dirname, 'build'),
}

module.exports = {
  entry: PATHS.app,
  output: {
    path: PATHS.target,
    filename: 'bundle.js',
  },
}
          

Modifs et watch


…
"scripts": {
  "build": "webpack",
  "dev": "webpack --watch"
},
…
          

Quand Webpack se soucie-t-il de mes nouveaux fichiers ?

Le watcher simple dans un premier temps

webpack-dev-server

À quoi ça sert ?

À plein de trucs :

Surveiller les fichiers et déclencher des rebuilds
(attention à la sauvegarde automatique dans votre EDI/éditeur)

Fournir un service HTTP par-dessus nos builds

Builder en mémoire pour aller plus vite, garder en cache au passage

Faire du rafraîchissement automatique

Permettre d’aller plus loin avec le Hot Module Replacement

C’est parti


npm install --save-dev webpack-dev-server
              

…
"scripts": {
  "build": "webpack",
  "dev": "webpack-dev-server"
},
…
              

module.exports = {
  devServer: {
    contentBase: PATHS.target,
    overlay: true,
  },
  entry: PATHS.app,
  …
              

Le rafraîchissement basique

Ne rien louper avec l’overlay

Hot Module Replacement

Principes


module.exports = {
  devServer: {
    contentBase: PATHS.target,
    hot: true,
    overlay: true,
  },
  …
  plugins: [
    new webpack.HotModuleReplacementPlugin(),
    new webpack.NamedModulesPlugin(),
  ],
}
              

…
require('./footer')

if (module.hot) {
  module.hot.accept()
}
              

Que renvoie-t-on quand ?

Ajouter des styles

Commençons simplement


git reset --hard debut-etape-3
npm install
              

…
require('./footer')

require('./styles/main.css')

module: {
  rules: [
    {
      test: /\.css$/,
      use: ['style-loader', 'css-loader'],
    },
  ],
},
              

Qu’est-ce qu’un loader ?

style-loader vs. css-loader

Constater le résultat

SASS, Stylus, etc.

Juste un loader en plus…


…
require('./styles/main.css')
require('./styles/titles.scss')

module: {
  rules: [
    …
    {
      test: /\.scss$/,
      use: [
        'style-loader',
        'css-loader',
        'sass-loader'
      ],
    },
  ],
},
              

PostCSS et Autoprefixer

Principes


…
use: [
  // style-loader, css-loader, puis :
  {
    loader: 'postcss-loader',
    options: {
      plugins: () => [
        require('autoprefixer')()
      ],
    },
  },
  // sass-loader / stylus-loader éventuels
],
              

> 1%
Last 2 versions
IE 9
              

Exemple avec display: flex

Source maps pour les styles


  …
        use: [
          { loader: 'css-loader', options: { sourceMap: true } },
          { loader: 'postcss-loader, options: {
            plugins: () => [require('autoprefixer')()],
            sourceMap: true,
          }},
          { loader: 'sass-loader', options: { sourceMap: true } },
        ],
  …
  devtool: 'cheap-module-source-map',
}
          

Modulariser

la configuration

Ça devient le bordel…

Ça commence à pas mal grandir…

Si ça continue on va se croire dans Gulp !

En plus y'a la question de la bascule de mode développement / production

Comment s’y retrouver ?

Comment éviter le copier-coller bête d’un projet à l’autre ?

webpack-merge

npm install --save-dev webpack-merge

La difficulté de base : extraire les multiples impacts d’une configuration précise (ex. HMR, extraction CSS…)

Le module permet d’exprimer des ensembles de fragments de configuration, injectables à l’envi.

Je vous déroule le refactoring de découpe…

Environnements


const devConfig = () => merge([
  CORE_CONFIG,
  …
])

const prodConfig = () => merge([
  CORE_CONFIG,
  …
])

module.exports = (env = process.env.NODE_ENV) =>
  env === 'production' ? prodConfig() : devConfig()
          

"build": "webpack --env production",
          

Extraire les fichiers CSS

extract-text-webpack-plugin

Deux parties : l’extracteur et les appels à l’extraction

Ajustement de notre configuration de production

git reset --hard etape-4

Extraire les fichiers CSS (suite)

Avant :

module: {
  rules: [
    {
      …
      use: [
        'style-loader',
        'css-loader',
        'sass-loader'
      ],
      …
    }
  ]
}
              
Après :

const ExtractTextPlugin =
  require('extract-text-webpack-plugin')

const cssPlugin = new ExtractTextPlugin({
  filename: 'app.css',
  allChunks: true,
})
…
plugins: […, cssPlugin],
…
        use: cssPlugin.extract({
          fallback: 'style-loader',
          use: ['css-loader', 'sass-loader']
        })
…
              

Ajouter des images

file-loader et url-loader


git reset --hard debut-etape-5
npm install
              

…
background: url(../images/background.jpg);
…
background: url(../images/icon.png);
…
              

exports.loadImages = () => ({
  module: {
    rules: [
      {
        test: /\.(?:jpe?g|png|gif)$/i,
        use: {
          loader: 'url-loader',
          options: { limit: 10000 }
        },
      },
    ],
  },
})
              

Examiner la différence, en dev et en prod

Et hors des styles ?

Ça marche pour les require / import dans JS, comme d’hab.


const icon = require('./images/icon.png')
          

On peut même aller chercher la même fonctionnalité pour les sources d’images dans HTML, JSX, Slim…

Utiliser Babel

Le loader


exports.babelize = ({ include, exclude }) => ({
  module: {
    rules: [
      {
        test: /\.js$/,
        include,
        exclude,
        use: {
          loader: 'babel-loader',
          options: {
            presets: [['env', { modules: false }]],
          },
        },
      },
    ],
  },
})
          

Tree shaking ?

Intérêt et limites actuelles

Exemple avec divers imports de lodash

Impact sur le rebuild

Minifier

Minifier JS


exports.stripNonProductionCode = () => ({
  plugins: [
    new webpack.optimize.DefinePlugin({
      'process.env': { NODE_ENV: '"production"' },
    }),
  ],
})

exports.minifyJS = () => ({
  plugins: [
    new webpack.optimize.UglifyJsPlugin({
      sourceMap: true,
    }),
  ],
})
          

Minifier CSS

cssnano est utilisable par défaut


exports.enableAutoMinifiers = () => ({
  plugins: [
    new webpack.LoaderOptionsPlugin({
      minimize: true,
      debug: false,
    }),
  ],
})
          

Avec un peu de config : clean-css, etc.

Optimiser les images ?

On ne va pas s’y amuser, mais y’a plein d’options, par exemple :

image-webpack-loader

imagemin-webpack-plugin

webpack-spritesmith

Améliorer la

cachabilité

Vendoring automatique

CommonsChunkPlugin


exports.autoVendor = () => ({
  plugins: [
    new webpack.optimize.CommonsChunkPlugin({
      name: 'vendor',
      minChunks: ({ resource }) =>
        resource &&
        resource.includes('node_modules')
    }),
  ],
})
              

…
entry: {
  app: PATHS.app,
},
output: {
  filename: '[name].js',
  …
},
…
              

Hashes intelligents

Pourquoi mettre des hashes dans les noms de fichiers ?

hash, chunkhash et contenthash

Limitation de la taille de hash utilisée


exports.hashFiles = () => ({
  output: {
    filename: '[name].[chunkhash:8].js',
  },
})
…
new ExtractTextPlugin({
  filename: 'app.[contenthash:8].css',
  allChunks: true,
})
          

Hashes intelligents (suite)

Les noms changent tout le temps !
Je dois mettre à jour mon HTML en permanence ?

Non, on peut générer du HTML qui est à jour sur les noms produits :


npm install --save-dev html-webpack-plugin
git rm build/index.html
          

const HTMLPlugin = require('html-webpack-plugin')
…
exports.htmlStub = () => ({
  plugins: [new HTMLPlugin()],
})
          

Il y a évidemment une tonne d’options
(et même des tas de plugins par-dessus).

Hashes intelligents (suite)

Les noms changent tout le temps !
Ça pourrit build/ au fil du temps…

Pas de soucis, on purge le dossier au début :


npm install --save-dev clean-webpack-plugin
          

const CleanupPlugin = require('clean-webpack-plugin')
…
exports.cleanup = ({ path, exclude }) => ({
  plugins: [new CleanupPlugin(path, { exclude })],
})
          

Là aussi, plusieurs options disponibles.

Hashes intelligents (suite)

Si je change juste du code pour app.js, ça modifie quand même le hash de vendor.js, c’est quoi ce binz ?

C’est le manifeste qui figure dans chaque bundle… On le sort !


exports.autoVendor = () => ({
  plugins: [
    new webpack.optimize.CommonsChunkPlugin({
      name: 'vendor',
      minChunks: ({ resource }) =>
        resource &&
        resource.includes('node_modules')
    }),
    new webpack.optimize.CommonsChunkPlugin({
      name: 'manifest',
      minChunks: +Infinity,
    }),
  ],
})
          

En savoir (beaucoup) plus

Notre formation Webpack !

Les docs officielles

SurviveJS

Le cours sur Udemy

Merci !

Et que Webpack soit avec vous.


Christophe Porteneuve

@porteneuve

Les slides sont sur bit.ly/pw-webpack