django-webpack-loader
Read http://owaislone.org/blog/webpack-plus-reactjs-and-django/ for a detailed step by step guide on setting up webpack with django using this library.
Use webpack to generate your static bundles without django's staticfiles or opaque wrappers.
Django webpack loader consumes the output generated by webpack-bundle-tracker and lets you use the generated bundles in django.
A changelog is also available.
Compatibility
Test cases cover Django>=2.0 on Python>=3.5. 100% code coverage is the target so we can be sure everything works anytime. It should probably work on older version of django as well but the package does not ship any test cases for them.
Install
npm install --save-dev webpack-bundle-tracker
pip install django-webpack-loader
Migrating from version < 1.0.0
In order to use django-webpack-loader>=1.0.0
, you must ensure that [email protected]
is being used on the JavaScript side. It's recommended that you always keep at least minor version parity across both packages, for full compatibility.
This is necessary because the formatting of webpack-stats.json
that webpack-bundle-tracker
outputs has changed starting at version 1.0.0-alpha.1
. Starting at django-webpack-loader==1.0.0
, this is the only formatting accepted here, meaning that other versions of that package don't output compatible files anymore, thereby breaking compatibility with older webpack-bundle-tracker
releases.
Configuration
Assumptions
Assuming BASE_DIR
in settings refers to the root of your django app.
import sys
import os
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
Assuming assets/
is in settings.STATICFILES_DIRS
like
STATICFILES_DIRS = (
os.path.join(BASE_DIR, 'assets'),
)
Assuming your webpack config lives at ./webpack.config.js
and looks like this
var path = require('path');
var webpack = require('webpack');
var BundleTracker = require('webpack-bundle-tracker');
module.exports = {
context: __dirname,
entry: './assets/js/index',
output: {
path: path.resolve('./assets/webpack_bundles/'),
filename: "[name]-[hash].js"
},
plugins: [
new BundleTracker({filename: './webpack-stats.json'})
]
}
Default Configuration
WEBPACK_LOADER = {
'DEFAULT': {
'CACHE': not DEBUG,
'BUNDLE_DIR_NAME': 'webpack_bundles/', # must end with slash
'STATS_FILE': os.path.join(BASE_DIR, 'webpack-stats.json'),
'POLL_INTERVAL': 0.1,
'TIMEOUT': None,
'IGNORE': [r'.+\.hot-update.js', r'.+\.map'],
'LOADER_CLASS': 'webpack_loader.loader.WebpackLoader',
}
}
CACHE
WEBPACK_LOADER = {
'DEFAULT': {
'CACHE': not DEBUG
}
}
When CACHE
is set to True, webpack-loader will read the stats file only once and cache the result. This means web workers need to be restarted in order to pick up any changes made to the stats files.
BUNDLE_DIR_NAME
WEBPACK_LOADER = {
'DEFAULT': {
'BUNDLE_DIR_NAME': 'bundles/' # end with slash
}
}
BUNDLE_DIR_NAME
refers to the dir in which webpack outputs the bundles. It should not be the full path. If ./assets
is one of your static dirs and webpack generates the bundles in ./assets/output/bundles/
, then BUNDLE_DIR_NAME
should be output/bundles/
.
If the bundle generates a file called main-cf4b5fab6e00a404e0c7.js
and your STATIC_URL is /static/
, then the <script>
tag will look like this
<script src="/static/output/bundles/main-cf4b5fab6e00a404e0c7.js"/>
NOTE: If your webpack config outputs the bundles at the root of your staticfiles
dir, then BUNDLE_DIR_NAME
should be an empty string ''
, not '/'
.
STATS_FILE
WEBPACK_LOADER = {
'DEFAULT': {
'STATS_FILE': os.path.join(BASE_DIR, 'webpack-stats.json')
}
}
STATS_FILE
is the filesystem path to the file generated by webpack-bundle-tracker
plugin. If you initialize webpack-bundle-tracker
plugin like this
new BundleTracker({filename: './webpack-stats.json'})
and your webpack config is located at /home/src/webpack.config.js
, then the value of STATS_FILE
should be /home/src/webpack-stats.json
IGNORE
IGNORE
is a list of regular expressions. If a file generated by webpack matches one of the expressions, the file will not be included in the template.
POLL_INTERVAL
POLL_INTERVAL
is the number of seconds webpack_loader should wait between polling the stats file. The stats file is polled every 100 milliseconds by default and any requests to are blocked while webpack compiles the bundles. You can reduce this if your bundles take shorter to compile.
NOTE: Stats file is not polled when in production (DEBUG=False).
TIMEOUT
TIMEOUT
is the number of seconds webpack_loader should wait for webpack to finish compiling before raising an exception. 0
, None
or leaving the value out of settings disables timeouts.
LOADER_CLASS
LOADER_CLASS
is the fully qualified name of a python class as a string that holds the custom webpack loader. This is where behavior can be customized as to how the stats file is loaded. Examples include loading the stats file from a database, cache, external url, etc. For convenience, webpack_loader.loader.WebpackLoader
can be extended; The load_assets
method is likely where custom behavior will be added. This should return the stats file as an object.
Here's a simple example of loading from an external url:
# in app.module
import requests
from webpack_loader.loader import WebpackLoader
class ExternalWebpackLoader(WebpackLoader):
def load_assets(self):
url = self.config['STATS_URL']
return requests.get(url).json()
# in app.settings
WEBPACK_LOADER = {
'DEFAULT': {
'CACHE': False,
'BUNDLE_DIR_NAME': 'bundles/',
'LOADER_CLASS': 'app.module.ExternalWebpackLoader',
# Custom config setting made available in WebpackLoader's self.config
'STATS_URL': 'https://www.test.com/path/to/stats/',
}
}
Usage
Manually run webpack to build assets.
One of the core principles of django-webpack-loader is to not manage webpack itself in order to give you the flexibility to run webpack the way you want. If you are new to webpack, check one of the examples, read my detailed blog post or check webpack docs.
Settings
Add webpack_loader
to INSTALLED_APPS
INSTALLED_APPS = (
...
'webpack_loader',
)
Templates
{% load render_bundle from webpack_loader %}
{% render_bundle 'main' %}
render_bundle
will render the proper <script>
and <link>
tags needed in your template.
render_bundle
also takes a second argument which can be a file extension to match. This is useful when you want to render different types for files in separately. For example, to render CSS in head and JS at bottom we can do something like this,
{% load render_bundle from webpack_loader %}
<html>
<head>
{% render_bundle 'main' 'css' %}
</head>
<body>
....
{% render_bundle 'main' 'js' %}
</body>
</head>
Preload
The is_preload=True
option in the render_bundle
template tag can be used to add rel="preload"
link tags.
{% load render_bundle from webpack_loader %}
<html>
<head>
{% render_bundle 'main' 'css' is_preload=True %}
{% render_bundle 'main' 'js' is_preload=True %}
{% render_bundle 'main' 'css' %}
</head>
<body>
{% render_bundle 'main' 'js' %}
</body>
</html>
Appending file extensions
The suffix
option can be used to append a string at the end of the file URL. For instance, it can be used if your webpack configuration emits compressed .gz
files.
qwe
{% load render_bundle from webpack_loader %}
<html>
<head>
<meta charset="UTF-8">
<title>Example</title>
{% render_bundle 'main' 'css' %}
</head>
<body>
{% render_bundle 'main' 'js' suffix='.gz' %}
</body>
</html>
Multiple webpack projects
Version 2.0 and up of webpack loader also supports multiple webpack configurations. The following configuration defines 2 webpack stats files in settings and uses the config
argument in the template tags to influence which stats file to load the bundles from.
WEBPACK_LOADER = {
'DEFAULT': {
'BUNDLE_DIR_NAME': 'bundles/',
'STATS_FILE': os.path.join(BASE_DIR, 'webpack-stats.json'),
},
'DASHBOARD': {
'BUNDLE_DIR_NAME': 'dashboard_bundles/',
'STATS_FILE': os.path.join(BASE_DIR, 'webpack-stats-dashboard.json'),
}
}
{% load render_bundle from webpack_loader %}
<html>
<body>
....
{% render_bundle 'main' 'js' 'DEFAULT' %}
{% render_bundle 'main' 'js' 'DASHBOARD' %}
<!-- or render all files from a bundle -->
{% render_bundle 'main' config='DASHBOARD' %}
<!-- the following tags do the same thing -->
{% render_bundle 'main' 'css' 'DASHBOARD' %}
{% render_bundle 'main' extension='css' config='DASHBOARD' %}
{% render_bundle 'main' config='DASHBOARD' extension='css' %}
<!-- add some extra attributes to the tag -->
{% render_bundle 'main' 'js' 'DEFAULT' attrs='async charset="UTF-8"'%}
</body>
</head>
File URLs instead of html tags
If you need the URL to an asset without the HTML tags, the get_files
template tag can be used. A common use case is specifying the URL to a custom css file for a Javascript plugin.
get_files
works exactly like render_bundle
except it returns a list of matching files and lets you assign the list to a custom template variable. For example,
{% get_files 'editor' 'css' as editor_css_files %}
CKEDITOR.config.contentsCss = '{{ editor_css_files.0.publicPath }}';
<!-- or list down name, path and download url for every file -->
<ul>
{% for css_file in editor_css_files %}
<li>{{ css_file.name }} : {{ css_file.path }} : {{ css_file.publicPath }}</li>
{% endfor %}
</ul>
Refer other static assets
webpack_static
template tag provides facilities to load static assets managed by webpack in django templates. It is like django's built in static
tag but for webpack assets instead.
In the below example, logo.png
can be any static asset shipped with any npm package.
{% load webpack_static from webpack_loader %}
<!-- render full public path of logo.png -->
<img src="{% webpack_static 'logo.png' %}"/>
The public path is based on webpack.config.js
output.publicPath.
From Python code
If you want to access the webpack asset path information from your application code then you can use the function in the webpack_loader.utils
module.
>>> utils.get_files('main')
[{'url': '/static/bundles/main.js', u'path': u'/home/mike/root/projects/django-webpack-loader/tests/assets/bundles/main.js', u'name': u'main.js'},
{'url': '/static/bundles/styles.css', u'path': u'/home/mike/root/projects/django-webpack-loader/tests/assets/bundles/styles.css', u'name': u'styles.css'}]
>>> utils.get_as_tags('main')
['<script src="/static/bundles/main.js" ></script>',
'<link href="/static/bundles/styles.css" rel="stylesheet" />']
How to use in Production
It is up to you. There are a few ways to handle this. I like to have slightly separate configs for production and local. I tell git to ignore my local stats + bundle file but track the ones for production. Before pushing out newer version to production, I generate a new bundle using production config and commit the new stats file and bundle. I store the stats file and bundles in a directory that is added to the STATICFILES_DIR
. This gives me integration with collectstatic for free. The generated bundles are automatically collected to the target directory and synched to S3.
./webpack_production.config.js
var config = require('./webpack.config.js');
var BundleTracker = require('webpack-bundle-tracker');
config.output.path = require('path').resolve('./assets/dist');
config.plugins = [
new BundleTracker({filename: './webpack-stats-prod.json'})
]
// override any other settings here like using Uglify or other things that make sense for production environments.
module.exports = config;
settings.py
if not DEBUG:
WEBPACK_LOADER.update({
'BUNDLE_DIR_NAME': 'dist/',
'STATS_FILE': os.path.join(BASE_DIR, 'webpack-stats-prod.json')
})
You can also simply generate the bundles on the server before running collectstatic if that works for you.
Extra
Jinja2 Configuration
If you need to output your assets in a jinja template we provide a Jinja2 extension that's compatible with the Django Jinja module and Django 1.8.
To install the extension add it to the django_jinja TEMPLATES
configuration in the ["OPTIONS"]["extension"]
list.
from django_jinja.builtins import DEFAULT_EXTENSIONS
TEMPLATES = [
{
"BACKEND": "django_jinja.backend.Jinja2",
"OPTIONS": {
"extensions": DEFAULT_EXTENSIONS + [
"webpack_loader.contrib.jinja2ext.WebpackExtension",
],
}
}
]
Then in your base jinja template:
{{ render_bundle('main') }}
Enjoy your webpack with django :)
Alternatives to Django-Webpack-Loader
Below are known projects that attempt to solve the same problem:
Note that these projects have not been vetted or reviewed in any way by me. These are not recommendation. Anyone can add their project to this by sending a PR.