Modern build tools transform the frontend development experience by providing features like hot module replacement, code splitting, automatic optimization, and seamless integration with CSS preprocessors and JavaScript transpilers. This chapter covers integrating Vite and Webpack with Django to create efficient development workflows and optimized production builds.
// package.json
{
"name": "django-vite-frontend",
"version": "1.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"watch": "vite build --watch"
},
"devDependencies": {
"vite": "^5.0.0",
"@vitejs/plugin-legacy": "^5.0.0",
"sass": "^1.69.0",
"autoprefixer": "^10.4.16",
"postcss": "^8.4.31",
"tailwindcss": "^3.3.5"
},
"dependencies": {
"alpinejs": "^3.13.0",
"axios": "^1.6.0"
}
}
// vite.config.js
import { defineConfig } from 'vite';
import { resolve } from 'path';
import legacy from '@vitejs/plugin-legacy';
export default defineConfig({
plugins: [
legacy({
targets: ['defaults', 'not IE 11']
})
],
// Build configuration
build: {
// Output directory (Django's STATIC_ROOT)
outDir: 'staticfiles/dist',
// Generate manifest for Django integration
manifest: true,
// Entry points
rollupOptions: {
input: {
main: resolve(__dirname, 'frontend/js/main.js'),
admin: resolve(__dirname, 'frontend/js/admin.js'),
blog: resolve(__dirname, 'frontend/js/blog.js')
},
output: {
// Organize output files
assetFileNames: (assetInfo) => {
const info = assetInfo.name.split('.');
const ext = info[info.length - 1];
if (/\.(css)$/.test(assetInfo.name)) {
return `css/[name]-[hash].${ext}`;
}
if (/\.(png|jpe?g|svg|gif|tiff|bmp|ico)$/i.test(assetInfo.name)) {
return `images/[name]-[hash].${ext}`;
}
if (/\.(woff2?|eot|ttf|otf)$/i.test(assetInfo.name)) {
return `fonts/[name]-[hash].${ext}`;
}
return `assets/[name]-[hash].${ext}`;
},
chunkFileNames: 'js/[name]-[hash].js',
entryFileNames: 'js/[name]-[hash].js'
}
},
// Code splitting
chunkSizeWarningLimit: 1000,
// Source maps for production debugging
sourcemap: true
},
// Development server
server: {
host: 'localhost',
port: 3000,
// Proxy API requests to Django
proxy: {
'/api': {
target: 'http://localhost:8000',
changeOrigin: true
},
'/admin': {
target: 'http://localhost:8000',
changeOrigin: true
}
}
},
// CSS configuration
css: {
preprocessorOptions: {
scss: {
additionalData: `@import "frontend/scss/variables.scss";`
}
},
postcss: {
plugins: [
require('tailwindcss'),
require('autoprefixer')
]
}
},
// Resolve configuration
resolve: {
alias: {
'@': resolve(__dirname, 'frontend'),
'@components': resolve(__dirname, 'frontend/js/components'),
'@utils': resolve(__dirname, 'frontend/js/utils'),
'@styles': resolve(__dirname, 'frontend/scss')
}
}
});
# settings.py
import os
import json
from pathlib import Path
# Vite configuration
VITE_DEV_MODE = DEBUG and os.environ.get('VITE_DEV_MODE', 'False').lower() == 'true'
VITE_DEV_SERVER_URL = 'http://localhost:3000'
if VITE_DEV_MODE:
# Development: Use Vite dev server
STATICFILES_DIRS = [
BASE_DIR / 'frontend',
]
else:
# Production: Use built assets
STATICFILES_DIRS = [
BASE_DIR / 'staticfiles' / 'dist',
]
# Vite manifest helper
def get_vite_manifest():
"""Load Vite manifest for production asset URLs."""
if VITE_DEV_MODE:
return {}
manifest_path = BASE_DIR / 'staticfiles' / 'dist' / 'manifest.json'
if manifest_path.exists():
with open(manifest_path) as f:
return json.load(f)
return {}
VITE_MANIFEST = get_vite_manifest()
# templatetags/vite_tags.py
from django import template
from django.conf import settings
from django.utils.safestring import mark_safe
from django.templatetags.static import static
register = template.Library()
@register.simple_tag
def vite_asset(entry_name):
"""
Generate script/link tags for Vite assets.
Usage:
{% vite_asset 'main.js' %}
{% vite_asset 'main.css' %}
"""
if settings.VITE_DEV_MODE:
# Development mode: use Vite dev server
if entry_name.endswith('.js'):
return mark_safe(
f'<script type="module" src="{settings.VITE_DEV_SERVER_URL}/{entry_name}"></script>'
)
elif entry_name.endswith('.css'):
return mark_safe(
f'<link rel="stylesheet" href="{settings.VITE_DEV_SERVER_URL}/{entry_name}">'
)
else:
# Production mode: use manifest
manifest = getattr(settings, 'VITE_MANIFEST', {})
if entry_name in manifest:
file_info = manifest[entry_name]
file_url = static(file_info['file'])
if entry_name.endswith('.js'):
html = f'<script type="module" src="{file_url}"></script>'
# Add CSS imports
if 'css' in file_info:
for css_file in file_info['css']:
css_url = static(css_file)
html += f'\n<link rel="stylesheet" href="{css_url}">'
return mark_safe(html)
elif entry_name.endswith('.css'):
return mark_safe(f'<link rel="stylesheet" href="{file_url}">')
return ''
@register.simple_tag
def vite_hmr():
"""Add Vite HMR client in development mode."""
if settings.VITE_DEV_MODE:
return mark_safe(
f'<script type="module" src="{settings.VITE_DEV_SERVER_URL}/@vite/client"></script>'
)
return ''
<!-- templates/base.html -->
{% load vite_tags %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}Django + Vite{% endblock %}</title>
<!-- Vite HMR in development -->
{% vite_hmr %}
<!-- Main CSS -->
{% vite_asset 'frontend/js/main.js' %}
{% block extra_css %}{% endblock %}
</head>
<body>
{% block content %}{% endblock %}
<!-- Page-specific JavaScript -->
{% block extra_js %}{% endblock %}
</body>
</html>
frontend/
├── js/
│ ├── main.js # Main entry point
│ ├── components/ # Reusable components
│ │ ├── modal.js
│ │ ├── dropdown.js
│ │ └── form-validator.js
│ ├── pages/ # Page-specific modules
│ │ ├── blog.js
│ │ ├── contact.js
│ │ └── dashboard.js
│ └── utils/ # Utility functions
│ ├── api.js
│ ├── dom.js
│ └── validation.js
├── scss/
│ ├── main.scss # Main stylesheet
│ ├── _variables.scss # SCSS variables
│ ├── _mixins.scss # SCSS mixins
│ ├── components/ # Component styles
│ └── pages/ # Page-specific styles
└── images/
└── icons/
// frontend/js/main.js
import '../scss/main.scss';
import Alpine from 'alpinejs';
import { initializeAPI } from './utils/api.js';
import { initializeComponents } from './components/index.js';
// Initialize Alpine.js
window.Alpine = Alpine;
Alpine.start();
// Initialize API utilities
initializeAPI();
// Initialize components
initializeComponents();
// Global utilities
window.app = {
csrfToken: document.querySelector('[name=csrfmiddlewaretoken]')?.value,
// API helper
async fetch(url, options = {}) {
const defaultOptions = {
headers: {
'X-CSRFToken': this.csrfToken,
'Content-Type': 'application/json',
},
credentials: 'same-origin'
};
return fetch(url, { ...defaultOptions, ...options });
}
};
// webpack.config.js
const path = require('path');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const { WebpackManifestPlugin } = require('webpack-manifest-plugin');
const TerserPlugin = require('terser-webpack-plugin');
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
const isDevelopment = process.env.NODE_ENV !== 'production';
module.exports = {
mode: isDevelopment ? 'development' : 'production',
entry: {
main: './frontend/js/main.js',
admin: './frontend/js/admin.js',
blog: './frontend/js/blog.js'
},
output: {
path: path.resolve(__dirname, 'staticfiles/dist'),
filename: isDevelopment ? 'js/[name].js' : 'js/[name].[contenthash].js',
chunkFilename: isDevelopment ? 'js/[name].chunk.js' : 'js/[name].[contenthash].chunk.js',
publicPath: '/static/dist/',
clean: true
},
module: {
rules: [
// JavaScript
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env'],
plugins: ['@babel/plugin-syntax-dynamic-import']
}
}
},
// CSS/SCSS
{
test: /\.(css|scss|sass)$/,
use: [
isDevelopment ? 'style-loader' : MiniCssExtractPlugin.loader,
'css-loader',
'postcss-loader',
'sass-loader'
]
},
// Images
{
test: /\.(png|jpe?g|gif|svg)$/i,
type: 'asset/resource',
generator: {
filename: 'images/[name].[hash][ext]'
}
},
// Fonts
{
test: /\.(woff2?|eot|ttf|otf)$/i,
type: 'asset/resource',
generator: {
filename: 'fonts/[name].[hash][ext]'
}
}
]
},
plugins: [
new CleanWebpackPlugin(),
new MiniCssExtractPlugin({
filename: isDevelopment ? 'css/[name].css' : 'css/[name].[contenthash].css',
chunkFilename: isDevelopment ? 'css/[name].chunk.css' : 'css/[name].[contenthash].chunk.css'
}),
new WebpackManifestPlugin({
fileName: 'manifest.json',
publicPath: '/static/dist/'
})
],
optimization: {
minimizer: [
new TerserPlugin({
terserOptions: {
compress: {
drop_console: !isDevelopment
}
}
}),
new CssMinimizerPlugin()
],
splitChunks: {
chunks: 'all',
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
chunks: 'all'
}
}
}
},
devtool: isDevelopment ? 'eval-source-map' : 'source-map',
devServer: {
static: {
directory: path.join(__dirname, 'staticfiles')
},
port: 3000,
hot: true,
proxy: {
'/api': 'http://localhost:8000',
'/admin': 'http://localhost:8000'
}
},
resolve: {
alias: {
'@': path.resolve(__dirname, 'frontend'),
'@components': path.resolve(__dirname, 'frontend/js/components'),
'@utils': path.resolve(__dirname, 'frontend/js/utils'),
'@styles': path.resolve(__dirname, 'frontend/scss')
}
}
};
# utils/webpack.py
import json
import os
from django.conf import settings
from django.contrib.staticfiles.storage import staticfiles_storage
class WebpackLoader:
"""Helper class for loading Webpack assets in Django templates."""
def __init__(self):
self.manifest = self._load_manifest()
def _load_manifest(self):
"""Load Webpack manifest file."""
if settings.DEBUG and os.environ.get('WEBPACK_DEV_MODE'):
return {}
manifest_path = os.path.join(settings.STATIC_ROOT or '', 'dist', 'manifest.json')
try:
with open(manifest_path, 'r') as f:
return json.load(f)
except (FileNotFoundError, json.JSONDecodeError):
return {}
def get_bundle(self, bundle_name):
"""Get bundle URLs from manifest."""
if settings.DEBUG and os.environ.get('WEBPACK_DEV_MODE'):
# Development mode
return {
'js': [f'http://localhost:3000/js/{bundle_name}.js'],
'css': []
}
# Production mode
js_files = []
css_files = []
# Main bundle
if f'{bundle_name}.js' in self.manifest:
js_files.append(staticfiles_storage.url(self.manifest[f'{bundle_name}.js']))
if f'{bundle_name}.css' in self.manifest:
css_files.append(staticfiles_storage.url(self.manifest[f'{bundle_name}.css']))
# Vendor bundle
if 'vendors.js' in self.manifest and bundle_name == 'main':
js_files.insert(0, staticfiles_storage.url(self.manifest['vendors.js']))
if 'vendors.css' in self.manifest and bundle_name == 'main':
css_files.insert(0, staticfiles_storage.url(self.manifest['vendors.css']))
return {
'js': js_files,
'css': css_files
}
webpack_loader = WebpackLoader()
# templatetags/webpack_tags.py
from django import template
from django.utils.safestring import mark_safe
from utils.webpack import webpack_loader
register = template.Library()
@register.simple_tag
def webpack_bundle(bundle_name):
"""Render script and link tags for a Webpack bundle."""
bundle = webpack_loader.get_bundle(bundle_name)
html = []
# CSS files
for css_url in bundle['css']:
html.append(f'<link rel="stylesheet" href="{css_url}">')
# JavaScript files
for js_url in bundle['js']:
html.append(f'<script src="{js_url}"></script>')
return mark_safe('\n'.join(html))
@register.simple_tag
def webpack_css(bundle_name):
"""Render only CSS tags for a Webpack bundle."""
bundle = webpack_loader.get_bundle(bundle_name)
html = []
for css_url in bundle['css']:
html.append(f'<link rel="stylesheet" href="{css_url}">')
return mark_safe('\n'.join(html))
@register.simple_tag
def webpack_js(bundle_name):
"""Render only JavaScript tags for a Webpack bundle."""
bundle = webpack_loader.get_bundle(bundle_name)
html = []
for js_url in bundle['js']:
html.append(f'<script src="{js_url}"></script>')
return mark_safe('\n'.join(html))
// package.json scripts
{
"scripts": {
"dev": "concurrently \"python manage.py runserver\" \"npm run vite:dev\"",
"vite:dev": "VITE_DEV_MODE=true vite",
"webpack:dev": "WEBPACK_DEV_MODE=true webpack serve",
"build": "vite build",
"build:webpack": "NODE_ENV=production webpack",
"watch": "vite build --watch",
"preview": "vite preview",
"lint": "eslint frontend/js --ext .js",
"lint:fix": "eslint frontend/js --ext .js --fix",
"format": "prettier --write frontend/js/**/*.js frontend/scss/**/*.scss"
}
}
# .env.development
DEBUG=True
VITE_DEV_MODE=True
WEBPACK_DEV_MODE=True
# .env.production
DEBUG=False
VITE_DEV_MODE=False
WEBPACK_DEV_MODE=False
# management/commands/build_frontend.py
from django.core.management.base import BaseCommand
import subprocess
import os
class Command(BaseCommand):
help = 'Build frontend assets'
def add_arguments(self, parser):
parser.add_argument(
'--tool',
choices=['vite', 'webpack'],
default='vite',
help='Build tool to use'
)
parser.add_argument(
'--watch',
action='store_true',
help='Watch for changes'
)
def handle(self, *args, **options):
tool = options['tool']
watch = options['watch']
if tool == 'vite':
cmd = ['npm', 'run', 'build']
if watch:
cmd = ['npm', 'run', 'watch']
else:
cmd = ['npm', 'run', 'build:webpack']
if watch:
cmd.extend(['--', '--watch'])
self.stdout.write(f'Building frontend assets with {tool}...')
try:
subprocess.run(cmd, check=True, cwd=os.getcwd())
self.stdout.write(
self.style.SUCCESS(f'Successfully built frontend assets with {tool}')
)
except subprocess.CalledProcessError as e:
self.stdout.write(
self.style.ERROR(f'Failed to build frontend assets: {e}')
)
# .github/workflows/deploy.yml
name: Deploy
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Build frontend assets
run: npm run build
- name: Setup Python
uses: actions/setup-python@v4
with:
python-version: '3.11'
- name: Install Python dependencies
run: |
pip install -r requirements.txt
- name: Collect static files
run: |
python manage.py collectstatic --noinput
- name: Deploy to production
run: |
# Your deployment commands here
# Dockerfile
FROM node:18-alpine AS frontend-builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY frontend/ ./frontend/
COPY vite.config.js ./
RUN npm run build
FROM python:3.11-slim
WORKDIR /app
# Copy Python requirements and install
COPY requirements.txt .
RUN pip install -r requirements.txt
# Copy Django application
COPY . .
# Copy built frontend assets
COPY --from=frontend-builder /app/staticfiles/dist ./staticfiles/dist
# Collect static files
RUN python manage.py collectstatic --noinput
EXPOSE 8000
CMD ["gunicorn", "myproject.wsgi:application", "--bind", "0.0.0.0:8000"]
// Dynamic imports for code splitting
const loadBlogModule = () => import('./pages/blog.js');
const loadDashboardModule = () => import('./pages/dashboard.js');
// Route-based code splitting
const routes = {
'/blog/': loadBlogModule,
'/dashboard/': loadDashboardModule
};
// Load modules based on current page
const currentPath = window.location.pathname;
if (routes[currentPath]) {
routes[currentPath]().then(module => {
module.default();
});
}
// vite.config.js - Advanced optimization
export default defineConfig({
build: {
rollupOptions: {
output: {
manualChunks: {
vendor: ['axios', 'alpinejs'],
utils: ['./frontend/js/utils/api.js', './frontend/js/utils/dom.js']
}
}
},
// Compression
minify: 'terser',
terserOptions: {
compress: {
drop_console: true,
drop_debugger: true
}
}
}
});
Modern build tools like Vite and Webpack transform Django frontend development by providing hot module replacement, automatic optimization, and seamless integration with CSS preprocessors and JavaScript transpilers. Choose Vite for faster development builds and simpler configuration, or Webpack for more complex build requirements and extensive plugin ecosystem. Both tools integrate well with Django's static files system and can be configured for efficient production deployments.
Integrating CSS and JavaScript
Modern web applications require sophisticated CSS and JavaScript integration to deliver rich user experiences. This chapter covers advanced techniques for organizing, processing, and optimizing CSS and JavaScript in Django applications, including preprocessors, module systems, and build workflows.
Using React or Vue with Django
Modern frontend frameworks like React and Vue enable building sophisticated single-page applications (SPAs) that communicate with Django backends through APIs. This chapter covers various integration patterns, from simple component embedding to full SPA architectures, including authentication, state management, and deployment strategies.