I am working on a Django project and want to enhance the front-end using npm and webpack. My web development experience has been primarily with Django, and I’m new to npm and webpack. I’ve run into a few issues during this integration process and need some guidance.
Here are some of the libraries I am including in my project:
- Django v3.2
- python 3.7.13
- Node Libraries I am using
- Bootstrap v5.3.3
- Bootstrap-icons v1.11.3
- jQuery v3.7.1
- jQuery-ui v1.13.3
- jQuery-validation v1.20.0
- metro4-dist v4.5.0
- webpack v5.91.0
- webpack-cli v5.1.4
- webpack-dev-server v5.0.4
- [etc]
Tutorials/Guides I’ve Used
Sharing guides I used to help others in my scenario along their way.
- Modern JavaScript for Django Developers : This helped me understand why npm and webpack were necessary.
- Definitive Guide to Django and Webpack : Great guide for detailed configuration.
- Learn Webpack – Full Tutorial for Beginners : Video that walks you through setup
- Django with Webpack : Another video explaining how to configure webpack with a Django Project.
Coming from “Modern Javascript for Django” put it best for me on my situation.
First you learn Django. You go through the tutorial, learn about
models and views, and get your very first “hello world” application up
and running. You’re on a roll!Then, inevitably, you want to do something that requires a little
front-end interaction. Maybe a modal dialog, some real-time form
validation, or a mobile menu. It doesn’t matter.“No problem!” you say. “I’ll use JavaScript for this!”
Django makes it easy to drop JavaScript directly into your HTML
templates, so you do some Googling and copy/paste some code from Stack
Overflow, maybe import jQuery and a plugin or two, and you’re up and
running.And it works great. Still on a roll!
Then, over time, there are more modals and more menus and more pages.
You start doing more complex things like managing state, dynamically
rendering components, and sharing code across templates.It starts to feel unwieldy, but you keep pressing on. A document
selector here, an event callback there, maybe a few more libraries
imported on various pages as you need them. The front-end code slowly
expands through your Django templates and static files until it feels
like JavaScript has crept its tentacles throughout the entire
codebase.At some point, your front-end code may start to feel like a spaghetti
monster—a tangled mess of code and dependencies spread across your
JavaScript and Django template files.
There are some great tutorials I have been able to find, but none seem to cater exactly where I need help. I think most of my issues boil down learning the concepts while following guides that have changed in implementation (i.e. webpack4 > webpack5)
Issues
-
openBrowser() does not work as intended
The function I wrote to launch a browser on the webpack-dev-server does not work as expected. Any suggestions for achieving this?
-
Configuring webpack-dev-server with Django for HMR
Integrating webpack-dev-server with Django’s server for Hot Module Replacement has been challenging. I tried a few different methods, but I kept getting a blank webpage with the error “Cannot Get /” based on guides I followed.
Currently, what does work is the
proxy
option on the devServer config andwriteToDisk
option for devMiddleware. I question if there is a better/optimal approach? Can I make it where Django’s server can read the bundled files via memory? -
Bundled Files Not Recognized with Certain URLs
When viewing the browser’s dev tools, I can see the page source for my bundeled files when my URL is:
- http://localhost:8080/
- http://localhost:8080/help
- http://localhost:8080/sysetms/
- http://localhost:8080/sites/
But my bundled files are not recognized if I try to navigate to a page like:
- http://localhost:8080/report/site/[some_name]/
- http://localhost:8080/report/system/[some_name]/
How can I troubleshoot and resolve this issue?
-
webpack-dev-server launches http://localhost:80801/
Why is port 8081 being used instead of 8080? Is this because Django’s server is using 8080 too? I can manually go to port 8080 and my application is available, but I don’t understand why this is happening.
Configuration
Notes
- I utilize some of Django’s template tags to help with organization of sub templates (i.e.
{% include subtemplate.html %}
or{$% block content %} {% endblock content %}
so not all HTML templates need the bundled files on page load – that is why I use theinject: false
option for HtmlWebpackPlugin. - I have several templates that Django has to be able to recognize, so I use the HtmlWebpackPlugin to create all those HTML templates in the
static/
folder of root directory. - When running the webpack-dev-server, I want to default using Google Chrome browser. If that is not available on the host machine, then use the host’s default browser.
- I use a lot of SVG sprites from Bootstrap-icons throughout my templates, one icon from metro4-dist, and I have a few .pdf documents that I want webpack to create in the
dist/
folder under their respective directories. I read its better to make SVG’s inline vs creating there own file, correct? - I know by default, webpack places the output files in
dist/
and Django looks for its source files instatic/
, so the configuration has to change for one of them. I made Django static url read from the “/dist/”
package.json
{
"name": "[removed for privacy]",
"version": "1.0.0",
"private": true,
"description": "[removed for privacy]",
"exports": "./index.js",
"scripts": {
"start": "webpack-dev-server --config webpack/webpack.config.dev.js",
"build": "webpack --config webpack/webpack.config.prod.js"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"clean-webpack-plugin": "^4.0.0",
"css-loader": "^7.1.1",
"css-minimizer-webpack-plugin": "^6.0.0",
"html-loader": "^5.0.0",
"html-webpack-plugin": "^5.6.0",
"mini-css-extract-plugin": "^2.9.0",
"open": "^10.1.0",
"prettier": "^3.2.5",
"prettier-plugin-organize-attributes": "^1.0.0",
"sass": "^1.77.0",
"sass-loader": "^14.2.1",
"style-loader": "^4.0.0",
"terser-webpack-plugin": "^5.3.10",
"webpack": "^5.91.0",
"webpack-cli": "^5.1.4",
"webpack-dev-server": "^5.0.4",
"webpack-merge": "^5.10.0"
},
"dependencies": {
"@floating-ui/dom": "^1.6.4",
"bootstrap": "^5.3.3",
"bootstrap-icons": "^1.11.3",
"jquery": "^3.7.1",
"jquery-ui": "^1.13.3",
"jquery-validation": "^1.20.0",
"metro4-dist": "^4.5.0"
},
"type": "module",
"engines": {
"node": ">=16"
}
}
webpack.config.common.js
import HtmlWebpackPlugin from "html-webpack-plugin"
import path from "path"
import {fileURLToPath} from "url"
const __dirname = path.dirname(fileURLToPath(import.meta.url))
export default {
entry: {
vendor: path.resolve(__dirname, "../src/vendor.js"),
main: path.resolve(__dirname, "../src/index.js"),
},
plugins: [
new HtmlWebpackPlugin({
template: path.resolve(__dirname, "../src/templates/base.html"),
filename: path.resolve(__dirname, "../templates/base.html"),
minify: false,
favicon: path.resolve(__dirname, "../static/images/favicon.ico"),
}),
new HtmlWebpackPlugin({
template: path.resolve(
__dirname,
"../src/templates/circuit_form.html",
),
filename: path.resolve(__dirname, "../templates/circuit_form.html"),
minify: false,
inject: false,
}),
new HtmlWebpackPlugin({
template: path.resolve(
__dirname,
"../src/templates/circuit_report.html",
),
filename: path.resolve(
__dirname,
"../templates/circuit_report.html",
),
minify: false,
inject: false,
}),
new HtmlWebpackPlugin({
template: path.resolve(
__dirname,
"../src/templates/csd-series_form.html",
),
filename: path.resolve(
__dirname,
"../templates/csd-series_form.html",
),
minify: false,
inject: false,
}),
new HtmlWebpackPlugin({
template: path.resolve(
__dirname,
"../src/templates/csd-series_report.html",
),
filename: path.resolve(
__dirname,
"../templates/csd-series_report.html",
),
minify: false,
inject: false,
}),
new HtmlWebpackPlugin({
template: path.resolve(__dirname, "../src/templates/footer.html"),
filename: path.resolve(__dirname, "../templates/footer.html"),
minify: false,
inject: false,
}),
new HtmlWebpackPlugin({
template: path.resolve(__dirname, "../src/templates/help.html"),
filename: path.resolve(__dirname, "../templates/help.html"),
minify: false,
inject: false,
}),
new HtmlWebpackPlugin({
template: path.resolve(__dirname, "../src/templates/home.html"),
filename: path.resolve(__dirname, "../templates/home.html"),
minify: false,
inject: false,
}),
new HtmlWebpackPlugin({
template: path.resolve(__dirname, "../src/templates/navbar.html"),
filename: path.resolve(__dirname, "../templates/navbar.html"),
minify: false,
inject: false,
}),
new HtmlWebpackPlugin({
template: path.resolve(__dirname, "../src/templates/results.html"),
filename: path.resolve(__dirname, "../templates/results.html"),
minify: false,
inject: false,
}),
new HtmlWebpackPlugin({
template: path.resolve(
__dirname,
"../src/templates/site_form.html",
),
filename: path.resolve(__dirname, "../templates/site_form.html"),
minify: false,
inject: false,
}),
new HtmlWebpackPlugin({
template: path.resolve(
__dirname,
"../src/templates/site_report.html",
),
filename: path.resolve(__dirname, "../templates/site_report.html"),
minify: false,
inject: false,
}),
new HtmlWebpackPlugin({
template: path.resolve(
__dirname,
"../src/templates/system_form.html",
),
filename: path.resolve(__dirname, "../templates/system_form.html"),
minify: false,
inject: false,
}),
new HtmlWebpackPlugin({
template: path.resolve(
__dirname,
"../src/templates/system_report.html",
),
filename: path.resolve(
__dirname,
"../templates/system_report.html",
),
minify: false,
inject: false,
}),
],
}
webpack.config.dev.js
import MiniCssExtractPlugin from "mini-css-extract-plugin"
import open from "open"
import path from "path"
import {fileURLToPath} from "url"
import {merge} from "webpack-merge"
import common from "./webpack.config.common.js"
const __dirname = path.dirname(fileURLToPath(import.meta.url))
const url = "http://localhost:8080"
function getBrowserName() {
if (process.platform === "darwin") {
return "Google Chrome"
} else if (process.platform === "linux") {
return "google-chrome"
} else {
return "chrome"
}
}
async function openBrowser(url) {
const browserName = getBrowserName()
try {
await open(url, {
app: {
name: browserName,
},
})
console.log(`Opened devServer with URL: ${url}`)
} catch (error) {
await open(url)
console.error(
"Failed to open devServer with Google Chrome browser. Opening in default browser instead.",
)
}
}
const config = merge(common, {
mode: "development",
output: {
filename: "[name].bundle.js",
path: path.resolve(__dirname, "../dist"),
clean: true,
},
devtool: "source-map",
devServer: {
hot: true,
proxy: [
{
context: ["**"],
target: url,
},
],
open: openBrowser(url),
devMiddleware: {
writeToDisk: true,
},
},
plugins: [new MiniCssExtractPlugin({filename: "[name].css"})],
module: {
rules: [
{
test: /.scss$/i,
use: [
MiniCssExtractPlugin.loader, // 3. Extract css into its own file
"css-loader", // 2. Turns css into commonjs
"sass-loader", // 1. Turns sass into css
],
},
{
test: /.html$/i,
use: ["html-loader"],
},
{
test: /.svg$/i,
type: "asset/inline",
generator: {
filename: "images/[name][ext]",
},
},
{
test: /.(png|jpe?g|gif)$/i,
type: "asset/resource",
generator: {
filename: "images/[name][ext]",
},
},
{
test: /.(pdf|txt)$/i,
type: "asset/resource",
generator: {
filename: "documents/[name][ext]",
},
},
],
},
})
export default config
webpack.config.prod.js
import CssMinimizerWebpackPlugin from "css-minimizer-webpack-plugin"
import MiniCssExtractPlugin from "mini-css-extract-plugin"
import path from "path"
import TerserPlugin from "terser-webpack-plugin"
import {fileURLToPath} from "url"
import {merge} from "webpack-merge"
import common from "./webpack.config.common.js"
const __dirname = path.dirname(fileURLToPath(import.meta.url))
const config = merge(common, {
mode: "production",
output: {
filename: "[name].[contenthash].bundle.js",
path: path.resolve(__dirname, "../dist"),
clean: true,
},
optimization: {
minimizer: [new CssMinimizerWebpackPlugin(), new TerserPlugin()],
},
plugins: [new MiniCssExtractPlugin({filename: "[name].[contenthash].css"})],
module: {
rules: [
{
test: /.scss$/i,
use: [
MiniCssExtractPlugin.loader, // 3. Extract css into its own file
"css-loader", // 2. Turns css into commonjs
"sass-loader", // 1. Turns sass into css
],
},
{
test: /.html$/i,
use: ["html-loader"],
},
{
test: /.svg$/i,
type: "asset/inline",
generator: {
filename: "images/[name].[hash][ext]",
},
},
{
test: /.(png|jpe?g|gif)$/i,
type: "asset/resource",
generator: {
filename: "images/[name].[hash][ext]",
},
},
{
test: /.(pdf|txt)$/i,
type: "asset/resource",
generator: {
filename: "documents/[name].[hash][ext]",
},
},
],
},
})
export default config
Django settings.py
[..some code..]
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/3.2/howto/static-files/
# These settings are for Django serving the static files (Custom files)
STATIC_URL = "/dist/"
STATICFILES_DIRS = [
os.path.join(BASE_DIR, "static"),
os.path.join(BASE_DIR, "dist"),
]
# This setting is for Apache serving the static files (Admin Page)
STATIC_ROOT = os.path.join(BASE_DIR, "static/admin")