UPDATE: I recently created a working project at ariera/django-vue-template using the latest version of the vue-webpack template and bundling all the changes highlighted in this post in just this commit 8a5c552. You may also be interested in checking a vue project template that CharlesAracil created taking inspiration from this entry.

I recently started a new web project based on both Django and Vuejs. It took a good deal of reading half-related and/or outdated posts, tons of documentation, and a good deal of trial and error to get it right.

In this post we will cover the key points on setting up a nice and solid development environment that is consisten with production and hence easy to deploy. The specifics of how to deploy, final configuration touches and (automation) will be discussed in a future article.

This is the solution that I found, there may be better alternatives (which I’d be happy to read) and it is utimately written with the intention of helping others as well as my future-self :)

django webpack vue logos

The problem

Both Django and Vue offer local web servers to facilitate development, but that confused me in the beginning. Does that mean that I need to be running both servers in parallel? And do I need to switch from one to another while developing? That sounds cumbersome! If you are building a full SPA with Vue as your frontend, and Django as a simple API backend that might be okey, but I want something else.

I want to remain flexible: some parts of my project will behave like an SPA, other parts will be rendered by Django, and some other will be a mixture: Django-rendered with Vue components. I want to be able to use my webpack-compiled css in my Django templates, as well as other static assets.

My goals

  1. minimise differences in between prod & dev
  2. use just one url in development (ie Django’s runserver should proxy to webpack’s dev server, or the other way around)
  3. preserve hot reload (ie. HMR) in development
  4. django backed pages should work without much ceremony

My solution

The solution I found is based around the django library django-webpack-loader and the webpack-bundle-tracker webpack plugin. The django module will read a manifest file, and a webpack plugin take care of updating it. That way, webpack will compile all the assets and Django will be able to use them in your templates.

All you will need to do while developing is launch both local web servers and point your browser to the Django one :)

Versions i’m using:

  • Vue.js 2.4.2
  • Django 1.11
  • vue-cli 2.8.2
  • Python 3.6.1

Setting up the dev environment

Creating the project

I have used the vue-cli to create a new project based on the webpack template, it will do most of the configuration and setup for us. Then, inside the new directory holding my project, I had created a new django project:

vue init webpack my_project
cd my_project
django-admin startproject my_project .

Now install both libraries

yarn add webpack-bundle-tracker --dev
pip install django-webpack-loader
pip freeze > requirements.txt

Add django-webpack-loader to your INSTALLED_APPS, we will get to the specific configuration in a minute.

# ./my_project/settings.py
INSTALLED_APPS = [
'webpack_loader',
]

And configure webpack to use BundleTracker.

// build/webpack.base.conf.js
let BundleTracker = require('webpack-bundle-tracker')
module.exports = {
// ...
plugins: [
new BundleTracker({filename: './webpack-stats.json'}),
],
}

Defining the statics paths

This part was one of the trickiest, there are several configuration options all with very similar names and they feel very confusing to new commers to the framework. I’ll spend a little explaining what and why every option does.

My goal here was to have in the end a directory named ./public (at the root of the project) where all the assest will be stored, with no unnecesary subdirectories (ie. grouping all javascript inside a js dir would be okey, but theres no need for deeper levels). The final ./public folder would look like this

- public/
    - js/ (webpack generated)
        - app.c611f8fd9b27da4ec95f.js
        - app.c611f8fd9b27da4ec95f.js.map
        - ...
    - css/ (webpack generated)
        - ...
    - img/ (django generated)
        - ...
    - admin/ (django generated)
        - ...

In order for this to happen we have to configure both worlds properly. Let’s take a look at the final configuration of both Django and webpack, and then we will discuss them.

