Back in the days when modularity in Javascript was introduced, there was no way to support modules within web browsers. To fix this issue, module bundlers such as Parcel, Webpack, and Rollup were developed. These helped to optimize multiple modules into a production-ready bundle, which can be executed by browsers.
Today, when it comes to the ecosystem of module bundling, the tool that resides the most on everyone’s lips is Webpack.
For me, webpack used to be quite intimidating to deal with, I would rather run to tools like vue-create
if I needed to set up a Vue project or create-react-app
to set up a React app, just to avoid all the complexities I felt while working with webpack.
If you are just the way I used to be, that is, you feel like setting up webpack from scratch with Babel, SASS, Vue, or TypeScript can be pretty confusing, then this guide is for you!
In this guide, you will learn how to create a frontend environment using Webpack, and like all things of life, the moment you give yourself the chance to learn and make sure to understand a concept thoroughly, you will realize that it is not too scary after all.
With lots of moving parts to it, we will learn various fundamental concepts to get you familiar with webpack and build a webpack boilerplate to help you get up and running when setting up any project from scratch with the widely popular module bundler.
What is Webpack?
Regardless of how complex webpack might come off to be, its core goal remains very straightforward; it takes a bunch of different assets, different files - of different types; JavaScript, images, SASS, PostCSS, whichever file it may be, and it combines them altogether (bundles them) into a smaller group of files, perhaps one file, or it could be one file for your JavaScript, one file for stylesheets, one for third-party JavaScript, and one for the index JavaScript file.
It is very configurable, and this is where it turns out to be a bit tedious to people who are new to it, however, the idea behind it is very simple. In addition to just bundling things together, it also manages dependencies of an application, thus, making sure that codes that need to load first - load first, in the same way, if for example, you write a file that depends on two other files, it means that those two other files need to be loaded first.
As per the official GitHub page:
"A bundler for javascript and friends. Packs many modules into a few bundled assets. Code Splitting allows for loading parts of the application on demand. Through "loaders", modules can be CommonJs, AMD, ES6 modules, CSS, Images, JSON, Coffeescript, LESS, ... and your custom stuff."
There is just so much to Webpack, and at this point in this guide, we are about to dive into getting familiar with core Webpack concepts.
Project Setup
In your preferred directory for this tutorial, create a new folder: webpack-app
, for housing the webpack project.
$ mkdir webpack-app
Next, let us initialize Node.js in the project folder by running the following commands in the terminal, from the project directory:
$ cd webpack-app
$ npm init --yes
This will create a package.json
file which makes it possible and easy to track application dependencies.
Now that Node is initialized in our project, we can begin to install the various packages that we will need. To proceed, let us install the core packages: webpack
and webpack-cli
.
$ yarn add --dev webpack webpack-cli
# Or
$ npm install --save-dev webpack webpack-cli
The webpack
package would allow us to produce bundled JavaScript modules for usage in a browser, while the webpack-cli
is a tool that provides a set of commands that make it easy for developers to set up a custom webpack project quickly.
To begin, let us create two new folders in the root directory src
and dist
. In the src
folder, create an index.js
file that would serve as the root file for our application.
Note: Out of the box, webpack doesn’t require a config file. It automatically assumes that the entry point of a project is the src/index.js
and it will output a minified and optimized result for production in the dst/main.js
file.
Configuring the Project
Next, in the root of our project directory, let us set up the configuration file webpack.config.js
to define how bundling should take shape within the project:
$ touch webpack.config.js
Mode
The mode defines the environment where the project currently operates. Its property has a default of none
, but can be set to either production
or development
, but you’d never actually use none
as a mode. In fact, what happens when the mode remains unset, webpack uses production
as a fallback option for optimization.
With development
set as the mode, we easily use source mapping to spot errors and know where the original files were.
With production
set as the mode, the priority is to shrink, optimized, and minify files:
const path = require("path");
module.exports = {
mode: "development", // It can also be production
};
Entry
Another configuration to set is the entry object. This defines what file we want our webpack to start building our application bundle from. If it is a Single Page Application, then it should have one entry point, while if it is a Multi-Page Application, it should have multiple entry points.
Of course, in a Single Page Application, that would be the index JavaScript file inside the src
folder, and in our case, it is the src/index.js
file:
const path = require("path");
module.exports = {
/* ... */
entry: {
main: path.resolve(__dirname, "src/index.js"),
},
};
Output
Finally, for a start, we will also be needing an output to declare where we want to output the files, assets, bundles, and other application resources after the application bundling processes have been completed. In our case, we set the path
for output to be the dist
folder, but you can rename it if you wish, perhaps to deploy
, build
, or whatever name you prefer.
We also need to set the filename
to define the name of the output bundle. A common name convention is [name].bundle.js
:
const path = require("path");
module.exports = {
/* ... */
output: {
path: path.resolve(__dirname, "dist"),
filename: "index.bundle.js", // using static name
// OR
// using the entry name, sets the name to main, as specific in the entry object.
filename: "[name].bundle.js",
},
};
We have now set up the minimum configuration required to bundle an application, that we can run through package.json
To achieve that, we need to update the predefined script object in package.json
. Let us create a new command called build
, which would have webpack
as its value. With this command saved, we can now run webpack.
But before we run the build
command, let us quickly include a few lines of JavaScript code in the src/index.js
file:
// src/index.js
console.log("Hello world! My name is Uche Azubuko.");
Next, we run webpack with the following command, from the terminal:
$ yarn build
## OR
$ npm run build
This outputs:
$ webpack
asset main.bundle.js 1.24 KiB [emitted] (name: main)
./src/index.js 54 bytes [built] [code generated]
webpack 5.75.0 compiled successfully in 98 ms
Done in 0.94s.
Essentially, when we run the build
command, webpack
is the command that is run under the hood; the configuration settings we defined would be read and the entry point, that is app.js
gets bundled, read through, and output as main.bundle.js
in the dist
folder.
If we keep building various points, we would get various names, but if we set the filename
to [name].[contenthash].js
, we would be sure to get a unique name for the compiled file, each time our application bundle is completed.
Now, when we build app again, we get a unique name for the file that is output into the dist
folder, such as main.96f31f8d4c5ec9be4552.js
:
$ yarn build
# Or
$ npm run build
This outputs:
$ webpack
asset main.96f31f8d4c5ec9be4552.js 1.24 KiB [emitted] [immutable] (name: main)
./src/index.js 54 bytes [built] [code generated]
webpack 5.75.0 compiled successfully in 88 ms
Done in 0.92s.
This way, the browser is able to cache application resources per build time, since there are unique names each time the app was built (more like version management for each build file).
This makes it possible for web applications and websites to have faster load time and unnecessary network traffic, however, when changes are made to the content, it could cause some difficulties since webpack doesn’t now know which file is needed or not needed.
Thus, the best thing to do is to clean up the dist
directory each time an application bundling process gets completed.
The clean-up can be achieved by setting the clean
option in the output
object to true
, so that webpack.config.js
is updated:
const path = require("path");
module.exports = {
mode: "development",
entry: {
main: path.resolve(__dirname, "src/index.js"),
},
output: {
path: path.resolve(__dirname, "dist"),
filename: "[name].[contenthash].js",
clean: true,
},
};
Now, every time we run the build
command, the dist
directory is first cleaned up before a new file gets emitted into it.
So that is about entry
and output
; where to get the files for bundling, and where to put the files after bundling.
There are two other main options with the configuration file that we can include: loaders
, and plugins
.
Plugins
To see plugins in action, we would make use of the HTMLWebpackPlugin
by which makes it possible to dynamically create HTML files, by installing the html-webpack-plugin
package:
$ yarn add --dev html-webpack-plugin
# Or
$ npm install --save-dev html-webpack-plugin
Then in the plugins sections of the config file, we pass in the title
of the HTML file to be built, and a filename
:
const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
module.exports = {
mode: "development",
entry: {
main: path.resolve(__dirname, "src/index.js"),
},
output: {
path: path.resolve(__dirname, "dist"),
filename: "[name].[contenthash].js",
clean: true,
},
plugins: [
new HtmlWebpackPlugin({
title: "Testing html file",
filename: "index.html",
}),
],
};
Now, when we run the build
command , a HTML file is created in the dist
folder. And in the index.html
file, you will notice that the unique js
file that had been bundled has been automatically appended:
Check out our hands-on, practical guide to learning Git, with best-practices, industry-accepted standards, and included cheat sheet. Stop Googling Git commands and actually learn it!
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Testing html file</title>
<meta name="viewport" content="width=device-width, initial-scale=1"><script defer src="main.b908ea8ce529b415a20a.js"></script></head>
<body>
</body>
</html>
If your wish is to generate a template to use as your index.html
page in the dist
folder, you can achieve this by attaching a template
option into the plugins
option of the config file, then define a template file in the src
folder which would be the value of the template
option:
module.exports = {
/* ... */
plugins: [
new HtmlWebpackPlugin({
title: "Testing html file",
filename: "index.html",
template: path.resolve(__dirname, "src/template.html"),
}),
],
};
Then in the template.html
file, we can define a few markups. One thing to note is that this template file has access to all that we have defined in the new HtmlWebpackPlugin
object, such as the title
, filename
, which we can import into the file:
// src/template.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title><%= htmlWebpackPlugin.options.title %></title>
</head>
<body>
<header>
<h1>Template File of <%= htmlWebpackPlugin.options.title %></h1>
</header>
</body>
</html>
Now, when we build our app again, we get the title in the generated dist/index.html
to be “Testing html file”:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Testing html file</title>
<script defer src="main.b908ea8ce529b415a20a.js"></script></head>
<body>
<header>
<h1>Template File of Testing html file</h1>
</header>
</body>
</html>
Loaders
By default, webpack understands JavaScript and JSON (JavaScript Object Notation), but it does not know what an image file is, or HTML, CSS, SASS, or SVG files are, including other types of files; it does not know how to handle them. Because of this challenge, loaders come in.
If you have got HTML, CSV, SVG, CSS, SASS, or image files, you can have loaders for all these different types of files that would look into the source folder, find them, and then turn them into modules that can be imported by JavaScript.
Let us have a typical example, by attaching this SVG file into the src
folder and a CSS file too. If we wanted to bring that in and have it well bundled, we would use a loader - our index.js
file, two things ought to done here; the index.js
file acts as a loader to load the type of file, then this loader gets imported to the dist
directory when the app is bundled, so that it becomes part of the application’s dependency graph.
Loading CSS files with Webpack
We can create a main.css
in the src
folder:
// src/main.css
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
Then update index.js
:
// src/index.js
import style from "./main.css";
console.log("Hello world! My name is Uche Azubuko!");
In the loaders
option in the config file, we can now set rules, for loading various types of files as modules during the bundling process:
module.exports = {
/* ... */
module: {
rules: [{ test: /\.css$/, use: ["style-loader", "css-loader"] }],
},
};
Here, we have created a rule that provides an array of loaders for loading any CSS files in the src
folder; being read from right to left. The CSS-loader looks for the file, turns it into a module and gives it to JavaScript, while the style-loader
takes the imported module, and injects it into the dist/index.html
file.
They are dev dependencies, thus, we have to also install them in the application:
$ yarn add --dev style-loader css-loader
# Or
$ npm install --save-dev style-loader css-loader
When we run the build
command, the resulting JavaScript file from the bundling process now knows about the CSS file and injects it into the dist
app.
Suppose you wish to launch the development version of your application on the browser. First, we add another option to the scripts
object in the package.json
file:
"dev": webpack serve
The command serve
is added along with webpack
. It compiles everything and holds it in memory. In the config file, we then add the two options: devtool
and devServer
. These are options that help to make working on the server optimal.
Devtools
The devtool
config option controls how and if source maps are generated during compilation. Source mapping keeps track of where all content came from, and makes the browser know where any potential error is coming from:
module.exports = {
/* ... */
devtool: "inline-source-map",
};
DevServer
This defines the setting for the server that we are setting up, and we define where the base of the app should run from. We also define hot module reloading (HMR) using the hot
option, set the port number wherein the app would run, watch for changes using the watchFiles
by setting its value to the src
folder, use the open
option to immediately open the browser when the compilation process for launching the development version is completed, and set the port number of your choice using the port
option:
module.exports = {
/* ... */
devServer: {
contentBase: path.resolve(__dirname, "dist"),
port: 5001, // default is often 8080
open: true,
hot: true,
watchFiles: [path.resolve(__dirname, 'src')],
},
};
In addition to the defined devServer
option, we also need to install the webpack-dev-server
package as a dev dependency, which is to help.
Now when we run the dev
command, the app is rendered on the browser at localhost:5001
, showing all content that has been injected into the dist/index.html
during the bundling process:
With hot module replacement being enabled, if you make a change during development, it immediately reflects on the browser when you save the changes.
Loading Images
Another thing that is made easier in version 5 of webpack is the built-in asset/resource
loader, which we can use to check for images in order to inject them into the app during the bundling process:
module.exports = {
/* ... */
module: {
/* ... */
{ test: /\.(svg|ico|png|webp|jpg|gif|jpeg)$/, type: "asset/resource" },
},
};
Here, we have written a check to find all files that are associated with the following file type and inject them into the dist
folder during bundling: svg, ico, png, webp, jpg, gif, jpeg.
Update src/index.html
to include the SVG file you downloaded earlier:
// src/index.js
import style from "./main.css";
import logo from "./logo.svg";
console.log("Hello world! My name is Uche Azubuko!");
Now, run the dev
command, and the SVG file should now be compiled and the bundling process should be successful.
Typically, we would want to make use of the SVG within a component file. For that, let us create a component.js
file in the src
folder. Therein, we would create a module that would use to create content on the page:
// src/component.js
import logo from "./logo.svg";
function component() {
let main = document.createElement("main");
let para = document.createElement("p");
let img = document.createElement("img");
main.append(para);
para.append(img);
img.src = logo;
img.alt = "logo for the app";
return main;
}
export default component;
For us to make use of the image, the first step would be to import it into the file. Then we also need to import the component.js
file in index.js
so that it can be bundled along and rendered on the DOM. The component is a function that is called and returns the main
element which was returned in the component.js
file, and is then injected into the body of the DOM:
// src/index.js
import style from "./main.css";
import logo from "./logo.svg";
import component from "./component";
console.log("Hello world! My name is Uche Azubuko!");
document.body.append(component());
Now, when we run the dev server again, we should have the SVG file now rendered on the DOM:
If we wish for our bundled images to maintain their original names after bundling is completed, in webpack.config.js
, we can set assetModuleFilename
to be "[name][ext]"
in the output
option:
output: {
path: path.resolve(__dirname, "dist"),
filename: "[name].[contenthash].js",
clean: true,
assetModuleFilename: "[name][ext]",
},
Now, when we restart the development server the name of the image should be the same as it was before bundling occurred.
Transpiling
It is often a good idea to transpile your code into older ES5 versions so that older browsers can make sense of your code, as well as newer browsers that use ES6 and ES7, can. To achieve that, we use the Babel tool.
Now, we include a rule that looks for all JavaScript files in the src
folder excluding the node_modules
folder, then transpile them using the babel-loader
which uses @babel/core
and would be installed as a dev dependency, along with setting the presets
options to @babel/preset-env
.
First, let us install the required packages:
$ yarn add --dev babel-loader @babel/core @babel/preset-env
# Or
$ npm install --save-dev babel-loader @babel/core @babel/preset-env
module.exports = {
/* ... */
module: {
/* ... */
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: "babel-loader",
options: { presets: ["@babel/preset-env"] },
},
},
},
};
Save the file, then re-run the development server. This time, our application now has support for older browsers that use ES5 modules and is more backward compatible now
Conclusion
The aim of this guide was to get you to see how things work under the hood; how to build JavaScript modules for cross-browser support, how to transpile with Babel, create source maps, and make your frontend environment as simple and advanced as you would like.
Even if you make use of tools like vue-create
to set up a Vue project or create-react-app
to set up a React app, the truth is that they use webpack behind the scenes.
Webpack is really not that hard to understand, and from this guide, it would be awesome for you to extend your learning to see how to work with SASS, PostCSS in webpack, and production optimization. If you find it challenging, feel free to reach out to me, I would be glad to help.
Once again, you can make use of the project we built in this guide as a boilerplate for applications that you might want to build using plain ol’ webpack, and you can easily set up anything else that you might like.
You can refer to all the source code used in this guide on GitHub.