Getting started with webpack - Part 5: Introduction to plugins

Introduction

In the previous part of the series, we learned how to use Babel to compile JavaScript files written in ES6+ syntax. This will prove useful especially if we are trying to keep our codebase up to date but still supporting older browsers, which may not yet have ES6+ support.

In this part of the series, we will dig deeper into webpack to see what else is possible. We will specifically try to use webpack plugins. We will demonstrate how you can use plugins to make your webpack build better.

Let’s get started.

NOTE: Source code of the application is available on GitHub.

Prerequisites

To follow along in this series, you need the following requirements:

  • Completed all previous parts of the series.
  • Basic knowledge of JavaScript.
  • Basic knowledge of the CLI.
  • A text editor. VS Code is recommended.
  • Node.js (>=6.11.5) and npm installed locally.

Let’s continue with the series.

Getting started with webpack plugins

At its core, webpack has very good support for plugins. The bundler itself has many internal components that are plugins. This means webpack itself makes use of the plugin system. What can plugins do?

Well it’s really up to the imagination what the plugins are capable of doing. As seen from their documentation, there are a lot of plugins available that do different things when activated. Let’s see the syntax for adding plugins to webpack.

Plugins are usually installed using npm and then added to the webpack configuration file so they can be used as a part of webpack’s bundling process. Here is an example of a webpack plugin being activated:

1var HtmlWebpackPlugin = require('html-webpack-plugin');
2    var path = require('path');
3    
4    module.exports = {
5      entry: 'index.js',
6      output: {
7        path: path.resolve(__dirname, './dist'),
8        filename: 'index_bundle.js'
9      },
10      plugins: [new HtmlWebpackPlugin()]
11    };

In the code above, we are importing the html-webpack-plugin at the top and then in the plugins array, we are initializing the plugin. All plugins you intend to add to webpack have to be added to this array so they can be called when webpack is processing files. For example, if there are multiple plugins you would have something like this:

1module.exports = {
2       // [...]
3       
4       plugins: [
5         new HtmlWebpackPlugin(),
6         new SomeOtherPlugin(),
7         new YetAnotherPlugin(),
8      ]
9       
10       // [...]
11     }

Now that we know how to add and activate webpack plugins, you should also know that you can, based on the environment, load or ignore certain plugins. The webpack configuration file is just a JavaScript file, and thus, we can write pure JavaScript in it.

In this file, we have access to the process.env property. The process.env property returns an object containing the user environment. With this, we can easily access the NODE_ENV property and check what environment webpack is running on.

This is useful because we can decide to activate some plugins when building for the production environment and omit them when building for the development environment.

Conditionally loading plugins

As mentioned earlier, we can use the value from the process.env.NODE_ENV to conditionally load the plugins for webpack. Let’s see how this will look:

1var HtmlWebpackPlugin = require('html-webpack-plugin');
2    var path = require('path');
3    
4    module.exports = {
5      entry: 'index.js',
6      output: {
7        path: path.resolve(__dirname, './dist'),
8        filename: 'index_bundle.js'
9      },
10      plugins: []
11    }; 
12    
13    // Load this plugin only when running webpack in a production environment
14    if (process.env.NODE_ENV == 'production') {
15      module.exports.plugins.push(
16        new HtmlWebpackPlugin()
17      )
18    }

As seen above, we have an if statement that checks which environment webpack is run on and if it is development, we can then load the HtmlWebpackPlugin. This works with custom environments also not just development and production. For example, we can have this:

1// [...]
2    
3    if (process.env.NODE_ENV == 'staging') {
4      module.exports.plugins.push(
5        new HtmlWebpackPlugin()
6      )
7    }

Above, we are now only loading the HtmlWebpackPlugin if the environment is staging. You can manually set the environment webpack is running on by appending the following during the build command:

    NODE_ENV=environment

For example, to run in production environment, you can run the following command:

    $ NODE_ENV=production npm run build

This will first set the NODE_ENV to production and then it’ll run the command to build using the environment we specified.

Adding plugins to our project

For this part, we will be building off the code in part four of the series. If you don’t have it already, you can download the project code from GitHub. We will be using the code there as a base for the modifications we are going to make going forward. When you have downloaded the project, open Part-4 in your code editor and follow along.

Before we get started, run the following command in the root of the project to install the npm dependencies:

    $ npm install

To get started, we need to decide the plugins we want to use and then install them using npm. After installing them, we will activate them in the webpack configuration file. We will conditionally load some of the plugins to demonstrate how you could do this in a real app.

The plugins we will use are as follows:

Let’s start adding the plugins one after the other.

UglifyJS webpack plugin

The first plugin we want to install and activate in the project is the UglifyJS plugin. We will be using this plugin to minify the outputted JavaScript. To install this package, run the following command in your terminal:

    $ npm install --save-dev uglifyjs-webpack-plugin

When the installation is complete, you can then open the webpack.config.js file and replace the contents with the following code:

1const webpack = require('webpack');
2    const path = require('path');
3    const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
4    
5    const env = process.env.NODE_ENV;
6    
7    module.exports = {
8      mode: env == 'production' || env == 'none' ? env : 'development',
9      entry: path.resolve(__dirname + '/src/index.js'),
10      output: {
11        path: path.resolve(__dirname + '/dist/assets'),
12        filename: 'bundle.js'
13      },
14      module: {
15        rules: [
16          {
17            test: /\.scss$/,
18            use: ['style-loader', 'css-loader', 'sass-loader']
19          },
20          {
21            test: /\.js$/,
22            exclude: /node_modules/,
23            loader: 'babel-loader'
24          }
25        ]
26      },
27      plugins: [],
28      optimization: {
29        minimizer: []
30      }
31    };
32    
33    if (env === 'production') {
34      module.exports.optimization.minimizer.push(new UglifyJsPlugin());
35    }

