Some Vue + Django tips

As I wrote last time, I currently develop a web app based on Django and Vue using the django-compressor-parceljs. I’d like to mention some small things that I’ve learned since last.

Vue in development mode

I found it interesting that parceljs described how to get Vue into development mode for several bundlers, but failed to mention parcel. After some digging around, I figured out that parcel respects the NODE_ENV environment variable, so starting the django server with NODE_ENV=development ./manage.py runserver does the trick. Now I get proper error messages from Vue.

Constantly rebundling

Another annoyance with the default configuration of the compressor, was that it compiles (bundles / compresses) the Vue files once every time the server is started. This really lengthened the change-build-test cycle time, so something needed to be done. It turns out that the solution is to set the COMPRESS_REBUILD_TIME to a really low value in the django settings, and the contents is compressed every time. I set it to one second on my development machine.

Disabling compression would break the Vue files, as the browser cannot run the Vue files natively, i.e. they need a compression to be possible to run.

The consequence of this configuration is that loading a view based on Vue takes a bit longer, but it takes way shorter time than restarting the django server, recreating the backend state and so on.

Exposing Django context to Javascript

Django exposes a set of context variables when loading a view. This avoids an unnecessary round-trip over the server to asynchronously request the info that I already know the view will need. I’ve developed a small convention for this.

In the Django template, I add something along these lines:

<script>
var context_title = '{{ title }}';
var context_actors = [
{% for actor in actors %}'{{ actor }}',
{% endfor %} ];
...
</script>

This copies the django context into Javascript space with the variable name prefix of context_. I then consume these in the Vue app’s mounted method, e.g.:

    ...
    mounted() {
        this.title = context_title;
        this.actors = context_actors;
        ...
    },
    ...

This copies the state into the state of the app, which means that it is now a part of what the Vue interface is reactive to.

Looking at Vue

All applications are more or less connected today. The time of files on a disk, or moving them with a USB stick (or floppy) are over. Even file based programs are often synced using Nextcloud, dropbox, google drive, etc.

At Eperoto I’m busy building a backend for a React frontend, but there I stay in my comfort zone at the backend. It is Python, databases and files, just as I know and like things to be. I also have my normal toolbox for debugging and know how to execute a rich set of unit and integration tests to ensure that things stay sane over time.

However, I have another side project. Finally I’ve reached a point where have to do take a dip in the sea of web frontend. I don’t mean messing about with the odd Javascript snippet or fighting the windmil^Wcss.

The choices I was looking at where React, Angular and Vue. I did not do a deep analysis when picking a framework to try. Instead my reasoning was a long these lines:

  • React, I can pick this up from Manos at Eperoto.
  • Angular, requires npm and a setting up an environment to get started.
  • Vue, just needs a script to be included into your web page.

So, me being lazy, I choose to view Vue, which is pronounced view. It seems that I like a bit of pronunciation ambiguity in my frameworks.

Starting Slow

The reason for choosing Vue was the ability to run right in the browser. The strength of today’s browsers is the easy of deployment, but a hidden strength takes me back to the 1980s and 90’s. Everyone has a development environment sitting right in front of them. However, instead of being used to load programs like with the C64, you now I have to look for the development tools to bring up the console.

https://ih0.redbubble.net/image.537660612.0177/pp,550x550.u10.jpg

I’m building a tool that is currently in closed alpha stage, so I cannot tell you too much about it. What I can show is one of my first Vue components. It is a text element, that when clicked turns into a text edit. I made two of these, one for editing a line, and one for editing a paragraph. It also sports a placeholder text if it is empty.

Vue.component('editable-text', {
     props: ['value', 'placeholder'],
     data: function() { return { editing: false, } },
     template: '{{value}}' + 
               '{{placeholder}}' +
               '',
     methods: {
         on_start_editing: function(e) {
             this.editing = true;
         },
         on_end_editing: function() {
             this.editing = false;
         },
     },
     directives: {
         focus: {
             inserted(el) {
                 el.focus();
             }
         }
     }
 });

