Mix Vue.js with Django templates
by Sebastien Mirolo on Thu, 11 Jun 2020We have an application written in Django that we want to piece-wise migrate to a Vue and API architecture. This post explores how we managed to combine Vue development inside a Django project, and all the frustrating dead-end along the way. Hopefully saving you time in your own process.
Introduction
Let's start by creating a sample Django project and sample Vue application in the same directory root.
# Create the Django project mkdir django-vue-sample python -m venv django-vue-sample/.venv source django-vue-sample/.venv/bin/activate pip install Django django-admin startproject djapp django-vue-sample # Create the Vue app cd django-vue-sample yarn global add @vue/cli @vue/cli-service-global --prefix .venv mkdir -p clients cd clients vue create --no-git vue
Replacing vue/public/index.html by Django template
The first thing we attempted to replace
cp djapp/templates/index.html clients/vue/public/index.html cd clients/vue yarn build cat dist/index.html
The index page is minified. This makes it difficult to figure out what vue-cli
did. We thus create a
module.exports = { chainWebpack: config => { config .plugin('html') .tap(args => { // modify the options... args[0].minify = false return args }) } }
Vue-cli is injecting a head
at the beginning of the file, before our {% block content %}
and script
nodes at the end of the file, after our {% endblock content %}
.
FAILED
The next step will thus be to figure out how to instruct vue-cli to insert the generated nodes where we want them.
Custom insertion points
Vue-cli relies on Webpack and
the html-webpack-plugin.
Fortunately recent versions of html-webpack-plugin introduced
custom insertion position through
htmlWebpackPlugin.tags.headTags
and htmlWebpackPlugin.tags.bodyTags
.
$ diff -u clients/vue/public/index.html @@ -1,5 +1,9 @@ {% extends "base.html" %} +{% block locaheader %} +<%= htmlWebpackPlugin.tags.headTags %> +{% endblock %} + {% block content %} <div> <h1>Tests</h1> @@ -8,3 +12,7 @@ </ul> </div> {% endblock %} + +{% block bodyscripts %} +<%= htmlWebpackPlugin.tags.bodyTags %> +{% endblock %} $ yarn build ... TypeError: Cannot read property 'headTags' of undefined ...
The current version of html-webpack-plugin shipped with vue-cli is missing the required features. Fortunately (at least we thought) we can upgrade html-webpack-plugin independently.
$ diff -u package.json "eslint-plugin-vue": "^6.2.2", + "html-webpack-plugin": "~4.3.0", "vue-template-compiler": "^2.6.11" $ yarn global add html-webpack-plugin --prefix .venv
The error persists. Investigating further reveals that
$ find node_modules -name 'html-webpack-plugin' node_modules/html-webpack-plugin node_modules/@vue/cli-service/node_modules/html-webpack-plugin node_modules/@vue/preload-webpack-plugin/node_modules/html-webpack-plugin less node_modules/@vue/cli-service/package.json ... "html-webpack-plugin": "^3.2.0", ... "webpack": "^4.0.0", ...
We read that "@vue/cli-service-global is a package that allows you to run vue serve and vue build without any local dependencies. @vue/cli-service is a package that actually doing those vue serve and vue build , both @vue/cli-service-global and @vue/cli depend on it."
- "@vue/cli-service": "~4.4.0", + "@vue/cli-service-global": "~4.4.0",
Unfortunately that did not change anything.
After much time trying to figure out the flow of control
of the html-webpack-plugin, it seems that the hook
used by preload-webpack-plugin, i.e. alterAssetTags
has
a different API signature and is expected to do something different than
it previously did. Apparently, the hook to use for adding preload tags seems
to be alterAssetTagGroups
. We ran out of time to investigate
further.
FAILED
Using a different version of html-webpack-plugin than the one bundled with vue-cli
The most amazing thing with Javascript package managers is that you are able
to use multiple versions of the same library in various parts of your
application.
So we can just write the following
const HtmlWebpackPlugin = require('html-webpack-plugin') module.exports = { configureWebpack: { plugins: [ new HtmlWebpackPlugin({ inject: false, minify: false, filename: "test1.html", template: "../../djapp/templates/test1.html" }) ] } }
The scripts are inserted where we expect them (Yeah!). The preload lines are no longer inserted though (Ouch).
Installing preload-webpack-plugin ends up with version 2.3.0, which is rather old, as well as incompatible with Webpack 4 and/or html-webpack-plugin 4.30.
We are left with resource-hints-webpack-plugin which seems to serve the same purpose of adding preload nodes. Adding the plugin does not produce any errors but the preload nodes haven't been added to the HTML output. We none-the-less have a working solution.
PASS*Running Django/Vue in development mode
Since we are serving the compiled Vue assets from the Django HTTP server, there is no need to run the vue-cli HTTP server. None-the-less, we want the Vue assets to be recompiled automatically when edits are saved.
We achieve this by running the command
yarn run vue-cli-service build --mode development --watch
as a daemon alongside python manage.py runserver
through
$ pip install supervisor $ cat etc/supervisord.conf [supervisord] logfile=var/log/supervisord.log ; main log file; default $CWD/supervisord.log logfile_maxbytes=50MB ; max main logfile bytes b4 rotation; default 50MB logfile_backups=10 ; # of main logfile backups; 0 means none, default 10 loglevel=info ; log level; default info; others: debug,warn,trace pidfile=var/pids/supervisord.pid ; supervisord pidfile; default supervisord.pid nodaemon=true ; start in foreground if true; default false silent=false ; no logs to stdout if true; default false minfds=1024 ; min. avail startup file descriptors; default 1024 minprocs=200 ; min. avail process descriptors;default 200 [program:django] command=python manage.py runserver stdout_logfile=/dev/stdout stdout_logfile_maxbytes = 0 stderr_logfile=/dev/stderr stderr_logfile_maxbytes = 0 [program:vue] command=yarn run vue-cli-service build --mode development --watch directory=clients/vue stdout_logfile=/dev/stdout stdout_logfile_maxbytes = 0 stderr_logfile=/dev/stderr stderr_logfile_maxbytes = 0
Hence with one command, we get both reload of server as python files are modified and rebuild of assets as Vue files are modified.
$ supervisord
We though do not yet have automatic reload of the page in the browser yet.
ALMOST PASS
There are a few solutions to auto-refresh your browser (or livereload) whenever you edit any files in your project. The solutions usually fall into two categories:
- They require a browser extension
- They inject a Javascript file into the HTML page sent to the browser
We settled on django-livereload-server which implements Javascript injection with a side server. Since we are already running two processes through supervisord, adding a third one is not an issue, and alleviate the need to install any browser extensions.
PASS
Cleaning up where build files are installed
Modern toolchains, especially nodejs-based ones, have a tendency to install everything in the directory under source control. That is really unfortunate on many levels:
- Each
grep
command is overly complex because it needs to filter out build files. - You are never sure if a file needs to be added to the
.gitignore or not. The more patterns in your.gitignore , the more likely you are not committing an important file. - It is complicated to blow-up the build directory with a single
rm
command. - It is difficult to run statistics on how much space the source tree, build tree and install (
dist ) tree take. - It is complicated to have multiple build configurations with a single source tree (clunky virtualenv or workspaces).
- Separate file permissions, disk encryption, NFS and other setup are difficult to implement.
Since all those problems have already been solved for C/C++ projects
in a Makefile environment, I was confident. How hard could it be to move
the
It turned out we haven't been able to figure it out yet. I guess some lessons have to be re-learned by a new generation of toolchains.
With various command line flags to npm and yarn we managed to install the following directory structure:
workspace/ bin/ lib/ node_modules/ node_modules/html-webpack-plugin/ node_modules/@vue reps/ project/ project/.git project/package.json
We then try to compile our code. Here all the attempts we made to no success.
$ cd workspace/reps/project $ yarn --global-folder ~/workspace --modules-folder ~/workspace/lib/node_modules run vue-cli-service build --mode development ERROR Error: Cannot find module 'html-webpack-plugin' $ NODE_PATH=~/workspace/lib/node_modules yarn --global-folder ~/workspace --modules-folder ~/workspace/lib/node_modules run vue-cli-service build --mode development ERROR Error: Cannot find module '@vue/cli-plugin-babel/preset' from '~/workspace/reps/project' $ npm run --prefix ~/workspace vue-cli-service build --mode development npm ERR! missing script: vue-cli-service $ NODE_PATH=~/workspace/lib/node_modules npm run --prefix ~/workspace vue-cli-service build --mode development npm ERR! missing script: vue-cli-service $ npm --prefix ~/workspace run ~/workspace/lib/node_modules/.bin/vue-cli-service build --mode development npm ERR! missing script: ~/workspace/lib/node_modules/.bin/vue-cli-service $ ls -la ~/workspace/lib/node_modules/.bin/vue-cli-service ~/workspace/lib/node_modules/.bin/vue-cli-service -> ../@vue/cli-service/bin/vue-cli-service.js
FAILED
More to read
If you are looking for more posts about building static assets, Building CSS/JS static assets and Django presents the previous implementation using django-webpack-loader. A related post is Serving static assets in a micro-services environment.
More technical posts are also available on the DjaoDjin blog, as well as business lessons we learned running a SaaS application hosting platform.