Django and Webpack - External Libraries, Loaders and Stylesheets

In this post, we'll extend our knowledge of Webpack and learn how to include external libraries, and how to include CSS stylesheets in our final bundles.

This post is a continuation of the previous post, which can be found here.

Often, you will be working with JavaScript modules such as HTMX or jQuery in your applications. It is not a best practice to load these packages over the network from CDNs, as this introduces network latency and additional potential points of failure. Instead, you want the source code for your third-party packages available on the server, and ready to be bundles and served via Webpack.

We'll learn how to do that in this post!

We'll also learn how to use Webpack Loaders, and how these can be used to include CSS stylesheets in your JavaScript bundles. Sounds strange, but we'll see how it's done later.

The associated video for this post can be found below.


Objectives

  • Learn how to install NPM packages and include them in our final bundle
  • Learn how to include CSS stylesheets in our final bundle
  • Understand the basics of Webpack Loaders
  • Learn how to use the CSS @import statement
  • Understand how to minify and optimize bundles

Bundling NPM packages

In the previous post, we saw how to bundle together different JavaScript files in our assets folder into a single final output file that can be deployed by Django.

We'll learn now how to include external, third-party JavaScript packages in our bundles, and will demonstrate their usage. We'll focus on HTMX and Alpine.js, two increasingly popular "low-JavaScript" tools, and will see how to bundle these libraries together with our own source code from the previous video.

Let's install these with an npm command.

npm install htmx.org alpinejs

This command will add the source code for these two packages to your environment's node_modules folder. You should also see these packages in your package.json file's dependencies object.

Now, we need to include the source code for these packages in our final bundle. How do we do that?

Let's go back to our entry file, index.js, and we'll start with HTMX. There are instructions for use with Webpack in the HTMX documentation, here. Let's follow this, and add the following to our index.js file.

import './helper1';
import './helper2';

// import HTMX and inject it into the window scope
window.htmx = require('htmx.org');


window.addEventListener('load', () => {
    document.getElementById('message').textContent = 'REBUNDLED BY WEBPACK!';
}); 

This, it turns out, is all we really need to do! When we bundle our code, it will include the HTMX source in our final bundle, allowing us to use HTMX attributes such as hx-get, hx-trigger etc, within our Django HTML templates.

Let's test it out. Run Webpack in watch mode by running the script we set up in the previous post: npm run dev. This will rebuild our bundle, and drop it into the static directory for Django.

Then, in our index.html template, let's add a button and hook up some HTMX attributes.

  <body>
    
    <p id="message">Hello</p>

    <button hx-get="{% url 'index' %}" hx-trigger="click" hx-swap="outerHTML">Click me</button>
    
  </body>

When the button is clicked, it'll send a GET request to our existing URL/view, and will replace the button with the returned content. Load the Django development server and test this out - if the content is being replaced, and you see HTMX-initiated network calls, you know that this has worked!

It's worth looking at the generated bundle file to see the difference between the code generated when HTMX is imported, and when it's not. Run Webpack in watch mode, and you can comment out the HTMX import and save the file. The generated bundle, without the HTMX import, should be quite a bit smaller than it is when HTMX is included.

Adding Alpine.js to our final bundle is similar, and it's also simple to do. Following the Alpine docs here, let's add these lines to our index.js entry file.

import './helper1';
import './helper2';
import Alpine from 'alpinejs'

// Add Alpine object to the window scope
window.Alpine = Alpine

// initialize Alpine
Alpine.start()

// import HTMX and inject it into the window scope
window.htmx = require('htmx.org');

window.addEventListener('load', () => {
    document.getElementById('message').textContent = 'REBUNDLED BY WEBPACK!';
}); 

If you're still running Webpack in watch-mode, then your bundle should be re-generated after saving these changes, and it should now allow you to use Alpine.js code in your project.