The code above makes it possible to use an editable-text tag. In order to be able to use the new tag, a Vue app needs to be created and mounted into the DOM of the web page. That means making it take over a div somewhere in the page, and let Vue manage its contents.

This is partially done in the Javascript file:

var app = Vue({
     el: '#app',
     data: [ text: '' ]
});

And the other half goes into the html:

<div id="app">
<editable-text v-model="text" placeholder="enter text"></editable-text>
</div>

<script src="js/vue-test.js"></script>

Finally I sprinkled some css on top of this to make it look sane. For instance, defining a placeholder class.

.placeholder {
     color: #999999;
}

Now we have something that works! I can include a development version of Vue into my page, which provides helpful feedback and point me in the right direction when I make mistakes. And then I can point this to a minified production build of Vue once I’m done.

Bring on the Django

For my backend needs I choose to go with Django. It might because I know Python, it might because of the extensive docs, the great intro tutorial (no damn video – text that I can read back and forth in my own order and pace), or maybe it is just because I’m old.

I like Django. Django plays nice. It is my choice of backend framework. End of discussion.

So, how do we make Vue and Django play nice?

First up, the index.html needs to be served from Django, and the CSRF cookie needs to be served along with it:

from django.views.decorators.csrf import ensure_csrf_cookie
@ensure_csrf_cookie
def my_view(request):
     context = { … }
     return render(request, 'app/index.html', context=context)

Then the index.html needs to be adapted, e.g. use {% static 'app/index.html' %} instead of direct URLs and such. Then I place the js files in the app/static directory tree and run migrate.py collectstatic to get it all in the correct location for nginx to serve it on the production server.

There is, of course, more to the app. I provide a REST-ish API passing JSON around via a set of views served under /api/…. I could probably have used the Django REST Framework here, but I tend to have to adapt things as the API isn’t really proper REST. I just picked out the convenient bits from it, so I roll my own for now. To make these requests, I use Axios, which plays nicely with the Django CSRF cookie.

What is the Point?

When using Vue like this, the question is really what the point is. For smaller stuff I’ve rolled this in plain Javascript. Including the AJAX (AJAJ?) calls and updating the DOM. What Vue helps me with is that I can share a single JSON state between the server and client, and Vue reactively renders it as needed on the client side.

Still, writing Vue as shown above kind of sucks. The template part of a component is html wrapped into a Javascript string. This means loads of fun when handing various string enclosing chars – ” and ‘, I’m looking at you two! Also, just stuffing everything in to a Javascript map makes everything a bit clumsy. It is really had to misspell something and it just stops working.

So, it was time to take it to the next level. Vue can handle single file components. This sounds a lot like classes to an old C++ guy like me, so bring it on!

The Computer Says No

Vue might know what a *.vue file is, but Firefox sure does not. It does not know what an include is either. It has lots of interesting errors about it, so it knows about a similar concept, but not what I needed it to know. Instead, I need a compiler and linker, or what the webistas call a bundler.

When mucking about with npm to get React or Vue things to work, I always ended up with webpack. But apparently web pack is old, boring, and slow (a bit like me). Having asked a friend, I learned that all the new kids use Parcel – a project that has an icon next to every heading on the whole site. Amazing…

What Parcel (and webpack or any other bundler) does is that it transform assets into Javascript, HTML and CSS, that the browser understands. It also combines the assets into fewer files, e.g. one js-file instead of a pile of them, thus reducing the number of files the browser has to request, thus making things a bit quicker.

If this sounds a lot like what a compiler (or transpiler) and linker would do – it is exactly what it is.

So, with a bundler like Parcel, I can build my *.vue files into Javascript, which I then can run in my Browser.

What is the Point – part 2

So, what does a Vue file look like?

