Load Webpack hash bundle in Django

Table of Contents

  1. Introduction
  2. Setup Webpack Project with Django
  3. Load Webpack bundles in Django
  4. Linting in Webpack
  5. Load Webpack hash bundle in Django
  6. Code splitting with Webpack
  7. How to config HMR with Webpack and Django
  8. How to use HtmlWebpackPlugin to load Webpack bundle in Django

If you want a quick start with Webpack and Django, please check python-webpack-boilerplate

Objective

By the end of this chapter, you should be able to:

  1. Understand hash, chunkhash, and contenthash in Webpack
  2. Learn what is Webpack PublicPath, and config it to work with Django.
  3. Use django-webpack-loader to load hash bundle file in Django.

The classic workflow in Django

In the previous chapter, we use Webpack to help us generate bundle files app.js, vendors.js and app.css.

The bundle files built by Webpack have no hash in the filename

So we import them to Django template using {% static 'js/app.js' %}.

Let's add code below to the django_webpack_app/settings.py

STATIC_ROOT = str(BASE_DIR / "staticfiles")

STATICFILES_STORAGE = 'django.contrib.staticfiles.storage.ManifestStaticFilesStorage'
(env)$ python manage.py collectstatic --noinput --clear

If we check the staticfiles, we can see something like below

├── css
│   ├── app.53180295af10.css
│   └── app.css
├── index.55d50b4f89d0.html
├── index.html
├── js
│   ├── app.647036872966.js
│   ├── app.js
│   ├── vendors.d722b18ccd2a.js
│   └── vendors.js
└── staticfiles.json

Notes:

  1. In collectstatic, the Django ManifestStaticFilesStorage would add hash to the static assets. The hash value come from the content of the file.
  2. If the Django app is running in the production mode, then {% static 'js/app.js' %} would generate js/app.647036872966.js.
  3. Only files that has been edited would have new hash, this makes the long-term caching possible.
  4. You can check Django doc: staticfiles to learn more.

Next, we will see how to use Webpack to add hash to the bundle files and how to load hash bundle in Django.

Hash, chunkhash, contenthash in Webpack

There are three types of hashing in webpack

hash

Hash is corresponding to build. Each chunk will get same hash across the build. If anything change in your build, corresponding hash will also change.

output: {
  filename: 'js/[name].[hash].js',
},
├── js
│   ├── app.849b559809914ad926f6.js
│   └── vendors.849b559809914ad926f6.js

chunkhash

Returns an entry chunk-specific hash. Each entry defined in the configuration receives a hash of its own. If any portion of the entry changes, the hash will change as well.

output: {
  filename: 'js/[name].[chunkhash].js',
},
├── js
│   ├── app.99321678924ea3d99edb.js
│   └── vendors.9555feebcadbf299d5d3.js

If you do not fully understand what is chunk, do not worry, I will talk about it later.

contenthash

Returns a hash generated based on content. It's the new default in production mode starting from webpack 5.

output: {
  filename: 'js/[name].[contenthash].js',
},
├── js
│   ├── app.9250e2d9da70bf8ad18c.js
│   └── vendors.110e1c99b895711ecc77.js

Notes:

  1. It is not recommend to use the above hash placeholder in development mode. (considering performance)
  2. You can check Adding Hashes to Filenames to learn more.

Config Webpack

Let's update webpack/webpack.config.prod.js

const Webpack = require('webpack');
const { merge } = require('webpack-merge');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const common = require('./webpack.common.js');

module.exports = merge(common, {
  mode: 'production',
  devtool: 'source-map',
  bail: true,
  output: {
    filename: 'js/[name].[chunkhash:8].js',
    chunkFilename: 'js/[name].[chunkhash:8].chunk.js',
  },
  plugins: [
    new Webpack.DefinePlugin({
      'process.env.NODE_ENV': JSON.stringify('production'),
    }),
    new MiniCssExtractPlugin({
      filename: 'css/app-[contenthash].css',
    }),
  ],
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: 'babel-loader',
      },
      {
        test: /\.s?css/i,
        use: [MiniCssExtractPlugin.loader, 'css-loader', 'postcss-loader', 'sass-loader'],
      },
    ],
  },
});

Notes:

  1. The output.filename is set js/[name].[chunkhash:8].js, the :8 is slicing operation.
  2. The MiniCssExtractPlugin.filename is css/app-[contenthash].css. Please check notes below why we should use contenthash

If you used chunkhash for the extracted CSS as well, this would lead to problems as the code points to the CSS through JavaScript bringing it to the same entry. That means if the application code or CSS changed, it would invalidate both.

You can also use contenthash with js file, please check the webpack config for Gatsby project

(frontend)$ npm run build

js/vendors.109d1a3a.js
js/app.f6aff166.js
css/app-e3e173a2b456b7aaf3a4.css

Load hash bundle file in Django

Now if we want to load it in Django template, we can use code {% static 'js/app.f6aff166.js' %}

What if the hash changed? Is there any other solution to fix this?

django-webpack-loader can help us solve this problem.

