Webpack is a module bundler. Its main purpose is to bundle JavaScript files for usage in a browser, yet it is also capable of transforming, bundling, or packaging. In this tutorial you will learn Webpack 4.30 from the basics to advanced!
Nowadays, we’re forced to use many accessory tools to facilitate, speed up and optimize our web development workflow. Often though, such tools add an extra layer of complexity into the stack. As a result, we need to utilize additional time and effort to learn, understand and use these tools correctly. The same is true for webpack.
When using Webpack 4.30 for the first time, it can be difficult to understand how it works and how it should be used. Although it has good documentation, it can be daunting for novices, and it has a steep learning curve. However, webpack is worth learning and can save considerable time and effort in the long run. In this tutorial, I’ll introduce all the core concepts to help you get started.
As its core, webpack is a static module bundler. In a particular project, webpack treats all files and assets as modules. Under the hood, it relies on a dependency graph. A dependency graph describes how modules relate to each other using the references (require and import statements) between files. In this way, webpack statically traverses all modules to build the graph, and uses it to generate a single bundle (or several bundles) — a JavaScript file containing the code from all modules combined in the correct order. “Statically” means that, when webpack builds its dependency graph, it doesn’t execute the source code but stitches modules and their dependencies together into a bundle. This can then be included in your HTML files.
Now, to expand the above cursory overview, let’s explore the main concepts webpack uses.
Webpack has some main concepts which we need to understand clearly before digging in its practical implementation. Let’s examine them one by one:
./src/index.js
, but we can specify a different module (or even multiple modules) in the webpack configuration file../dist/main.js
for the main bundle and ./dist
for other generated files — such as images, for example. Of course, we can specify different values in the configuration depending on our needs.In this section we’ll examine how webpack works. Even a simple project contains HTML, CSS and JavaScript files. Also, it can contains assets such as fonts, images, and so on. So, a typical webpack workflow would include setting up an index.html
file with the appropriate CSS and JS links, and the necessary assets. Also, if you have many CSS and JS modules which depend on each other, they need to be optimized and properly combined in one unit ready for production.
To do all this, webpack relies on configuration. Although webpack 4 comes with reasonable defaults, for any non-trivial project you’ll need to provide a special configuration file webpack.config.js
, which describes how the files and assets should be transformed and what kind of output should be generated. This file can quickly become quite monolithic, which makes it hard to understand how webpack does its job unless you know the main concepts behind its working.
Based on the provided configuration, webpack starts from the entry points and resolves each module it encounters while constructing the dependency graph. If a module contains dependencies, the process is performed recursively against each dependency until the traversal has completed. Then webpack bundles all project’s modules into a small number of bundles — usually, just one — to be loaded by the browser.
Note: you can find the files for our project in the GitHub repo.
Now that we have solid theoretical foundation, let’s implement it in practice.
To start, we’ll create a new directory and switch to it. Then we’ll initialize a new project:
mkdir learn-webpack
cd learn-webpack
npm init -y
Next, we need to install webpack and webpack CLI locally:
npm install webpack webpack-cli --save-dev
Now, the content of the generated package.json
should be similar to the following:
{
"name": "learn_webpack",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"webpack": "^4.30.0",
"webpack-cli": "^3.3.0"
}
}
Besides bundling modules, webpack can be used as a simple task runner. We can create webpack tasks by including the name of our task followed by its instructions in the scripts
section of the package,json
file. Let’s try this now. Open package.json
and change the scripts
object to the following:
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"dev": "webpack --mode development",
"build": "webpack --mode production"
},
Within the scripts
property, webpack allows us to reference locally installed npm packages by their names. We use that and the --mode
flag to define dev
and build
tasks, which will run webpack in development (npm run dev
) and production (npm run build
) mode respectively.
Before we test the tasks we’ve just created, let’s create a src
directory and put an index.js
file in it so that it contains console.log("Hello webpack");
. Now we can already to run the dev
task to start webpack in development mode:
$ npm run dev
> learn_webpack@1.0.0 dev C:\Users\User\Webpack\learn_webpack
> webpack --mode development
Hash: 5bb3bdc1efd7b7f4b627
Version: webpack 4.30.0
Time: 226ms
Built at: 2019-04-16 17:48:32
Asset Size Chunks Chunk Names
main.js 3.8 KiB main [emitted] main
Entrypoint main = main.js
[./src/index.js] 27 bytes {main} [built]
Great! It works as expected. But to verify that we get the correct output, we need to display the result in the browser. To do that, let’s create an index.html
file in the dist
directory:
Getting Started
Now, if we open the file in the browser, we should see the Hello webpack message in the console.
So far, so good. But writing our index.html
file manually can be problematic in some cases. For example, if we change the name of our entry point, the generated bundle will be renamed, but our index.html
file will still reference the old name. So, we’ll need to update our HTML file manually every time we rename an entry point or add new one. Fortunately, we can easily fix that with the html-webpack-plugin
. Let’s install it now:
npm install html-webpack-plugin --save-dev
At this point, to activate the plugin, we need to create a webpack.config.js
file in the root directory with the following content:
const HtmlWebpackPlugin = require("html-webpack-plugin");
module.exports = {
plugins: [
new HtmlWebpackPlugin({
title: "Webpack Output",
}),
],
};
As you can see, to activate a webpack plugin, we need to include it and then add it to the plugins
array. If needed, we also pass options to the plugin.
Let’s run our build now to see what will happen:
$ npm run build
> learn_webpack@1.0.0 build C:\Users\User\Webpack\learn_webpack
> webpack --mode production
Hash: e56a796f5ccfebcc8270
Version: webpack 4.30.0
Time: 1088ms
Built at: 2019-04-16 20:44:47
Asset Size Chunks Chunk Names
index.html 183 bytes [emitted]
main.js 956 bytes 0 [emitted] main
Entrypoint main = main.js
[0] ./src/index.js 27 bytes {0} [built]
Child html-webpack-plugin for "index.html":
1 asset
Entrypoint undefined = index.html
[2] (webpack)/buildin/global.js 472 bytes {0} [built]
[3] (webpack)/buildin/module.js 497 bytes {0} [built]
+ 2 hidden modules
Let’s open the index.html
. As we can see, the plugin automatically creates an updated index.html
file for us which uses the title option from the configuration:
Webpack Output
Let’s now expand our project and specify custom names for the entry
and output
properties. In webpack.config.js
we add the following before the plugins
property:
entry: './src/app.js',
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist')
},
Now, we’ll create an src/component.js
file:
export default (text = "Hello webpack") => {
const element = document.createElement("p");
element.innerHTML = text;
return element;
};
Next, we rename index.js
to app.js
to reflect our changes, and swap its content with the following:
import component from "./component";
document.body.appendChild(component());
Now, let’s run webpack in production mode:
$ npm run build
> learn_webpack@1.0.0 build C:\Users\User\Webpack\learn_webpack
> webpack --mode production
Hash: 9f78936f8a2a21061f0b
Version: webpack 4.30.0
Time: 1689ms
Built at: 2019-04-17 23:43:40
Asset Size Chunks Chunk Names
index.html 190 bytes [emitted]
main.bundle.js 1.04 KiB 0 [emitted] main
Entrypoint main = main.bundle.js
[0] ./src/app.js + 1 modules 227 bytes {0} [built]
| ./src/app.js 79 bytes [built]
| ./src/component.js 148 bytes [built]
Child html-webpack-plugin for "index.html":
1 asset
Entrypoint undefined = index.html
[2] (webpack)/buildin/global.js 472 bytes {0} [built]
[3] (webpack)/buildin/module.js 497 bytes {0} [built]
+ 2 hidden modules
Let’s examine and clarify the information from the webpack output. Beginning from the top, we see the hash of the build, webpack version, and the time it took to execute the build. Next, we see the files generated in the dist
directory (index.html
and main.bundle.js
). Below them, we see the entry module (app.js
) and its dependency (component.js
). The output after Child html-webpack-plugin for "index.html":
is related to the internal work of the html-webpack-plugin
and we can safely ignore it.
So now, in the dist
folder, we have the newly generated bundle file main.bundle.js
. If we open index.html
in the browser, we should see Hello webpack displayed on the page. Also, if we check the source of index.html
, we’ll see that the value of the src
property in the script
tag is updated to main.bundle.js
.
In this section, we’ll discover how ES6 can be transpiled to ES5-compliant code which works in all browsers. Let’s start by running the following command:
npm run dev -- --devtools false
Next, let’s open main.bundle.js
:
/***/ "./src/component.js":
/*!**************************!*\
!*** ./src/component.js ***!
\**************************/
/*! exports provided: default */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony default export */ __webpack_exports__["default"] = ((text = "Hello webpack") => {
const element = document.createElement("p");
element.innerHTML = text;
return element;
});
/***/ })
As you can see, the modern ES6 features (the arrow function and the const
declaration) from component.js
module are not transformed to ES5-compliant code by default. To make our code work in older browsers, we must add the Babel loader:
npm install babel-loader @babel/core @babel/preset-env --save-dev
Then, in webpack.config.js
add module
after the output
property:
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env']
}
}
}
]
},
When we define rules for a webpack loader, there are usually three main properties we need to define:
test
, which describes what kind of files should be transformed.exclude
, which defines the files which should not be processed from the loader(s), if we have such.use
, which tells which loader(s) should be used against the matched modules.Run the following command again:
npm run dev -- --devtools false
This time, the code in main.bundle.js
is compiled:
/***/ "./src/component.js":
/*!**************************!*\
!*** ./src/component.js ***!
\**************************/
/*! exports provided: default */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony default export */ __webpack_exports__["default"] = (function () {
var text = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : "Hello webpack";
var element = document.createElement("p");
element.innerHTML = text;
return element;
});
/***/ })
Perfect. Now we can use the modern JS features, and webpack will transform our code so it can be executed by older browsers.
In this section, we’ll see how we can add some styles to our project. To do this, we need to install two loaders:
npm install css-loader style-loader --save-dev
css-loader
parses the CSS into JavaScript and resolves any dependencies, style-loader
outputs our CSS into a `` tag in the HTML document.
Let’s add the necessary configuration in webpack.config.js
:
{
test: /\.css$/,
use: [{ loader: 'style-loader' }, { loader: 'css-loader' }],
},
Here, the order of loaders is important. They’re evaluated in reverse order — that is, from right to left and from bottom to top.
Now, let’s create a file src/style.css
:
p {
color: red;
}
Then we import it into app.js
:
import './style.css'
When we run webpack and then open the index.html
, we should see the Hello webpack message in red.
Most often your project will contain assets such as images, fonts, and so on. Here we’ll explore an example with images. First, we’ll need to install a file loader:
npm install file-loader --save-dev
Next, we’ll add new rule in the webpack.config.js
:
{
test: /\.(png|jpg|gif)$/,
use: [
{
loader: 'file-loader'
},
],
},
Now, to test the loader we’ll create an image-component.js
file, in the src
directory, with the following content:
import image from "./image.png"
const img = document.createElement("img")
img.src = image
document.body.appendChild(img)
Here, we import our image as a module and use it to create an `` tag. We need to put that image in the src
directory.
The next thing is to import our image component in app.js
:
import './image-component'
And voila. Now, when we run webpack and open the page, we should see the image above the Hello webpack message.
webpack-dev-server
Currently, we need to rebuild our code every time we make a change. Fortunately, webpack offers a live-reloading web server which automatically builds and refreshes the page. To install it:
npm install webpack-dev-server --save-dev
We need to update our dev
script, in package.json
, to use the server:
"dev": "webpack-dev-server --mode development"
Now let’s configure the server in webpack.config.js
by adding the following property:
devServer: {
contentBase: './dist',
open: true
},
This tells webpack-dev-server
to serve the files from the dist
directory and to open the entry page automatically.
Now, if we run webpack (npm run dev
), we should see how the page is automatically opened in the browser on localhost:8080
:
i 「wds」: Project is running at http://localhost:8080/
i 「wds」: webpack output is served from /
i 「wds」: Content not from webpack is served from ./dist
If we now change any of the source files and save them, the web server will automatically reload the page after the code has been compiled. Try to change the color property in our CSS file to green, for example, and you should see how the color is updated appropriately in the page.
As our project progress, the dist
folder might become quite cluttered. On every build, webpack will generate the bundles and put them in the dist
folder, but it doesn’t keep track of which files are actually in use by your project. So it’s good practice to clean the dist
folder before each build, so that only the files in use will be generated. To do this, we need to install and configure the clean-webpack-plugin
:
npm install --save-dev clean-webpack-plugin
In webpack.config.js
:
const CleanWebpackPlugin = require('clean-webpack-plugin');
...
plugins: [
...
new CleanWebpackPlugin()
],
Now, run webpack (npm run build
) and inspect the dist
folder. You should now only see the files generated from the build without old and unused files. In our case, the file which should be deleted is main.js
.
Webpack is a useful and powerful tool. This tutorial introduces only the core concepts, but webpack offers many more features, plugins, and different techniques to apply them, which you can adopt as your knowledge grows.
#webpack #web-development #javascript