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

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:

  1. test takes a regEx and will test every file we import into our project.
  2. use tells webpack what loader to run.
  3. exclude files to be ignored from our loader.
  4. 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