How to config HMR with Webpack and 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 default, Django devserver will reload if we change .py file of the project.

In this blog, let's add the live reloading feature to the frontend project, if we edit SCSS or JS, the page will reload automatically.

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

  1. Learn to make webpack-dev-server work smoothly with Django
  2. Understand what is Hot Module Replacement (HMR) and the workflow

Advantage

In the previous chapter, we are using Webpack watch mode. (webpack can watch files and recompile whenever they change)

So some people might ask, why do we need webpack-dev-server?

webpack-dev-server will launch a server process, which makes live reloading possible.

So the whole workflow can work like this:

  1. We edit js or scss in the editor.
  2. The webpack-dev-server detect the change, it recompile the files. And send message to the browser. (It is built on WebSocket and I will talk about in a bit)
  3. Browser refresh the page automatically to let us see the change in realtime. (We do not have to manually refresh the page anymore)

Next, let's migrate from webpack watch mode to server mode.

Config

Update frontend/package.json

"start": "webpack serve --config webpack/webpack.config.dev.js"

We changed the start command to webpack server so it would launch webpack-dev-server

Update webpack/webpack.config.dev.js

const Path = require('path');
const Webpack = require('webpack');
const { merge } = require('webpack-merge');
const StylelintPlugin = require('stylelint-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');

const common = require('./webpack.common.js');

module.exports = merge(common, {
  mode: 'development',
  devtool: 'inline-cheap-source-map',
  output: {
    chunkFilename: 'js/[name].chunk.js',
    publicPath: 'http://localhost:9091/',
  },
  devServer: {
    inline: true,
    hot: true,
    port: 9091,
  },
  plugins: [
    new Webpack.DefinePlugin({
      'process.env.NODE_ENV': JSON.stringify('development'),
    }),
    new StylelintPlugin({
      files: Path.join('src', '**/*.s?(a|c)ss'),
    }),
    new MiniCssExtractPlugin({filename: 'css/app.css',})
  ],
  module: {
    rules: [
      {
        test: /\.js$/,
        include: Path.resolve(__dirname, '../src'),
        enforce: 'pre',
        loader: 'eslint-loader',
        options: {
          emitWarning: true,
        },
      },
      {
        test: /\.html$/i,
        loader: 'html-loader',
      },
      {
        test: /\.js$/,
        include: Path.resolve(__dirname, '../src'),
        loader: 'babel-loader',
      },
      {
        test: /\.s?css$/i,
        use: [MiniCssExtractPlugin.loader, 'css-loader?sourceMap=true', 'postcss-loader', 'sass-loader'],
      },
    ],
  },
});

Notes:

  1. We can add config of the webpack-dev-server in the devServer
  2. Here we set the port: 9091 to specify the port the server is listening
  3. Please note we changed publicPath from /static/ to http://localhost:9091/. Because now the bundle files are served by webpack-dev-server
(frontend)$ npm run start
# run in another terminal
(frontend)$ cat webpack-stats.json
{
  "status": "done",
  "chunks": {
    "app": [
      "js/jquery.js",
      "js/vendors.js",
      "css/app.css",
      "js/app.js"
    ]
  },
  "publicPath": "http://localhost:9091/",
  "assets": {
    "css/app.css": {
      "name": "css/app.css",
      "publicPath": "http://localhost:9091/css/app.css"
    },
    "js/app.js": {
      "name": "js/app.js",
      "publicPath": "http://localhost:9091/js/app.js"
    },
    "js/jquery.js": {
      "name": "js/jquery.js",
      "publicPath": "http://localhost:9091/js/jquery.js"
    },
    "js/vendors.js": {
      "name": "js/vendors.js",
      "publicPath": "http://localhost:9091/js/vendors.js"
    },
    "public/.gitkeep": {
      "name": "public/.gitkeep",
      "publicPath": "http://localhost:9091/public/.gitkeep"
    },
    "index.html": {
      "name": "index.html",
      "publicPath": "http://localhost:9091/index.html"
    }
  }
}

Notes:

  1. The bundle files are server on http://localhost:9091
  2. We can test by visiting http://localhost:9091/js/app.js in the browser.
  3. We can also check http://localhost:9091/webpack-dev-server to see what files are served by webpack-dev-server
  4. If we check the frontend/build directory, we can see the directory is empty. I will talk about it in next section.

