Webpack comme des grand·e·s !
Un atelier de Christophe Porteneuve at Paris Web 2017
Un atelier de Christophe Porteneuve at Paris Web 2017
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'
],
}
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
Vitesse
Hot Module Replacement (HMR)
Approche graphe (on y vient)
Richesse d’optimisations proposées
(en particulier autour du
bundle splitting et
code splitting)
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
…
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
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: '…',
}
}
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',
},
}
…
"scripts": {
"build": "webpack",
"dev": "webpack --watch"
},
…
Quand Webpack se soucie-t-il de mes nouveaux fichiers ?
Le watcher simple dans un premier temps
À 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
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
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 ?
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
Juste un loader en plus…
…
require('./styles/main.css')
require('./styles/titles.scss')
…
module: {
rules: [
…
{
test: /\.scss$/,
use: [
'style-loader',
'css-loader',
'sass-loader'
],
},
],
},
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
…
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',
}
Ç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 ?
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…
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",
Deux parties : l’extracteur et les appels à l’extraction
Ajustement de notre configuration de production
git reset --hard etape-4
module: {
rules: [
{
…
use: [
'style-loader',
'css-loader',
'sass-loader'
],
…
}
]
}
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']
})
…
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
Ç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…
exports.babelize = ({ include, exclude }) => ({
module: {
rules: [
{
test: /\.js$/,
include,
exclude,
use: {
loader: 'babel-loader',
options: {
presets: [['env', { modules: false }]],
},
},
},
],
},
})
Intérêt et limites actuelles
Exemple avec divers imports de lodash
Impact sur le rebuild
exports.stripNonProductionCode = () => ({
plugins: [
new webpack.optimize.DefinePlugin({
'process.env': { NODE_ENV: '"production"' },
}),
],
})
exports.minifyJS = () => ({
plugins: [
new webpack.optimize.UglifyJsPlugin({
sourceMap: true,
}),
],
})
cssnano est utilisable par défaut
exports.enableAutoMinifiers = () => ({
plugins: [
new webpack.LoaderOptionsPlugin({
minimize: true,
debug: false,
}),
],
})
Avec un peu de config : clean-css, etc.
On ne va pas s’y amuser, mais y’a plein d’options, par exemple :
…
exports.autoVendor = () => ({
plugins: [
new webpack.optimize.CommonsChunkPlugin({
name: 'vendor',
minChunks: ({ resource }) =>
resource &&
resource.includes('node_modules')
}),
],
})
…
entry: {
app: PATHS.app,
},
output: {
filename: '[name].js',
…
},
…
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,
})
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).
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.
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,
}),
],
})
Christophe Porteneuve
Les slides sont sur
bit.ly/pw-webpack