Load Webpack bundles in Django


Michael Yin

Full Stack Developer

Last updated on Feb 26 2021


Table of Contents

Django + Webpack Series:

  1. Introduction
  2. Setup Webpack Project with Django
  3. Load Webpack bundles in Django (this article)
  4. Linting in Webpack (coming soon)
  5. Loading Webpack hash bundle in Django (coming soon)
  6. Code splitting with Webpack (coming soon)
  7. How to use webpack-dev-server with Django to support HMR (coming soon)
  8. How to load Webpack bundle in Django without using 3-party package (coming soon)

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 generate 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

Django + Webpack Series:

  1. Introduction
  2. Setup Webpack Project with Django
  3. Load Webpack bundles in Django (this article)
  4. Linting in Webpack (coming soon)
  5. Loading Webpack hash bundle in Django (coming soon)
  6. Code splitting with Webpack (coming soon)
  7. How to use webpack-dev-server with Django to support HMR (coming soon)
  8. How to load Webpack bundle in Django without using 3-party package (coming soon)

Michael Yin

Michael is a Full Stack Developer from China who loves writing code, tutorials about Django, Wagtail CMS and React.

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

He is also the founder of the AccordBox which provides the web development services.


Table of Contents

This book will teach you how to build a SPA (single-page application) with React and Wagtail CMS

Read More

Subscribe

Get notified about new great Web Development Tutorial