writeToDisk

By default, the webpack-dev-server would serve files from memory, which means, we can not see the bundle files on the disk. (frontend/build in this project)

However, we can change this behavior to make it work the same as webpack watch mode.

Update webpack/webpack.config.dev.js

devServer: {
  inline: true,
  hot: true,
  port: 9091,
  writeToDisk: true,
},
  1. We set devServer.writeToDisk to true

Now if we rerun the npm run start, we can see the files in frontend/build directory.

./build
├── css
│   └── app.css
├── index.html
├── js
│   ├── app.js
│   ├── jquery.js
│   └── vendors.js
└── public

Hot Module Replacement (HMR)

Workflow

The HMR feature is built on WebSocket.

WebSocket is a computer communications protocol, providing full-duplex communication channels over a single TCP connection.

  1. With WebSocket, the browser and server would establish a long-lived connection. They can send or receive message between each other.
  2. If webpack-dev-server detect code change, it would recompile and send message to the browser.
  3. Then browser would refresh the web page after receiving the message.

Config

Update webpack/webpack.config.dev.js

const Path = require('path');
const Webpack = require('webpack');
const { merge } = require('webpack-merge');
const StylelintPlugin = require('stylelint-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');

const common = require('./webpack.common.js');

module.exports = merge(common, {
  target: 'web',
  mode: 'development',
  devtool: 'inline-cheap-source-map',
  output: {
    chunkFilename: 'js/[name].chunk.js',
    publicPath: 'http://localhost:9091/',
  },
  devServer: {
    inline: true,
    hot: true,
    port: 9091,
    writeToDisk: true,
  },

  // ...

});

Notes:

  1. Here we add target: 'web', we do this because of the bug of webpack-dev-server
  2. The inline: true, is needed (a script will be inserted in your bundle to take care of live reloading)
  3. The hot: true, in devServer means the HMR feature is enabled with webpack-dev-server.

Next, let's check how HMR work with webpack-dev-server.

  1. Rerun npm run start
  2. Open devtool in the browser.
  3. Click the network tab so you can check the web requests.
  4. In network tab, click the WS (WebSocket)
  5. Visit http://localhost:9091 in browser. (The html come from src/index.html and I will talk about it later)

We would see something like this

Notes:

  1. A websocket ws://localhost:9091/sockjs-node/346/dtoksgqu/websocket has been established.
  2. In the messages, we can see the detailed messages from the webpack-dev-server.

Next, let's update 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 from michaelyin");
});

Notes:

  1. We changed the success message in window.console.log
  2. We will see the web page refreshed automatically and dom ready from michaelyin show up on the console of the browser devtool

HMR with Django

Now we already have a good understanding of HMR and let's make it work with Django.

(env)$ ./manage.py runserver

If we try to change src/index.js and see if it works, we get exception from the console of the browser devtool.

Access to fetch at 'http://localhost:9091/app.5c361815e51c8082dd52.hot-update.json' from origin 'http://127.0.0.1:8000' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.

Notes:

  1. Here we try to fetch some file on localhost:9091 from origin 127.0.0.1:8000 in browser.
  2. The browser denied this because this is risky.
  3. You can check Cross-origin resource sharing to learn more.

Update webpack/webpack.config.dev.js

devServer: {
  inline: true,
  hot: true,
  port: 9091,
  writeToDisk: true,
  headers: {
    "Access-Control-Allow-Origin": "*",
  }
},

Notes:

  1. We added "Access-Control-Allow-Origin": "*" to the headers so CORS error would be resolved.

Now rerun npm run start, and update src/styles/index.scss

$primary: red;

@import "~bootstrap/scss/functions";
@import "~bootstrap/scss/variables";
@import "~bootstrap/scss/mixins";
...

Notes:

  1. This time, we update the index.scss and see if it works
  2. We add $primary: red; to the top.
  3. We can see the button color changed automatically.

The console of the browser devtool also have output like this

[HMR] Updated modules:
[HMR]  - ./src/styles/index.scss
[HMR] App is up to date.
[HMR] Reload all css

If we check the network tab carefully, we will see the web page is not full reloaded.

Conclusion

You can also check Hot Module Replacement to learn more about HMR.

  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