Configure Webpack 4
Webpack is a module bundler for JavaScript applications capable of transforming, bundling, and packing just about any resource or asset.
Setting up webpack is a fairly straight forward process, but it's easy to get lost in the process as it is highly customizable and modular.
Get the code :metal:
Contents
- Installing webpack
- DevServer
- Loaders
- Babel Loader
- CSS
- Sass
- Webpack Mode
- Package.json Scripts
- CSS Production Optimization
- Clean up Build folder
- Hooking up React
- Vendor Splitting
- Cache busting
- Handling Images
Installing webpack
Installing webpack locally is what is recommend for most projects. It makes it easier to upgrade projects individually when breaking changes are introduced.
Make sure you have Node 8.x
or later installed. Let's create our project directory and cd into it, initialize a default package.json
file, and install webpack as a development dependency.
mkdir webpack-demo && cd webpack-demo npm init -y npm install webpack webpack-cli --save-dev
As of version 4, webpack doesn't require any configuration file. Run webpack --help
to view all possible flags to run webpack without a config file. However, most projects need a config file for more complex operations. Let's create one:
webpack-demo |- package.json + |- webpack.config.js
The webpack
configuration file exports an object in the commonJS style. Let's define the entry and output of our application. Also, import the path module from node; it will give us the location of the executing file.
By convention the output filename is named bundle.js
. If we prefix [name]-
to the output filename
the entry point key's name will be added. On this case, the final output file will be named: main-bundle.js
. We have the flexibility to define multiple entry points.
webpack.config.js
const path = require('path'); module.exports = { entry: { main: './src/App.js', //entry point to our App }, output: { filename: '[name]-bundle.js', //[name] is the entry name path: path.resolve(__dirname, 'dist'), //distribution code }, };
In the project directory, create the directories and corresponding files for the entry point of our App, the public files, and the distribution files.
webpack-demo |- package.json |- webpack.config.js + |- /dist |- // distribution files will be generated + |- /public |- index.html //public files + |- /src |- App.js //entry point
The HtmlWebpackPlugin will generate a copy of ./public/index.html
and any other assets that are in the public
directory. index.html
will have all the webpack bundles in the HTML body
with their corresponding script tags.
Install HtmlWebpackPlugin:
npm install --save-dev html-webpack-plugin
Require html-webpack-plugin
on webpack.dev.js
and add a plugins
key array. Eventually, we are going to have multiple plugins in this array.
webpack.config.js
const path = require('path'); const HtmlWebpackPlugin = require('html-webpack-plugin'); module.exports = { entry: './src/App.js', output: { filename: '[name]-bundle.js', path: path.resolve(__dirname, 'dist'), }, plugins: [ new HtmlWebpackPlugin({ template: './public/index.html', }), ], };
To build
our dist
files on the command line type:
npx webpack --config webpack.config.js
After running webpack look in the dist
directory. We should have two files: main-bundle.js
and index.html
with <script type="text/javascript" src="main-bundle.js"></script>
Add Dev server
On the project directory, install the webpack-dev-server and save as a development dependency by typing on the command line:
npm install --save-dev webpack-dev-server
The DevServer
has many configurations. We are going to use contentBase: 'dist'
to serve static files on the dist
directory.
webpack.config.js
const path = require('path'); const HtmlWebpackPlugin = require('html-webpack-plugin'); module.exports = { entry: './src/App.js', output: { filename: '[name]-bundle.js', path: path.resolve(__dirname, 'dist'), }, devServer: { contentBase: 'dist', }, plugins: [ new HtmlWebpackPlugin({ template: './public/index.html', }), ], };
In the command line start the webpack dev server:
webpack-dev-server --config=webpack.config.js
"webpack-dev-server doesn't write any output files after compiling. Instead, it keeps bundle files in memory and serves them as if they were real files mounted at the server's root path."
Loaders
Webpack relies on a ecosystem of loaders to handle different file types. Loaders provide a way to preprocess files before they are bundled and are commonly use to transpile ES6, CSS, or any other Frontend asset.
On the webpack.config.js
file, add a new property called module
and inside add a rules
array. This is where we will add our loaders.
webpack.config.js
const path = require('path'); const HtmlWebpackPlugin = require('html-webpack-plugin'); module.exports = { entry: './src/App.js', output: { filename: '[name]-bundle.js', path: path.resolve(__dirname, 'dist'), }, devServer: { contentBase: 'dist', }, module: { rules: [ //loaders ], }, plugins: [ new HtmlWebpackPlugin({ template: './public/index.html', }), ], };
Babel Loader
Babel is a JavaScript compiler mainly used to convert ES6+ code into a backwards compatible version of JavaScript.
Install Babel:
webpack 4.x | babel-loader 8.x | babel 7.x
Babel loader allows webpack and Babel to transpile JS files, the @babel/preset-env loader is a full feature stack that allows us to use the latest JavaScript very easily. And finally we need Babel Core.
Let's install these three loaders and save them as a development dependency:
npm install --save-dev babel-loader @babel/core @babel/preset-env
Loader properties:
test
takes a regEx and will test every file we import into our project.use
tells webpack what loader to run.exclude
files to be ignored from our loader.options
can be passed to Babel in a variety of ways (we set our options via .babelrc - explained below)
webpack.config.js
module.exports = { //.. other settings module: { rules: [ { test: /\.js$/, use: { loader: 'babel-loader', }, exclude: /node_modules/, }, ], }, };
Next, create the .babelrc file. This is a hidden file that will contain some additional configuration and plugin rules that Babel will live by.
webpack-demo |- package.json |- webpack.config.js |- /dist |- main-bundle.js |- /public |- index.html |- /src |- App.js + |- .babelrc
We need two more plugins to complete our Babel setup. @babel/plugin-transform-runtime will help inject helper code that Babel will need for polyfills. And finally, @babel/runtime a runtime dependency.
npm install --save-dev @babel/plugin-transform-runtime
npm install --save @babel/runtime
.babelrc
{ "presets": [ "@babel/preset-env" ], "plugins": ["@babel/plugin-transform-runtime"] }
Now, we can write ES6+ code and know that is cross browser compatible. Check out Babel for more configuration options.
CSS Loader
Let's install css-loader to handle our CSS and style-loader which is responsible for injecting the css into our DOM.
npm install --save-dev style-loader css-loader
Let's add a configuration rule to handle our CSS
files. First, we need RegEx
to test for the file extension /\.css$/
. Loaders run from right to left. The order is important for these loaders to work properly. Under use
, add an array and chain the style-loader
and css-loader
.
webpack.config.js
module.exports = { //.. other settings module: { rules: [ //.. other settings { test: /\.css$/, use: ['style-loader', 'css-loader'], }, ], }, };
Under the ./src
directory, make a styles
folder and create the styles.css
file.
webpack-demo |- package.json |- webpack.config.js |- /dist |- main-bundle.js |- /public |- index.html |- /src |- App.js + |- /styles + |- styles.css |- .babelrc
Open styles.css
and add a simple rule:
body { background-color: red; }
Next, open App.js
and import styles.css
.
App.js
import '../styles/styles.css';
Make sure the webpack-dev-server
is running. On your browser DevTools inspect index.html
and the CSS
should be rendering in the <head>
of the document.
Sass
To use Sass instead of CSS
, we need to install the Sass Loader and node-sass
as a required dependency.
npm install --save-dev sass-loader node-sass
Update the RegEx, from /\.css$/
to /\.scss$/
to help the loader recognize this file extension. Add the sass-loader
last on the use
array. Keep the css-loader
, we still need it.
webpack.config.js
module.exports = { //.. other settings module: { rules: [ //.. other settings { test: /\.scss$/, use: [ 'style-loader', // creates style nodes from JS strings 'css-loader', // translates CSS into CommonJS 'sass-loader', // compiles Sass to CSS, using Node Sass by default ], }, ], }, };
Make sure to rename styles.css
to styles.scss
. Also, don't forget to update the import file's name on App.js
.
App.js
import '../styles/styles.scss';
Webpack Mode
Development and production builds have different priorities. During development we want: a localhost server, source mapping, live reloading, hot module replacement(HMR), or tooling for debugging. While in production, our build priorities shift towards: minified bundles, asset optimization, or omitting development-specific code. However, both development and production builds also share some commonality. Let's organize our webpack configuration into three files.
First, rename webpack.config.js
to webpack.common.js
for clarity. This file will contain the common configuration needed for both environments. Second, separate environment-specific configurations to webpack.dev.js
(development) and webpack.prod.js
(production) accordingly.
webpack-demo |- package.json - |- webpack.config.js + |- webpack.common.js + |- webpack.dev.js + |- webpack.prod.js |- /dist |- main-bundle.js |- /public |- index.html |- /src |- App.js |- /styles |- main.scss |- .babelrc
To merge webpack's configuration object from webpack.common.js
into the environment-specific configurations, we can use webpack merge.
npm install --save-dev webpack-merge
Define the environment Mode, and require webpack-merge
and ./webpack.common.js
for both environment-specific configurations accordingly.
Move the devServer
and the Sass
loader to webpack.dev.js
webpack.dev.js (Development)
const merge = require('webpack-merge'); const common = require('./webpack.common.js'); module.exports = merge(common, { mode: 'development', devServer: { contentBase: 'dist', }, rules: [ { test: /\.scss$/, use: ['style-loader', 'css-loader', 'sass-loader'], }, ], });
We'll start our prod configuration next.<br> webpack.prod.js (Production)
const merge = require('webpack-merge'); const common = require('./webpack.common.js'); module.exports = merge(common, { mode: 'production', });
webpack.common.js (Common configuration)
const path = require('path'); const HtmlWebpackPlugin = require('html-webpack-plugin'); module.exports = { entry: './src/App.js', output: { filename: '[name]-bundle.js', path: path.resolve(__dirname, 'dist'), }, module: { rules: [ { test: /\.js$/, use: { loader: 'babel-loader', }, exclude: /node_modules/, }, ], }, plugins: [ new HtmlWebpackPlugin({ template: './public/index.html', }), ], };
Package.json Scripts
Let's simplify how we are running our bundles from the command line by adding them to package.json
.
package.json
{ "scripts": { "start": "webpack-dev-server --config=webpack.dev.js", "build": "webpack --config=webpack.prod.js" } }
To start the development environment:
npm run start
Generate distribution files:
npm run build
CSS Production Optimization
First, let's extract our CSS from the header
and put it in its own separate file by using mini-css-extract-plugin.
npm install --save-dev mini-css-extract-plugin
We need the same Scss
loader rules that we have in webpack.dev.js
. Except, that on webpack.prod.js
we are replacing style-loader
with mini-css-extract-plugin
.
Create a plugins array. Webpack plugins usually work at the end of the bundle process. As opposed to loaders that work during or before the bundle is generated.
Inside the plugins
array, define a new
instance of MiniCssExtractPlugin
. View more configuration options.
webpack.prod.js
const merge = require('webpack-merge'); const common = require('./webpack.common.js'); const MiniCssExtractPlugin = require('mini-css-extract-plugin'); module.exports = merge(common, { mode: 'production', module: { rules: [ { test: /\.scss$/, use: [MiniCssExtractPlugin.loader, 'css-loader', 'sass-loader'], }, ], }, plugins: [ new MiniCssExtractPlugin({ filename: 'styles.css', chunkFilename: '[id].css', }), ], });
Optimize/minimize the CSS
Now that we have an external CSS file, we can optimize/minimize the CSS by using: optimize-css-assets-webpack-plugin.
npm install --save-dev optimize-css-assets-webpack-plugin
Import the optimize-css-assets-webpack-plugin
and add it to the plugins array. View Documentation for configuration options.
webpack.prod.js
const merge = require('webpack-merge'); const common = require('./webpack.config.js'); const MiniCssExtractPlugin = require('mini-css-extract-plugin'); const OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin'); module.exports = merge(common, { mode: 'production', module: { rules: [ { test: /\.scss$/, use: [MiniCssExtractPlugin.loader, 'css-loader', 'sass-loader'], }, ], }, plugins: [ new MiniCssExtractPlugin({ filename: 'styles.css', chunkFilename: '[id].css', }), new OptimizeCssAssetsPlugin({ assetNameRegExp: /\.css$/g, cssProcessor: require('cssnano'), cssProcessorOptions: { discardComments: { removeAll: true }, }, canPrint: true, }), ], });
Test the production build:
npm run build
A new styles.css
linked into index.html
should be in the dist
directory.
Clean up Build folder
To ensure that we have the latest and relevant files in our dist
directory after every successful rebuild install Clean plugin for webpack.
npm install --save-dev clean-webpack-plugin
Import CleanWebpackPlugin
and add it to the plugins array. View the documentation for configuration options.
webpack.prod.js
const merge = require('webpack-merge'); const common = require('./webpack.common.js'); const MiniCssExtractPlugin = require('mini-css-extract-plugin'); const OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin'); const CleanWebpackPlugin = require('clean-webpack-plugin'); module.exports = merge(common, { mode: 'production', module: { rules: [ { test: /\.scss$/, use: [MiniCssExtractPlugin.loader, 'css-loader', 'sass-loader'], }, ], }, plugins: [ new CleanWebpackPlugin(), new MiniCssExtractPlugin({ filename: 'styles.css', chunkFilename: '[id].css', }), new OptimizeCssAssetsPlugin({ assetNameRegExp: /\.css$/g, cssProcessor: require('cssnano'), cssProcessorOptions: { discardComments: { removeAll: true }, }, canPrint: true, }), ], });
Hooking up React
Let's create a simple React application to see how it is incorporated into a webpack configuration.
Install React:
npm install --save react react-dom
On the ./public/index.html
create a div
and add an id.
index.html
<!DOCTYPE html> <html> <body> <div id="root"></div> </body> </html>
Create the smallest React app in the world on App.js
import React from 'react'; import ReactDOM from 'react-dom'; ReactDOM.render(<h1>Hello, world!</h1>, document.getElementById('root'));
We need to install a JSX preset to our Babel configuration.
npm install --save-dev @babel/preset-react
Open .babelrc
and add the @babel/preset-react
preset.
.babelrc
{ "presets": ["@babel/preset-env", "@babel/preset-react"], "plugins": ["@babel/plugin-transform-runtime"] }
Restart the server, and our React app should be rendering.
npm run start
Vendor Splitting
Our current build
configuration outputs one bundle file that contains the code we write along with third party library code. This is not ideal.
Let's code split the code we write from third party libraries, and take advantage of browser caching to improve performance.
SplitChunksPlugin comes out of the box with webpack, and it can help us to effectively split bundles and prevent duplicated dependencies.
To take advantage of this plugin. On webpack.prod.js
add an optimization
key to our main module
object. View configuration options.
webpack.prod.js
module.exports = merge(common, { //.. other settings optimization: { splitChunks: { chunks: 'all', cacheGroups: { commons: { test: /[\\/]node_modules[\\/]/, name: 'vendors', chunks: 'all', }, }, }, }, //.. other settings });
Cache busting
Now that we have a bundle for our code and third party code, we can take advantage of browser cache by just adding [contenthash]
to our output: filename
.
contenthash
will generate a string of characters when any changes are detected on the corresponding bundle. This will help the browser identify new files. The html-webpack-plugin
that we installed at the beginning will continue keeping all the new files linked accordingly.
webpack.prod.js
module.exports = merge(common, { //.. other settings output: { filename: '[name].[contenthash]-bundle.js', path: path.resolve(__dirname, 'dist'), }, //.. other settings });
Images
We can use the file-loader to work with multiple image/graphic types in webpack.
The file-loader
resolves the file import/require()
into a url and it generates the appropriate asset into the output directory. Install the file-loader:
npm install --save-dev file-loader
Let's add a rule RegEx
to test for the following file formats: jpg, png, gif, or svg
. Under options we are passing an argument to set the output directory, name of the file with an eight character hash, and the file extension.
webpack.common.js
module.exports = { module: { rules: [ { test: /\.(jpe?g|png|gif|svg)$/, use: [ { loader: 'file-loader', options: { name: 'images/[name]-[hash:8].[ext]', }, 'image-webpack-loader' //optimizer } ], } ] }, };
Finally, we can automatically optimize our images by using image-webpack-loader
, view configuration options. Install the loader:
npm install image-webpack-loader --save-dev
Hey, I'm Ignacio Villamar
Senior Frontend Engineer, living in the NYC metro area.
Follow @ivstudio