Set Up Webpack 5 To Work With Static Files

matheus4lves

Matheus Alves

Posted on August 1, 2023

Set Up Webpack 5 To Work With Static Files

Set Up Webpack 5 To Work With Static Files

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.

Table of contents

Why do we need to to that?

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.

Important terms

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 😉

  1. 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 🙂

  2. Loaders:

    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.

  3. Plugins: This is an advanced topic so let’s stick to the following simplified definition:

    They also serve the purpose of doing anything else that a loader cannot do.

  4. css-loader:

    The css-loader interprets @import and url() like import/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) 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.

  5. style-loader: The style-loader injects CSS into the DOM. While the css-loader makes webpack understand CSS, it’s this loader's job to apply the CSS to the page.

  6. html-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 Hyper*Text **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 when using APIs, we cannot simply insert the content of an HTML file inside of a JavaScript file. But JavaScript understand 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 APIs.

    If you want to learn more about APIs, see Client-side web APIs.

  7. html-webpack-plugin: To understand the importance of this plugin, we have to remember that in order to execute JavaScript in the browser,
    a. we have to embed our scripts using <script> elements in an HTML or

    b. link to external scripts (by using the src 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 using script 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.

Understanding the project structure


Project structure

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:

  • .gitignore ignores the node_modules directory and other files we don't want to commit.
  • .prettierrc instructs Prettier on how to format our code (OPTIONAL, for those who use Prettier).
  • package.json was automatically created by running npm init -y. It holds all sorts of information about our project: name, version, dependencies, development dependencies, etc.
  • webpack.config.js and webpack.parts.js will be used to compose the webpack configuration, a strategy I have learned with SurviveJS, at the Composing Configuration section of the book.

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>
Enter fullscreen mode Exit fullscreen mode

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.

Understanding the overall setup

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 is, guess what, webpack. This is the core package.
  • webpack-cli allows us to further config webpack via CLI (terminal/prompt) and from our scripts.
  • webpack-dev-server provides us with a local development server.
  • webpack-merge facilitates composing configuration by basically merging objects and concatenating arrays, returning an object after the operations.

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 }]);
Enter fullscreen mode Exit fullscreen 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"
Enter fullscreen mode Exit fullscreen mode

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.

Working with HTML

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.

Configuring the html-loader

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",
      },
    ],
  },
});
Enter fullscreen mode Exit fullscreen mode

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");
Enter fullscreen mode Exit fullscreen mode

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(),
]);
Enter fullscreen mode Exit fullscreen mode

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.

Configuring the html-webpack-plugin

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 })],
});
Enter fullscreen mode Exit fullscreen mode

And, at the top of the same file, don’t forget to require the plugin:

const HtmlWebpackPlugin = require("html-webpack-plugin");
Enter fullscreen mode Exit fullscreen mode

Now, call this function right after the call to loadHTML() in webpack.config.js:

parts.generateHTML({ template: "./app/index.html" }),
Enter fullscreen mode Exit fullscreen mode

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.

Working with CSS

Two things are required in order to work with CSS:

  1. We have to make webpack understand CSS.
  2. We have to apply the CSS to the page.

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.

Configuring the css-loader and the style-loader

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"],Thanks 
      },
    ],
  },
});
Enter fullscreen mode Exit fullscreen mode

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(),
Enter fullscreen mode Exit fullscreen mode

To test the configuration, add this to styles.css:

body {
  background: #00ff00;
}
Enter fullscreen mode Exit fullscreen mode

And this to index.js:

import "../styles/styles.css";
Enter fullscreen mode Exit fullscreen mode

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.

Working with images

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.

Using Asset Modules

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",
      },
    ],
  },
});
Enter fullscreen mode Exit fullscreen mode

Then, make a call to this function in webpack.config.js:

parts.loadImages(),
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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.

Final touches

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%);
}
Enter fullscreen mode Exit fullscreen mode

You should get this result


Final version of the page

Summary

Here’s a list with the main points of this article:

  • By default webpack only understands JavaScript. Therefore, if we want to work with static files we need to expand its functionality through loaders and plugins.
  • If we want to work with HTML, we need to set up the html-loader and the html-webpack-plugin. The former makes webpack understand HTML and allows us to use a .html file as a template while the latter automatically generates an HTML5 that includes all our bundles in the head using <script> tags.
  • To work with CSS we need to make webpack understand it first by configuring the css-loader, and then tell webpack to apply our styles to our pages by configuring the style-loader.
  • Since webpack 5, if we want to work with images that are dynamically added we don’t need to set up additional loaders because webpack provides Asset Modules. We just need to set up the test condition and the proper type of resource.

Conclusion

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:

  1. Install the loader or plugin you need.
  2. Configure it.

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

💖 💪 🙅 🚩
matheus4lves
Matheus Alves

Posted on August 1, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related