Evolving your Django frontend
Having a webpack build process of your assets is pretty standard, but what happens when you need to include files from angular-cli as well?
The front end requirements for a Django application I’ve been working on have been growing for some time. It started off simple. A bit of inline javascript and a few jQuery widgets were all we needed. It was a classic, get it out, get it working, there’ll be time for better engineering later scenario.
However, requirements just kept on growing, the jQuery got more complex, and finally it got so complex that any small change left us rather anxious as to what might happen to the overall functionality. We had built our very own Spaghetti Monster, and it was time for a change.
Angular enters, stage left
“Let’s use a frontend framework” suggested someone (it was probably me).
We clearly needed more structure. We hadn’t had the time to put any effort into selenium testing, without which our front end was basically untest-able. And we didn’t have the devops or QA resources to solve that problem. Having more structure and a framework that came with testing would let us reduce bugs and improve stability for future changes.
“Which one should we use?” we all asked.
As a team, we didn’t have a huge amount of experience in any of them, but were generally leaning toward React. But the company as a whole had a preference for moving all their stuff toward Angular. So we started developing with Angular.
What we didn’t want to do was refactor everything in one go. Most of the site was working perfectly fine as it was. We’re a small team, we needed to only change some of the application, but we had a longer term goal of gradually refactoring it all over time. So we wanted to be able to combine our solutions. Some of our pages would be angular, and some would still be the old jQuery implementations that we would aim to retire later.
That’s a lot of background, but it’s really to head off the inevitable criticism of the solution that I am about to describe, which is that this isn’t how Angular is supposed to be used. I know this, and if you don’t need this information, feel free to move along.
Loading our assets
We were already using the django webpack loader to include our normal JS and stylesheet assets in our templates, and of course we would need to do something similar with production assets to ensure that we could cache bust browsers loading the angular assets. I found this post which was essentially describing what I needed to do: add to the angular-cli webpack process by using ng eject
to extract the webpack configuration, and then customise from there.
But this was for an older version of Angular. It no longer supports ng eject
, and it was a bit ugly. It basically meant that the entire angular-cli standard commands would no longer work. So almost what I needed but not quite.
And then I found this rather helpful post about custom builders as an alternative to ng-eject
. Finally I had a way of adding to the build process to get the bundle data into a useable form for django template rendering.
The Solution So Far
The solution we have is not perfect, but it seems to be doing what we need it do at this point. We have our angular code installed in a subdirectory off the project root, called angular
(10/10 for originality there of course).
First off, we needed to install our angular builder dependencies:
cd angular
npm install @angular-devkit/custom-webpack --save
npm install @angular-builders/custom-webpack --save
And then we were going to need the bundle tracker as well:
npm install webpack-bundle-tracker --save
Going into the angular.json
file, we then changed the configuration of the build process to use the custom webpack builder:
"architect": {
"build": {
"builder": "@angular-builders/custom-webpack:browser",
"options": {
"customWebpackConfig": {
"path": "./extra-webpack.config.js"
},
"outputPath": "../assets/bundles/angular",
This tells angular to use the additional config in angular/extra-webpack.config.js
as part of its build process. The outputPath
option was telling angular to put the built files into a common path that made sense for django as well.
In the extra-webpack.config.js
file, we define the bundle tracker behaviour:
const BundleTracker = require('webpack-bundle-tracker');
module.exports = {
plugins:[
new BundleTracker({filename: '../webpack-stats-angular.json'})
],
output: {
path: require('path').resolve('../assets/bundles/angular'),
filename: "[name]-[hash].js",
}
};
We are telling BundleTracker
to output the bundle details to a stats file in the root of the project with the filename
option, and build these stats from the files put into the angular outputPath
location.
From here, it was simply a case of then configuring django to use this new stats file under a different webpack project configuration. In settings.py
we added:
WEBPACK_LOADER = {
'DEFAULT': {
'BUNDLE_DIR_NAME': 'dist/',
'STATS_FILE': os.path.join(BASE_DIR, 'webpack-stats-prod.json'),
},
'ANGULAR': {
'BUNDLE_DIR_NAME': 'bundles/angular/',
'STATS_FILE': os.path.join(BASE_DIR, 'webpack-stats-angular.json'),
}
}
And then finally our angular bootstrap template file could extend the base of our project, and render the right assets:
{% extends 'base.html' %}
{% load render_bundle from webpack_loader %}{% block content %}
<app-root></app-root>
{% endblock content %}
{% block bottom_script_container %}
{% render_bundle 'main' 'js' 'ANGULAR' %}
{% endblock bottom_script_container %}
In our real template there are multiple render_bundle
calls for the different files being produced, but this has gone on long enough!
Limitations
There are a couple of problems with this at the moment:
- We are restricted to using the
js
version of the angular generated css. I believe that the css file that can be generated is a bit smaller. So it would be nice to resolve that. - The hashes generated on the files are build hashes rather than file content hashes. As a result they will change for each build, regardless of whether things have changed.
- We can’t have a different custom builder for production vs development. For our legacy build process, we don’t include hashes in the filenames, and output the files to a distinct location for dev and prod. It would have been nice to mirror this, but I am not going to lose sleep over it.
Summary
It was a challenge to get to this point, not least because I think it’s a bit of an unusual use case for Angular. But if you do have the need, it’s nice to know it’s do-able, and I suspect it will also give the opportunity for other webpack tweaks, should they prove necessary in the future!