Let's test - to our index.html template, let's create an Alpine component. To the body, add the following:

  <body>
    
    <p id="message">Hello</p>

    <button hx-get="{% url 'index' %}" hx-trigger="click" hx-swap="outerHTML">Click me</button>

    <hr /> 

    <div x-data="{showMe: false, msg: 'Hello from Alpine'}">
      <p x-show="showMe" x-text="msg"></p>
      <button @click="showMe = !showMe">Toggle</button>
    </div>
    
  </body>

You should now be able to toggle the message - if so, everything is working!

At this point we've successfully bundled together our three custom JavaScript files, along with the Alpine and HTMX packages that we grabbed with NPM. We use Webpack to bundle all of this up into a single output file, and that file is what is served to our users, in an efficient manner with a single request.

And the great thing is, we have access to all the power of HTMX and Alpine.js in our Django templates, with this simple setup.

Let's now move on to Webpack Loaders, and we'll learn how to include stylesheets in our bundled files.

Webpack Loaders

Loaders are another important Webpack concept that we need to understand. The documentation can be found here, but we'll explain a bit below.

Webpack is designed to work with JavaScript files and JSON files, out of the box. By default, without using any loaders, Webpack will only work with these file types.

However, with loaders, we can work with other assets, and include more in our bundles. For example, CSS files, images, TypeScript files, JSX files, Vue files, and more.

The official docs states:

Loaders allow webpack to process other types of files and convert them into valid modules that can be consumed by your application and added to the dependency graph.

We're going to add loaders to our Webpack configuration, and we're going to start by using them to include CSS stylesheets in our application bundles. Later, we'll look at other file-types, too.

Let's start by adding a CSS file to our assets/styles folder. In there, create a file called button-styles.css. We can use that to style the buttons on our page.

So, in assets/styles/button-styles.css, add the following code.

button {
    background: blue;
    padding: 8px;
    color: white;
}

Now, we want our final bundle.js file that Webpack builds to include this stylesheet, as well as the JavaScript/HTMX/Alpine stuff. How do we achieve this?

We need to add two loaders here that typically used when including CSS in your Webpack builds:

  • style-loader - used to inject CSS into the DOM, therefore allowing you to include CSS files in Webpack builds.
  • css-loader - resolves @import and url() code within your CSS files, transforming to JavaScript import/require statements.

Let's start by installing these two loaders with NPM. Run the following command:

npm install -D css-loader style-loader

Once installed, we will then amend our Webpack configuration to include these two loaders when processing CSS files.

Loaders are defined within a top-level module.rules array in your Webpack configuration, as you will see below. Add the following to your Webpack configuration file:

const path = require('path');

module.exports = {
    entry: './assets/scripts/index.js',
    output: {
        'path': path.resolve(__dirname, 'core', 'static'),
        'filename': 'bundle.js'
    },
    module: {
        rules: [
            {
                test: /\.css$/i,
                use: [
                    'style-loader',
                    'css-loader'
                ]
            }
        ]
    }
}

Each object within this module.rules array defines a particular strategy for loading a file-type. In our case, on line 12, we use the test key to define which file types this loader should be applied to - for us, this is CSS files, so we use a regular expression that looks at any files that end in ".css".

We then define a use array. This tells us which loaders to apply to the CSS files. This is evaluated from bottom-to-top, so we will apply the css-loader (which transforms CSS @import and url() expressions to something JavaScript can understand) before we use the style-loader to inject our CSS into the DOM in the bundle.

Adding CSS Stylesheets to the Webpack Bundle

Now that we've added the loaders for CSS files, we can import CSS files directly from our JavaScript modules! By doing this, the styles will be included in our Webpack bundle when we run our build command.

Let's do this now - add the following import to the entry file, index.js:

import '../styles/button-styles.css';

Now, while it may be strange to import CSS files into JavaScript files, the Webpack loaders that we have added will be able to understand and deal with this. The end result is that, if we run our npm run dev script, we should now see the styles applied to the buttons in our template.

So we're now including a stylesheet in the emitted bundle.js file that's build by Webpack, along with all of our JavaScript. This results in a single request from our clients to the server, in order to fetch the bundle file, rather than multiple separate CSS and JS imports. Quite powerful!

