Load Webpack bundles 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. Import Bootstrap to the frontend project using npm install.
  2. Load Webpack bundle files with Django static template tag.
  3. Learn the difference between development mode and production mode in Webpack.

Import Bootstrap

Let's first import jquery and bootstrap to our Webpack project.

$ cd frontend

(frontend)$ npm install jquery
(frontend)$ npm install bootstrap

If we check frontend/package.json, we will see the packages exists in dependencies

"dependencies": {
  "@babel/polyfill": "^7.12.1",
  "bootstrap": "^4.6.0",
  "core-js": "^3.8.1",
  "jquery": "^3.5.1"
}

Update frontend/src/scripts/index.js

import "../styles/index.scss";

import $ from "jquery/dist/jquery.slim";
import "bootstrap/dist/js/bootstrap.bundle";

$(document).ready(function () {
  window.console.log("dom ready");
});

Notes:

  1. We import the index.scss file at the top. index.scss is the SCSS index file.
  2. And then we import jquery and bootstrap js bundle file (They are installed by npm install and exists at node_modules)
  3. Here we only use jquery.slim because we only need jquery to do some DOM operation.

Update frontend/src/styles/index.scss

@import "~bootstrap/scss/bootstrap.scss";

Notes:

  1. The tilde (~) means the relative path of the node_modules, this features is added by sass-loader
  2. Here we import Bootstrap SCSS files and webpack would help us build SCSS to CSS files.

Let's rerun npm run start

(frontend)$ npm run start

  modules by path ./node_modules/ 741 KiB
    ./node_modules/jquery/dist/jquery.slim.js 229 KiB [built] [code generated]
    ./node_modules/bootstrap/dist/js/bootstrap.bundle.js 231 KiB [built] [code generated]
    ./node_modules/jquery/dist/jquery.js 281 KiB [built] [code generated]
webpack 5.10.0 compiled successfully in 2665 ms
[webpack-cli] watching files for updates...

If we do not see any errors, which means our code is working

build
├── css
│   └── app.css
├── index.html
├── js
│   ├── app.js
│   └── vendors-node_modules_bootstrap_dist_js_bootstrap_bundle_js-node_modules_jquery_dist_jquery_slim_js.js
└── public

Code splitting

In the above section, after we import jquery and bootstrap in the index.js

We saw a new js file was generated which has a very long filename. From the filename, we can guess the file contains jquery and bootstrap code.

So let's think about this problem (assume you are the core developer of the webpack) and there are two options for you:

  1. Compile all js files to one file (app.js)
  2. Compile common part which does not change frequently to vendor-XXX.js, and the app logic part to app.js

Which one is better?

Solution 2 is better because in most cases, changes made to the app would only happen in app.js, so user who already use your products does not need to download the vendorjs file again. (And this can make the application run faster)

This behavior in webpack is controlled by SplitChunksPlugin, we will learn more about this in later chapter.

In this chapter, we choose the simplest plan, we can put the vender all in one file which has fixed filename.

Update frontend/webpack/webpack.common.js

optimization: {
  splitChunks: {
    chunks: 'all',
    name: 'vendors',
  },
},

We set the splitChunks.name to vendors, which is a string.

Let's rerun npm run start again.

(frontend)$ npm run start
./build
├── css
│   └── app.css
├── index.html
├── js
│   ├── app.js
│   └── vendors.js
└── public

Notes:

  1. Now app.js contains code from src/index.js
  2. And vendors.js contains code from jquery and bootstrap

Django Config

Update wagtail_bootstrap_blog/settings/base.py

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [
            str(BASE_DIR / 'django_webpack_app/templates'),
        ],
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        },
    },
]


STATIC_URL = '/static/'

# can be found at the bottom

STATICFILES_DIRS = [
    str(BASE_DIR / 'frontend/build'),
]
  1. We added str(BASE_DIR / 'django_webpack_app/templates') to the DIRS of TEMPLATES
  2. We added str(BASE_DIR / 'frontend/build') to STATICFILES_DIRS so Django can find the compiled bundle files.

Update django_webpack_app/templates/index.html

{% load static %}

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <link href="{% static 'css/app.css' %}" rel="stylesheet">
</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>

<script src="{% static 'js/vendors.js' %}"></script>
<script src="{% static 'js/app.js' %}"></script>

</html>

Notes:

  1. In head we imported css file using {% static 'css/app.css' %}
  2. At the bottom, we import js file using {% static 'js/vendors.js' %} and {% static 'js/app.js' %}

Update django_webpack_app/urls.py

from django.contrib import admin
from django.urls import path
from django.views.generic import TemplateView