Workflow

  1. django-webpack-loader has a sister project webpack-bundle-tracker, and they should be used together.
  2. webpack-bundle-tracker would help generate a webpack stats file.
  3. And then django-webpack-loader would use the webpack stats file to load the hash bundle file in Django template.

Install webpack-bundle-tracker

(frontend)$ npm install [email protected]

Edit webpack/webpack.common.js

const Path = require('path');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const CopyWebpackPlugin = require('copy-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const BundleTracker = require('webpack-bundle-tracker');

module.exports = {
  entry: {
    app: Path.resolve(__dirname, '../src/scripts/index.js'),
  },
  output: {
    path: Path.join(__dirname, '../build'),
    filename: 'js/[name].js',
  },
  optimization: {
    splitChunks: {
      chunks: 'all',
      name: 'vendors',
    },
  },
  plugins: [
    new CleanWebpackPlugin(),
    new CopyWebpackPlugin({ patterns: [{ from: Path.resolve(__dirname, '../public'), to: 'public' }] }),
    new HtmlWebpackPlugin({
      template: Path.resolve(__dirname, '../src/index.html'),
    }),
    new BundleTracker({filename: './webpack-stats.json'}),
  ],
  resolve: {
    alias: {
      '~': Path.resolve(__dirname, '../src'),
    },
  },
  module: {
    rules: [
      {
        test: /\.mjs$/,
        include: /node_modules/,
        type: 'javascript/auto',
      },
      {
        test: /\.(ico|jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2)(\?.*)?$/,
        use: {
          loader: 'file-loader',
          options: {
            name: '[path][name].[ext]',
          },
        },
      },
    ],
  },
};

Notes:

  1. We import BundleTracker at the top
  2. We add new BundleTracker({filename: './webpack-stats.json'}), to the plugins to generate webpack-stats.json.
(frontend)$ npm run build
(frontend)$ cat webpack-stats.json
{
  "status": "done",
  "chunks": {
    "app": [
      "js/vendors.109d1a3a.js",
      "js/app.1e309b17.js",
      "css/app-e3e173a2b456b7aaf3a4.css"
    ]
  },
  "assets": {
    "js/app.1e309b17.js": {
      "name": "js/app.1e309b17.js"
    },
    "js/vendors.109d1a3a.js": {
      "name": "js/vendors.109d1a3a.js"
    },
    "public/.gitkeep": {
      "name": "public/.gitkeep"
    },
    "js/vendors.109d1a3a.js.LICENSE.txt": {
      "name": "js/vendors.109d1a3a.js.LICENSE.txt"
    },
    "js/app.1e309b17.js.map": {
      "name": "js/app.1e309b17.js.map"
    },
    "js/vendors.109d1a3a.js.map": {
      "name": "js/vendors.109d1a3a.js.map"
    },
    "css/app-e3e173a2b456b7aaf3a4.css": {
      "name": "css/app-e3e173a2b456b7aaf3a4.css"
    },
    "css/app-e3e173a2b456b7aaf3a4.css.map": {
      "name": "css/app-e3e173a2b456b7aaf3a4.css.map"
    },
    "index.html": {
      "name": "index.html"
    }
  }
}

Notes:

  1. From the webpack-stats.json, we can get many low-level details about our hash bundles, which can help us better understand Webpack.

PublicPath

If we check frontend/build/index.html

<!doctype html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width,initial-scale=1">
  <title>webpack starterkit</title>
  <link href="css/app-e3e173a2b456b7aaf3a4.css" rel="stylesheet">
</head>
<body><h1>webpack starter</h1>
<p>✨ A lightweight foundation for your next webpack based frontend project.</p>
<script src="js/vendors.109d1a3a.js"></script>
<script src="js/app.1e309b17.js"></script>
</body>
</html>

Please note the JS link is something like js/app.1e309b17.js

The publicPath configuration option can be quite useful in a variety of scenarios. It allows you to specify the base path for all the assets within your application.

Update webpack/webpack.common.js

output: {
  path: Path.join(__dirname, '../build'),
  filename: 'js/[name].js',
  publicPath: '/static/',
},

Notes:

  1. We set publicPath to /static/, which is the base path for all bundle files.
(frontend)$ npm run build
(frontend)$ cat webpack-stats.json
{
  "status": "done",
  "chunks": {
    "app": [
      "js/vendors.109d1a3a.js",
      "js/app.1e309b17.js",
      "css/app-e3e173a2b456b7aaf3a4.css"
    ]
  },
  "publicPath": "/static/",
  "assets": {
    "js/app.1e309b17.js": {
      "name": "js/app.1e309b17.js",
      "publicPath": "/static/js/app.1e309b17.js"
    },
    "js/vendors.109d1a3a.js": {
      "name": "js/vendors.109d1a3a.js",
      "publicPath": "/static/js/vendors.109d1a3a.js"
    },
    "public/.gitkeep": {
      "name": "public/.gitkeep",
      "publicPath": "/static/public/.gitkeep"
    },
    "js/vendors.109d1a3a.js.LICENSE.txt": {
      "name": "js/vendors.109d1a3a.js.LICENSE.txt",
      "publicPath": "/static/js/vendors.109d1a3a.js.LICENSE.txt"
    },
    "js/app.1e309b17.js.map": {
      "name": "js/app.1e309b17.js.map",
      "publicPath": "/static/js/app.1e309b17.js.map"
    },
    "js/vendors.109d1a3a.js.map": {
      "name": "js/vendors.109d1a3a.js.map",
      "publicPath": "/static/js/vendors.109d1a3a.js.map"
    },
    "css/app-e3e173a2b456b7aaf3a4.css": {
      "name": "css/app-e3e173a2b456b7aaf3a4.css",
      "publicPath": "/static/css/app-e3e173a2b456b7aaf3a4.css"
    },
    "css/app-e3e173a2b456b7aaf3a4.css.map": {
      "name": "css/app-e3e173a2b456b7aaf3a4.css.map",
      "publicPath": "/static/css/app-e3e173a2b456b7aaf3a4.css.map"
    },
    "index.html": {
      "name": "index.html",
      "publicPath": "/static/index.html"
    }
  }
}

As you can see, now all the assets have publicPath which have /static/ prefix.

If we check frontend/build/index.html


<!doctype html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width,initial-scale=1">
  <title>webpack starterkit</title>
  <link href="/static/css/app-e3e173a2b456b7aaf3a4.css" rel="stylesheet">
</head>
<body><h1>webpack starter</h1>
<p>✨ A lightweight foundation for your next webpack based frontend project.</p>
<script src="/static/js/vendors.109d1a3a.js"></script>
<script src="/static/js/app.1e309b17.js"></script>
</body>
</html>

The assets URL seems correct now.

Now the frontend part is ready, let's config Django to read the webpack-stats.json.

Setup django-webpack-loader

Add django-webpack-loader to the requirements.txt

django
django-webpack-loader==1.0.0
(env)$ pip install -r requirements.txt

Update django_webpack_app/settings.py

INSTALLED_APPS = [
    'webpack_loader',

    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
]


WEBPACK_LOADER = {
    'DEFAULT': {
        'STATS_FILE': str(BASE_DIR / 'frontend' / 'webpack-stats.json'),
    },
}

Notes:

  1. Add webpack_loader to INSTALLED_APPS
  2. Add WEBPACK_LOADER to read the generated webpack-stats.json

Edit django_webpack_app/templates/index.html

{% load static %}

{% load render_bundle from webpack_loader %}

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  {% render_bundle 'app' 'css' %}
</head>
<body>

<div class="jumbotron">
  <div class="container">
    <h1 class="display-3">Hello, world!</h1>
    <p>This is a template for a simple marketing or informational website. It includes a large callout called a
      jumbotron and three supporting pieces of content. Use it as a starting point to create something more unique.</p>
    <p><a class="btn btn-primary btn-lg" href="#" role="button">Learn more »</a></p>
  </div>
</div>

</body>

{% render_bundle 'app' 'js' %}

</html>

Notes:

  1. We add {% load render_bundle from webpack_loader %} at the top
  2. We use {% render_bundle 'app' 'css' %} and {% render_bundle 'app' 'js' %} to load css and js from the frontend/build directory.
  3. We do not need to touch the hash filename here!
(env)$ ./manage.py runserver

If we check http://127.0.0.1:8000/, then in the HTML source code, we can see something like this

<script type="text/javascript" src="/static/js/vendors.109d1a3a.js" ></script>
<script type="text/javascript" src="/static/js/app.1e309b17.js" ></script>

Notes:

  1. npm run build generate the hash bundle files and webpack-stats.json
  2. webpack_loader help us find the hash bundle file so in Django template we can write code like {% render_bundle 'app' 'js' %}
  3. From django-webpack-loader>=1.0.0, the package supports auto load dependency, so {% render_bundle 'app' 'js' %} will also help load vendors automatically.

Local Development

  1. If we run npm run start, the output.filename is 'js/[name].js', no hash is in the filename. This is recommended for better performance.
  2. The webpack-stats.json will also be generated.
  3. The webpack_loader can still work without problems in this case.

Conclusion:

If your Webpack project generate hash bundle, then you might need django-webpack-loader to load them through the `webpack-stats.json

  1. Introduction
  2. Setup Webpack Project with Django
  3. Load Webpack bundles in Django
  4. Linting in Webpack
  5. Load Webpack hash bundle in Django
  6. Code splitting with Webpack
  7. How to config HMR with Webpack and Django
  8. How to use HtmlWebpackPlugin to load Webpack bundle in Django

If you want a quick start with Webpack and Django, please check python-webpack-boilerplate

Launch Products Faster with Django

SaaS Hammer helps you launch products in faster way. It contains all the foundations you need so you can focus on your product.

Michael Yin

Michael Yin

Michael is a Full Stack Developer from China who loves writing code, tutorials about Django, and modern frontend tech.

He has published some ebooks on leanpub and tech course on testdriven.io.

© 2024 SaaS Hammer