Out of the box webpack only understands JavaScript. However, we can expand its functionality by using loaders and plugins. In this tutorial, you’ll learn how to work with static files, namely, HTML, CSS, and some common image types.
If you read the intro of this article with attention, you already know the answer: It’s because without any further configuration webpack only understands JavaScript. Therefore, we need to use loaders and plugins that allow webpack process and bundle other types of files.
I decided to provide not only a definition but also an explanation for the necessity of the loaders and plugin we’re going to use. Feel free to skip this section if you have confidence you understand the terms listed bellow.
Note: I’m not following the alphabetical order on purpose. You’ll understand why 😉
Assets: At the home page of webpack, you can see that assets, scripts, images and styles are treated as different things. Nevertheless, at the glossary of the website we read that an
asset is a general term for the images, fonts, media, and any other kind of files that are typically used in websites and other applications.
So you can see that in the glossary an image is an asset. I prefer to define assets as the resources we use as they are, for example, images, fonts, audio and video. In other words, we don’t code them ourselves, but we use them to build our project.
Note: We usually put configuration files (.gitignore, webpack.config.js, etc) in the root directory of the project while the files and assets that are used to build the project are put in a sub-directory called src (short for source) or app.
I prefer to think of the source as everything we have in our project. Thus, I prefer to call app the sub-directory that contains our files and assets.
Agree? Disagree? Let me know in the comments 🙂
Loaders are transformations that are applied to the source code of a module. They allow you to pre-process files as you
import
or “load” them.
Putting it simple, loaders allow us to work with file types other than JavaScript.
[…] They also serve the purpose of doing anything else that a loader cannot do.
The
css-loader
interprets@import
andurl()
likeimport/require()
and will resolve them.
To work with CSS, we’ll have to import .css files into .js files. But, as CSS syntax is not valid JavaScript, we must set up the css-loader, which will interpret @import
and url()
(CSS syntax) as import/require()
(JavaScript Syntax), and resolve them.
“Resolve” here simply means to identify the correct path to the specified file.
This loader must be used with the style-loader.
Exports HTML as string, require references to static resources.
What? 😕 Let me explain it to you 😉. The content of an HTML file is written using the HyperText Markup Language (hence the name HTML), which is a text-based approach for describing how the content is structured. For example, if you have an <h1>Heading level 1</h1>
inside of your .html file, when a browser parses your file, it will know exactly how to display your heading, because the browser understands the contents of a .html file. If you have a <img src="/images/logo.png" alt="Logo" />
, it will understand the src
attribute and will locate and display the image.
I think you already know that we can manipulate the content of an HTML file using JavaScript. For instance, we can create elements, add/remove classes, etc. This is only possible because of the APIs that allow us to do that.
Even using APIs, we cannot simply insert the content of an HTML file inside of a JavaScript file. But JavaScript understands strings. Therefore, the html-loader reads the contents of an .html file and returns it as an string that can be used by JavaScript.
JavaScript does not understand the HTML src
attribute but it understands the JavaScript require()
statement! That’s why references to static resources like src
are translated into require()
statements.
Having the HTML string and the requires, JavaScript can manipulate the DOM via web APIs.
If you want to learn more about APIs, see Client-side web APIs.
html-webpack-plugin: To understand the importance of this plugin, we have to remember that in order to execute JavaScript in the browser,
<script>
elements in an HTML orsrc
attribute of <script>
elements) from the HTML file.
In other words, we need an HTML file as an entry point for executing our scripts (or bundles) in the browser!
This situation arises the following question: How can we work with an HTML file if webpack only understands JavaScript? I have two answers for this question, since there are two approaches that can be followed for handling this situation:
The first approach involves manually creating an .html file, adding the bundle(s) manually, and making a copy of this file at the build folder.
The second approach involves using the html-webpack-plugin and letting it do all the work for us.
The plugin will generate an HTML5 file for you that includes all your
webpack
bundles in the head usingscript
tags.
You can either let the plugin generate an HTML file for you, supply your own template using lodash templates or use your own loader.
In this tutorial, we’re going to use an HTML template. Therefore, we’ll have to combine this plugin with the html-loader.
In the root directory of the project (our repository) we have some configuration files and a sub-directory named app that contains the files and assets for building the app.
Here’s a brief description of the role each configuration file plays:
npm init -y
. It holds all sorts of information about our project: name, version, dependencies, development dependencies, etc.
You should add this code to your index.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>Document</title>
</head>
<body>
<div class="quote">
<p class="quote__text">Knowledge is freedom</p>
<img class="quote__img1" src="./assets/images/bookshelf.png" alt="A bookshelf illustration" width="1280" height="640" />
</div>
</body>
</html>
For now, the rest of the files are empty. As for the images, you can download them from the repository: https://github.com/matheus4lves/article-serve-static-files.
Note: The second image will be dynamically added later.
First, we need to install our basic development dependencies (devDependencies). Run the following command: npm install --save-dev webpack webpack-cli webpack-dev-server webpack-merge
.
This is why we need these packages:
webpack.config.js
const { merge } = require("webpack-merge");
const path = require("path");
const commonConfig = merge([
{
entry: ["./app/assets/scripts/index.js"],
output: {
path: path.resolve(__dirname, "./build"),
filename: "bundle.js",
},
},
]);
const configs = {
development: merge([]),
production: merge([]),
};
module.exports = (_, argv) => merge([commonConfig, configs[argv.mode], { mode: argv.mode }]);
We start by splitting our configuration into three categories: common configuration, development configuration, and production configuration.
Instead of exporting an object from the module, we export a function which gives us access to the arguments we pass to the command line or to our scripts. We want to do that because we want to get the mode
from an argument.
If we were to run the development server right now, we would have to run the following command from the CLI: npx webpack serve --config webpack.config.js --mode development
. Instead, we’re going to set up two scripts that will help us develop and build our app.
Inside the scripts
property of your package.json file, add these lines right after the test
property:
"dev": "webpack serve --mode development",
"build": "webpack --mode production"
Now if we want to start the development server we can simple run: npm run dev
. Much simpler, isn’t it?
For the time being we have the most basic configuration. We defined an array of entry
points (because we can have more than one) and the output
property which tells webpack the name of our bundle and the location it should be emitted to when we build the project.
Note that instead of exporting an object from the module we export a function, which gives us access to the argv
object. This object contains the arguments that come from the CLI/scripts. Then, we access the mode
property and create the configuration based on it.
Note: To keep things simple I'll not touch the development and production configurations.
In this project we’re going to generate an .html file based on a .html template. We can also use other types of templates, for instance, EJS, handlebars, etc. We can, by the way, use no template at all, and generate our HTML based on plain JavaScript (As we will do later).
Since we’re generating an static file based on a static file, the basic difference between our .html template and the generated .html file is that the generated file contains an <script>
tag with our bundle.
To be able to use an .html file as a template, we must make webpack understand HTML. To do so, we’ll set up the html-loader.
Then, we’ll set up the html-webpack-plugin so that it can generate the final .html for us.
Summarizing, we’ll read from an .html file to generate a .html file. Again, this is because we’re generating an static file based on a static file. In a more realistic situation, we would generate the .html file based on vanilla JavaScript (plain JavaScript) or based on a JavaScript library/templating language, thus generating static content based on dynamic content.
First, install the html-loader by running npm install --save-dev html-loader
.
Then, add this function to wepback.parts.js:
exports.loadHTML = () => ({
module: {
rules: [
{
test: /\.html$/i,
loader: "html-loader",
},
],
},
});
This function returns an object literal that we have to merge to our configuration. Go to webpack.config.js and add this line:
const parts = require("./webpack.parts");
Now, add a call to loadHTML()
as the second element of the array passed as the argument of the merge
function that is creating the commonConfig
:
const commonConfig = merge([
{
entry: ["./app/assets/scripts/index.js"],
output: {
path: path.resolve(__dirname, "./build"),
filename: "bundle.js",
},
},
parts.loadHTML(),
]);
Webpack now understands HTML.
If you run the development server and visit the address your project is running at, you’ll be able to see the image even though we haven’t set up webpack to work with images yet. If you want to understand why read about the loader in the Important terms section of this article.
First, install the plugin by running npm install --save-dev html-webpack-plugin
.
Then, add this function to webpack.parts.js:
exports.generateHTML = ({ template } = {}) => ({
plugins: [new HtmlWebpackPlugin({ template })],
});
And, at the top of the same file, don’t forget to require the plugin:
const HtmlWebpackPlugin = require("html-webpack-plugin");
Now, call this function right after the call to loadHTML()
in webpack.config.js :
parts.generateHTML({ template: "./app/index.html" }),
Don’t forget to specify the template!
If you run npm run build
now, you should see an index.html file inside the build directory.
Two things are required in order to work with CSS:
For the first step we need to install and config the css-loader. For the second we need to do the same with the style-loader. Since there’s no point in making webpack understand CSS without applying the styles to the page, we’re going to set up these loaders together.
Let’s start by installing the required packages. Run: npm install --save-dev css-loader style-loader
.
Then, add this to your webpack.parts.js:
exports.loadCSS = () => ({
module: {
rules: [
{
test: /\.css$/i,
use: ["style-loader", "css-loader"],
},
],
},
});
Note: this order is important because webpack reads the loaders from right to left.
As with the other functions we’ve exported from this module, we have to call it from webpack.config.js. Therefore, add the following line right after the call to the generateHTML()
function:
parts.loadCSS(),
To test the configuration, add this to styles.css:
body {
background: #00ff00;
}
And this to index.js:
import "../styles/styles.css";
If you run the development server and visit the address the project is running at, you should see that the page has a green background.
If we wanted to work with images in webpack prior to its fifth version, we would have to set up the file-loader, but webpack 5 introduced Asset Modules, which we’re going to use instead.
According to the official webpack documentation,
Asset Modules is a type of module that allows one to use asset files (fonts, icons, etc) without configuring additional loaders.
To allow webpack load images that are dynamically added, add this function to webpack.config.js:
exports.loadImages = () => ({
module: {
rules: [
{
test: /\.(png|svg|jpg|jpeg|gif)$/i,
type: "asset/resource",
},
],
},
});
Then, make a call to this function in webpack.config.js:
parts.loadImages(),
To test the configuration, let’s dynamically add the second image to our page:
Update the index.js to this:
import "../styles/styles.css";
import brokenHandcuffs from "../images/broken-handcuffs.png";
const img = document.createElement("img");
img.className = "quote__img2";
img.setAttribute("src", brokenHandcuffs);
img.setAttribute("alt", "Someone's upraised arms bound to the pieces of a handcuff that has just been broken.");
img.setAttribute("width", "817");
img.setAttribute("height", "460");
const imgContainer = document.querySelector(".quote");
imgContainer.appendChild(img);
Webpack will process this image, which means that the image will be added to the output directory (build, in our case) and the variable brokenHandcuffs
will contain the final URL of the image after processing.
If you run npm run build
, you’ll see the index.html file with the images in the build directory. Notice how the name of the images have changed (The final URL matches the name of the images).
If you run the development server, you should see both images on the page.
In the next section we’ll update the CSS to make the page a bit prettier.
Update styles.css to this:
.quote {
height: 100vh;
position: relative;
}
.quote__text {
color: #175f73;
font-size: 20px;
position: absolute;
bottom: 380px;
left: 50%;
transform: translateX(-55%);
}
.quote__img1 {
width: 700px;
height: auto;
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
}
.quote__img2 {
width: 400px;
height: auto;
position: absolute;
bottom: 300px;
left: 50%;
transform: translateX(-50%);
}
You should get this result:
Here’s a list with the main points of this article:
<script>
tags.Thanks to its rich ecosystem of loaders and plugins webpack became much more than a module bundler, allowing us to to things that would previously required a task manager and other tools.
If you want to expand webpack’s functionality, this is the basic workflow:
I hope this tutorial is useful for you. Thanks for reading! You can find the source code of this project at this repo: https://github.com/matheus4lves/article-serve-static-files