Above, we imported the UglifyJsPlugin, then we updated the mode config property to be automatically set depending on the NODE_ENV set when running webpack. To activate the plugin however, we are not using the plugins array because webpack has an optimization property with a minimizer array. This is the place where we added our Uglify plugin.

Before activating the Uglify plugin though, we check if webpack is being run for a production environment.

Since we now have a production environment, let’s update the scripts in our package.json file to support production builds. Open the package.json and replace the code in it with the following:

1// File: package.json
2    {
3      "name": "webpack-part-5",
4      "version": "1.0.0",
5      "description": "",
6      "main": "index.js",
7      "scripts": {
8        "build": "webpack",
9        "prod": "NODE_ENV=production webpack",
10        "watch": "npm run build -- --watch"
11      },
12      "keywords": [],
13      "author": "",
14      "license": "ISC",
15      "devDependencies": {
16        "@babel/core": "^7.2.2",
17        "@babel/preset-env": "^7.2.0",
18        "babel-loader": "^8.0.4",
19        "css-loader": "^2.0.0",
20        "node-sass": "^4.11.0",
21        "sass-loader": "^7.1.0",
22        "style-loader": "^0.23.1",
23        "webpack": "^4.26.1",
24        "webpack-cli": "^3.1.2",
25        "uglifyjs-webpack-plugin": "^2.0.1"
26      },
27      "dependencies": {
28        "express": "^4.16.4"
29      }
30    }

If you are using a Windows machine, read here before continuing.

Above, we have added the new prod build script. This makes it so we are running webpack in a production environment anytime we run the command below:

    $ npm run prod

To build our script and see if it works, run the following command:

    $ npm run prod

This should generate a minified JavaScript file in dist/assets/bundle.js. If you run the other command:

    $ npm run build

Then you should see an unminified JavaScript bundle file.

Next, let’s install the other dependencies:

    $ npm install --save-dev mini-css-extract-plugin optimize-css-assets-webpack-plugin

When the installation is complete, open the webpack.config.js file and replace the contents with the following code:

1// File: ./webpack.config.js
2    const webpack = require('webpack');
3    const path = require('path');
4    const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
5    const MiniCssExtractPlugin = require('mini-css-extract-plugin');
6    const OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin');
7    
8    const env = process.env.NODE_ENV;
9    
10    module.exports = {
11      mode: env == 'production' || env == 'none' ? env : 'development',
12      entry: path.resolve(__dirname + '/src/index.js'),
13      output: {
14        path: path.resolve(__dirname + '/dist/assets'),
15        filename: 'bundle.js'
16      },
17      module: {
18        rules: [
19          {
20            test: /\.scss$/,
21            use: [MiniCssExtractPlugin.loader, 'css-loader', 'sass-loader']
22          },
23          {
24            test: /\.js$/,
25            exclude: /node_modules/,
26            loader: 'babel-loader'
27          }
28        ]
29      },
30      plugins: [
31        new MiniCssExtractPlugin({
32          filename: '[name].css',
33          chunkFilename: '[id].css'
34        }),
35        new OptimizeCssAssetsPlugin({
36          cssProcessorPluginOptions: {
37            preset: ['default', { discardComments: { removeAll: true } }]
38          }
39        })
40      ],
41      optimization: {
42        minimizer: []
43      }
44    };
45    
46    if (env === 'production') {
47      module.exports.optimization.minimizer.push(
48        new UglifyJsPlugin()
49      );
50    }

Above, we have added both the MiniCssExtractPlugin and the OptimizeCssAssetsPlugin to the webpack configuration. These two plugins will make sure the CSS is extracted to its own file and minified so it has a smaller size.

Now let’s see if our plugins work. Open your terminal and run the following command:

    $ npm run prod

If all goes well, you should have a new main.css file in the dist/assets folder. The CSS should be minified.

Quick gotcha

Webpack comes with uglification bundled automatically. When you set the mode to production, all JavaScript code will be minified automatically. With this knowledge, you can remove all the references to the UglifyJsPlugin in the code as we do not need it.

Previewing our application

To preview the app with the new changes, we need to update a few files. Open the dist/index.html file and replace the contents with the following:

1<!DOCTYPE html>
2    <html lang="en">
3      <head>
4        <meta charset="UTF-8" />
5        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6        <meta http-equiv="X-UA-Compatible" content="ie=edge" />
7        <title>Webpack Sample</title>
8        <link rel="stylesheet" href="/assets/main.css" />
9      </head>
10      <body>
11        <div class="container"><h1></h1></div>
12        <script src="./assets/bundle.js"></script>
13      </body>
14    </html>

In the code above, we just added a link to the CSS file that we know will be generated by webpack.

Next, open the dist/server.js and replace the contents of the file with the following:

1const express = require('express');
2    const app = express();
3    const port = 3000;
4    const path = require('path');
5    
6    app.get('/assets/bundle.js', (req, res) => res.sendFile(path.join(__dirname + '/assets/bundle.js')));
7    
8    app.get('/assets/main.css', (req, res) => res.sendFile(path.join(__dirname + '/assets/main.css')));
9    
10    app.get('/', (req, res) => res.sendFile(path.join(__dirname + '/index.html')));
11    
12    app.listen(port, () => console.log(`Example app listening on port ${port}!`));

Above, we added a new route to handle the serving of the CSS asset file. That’s all. Now run the following command to start the Node.js server:

    $ node dist/server.js

When the app is running, you can see it at http://localhost:3000.

Conclusion

In this part of the series, we have learned how use plugins in webpack. We also learned about optimizations in webpack and how we can use some plugins as minimizers. However, webpack is a lot more powerful than this. We will dive a little deeper in the next part.

The source code to this application is available on GitHub.