urlpatterns = [
    path('', TemplateView.as_view(template_name='index.html')),
    path('admin/', admin.site.urls),
]

The Django would return index.html when we visit the root URL.

(env)$ python manage.py runserver

Notes:

  1. If we check on http://127.0.0.1:8000/, everything would still work as expected.
  2. We can also see message dom ready in the console of the devtool (code in frontend/src/scripts/index.js), which means the js run successfully.

SCSS

Let's take a step further

Update frontend/src/styles/index.scss

$primary: red;

// copied from "~bootstrap/scss/bootstrap.scss";

@import "~bootstrap/scss/functions";
@import "~bootstrap/scss/variables";
@import "~bootstrap/scss/mixins";
@import "~bootstrap/scss/root";
@import "~bootstrap/scss/reboot";
@import "~bootstrap/scss/type";
@import "~bootstrap/scss/images";
@import "~bootstrap/scss/code";
@import "~bootstrap/scss/grid";
@import "~bootstrap/scss/tables";
@import "~bootstrap/scss/forms";
@import "~bootstrap/scss/buttons";
@import "~bootstrap/scss/transitions";
@import "~bootstrap/scss/dropdown";
@import "~bootstrap/scss/button-group";
@import "~bootstrap/scss/input-group";
@import "~bootstrap/scss/custom-forms";
@import "~bootstrap/scss/nav";
@import "~bootstrap/scss/navbar";
@import "~bootstrap/scss/card";
@import "~bootstrap/scss/breadcrumb";
@import "~bootstrap/scss/pagination";
@import "~bootstrap/scss/badge";
@import "~bootstrap/scss/jumbotron";
@import "~bootstrap/scss/alert";
@import "~bootstrap/scss/progress";
@import "~bootstrap/scss/media";
@import "~bootstrap/scss/list-group";
@import "~bootstrap/scss/close";
@import "~bootstrap/scss/toasts";
@import "~bootstrap/scss/modal";
@import "~bootstrap/scss/tooltip";
@import "~bootstrap/scss/popover";
@import "~bootstrap/scss/carousel";
@import "~bootstrap/scss/spinners";
@import "~bootstrap/scss/utilities";
@import "~bootstrap/scss/print";

Notes:

  1. The top $primary: red set primary color in Bootstrap to red.
  2. The rest of the import statement come from bootstrap.scss, we can comment out some code to decrease the final css file size.
  3. You can check Theming bootstrap to learn more.

Now if we refresh and check on http://127.0.0.1:8000/ again, we will see the button color has changed. (Please remember to disable cache in browser when developing)

Production build

Please terminate the npm run start before reading content below.

Let's check build command in the frontend/package.json

For npm projects, 'npm run start' is for development and 'npm run build' is for production

"scripts": {
  "build": "cross-env NODE_ENV=production webpack --config webpack/webpack.config.prod.js",
},

We can see it set env NODE_ENV=production and run webpack with webpack.config.prod.js command.

Let's check frontend/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',
  stats: 'errors-only',
  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: 'bundle.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. Here we saw the mode: 'production',
  2. In production mode, Webpack would focus on minified bundles, lighter weight source maps, and optimized assets to improve load time.

Update frontend/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].js',
    chunkFilename: 'js/[name].[chunkhash:8].chunk.js',
  },
  plugins: [
    new Webpack.DefinePlugin({
      'process.env.NODE_ENV': JSON.stringify('production'),
    }),
    new MiniCssExtractPlugin({
      filename: 'css/app.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. We removed stats: 'errors-only' so we can get more useful info about the build process.
  2. We change the output.filename to js/[name].js, because we do not need the chunkhash here. (I will talk more about hash in later chapter)
  3. We changed the MiniCssExtractPlugin.filename to css/app.css. Since dev and prod both contains this config, you can move them to webpack.common.js on your own.
(frontend)$ npm run build

The files in frontend/build

build
├── css
│   ├── app.css
│   └── app.css.map
├── index.html
├── js
│   ├── app.js
│   ├── app.js.map
│   ├── vendors.js
│   ├── vendors.js.LICENSE.txt
│   └── vendors.js.map
└── public

Notes:

  1. app.js, vendors.js and app.css are also created in production mode.
  2. The sourcemap files are generated because of devtool: 'source-map'
  3. When we deploy our project to the production server, we should use npm run build instead of npm run start.

Conclusion

  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

Unlock the power of Django combined with Hotwire through SaaS Hammer. Supercharge productivity, tap into Python's rich ecosystem, and focus on perfecting your product!

Michael Yin

Michael Yin

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

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

© 2024 SaaS Hammer