Mix Vue.js with Django templates

by Sebastien Mirolo on Thu, 11 Jun 2020

We 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 vue/public/index.html by the Django template we want to introduce Vue into.

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 vue.config.js that disables minification.

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 cli-service, as well as preload-webpack-plugin use their own local version of html-webpack-plugin - version 3.2.0 :(.

$ 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 vue.config.js

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 supervisord.

$ 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 node_modules outside the source tree?

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 subscription hosting platform.

by Sebastien Mirolo on Thu, 11 Jun 2020


Bring fully-featured SaaS products to production faster.

Follow us on