// ./config/index.js
module.exports = {
build: {
assetsRoot: path.resolve(__dirname, '../dist/'),
assetsSubDirectory: '',
assetsPublicPath: '/static/',
// ...
},
dev: {
assetsPublicPath: 'http://localhost:8080/',
// ...
}
}
# ./my_project/settings.py
STATICFILES_DIRS = (
os.path.join(BASE_DIR, 'dist'),
os.path.join(BASE_DIR, 'static'),
)
STATIC_ROOT = os.path.join(BASE_DIR, 'public')
STATIC_URL = '/static/'

WEBPACK_LOADER = {
'DEFAULT': {
'BUNDLE_DIR_NAME': '',
'STATS_FILE': os.path.join(BASE_DIR, 'webpack-stats.json'),
}
}

Let’s analyse this step by step.

  1. build.assetsRoot: path.resolve(__dirname, '../dist/')

    This is the output directory of our npm run build task, that takes care of compiling and building all of our webpack-controlled assets. We will run this task as part of the deployment process. It will take care of analysing all our webpack entry points, traversing and compiling them. The out put will be stored in the ./dist directory at the root of our project (which should be gitignored).

  2. STATICFILES_DIRS and STATIC_ROOT

    When deploying to production we will also run the Django script collectstatic that takes care of finding all the static files our project makes use of, and putting them in the correct folder. With STATICFILES_DIRS we are telling it were to look for these files. With STATIC_ROOT we specify where to place them.

    In STATIC_DIRS we have specified 2 directories: one is the ./static (Django’s default), and the above defined ./dist where webpack will place its compiled assets.

  3. build.assetsSubDirectory: '' and build.assetsPublicPath: '/static/'

    Now we tell webpack that (in production) any asset reference should point to /static (ie at the root of the domain). For example, if our code reads:

     <img src="images/logo.png">
    

    the final compiled version will be:

     <img src="/static/images/logo.png">
    
  4. STATIC_URL = '/static/'

    The same thing but for Django. Everytime one of our templates referes to a static file it will render it under the /static path.

  5. dev.assetsPublicPath: 'http://localhost:8080/'

    This is one of the key points in which we achieve our goal number 2 (use only one url in dev). We want the webpack dev server to serve our webpack assets. While we point our browser to Django’s localhost:8000, webpack will compile our code to point at localhost:8080 (notice those are 2 different ports).

  6. WEBPACK_LOADER

    In WEBPACK_LOADER['DEFAULT']['STATS_FILE'] we point to the json manifest file generated by webpack’s BundleTracker (see above in file build/webpack.base.conf.js).

    And we set to blank WEBPACK_LOADER['DEFAULT']['BUNDLE_DIR_NAME'] because we don’t want to nest our webpack assets into any subdirectory.

Referencing static files from both Vue and Django

One final touch. I would like to have a place to put images, fonts, or generic statics assets that can be used from both worlds: Django tempaltes and Vue components. In other words, I want to have a ./static directory at the root of the project where I could put (for example) my logo and be able to write this code:

<!-- in a Django template -->
<img src="{% static 'logo.png' %}">

<!-- in a Vue component -->
<img src="static/logo.png">

The Django example already works thanks to the config above described (see STATICFILES_DIRS), but for Vue/Webpack to work you’d need to add an alias to the wbepack config, like this.

// build/webpack.base.conf.js
module.exports = {
resolve: {
alias: {
// ...
'__STATIC__': resolve('static'),
},
}

Thanks to this we can now write our Vue code like this (mind the ~):

<img src="~__STATIC__/logo.png">

Hot realod or Hot Module Replacement (HMR)

While in development we are only accessing Django’s server, and proxying our asset requests to the webpack server (see above dev.assetsPublicPath). Because of this HMR or hot reload is broken. We need to configure webpack’s hot middleware to point to the right path, and we need to add a new header to the webpack dev server so it allows for CORS requests.

// build/dev-client.js
// before
var hotClient = require('webpack-hot-middleware/client?noInfo=true&reload=true')
// after
var hotClient = require('webpack-hot-middleware/client?noInfo=true&reload=true&path=http://localhost:8080/__webpack_hmr')
// build/dev-server.js
var devMiddleware = require('webpack-dev-middleware')(compiler, {
headers: {
"Access-Control-Allow-Origin":"\*"
},
// ...
})
UPDATE 2017.11.28

The official vue webpack template was updated around 2 weeks ago to start using the official webpack-dev-server.

In order to properly configure HMR after the update you simply need to add the following to the build/webpack.dev.conf.js:

// build/webpack.dev.conf.js
devServer: {
headers: {
"Access-Control-Allow-Origin":"\*"
},
// ...
}

Thank you very much to Emilien and LinusBorg for the feedback on this :)