You can add as many stylesheets as you'd like, and import these into your entry file, in order to have the styles available in the bundle that we're importing from the static directory.

Let's add another: assets/styles/text.css, with the following code:

p {
    font-size: 24px;
}

We can then import that, along with our other stylesheet, in the index.js entry-point.

import '../styles/button-styles.css';
import '../styles/text.css';

If you're running Webpack in watch mode, it will re-generate the bundle - refresh your page and you should see the styles applied!

Now, imagine we had 20 stylesheets, and we want to include them all in the final bundle. You could import all 20 in the index.js file, but this can become messy and difficult to maintain.

What we can do instead is create a CSS file that imports all the styles from the other stylesheets, and then simply import that single CSS file into our index.js.

To do this, create an assets/styles/index.css file, and add the following code:

@import './button-styles.css';
@import './text.css';

This imports the styles from our two stylesheets into this single file. We can now add a single import to our index.js file, as below:

import '../styles/index.css'

This isn't a big deal for us, as we only have two CSS files, but if we imagine having 10 or more, it can help keep our entry-point file clean.

Minifying and Optimizing Bundles

As a final task, we are going to learn how to minify our bundle file. Minification refers to removing unnecessary code, comments and spaces in order to reduce the file size of an asset.

This helps our application load their static assets more efficiently, because the amount of data that needs to be transferred from the server to our users is smaller, thus faster.

For example, in a JavaScript minification, we might remove comments, tabs, spaces and may even replace long variable and function names with shorter, more concise names. These steps will all reduce the size of the file.

With Webpack 5, minification is enabled by default in production mode. Recall that our Webpack build is running in development mode - check the dev script in package.json to see this. If you inspect the generated bundle file, you can see the code is not minified.

Stop the Webpack build process if you're running in watch mode, and change this option in package.json to production mode, as below:

  "scripts": {
    "dev": "webpack --mode production --watch"
  }

If you re-run the script, and inspect the bundle, you should now see that this code is minified. With our current setup, the file-size of the final, minified bundle is less than half the size of the original, unminified bundle. This is a big win when we are transferring assets to our users - they'll get the response more quickly, improving their user experience on our websites.

Note that, as well as minifying, running Webpack in production mode performs a number of additional optimizations, such as concatenating modules. Webpack provides sensible defaults for production that can help optimize the deployed code.

It's possible to minify in development mode, too. Change the above script back to development mode. To minify in development mode, you can add the following to our webpack.config.js:

const path = require('path');

module.exports = {
    entry: './assets/scripts/index.js',
    output: {
        'path': path.resolve(__dirname, 'core', 'static'),
        'filename': 'bundle.js'
    },
    module: {
        rules: [
            {
                test: /\.css$/i,
                use: [
                    'style-loader',
                    'css-loader'
                ]
            }
        ]
    },
    optimization: {
        minimize: true
    }
}

Note the new section at the bottom - the optimization object. This allows us to specify options for a number of optimization techniques. Here, we are telling Webpack to perform minification of our final bundle, by setting the minize option to true.

This will reduce the size, but not by as much as running in production mode, because running Webpack in production mode introduces a number of additional optimizations, as well as just minifying.

For future tutorials, I do not recommend running this minification option in development. This was merely to demonstrate this option and the optimization block, as well as to make clear that when you run your Webpack builds in production mode, optimizations such as minifying are performed automatically, unless you specify otherwise in your configuration. 

Summary

And that's it for this post! We've learned how to add external NPM packages to our builds, and how to add CSS stylesheets to our builds. We also learned how Webpack minifies our bundle when running in production mode, and that we have an optimization block in our configuration file that allows us to customize the optimization of our bundles.

In the next post, we're going to look at including TypeScript files, transpiling experimental JavaScript with Babel, as well as loading other asset files such as images, fonts and icons.

We'll also start looking at more complex topics, such as creating multiple bundles, code splitting, and adding hashes to our bundle names, in future posts.

If you enjoyed this post, please subscribe to our YouTube channel and follow us on Twitter to keep up with our new content!

Please also consider buying us a coffee, to encourage us to create more posts and videos!

;