- 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, andcontenthashin Webpack - Learn what is Webpack
PublicPath, and config it to work with Django. - Use
django-webpack-loaderto 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 DjangoManifestStaticFilesStoragewould addhashto the static assets. The hash value come from the content of the file. - If the Django app is running in the
productionmode, then{% static 'js/app.js' %}would generatejs/app.647036872966.js. - Only files that has been edited would have
newhash, 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
developmentmode. (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.filenameis setjs/[name].[chunkhash:8].js, the:8is slicing operation. - The
MiniCssExtractPlugin.filenameiscss/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-loaderhas a sister projectwebpack-bundle-tracker, and they should be used together.webpack-bundle-trackerwould help generate awebpack stats file.- And then
django-webpack-loaderwould use thewebpack stats fileto 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
BundleTrackerat 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
publicPathto/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_loadertoINSTALLED_APPS - Add
WEBPACK_LOADERto 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/builddirectory. - We do not need to touch the
hashfilename 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 buildgenerate the hash bundle files andwebpack-stats.jsonwebpack_loaderhelp 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 loadvendorsautomatically.
Local Development
- If we run
npm run start, theoutput.filenameis'js/[name].js', no hash is in the filename. This is recommended for better performance. - The
webpack-stats.jsonwill also be generated. - The
webpack_loadercan 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