Introduction
In this article, I will introduce the core concepts with Webpack in a practical easy to follow manner. I will explain setting up the webpack configuration file from scratch and what each of the configurations such as entry point, CSS, loaders, plugins mean.
What many people may not realize is that Webpack is what runs under the hood when you are bootstrapping an Angular or a React project with an angular-cli or create-react-app.
I have written this article as a follow-along. You could get the entire code for the webpack starter described in the article from the Github repository.
What is Webpack?
Webpack is a static module bundler for modern JavaScript applications. When webpack processes your application, it internally builds a dependency graph that maps every module your project needs and generates one or more bundles.
The illustration shown below can explain it in a much simpler way as to what Webpack actually does.
How does Webpack help?
Let us take an example of any web application. It typically comprises of an index.html and within it referencing many script tags as shown below.
<body>
...
<script src='src/blog.js'></script>
<script src='src/about.js'></script>
<script src='src/contact.js'></script>
<script src='src/index.js'></script>
</body>
While the above way of including script tags within an HTML page works, it also presents a couple of challenges such as:
- The script tags need to be included in a certain order. This is necessary, so that, the script referencing a function inside another script is loaded before itself. In the above example, about.js, contact.js, and blog.js must be loaded into the browser before index.js, since index.js is most likely to refer a function within each of the other scripts.
- In addition, the above method is error-prone to typos.
Webpack precisely solves this problem and by using a bundler, you do not have to worry about including every script tag within your index.html and certainly not worry about the order.
<body>
...
<script src='dist/index_bundle.js'></script>
</body>
Module bundling is just one aspect that Webpack solves. But, certainly, it is much much more powerful in terms of having the power to apply transformations to your HTML, CSS, and JS files before including them in the bundler. Let us jump right into how to install and set up Webpack.
Installing Webpack
To start with, you'll need to install the following two packages to use Webpack.
//Create a new folder
$mkdir webpack-example
//Initialize a new NPM projects (Creates a package.json with default values)
> webpack-example$npm init -y
//Include the packages webpack and webpack-cli as dev dependencies
> webpack-example$npm install webpack webpack-cli --save-dev
Things to Note:
- If you wish to follow along, please create a new folder. Open your terminal, cd into the new folder, and run the above set of command.
- The option '—save-dev' adds the packages into your package.json as a dev dependency. What this means is that these packages will not be included with your final production build.
The Configuration File
The file webpack.config.js is the main place where most of the action happens. It is where you will provide a set of instructions to the 'Webpack' to let it know what to do with your project files and how to bundle them in a way you would like to consume.
Note: As per Webpack official docs, as of Webpack 4.0.0, you don't need a configuration file to set up the bundler. However, the configuration file is probably the most important part of Webpack that you'll need to customize as per your project needs.
We will cover the following core concepts of the Config file:
- Entry
- Loaders
- Output
- Plugins
- Mode
Before, we begin looking at the concepts, first create the webpack.config.js at the root structure of your project.
> webpack-example$touch webpack.config.js
Webpack does the following in the specified order:
- How do I locate the files that I need to bundle? Or optionally apply transformations on?
- What do I need to do once I access those files? Do I need to apply any specific transformations?
- Where do I need to output (save) the bundle generated by me?
The Entry Point
The single file that kicks off everything is typically the entry point for your Webpack. It is generally an 'index.js' or an 'app.js'.
You can visualize the import structure shown below, as a sort of how the Webpack creates the dependency graph.
index.js
imports about.js
imports contact.js
imports blog.js
imports util.js
imports api.js
Let us create the 'index.js' within the app folder.
> webpack-example$mkdir app
> webpack-example$cd app
> webpack-example$touch index.js
Let us add the entry point to the webpack.config.js
module.exports {
entry: './app/index.js'
}
Loaders
Now that we've set up the entry point, the next thing to tell our Webpack is what should it do with the files within our project. In other words, what kind of transformations that need to be applied to our files.
To do that, we have something called the 'loaders'. By default, Webpack looks at all the JSON and JS files to build the dependency graph as shown above.
import contact from './app/config' // 👍
import config from './utils/config.json' // 👍
import './css/app.css' // ❌
In the above, the CSS import will be ignored by Webpack and Loaders is precisely what we need here to assist Webpack to help process files other than JS and JSON.
Let us look at how to the following steps to add a CSS Loader.
> webpack-example$npm install css-loader --save-dev
We will need to add the loaders within the property 'modules.rules' array. Webpack will look at the rules array, to determine the loaders setup and the associated rules for each file type.
module.exports = {
entry: './app/index.js',
module: {
rules: []
}
}
We will need to specify the loader properties within the rules array. Every loader has 2 attributes that need to be defined:
- use - The name of the loader.
- test - The regex to match the file path.
module.exports = {
entry: './app/index.js',
module: {
rules: [
{ test: /\.css$/, use: 'css-loader' }
]
}
}
Now, if we use a CSS anywhere within our project, Webpack will recognize it through the help of loaders and import it within our project. Basically, the above CSS import statement which had a ❌ will now have a 👍.
import contact from './app/config' // 👍
import config from './utils/config.json' // 👍
import './css/app.css' // 👍
DOM Injection
While we have succeeded in importing a CSS file using webpack configuration, there's one more thing that needs to be done. We will need to inject the style elements into the DOM.
In simple words, the CSS style needs to be included as a 'Style' tag within the HTML.
To help us achieve that, we will need to use the 'style-loader'.
> webpack-example$npm install style-loader --save-dev
And modify the webpack.config.js to add the 'style-loader' to modules.rule array.
module.exports = {
entry: './app/index.js',
module: {
rules: [
{ test: /\.css$/, use: [ 'style-loader', 'css-loader' ] }
]
}
}
Things to Note:
- Since there were 2 variables to be used within the 'use' property, we changed it to an array.
- The order of mentioning the loader is important, since, webpack will process them in the reverse order. So, the 'css-loader' will first interpret the import command and then the 'style-loader' will inject the CSS into the DOM.
There are a lot more things a loader can do and a very popular example is the use of Babel. Babel is used to transforming every JavaScript file into the appropriate ES syntax based on the configuration specified in the webpack.config.js.
You can check the full list of loaders over here..
Output
As the name suggests, this configuration parameter simply tells the Webpack where to output the bundle that it creates.
//Import 'path' to resolve the file path
const path = require('path');
//Add this configuration after module.rules config
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'index_bundle.js'
}
Things to Note:
- We are importing the 'path' package to help with resolving the file path.
- The output configuration requires the path parameter to inform the webpack as to where to save the bundle file. In addition, using the filename parameter, you can specify the 'name' of the bundle that is generated.
Quick Recap
So until this point, we have seen the entry, loader, and output configurations. Combining all the configuration definitions so far, the webpack.config.js looks like this:
//Import 'path' to resolve the file path
const path = require('path')
module.exports = {
entry: './app/index.js',
module: {
rules: [
{ test: /\.css$/, use: [ 'style-loader', 'css-loader' ] }
]
},
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'index_bundle.js'
}
}
A Quick Recap:
- Webpack finds out the entry point located at
./app/index.js
. - It examines all the
import
andrequire
statements within the project and creates a dependency graph. - Then it starts creating a bundle, whenever it comes across a path we have a loader for, it transforms the code according to that loader then adds it to the bundle.
- Finally, It bundles and outputs it the location mentioned in the configuration which is at
dist/index_bundle.js
.
Hope you are able to follow up to this point. We have two more configurations 'plugins' and 'mode' to cover. So, just hang tight, we are almost there.
Plugins
While loaders are used to transform certain types of modules, plugins can be leveraged to perform a wider range of tasks like bundle optimization, asset management, and injection of environment variables.
Plugins allow you to execute certain tasks after the bundle has been created. Because of this, these tasks can be on the bundle itself or simply applied to the source codebase.
Let us look at two examples of such Plugins:
HtmlWebpackPlugin
Earlier we saw that the main benefit of webpack was that it would generate a single bundle for us that we could then use to reference inside of our main index.html
page.
What HtmlWebpackPlugin
does is that it will generate the index.html
page for us, stick it inside of the same directory where our bundle is put, and automatically include a <script>
tag which references the newly generated bundle.
As always, the first step involves downloading the plugin from npm.
> webpack-example$npm install html-webpack-plugin --save-dev
Next, we add a plugins property which is an array to our webpack config.
//Import the HtmlWebpackPlugin
const HtmlWebpackPlugin = require('html-webpack-plugin')
//Add this to the module.exports configuration
plugins: [
new HtmlWebpackPlugin()
]
Now your entire webpack.config.js will look like this:
//Import 'path' to resolve the file path
const path = require('path')
//Import the HtmlWebpackPlugin
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
entry: './app/index.js',
module: {
rules: [
{ test: /\.css$/, use: [ 'style-loader', 'css-loader' ] }
]
},
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'index_bundle.js'
},
plugins: [
new HtmlWebpackPlugin()
]
}
HtmlWebpackPlugin is one of the most basic plugins available, you can check out the rest of the plugins offered by Webpack over here.
MiniCssExtractPlugin
MiniCssExtractPlugin is the second example of a plugin that we will look at. If you remember from the earlier example, we had used a Style-loader, but using this plugin we can completely separate the CSS file instead of inserting only the style elements of the CSS.
As usual, we will start by installing the dependency as the first step:
> webpack-example$npm install --save-dev mini-css-extract-plugin
Add the following to webpack.config.js file:
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
plugins: [
new MiniCssExtractPlugin(),
],
And last but not the least, we will replace the style-loader with MiniCssExtractPlugin.loader:
{
test: /\.css$/,
use: [
MiniCssExtractPlugin.loader, // instead of style-loader
'css-loader'
]
}
Now when you run the webpack, it will output the main.css file in the dist folder and will be referenced from the index.html file using the link header.
Mode
Using the mode parameter you can enable webpack's built-in optimizations based on the value 'development', 'production', or 'none'. The default value is 'production'.
mode: 'production' //Other values include 'development' or 'none'
An example of build optimization run by Webpack when the mode is set to Production is that it'll minify the code and strip out warnings. For a detailed list of optimizations run behind the scenes, you can read it over here.
Running Webpack
If you've followed along, your webpack.config.js at this point should look like this:
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
entry: './app/index.js',
module: {
rules: [{ test: /\.css$/, use: ['style-loader', 'css-loader'] }],
},
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'index_bundle.js',
},
plugins: [new HtmlWebpackPlugin()],
mode: 'production',
};
Let us now proceed to run Webpack. First, we'll need to add the following configuration to 'package.json'.
"scripts": {
"build": "webpack"
}
Now run the following command from your command terminal.
> webpack-example$npm run build
Webpack will execute and create an optimized bundle named index_bundle.js and put it inside of the dist directory.
You should see the final index.html created within the 'dist' folder.
Webpack DevServer
Webpack DevServer is a development server for webpack, that'll keep track of your files in memory and serve them via a local server instead of generating a dist directory.
But the best part is that it supports live reloading. What that means is whenever you make a change in your code, webpack-dev-server will quickly recompile your code and reload the browser with those changes.
We'll need to install the package from npm.
> webpack-example$npm install webpack-dev-server --save-dev
Then we will need to update the "scripts" attribute in the package.json to run webpack-dev-server.
"scripts": {
"start": "webpack-dev-server"
}
Also, we'll make a small change to our 'mode' parameter in the webpack.config.js to default to 'development' if 'production' is not specified in the environment variable.
mode: process.env.NODE_ENV === 'production' ? 'production' : 'development'
//If you’re on Windows, then the command is as follows:
"SET NODE_ENV='production' && webpack"
That's it! If you run 'npm start' from the terminal then your project will be served at localhost:8080.
> webpack-example$npm start
The complete project with customization is available in the following Github Repository.
Conclusion
While this turned out to be a long article, to be truthful, we have barely touched the surface of Webpack and the endless possibilities of the configuration it supports.
To summarize, we covered the following in this article:
- What is Webpack and What benefit it provides?
- The core components of Webpack:
- Entry Point
- Loaders
- Output
- Plugins
- Mode
- Running Webpack
- In Local & Production Mode
I hope you found the article useful. If you find my articles interesting, please do not forget to subscribe to my newsletter.