Webpack and Django#
Where we started#
I recently started to modernize the JavaScript part of a medium sized Django site we run at DKRZ to manage our projects. We have used a version of this site since 2002 and the current Django implementation was initially developed in 2011.
Back then JavaScript was in the form of small scripts embedded into the Django templates. jQuery was used abundantly. All in all, JavaScript was handled very haphazardly because we wanted to get back to working with Python as soon as possible.
This didn’t change much over the years but the overall amount of JavaScript code grew and also included a couple of packages from Bootstrap over TinyMCE to charts with D3.js. Keeping those packages up to date became a huge pain as well as maintaining the JavaScript code we wrote ourselves.
What are the options#
So I started looking around how people in a similar situation tackled the problem and found - surprisingly little. It seemed the ecosystems of Django and JavaScript didn’t overlap very much. For JavaScript people, Django is a backend at best and all you need from it is the ORM with a thin REST layer on top to serve a complex PWA built with React or Vue.js. While there is merit to this approach, I think most developers who maintain an existing Django site do not want to rewrite everything from scratch just to have less work with package updates and use some ES6 or TypeScript for a change.
For a less intrusive solution I found bits and pieces like Pascal’s article on how to use staticfiles to serve webpack bundles or the one by Valentino in which he discusses the importance of chunk hashes. But I didn’t find a place where the entire process was spelled out. Of course, this is pretty specific to this one site but you might find aspects of it helpful for your work.
I won’t go into the details of how to write a Django site or how to start with webpack. Those projects have excellent docs to which I can add nothing.
What I will talk about is how to go from a site as described above to one with a frontend built with webpack.
Extract Javascript from Django templates#
Hopefully you never did this but we had lots of JavaScript inside our templates and to make things worse, there were Django context variables sprinkled into the JavaScript code. Why? Because it was easy to do and solved the problem of passing information from Django to JavaScript. I won’t recount all the reasons why this is a bad idea and instead focus on how to disentangle this situation.
Move all code from each template into its own .js module. There should be one function in the module which is exported as default.
Include any external packages needed by this page.
If you need dynamic information from the backend, then use Django’s json_script tag to put your context data on the page. Your JavaScript module can parse this data with JSON.parse. Simple variables can also be passed through data attributes.
It’s probably a good idea to start only with one or two pages and then see how the module is actually loaded. Here is one simple example.
export default function suggestUserName() {
const applyButton = document.getElementById('apply');
if (applyButton) {
applyButton.addEventListener('click', (event) => {
const { newUserName } = event.target.dataset;
document.getElementById('id_username').value = newUserName;
});
}
}
It adds an event listener to a <button> element which will fill in an <input> element once clicked. The data comes from a data attribute which was generated by the Django backend.
The central App#
Now we are going to create the central module of the JavaScript application which will be run for every page. All it does is to dynamically load one or more of those modules we created above. How does it know which modules that should be? We tell it by writing the information into the template.
Here is the simplified code
document.addEventListener('DOMContentLoaded', () => {
document.querySelectorAll(
'.frontend-module',
).forEach((element) => {
if ('name' in element.dataset) {
import(
/* webpackChunkName: "[request]" */
`./${element.dataset.name}`
).then((module) => {
module.default();
});
}
});
});
Once the DOM is loaded, we look for elements of class frontend-module. They should have a data attribute name with the name of modules containing code for this page. Webpack does all the heavy lifting here by letting the page load this code only when we actually need it. As I mentioned above, some of our pages require very hefty packages like TinyMCE which we certainly don’t want to load on any other page. Once the promise is fulfilled i.e. the module is imported, we run the default function which does the actual work.
Here is how to reference a module in the template.
<span class="frontend-module" data-name="users/new_user"></span>
Webpack configuration#
The webpack.config.js will look something like this
module.exports = {
entry: {
main: './frontend/index.js',
},
There is one entry with the code of the main module-loading application. For our site we also do some additional stuff like importing bootstrap which is used on every page.
output: {
filename: '[name].[contenthash].bundle.js',
path: path.resolve(__dirname, 'static/chunks'),
publicPath: '/static/chunks/',
},
There are many ways to name the output but I would recommend to include the hash. We will see later how to load the initial chunks in the Django template. The path points to a path managed by Django’s staticfiles. You don’t want the generated chunks in your source repository so I put them in a separate directory which is ignored by the SCM.
Module loaders are up to you. Webpack has all relevant information about them.
plugins: [
new HtmlWebpackPlugin({
inject: false,
templateContent: ({htmlWebpackPlugin}) => `${htmlWebpackPlugin.tags.headTags}${htmlWebpackPlugin.tags.bodyTags}`,
filename: `../../templates/generated/main_bundle.html`,
}),
],
You will use more plugins but here I want to explain the HtmlWebpackPlugin. We have the problem that webpack creates a bunch of chunks which have a different name each time we change the code or a package gets updated. These have to be included into our main Django template. The modules with the individual page code will be imported later by the app itself so we don’t have to worry about those.
The HtmlWebpackPlugin solves this problem by generating a page with all the most recent script and link tags. We could let the plugin generate our entire template but I opted just for the tags which is what the inline templateContent gives us. The file is usually created in the main output path along with the chunks. For Django we need it to be found by the template engine so we use a relative path to get there. Again, this file should be ignored by SCM because it is generated and changes all the time.
In the main Django template we simply include the generated file
{% include 'generated/main_bundle.html' %}
Other plugins I found helpful#
Since Django still renders most of the page, we can get an ugly FOUC because relevant CSS may be injected only after the bundle initialized. Use the MiniCssExtractPlugin to get around this.
If some chunks are still too large, the splitChunks optimization can help.
Use ESLintPlugin to catch errors and enforce coding standards.
For TinyMCE I found that the CopyPlugin works to get the skin assets to the static output directory. TinyMCE docs recommend the file-loader but that didn’t work for me and the file-loader is deprecated anyway.
Conclusions#
Webpack allowed us to modernize the frontend of our Django site and made it much easier to maintain code and packages and add new features.
Each page can still have individual JavaScript code which now lives in ES6 modules.
The name of the module(s) need only to be referenced on the page’s template. No need to change or add anything to the central app or webpack configuration.
Webpack creates cache friendly chunks for all your modules which will be loaded on demand. Users who don’t access pages with some big package will never load that code.
No additional Python packages are needed to interface with Django. Only a simple template fragment is included which webpack generates for us.