<template>
    <span v-if="(!editing) && (!!value)" @click="on_start_editing()" >{{value}}</span>

    <span v-else-if="(!editing) && (!(!!value))" @click="on_start_editing()" class="placeholder">{{placeholder}}</span>

    <span v-else-if="editing">
        <input v-focus="" :value="value" @input="$emit('input', $event.target.value)" @keydown.enter="on_end_editing()" @blur="on_end_editing()" type="text" class="form-control">
    </span>',
</template>

<script>
export default {
    name: "editable-text",
    components: {},
    props: ['value', 'placeholder'],
    data: function() { return { editing: false, } },
    methods: {
        on_start_editing: function(e) {
            this.editing = true;
        },
        on_end_editing: function() {
            this.editing = false;
        },
    },
    directives: {
        focus: {
            inserted(el) {
                el.focus();
            }
        }
    }
}
</script>

Apparently you can stuff some css in there as well. But still, the whole script part is just the same mess as before. Why, when making a transpiler, didn’t anyone come up with a sane syntax for properties, data, methods and directives?

Django and the Modern Frontend

Ignoring my annoyances with the the single file Vue components, it is still an improvement, so let’s continue.

I would like to build my frontend and backend in one go. I’d like a single git hash and know that the frontend and backend there fits together. At the same time, I don’t want to deploy my entire development toolchain to the production server. What I want is:

  • The Django stuff running an an uwsgi application.
  • All static files served directly from nginx.
  • All the frontend stuff to be a set of static files.

Enter: django-compressor-parceljs. Using this Django extension I can have parceljs do it’s work on the fly, in the Django app – and it allows me to build for production as well. More on this later.

However, going for parceljs took me one step beyond what I could do with simply including scripts into my web pages, I needed to install npm. I have a lot to say about this tool. To some extent it feels like it is trying to solve a problem with tooling more complex than the system, but let’s get to some specifics:

  • parceljs (and lots of command line tools) wants to get installed globally. This will collide head on with the distro, so it is a no-go for me.
  • When installing locally (in your dev tree) with npm, the executables end up in node_modules/.bin. A hidden directory. Thanks. Still, fixable by updating PATH.
  • The number of dependencies pulled in for a fairly small project is quite astonishing. The number of dependencies is huge. My package-lock.json clocks in at 6243 lines… That is for Vue, Axios and Parcel.
  • Many dependencies likes to not install their dependencies, but instead to tell me to do so in the form of error messages. I’m not sure I get the point of this – just install it, or have a recommends option or something.

Having setup django-compressor-parceljs and updated my PATH to include node_modules/.bin, it actually works. However, since the vue-files are compressed, the development experience is not perfect. I’d much rather be able to debug the vue-files directly via the Django server. If anyone knows how to, please tell :-)

I also created a small script that sets the COMPRESS_OFFLINE to true and then compil^Wcompresses the frontend on my development machine, meaning that I don’t have to install npm and all the dependencies of vue, parceljs, and axios on the server.

Conclusions

I’m still learning. I bet there is a lot that can be improved in this workflow. I also need to work on my CI/CD to do the offline compression automatically, and such. Still, I have some reflections to make from the viewpoint of a C++, Qt, QML, Python perspective:

  • All my web frontend friends appreciate the separation of behaviour from style from state. This can be done in QML, but requires discipline.
  • To me, the vue files are only half-way there. The script part of the file would benefit from a domain specific language such as QML, instead of putting everyting into a dictionary based on conventions.
  • To a large extent it feels like the web frontend world reinvents terminology – I really hope that there are some people carrying over experiences (in both directions). For instance, a bundler is just a transpiler + linker. Why not reusing common Makefile systems instead of re-inventing the wheel?
  • Why can’t I do nice inheritance and templating within Vue components? I.e. why do I have to duplicate all code for a line edit version of a clickable text when I have the text area version already? What am I missing?

Is anyone else doing this? How does your setup look and what does your working process look like?

Also, Happy Holidays and God Jul!