The basic Django template

We will define a Django template reusable by other parts of our application that will take care of including the webpack assets.

<!-- ./templates/my_project/base.html -->
{% load render_bundle from webpack_loader %}
<html>
<body>
{% block content %}
{% endblock %}
{% render_bundle 'app' %}
</body>
</html>

Now other parts of our project could just extend this template and benefit from having all the styles from css or javascript accessible. In fact, let’s create a second template, extending this one, to serve as the root of our SPA:

<!-- ./templates/my_project/spa.html -->
{% extends "my_project/base.html" %}
{% block content %}
<div id="app"></div>
{% endblock %}

Notice that in order for this template to be detected by Django you need to add the following to your settings.py

# ./my_project/settings.py
TEMPLATES = [
{
'DIRS': [os.path.join(BASE_DIR, 'templates')],
# ...
}
]

And in our main.js where we define the root component of our SPA, we will point to this div#app:

import App from './App'
new Vue({
el: '#app',
template: '<App/>',
components: { App },
})

The last step would be to define a new url route in our Django settings:

# ./my_project/urls.py
from django.views.generic import TemplateView
urlpatterns = [
url(r'^$', TemplateView.as_view(template_name='my_project/spa.html'), name='home'),
# ...
]

And we can now open http://localhost:8000 in our browser and enjoy our fully functional dev environment :)

Clean unnecessary files and options

UPDATE: It is better ignore everything related to HtmlWebpackPlugin in this part, it was breakign the end-to-end tests.

Before we conclude let’s do a little cleanup.

Remove the index.html file at the root of your project and stop using HtmlWebpackPlugin in development. Since Django will render the initial html and include any assets there we don’t need HtmlWebpackPlugin to generate an initial html file for us.

// build/webpack.dev.conf.js
// remove these lines:
new HtmlWebpackPlugin({
filename: 'index.html',
template: 'index.html',
inject: true
}),

For the same reason we want to remove it from the production config, but we want to keep it in tests. Simply move this bit of conde from build/webpack.prod.conf.js to build/webpack.test.conf.js

new HtmlWebpackPlugin({
filename: process.env.NODE_ENV === 'testing'
? 'index.html'
: config.build.index,
template: 'index.html',
inject: true,
minify: {
removeComments: true,
collapseWhitespace: true,
removeAttributeQuotes: true
// more options:
// https://github.com/kangax/html-minifier#options-quick-reference
},
// necessary to consistently work with multiple chunks via CommonsChunkPlugin
chunksSortMode: 'dependency'
}),

And last but not least lets add the following to your .gitignore file:

webpack-stats.json
public/
dist/

Conclusions:

To have a good dev environment we have used Django as our main dev server, which will in turn proxy any asset request to webpack. This required a good deal of configuration. In doing so we have simplified the development workflow so we can spend more time programming and less time switching between browsers or fine tuning the config for new cases that may appear.

We have also paved the path to production deployment, but that’s something that we will cover in the next post.

Until then,

Happy hacking!

References used

# Getting ready for production https://docs.djangoproject.com/en/1.11/ref/templates/api/#writing-your-own-context-processors https://git.embl.de/mainar/my.embo.org/compare/vue-bulma-bootstraping...django-with-vue?view=parallel