- Introduction
- Setup Webpack Project with Django
- Load Webpack bundles in Django
- Linting in Webpack
- Load Webpack hash bundle in Django
- Code splitting with Webpack
- How to config HMR with Webpack and Django
- 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:
- Understand
hash
,chunkhash
, andcontenthash
in Webpack - Learn what is Webpack
PublicPath
, and config it to work with Django. - 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:
- In
collectstatic
, the DjangoManifestStaticFilesStorage
would addhash
to the static assets. The hash value come from the content of the file. - If the Django app is running in the
production
mode, then{% static 'js/app.js' %}
would generatejs/app.647036872966.js
. - Only files that has been edited would have
new
hash, this makes the long-term caching possible. - 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:
- It is not recommend to use the above hash placeholder in
development
mode. (considering performance) - 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:
- The
output.filename
is setjs/[name].[chunkhash:8].js
, the:8
is slicing operation. - The
MiniCssExtractPlugin.filename
iscss/app-[contenthash].css
. Please check notes below why we should usecontenthash
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
django-webpack-loader
has a sister projectwebpack-bundle-tracker
, and they should be used together.webpack-bundle-tracker
would help generate awebpack stats file
.- And then
django-webpack-loader
would use thewebpack 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:
- We import
BundleTracker
at the top - We add
new BundleTracker({filename: './webpack-stats.json'}),
to the plugins to generatewebpack-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:
- 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:
- 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:
- Add
webpack_loader
toINSTALLED_APPS
- Add
WEBPACK_LOADER
to read the generatedwebpack-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:
- We add
{% load render_bundle from webpack_loader %}
at the top - We use
{% render_bundle 'app' 'css' %}
and{% render_bundle 'app' 'js' %}
to load css and js from thefrontend/build
directory. - 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:
npm run build
generate the hash bundle files andwebpack-stats.json
webpack_loader
help us find the hash bundle file so in Django template we can write code like{% render_bundle 'app' 'js' %}
- From
django-webpack-loader>=1.0.0
, the package supports auto load dependency, so{% render_bundle 'app' 'js' %}
will also help loadvendors
automatically.
Local Development
- If we run
npm run start
, theoutput.filename
is'js/[name].js'
, no hash is in the filename. This is recommended for better performance. - The
webpack-stats.json
will also be generated. - 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
- Introduction
- Setup Webpack Project with Django
- Load Webpack bundles in Django
- Linting in Webpack
- Load Webpack hash bundle in Django
- Code splitting with Webpack
- How to config HMR with Webpack and Django
- 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