Skip to content

A way to integrate Django and Vue 3 applications using vue-cli

License

Notifications You must be signed in to change notification settings

RobertoMaurizzi/integrating_vue

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

11 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

Django/Vue-cli integration tutorial

Overview

A typical Django "project" is composed of a number of "applications" each with their templates and static subfolders, to name only those that will be relevant to integrate Django and Webpack in a possibly decent way

MyDjangoProject/
β”œβ”€β”€ MyDjangoProject/
β”œβ”€β”€ MyFirstApp
β”‚     β”œβ”€β”€ static
β”‚     └── templates
└── MySecondApp
    β”œβ”€β”€ static
    └── templates

If ours is a legacy project, we'll have a number of templates under each application's template folder, with a directory structure like:

Appname/
└── templates
    β”œβ”€β”€ Appname
    β”‚   β”œβ”€β”€ somepage.html
    β”‚   └── index.html
    └── anotherApp
        └── overriddentemplate.html

The App's static folder instead contains any "pure JS" code written for our application (meaning, handcrafted JS code that isn't processed by Django as a template nor transpiled or bundled unless you do it manually, like running riot-compiler on a bunch of tags) together with our static assets and maybe some 10 years old Javascript library that our project needs to use:

Appname/
└── static
    β”œβ”€β”€ js
    β”‚   β”œβ”€β”€ appname.js
    β”‚   └── c3-v3.2.1.js
    β”œβ”€β”€ img
    β”‚   └── logo.png
    └── css
        └── base.css

The important observation made by Pascal Widdershoven here is that Django will:

  1. serve all content from /static/ folders during development
  2. collect, pack, "manifestize" and eventually upload somewhere all that same content when we run ./manage.py collectstatics

The general idea is that, if we can tell Webpack to store its output in a /static/ directory, either an existing one or one we add for its private use, then we'll be able to access those files directly from Django without needing any additional tool or integration: Webpack's dev server will output its temporary JS in those folders and the final output of a build will also be there, waiting for a collectstatic to deploy it where it needs to be.

New project structure

The idea behind the integration is to extend an old Django application by "merging" on top of it a Vue 3 application created with vue-cli . We can get our Django project_folder and run a vue create project_folder to have Vue-cli add all the required structure to use Vue, its plugins, Webpack and the rest of the merry madness that comes with it.

# the most common case is you want to add Vue to an existing Django project, but if not...
django-manage startproject integrating_vue
vue create integrating_vue
# choose merge then Vue 3
cd integrating_vue
# create our Django apps
./manage.py startapp app_one
./manage.py startapp app_two
# create useful directories
mkdir integrating_vue/templates
mkdir -p app_one/templates/app_one
mkdir -p app_two/templates/app_two
mkdir app_one/frontend app_one/assets app_ome/components
mkdir app_two/frontend app_two/assets app_two/components
rm -r src public
# or copy files from src/assets, src/components and public/index.html to your applications as starting point

vue.config.js file for Django integration

To make this work we need to tell Vue-cli quite a bit of things about where are our files and where we want to put the result of all the packing and bundling

// vue.config.js

module.exports = {
    // we need one Vue 'page' for each Django application we want to use Vue with
    // of course if it's an option to have a separate frontend or an SPA we'll only create one
    pages: {
        app_one: {
            // entry point for the app's page
            entry: 'app_one/frontend/one_main.js',
            // the source EJS template (can contain Django template code)
            template: 'app_one/frontend/one_index.html',
            // the output template needs to be in this app's templates directory
            // these compiled templates should not be included in git
            filename: '../app_one/templates/app_one/ejs_index.html',
            // when using title option,
            // template title tag needs to be <title><%= htmlWebpackPlugin.options.title %></title>
            title: 'App One Index Page',
            // chunks to include on this page, by default includes
            // extracted common chunks and vendor chunks.
            chunks: ['chunk-vendors', 'chunk-common', 'index']
        },
        app_two: {
            entry: 'app_two/frontend/two_main.js',
            template: 'app_two/frontend/two_index.html',
            filename: '../app_two/templates/app_two/ejs_index.html',
            title: 'App Two Index Page',
            chunks: ['chunk-vendors', 'chunk-common', 'index']
        },
    },
    devServer: {
        port: 8081,   // any unused port is fine
    	  // this is the main change that allow this strategy: create files that will be served by Django
        writeToDisk: true,
        // useful to see compilation warning/errors in the browser
        overlay: {
            warnings: true,
            errors: true
        },
    },
    // we want to use html-webpack-plugin to inject the required CSS and JS files in our templates
    // this requires "a bit" of setup: each page/app has a dedicated instance of the plugin and we need
    // to change its configuration to avoid automatic injection (wouldn't work in a partial template) and
    // to include the app's JS bundle in the htmlWebpackPlugin.files objects
    chainWebpack: config => {
        config
            .plugin('html-app_one')
            .tap(args => {
                args[0].inject = false
                args[0].chunks.push('app_one')
                return args
            })
        config
            .plugin('html-app_two')
            .tap(args => {
                args[0].inject = false
                args[0].chunks.push('app_two')
                return args
            });
    },
    // output files to <django project>/dist/static
    // this directory shouldn't be committed to git (will be rebuilt on deployment)
    // you also need to add this directory to STATICFILES_DIR in settings.py, for example:
	  /***
  		  STATICFILES_DIRS = (
      		  BASE_DIR / 'dist' / 'static',   # Vue assets static dir
    		)
    ***/
    outputDir: './dist',
    assetsDir: "static",
}

This will create a integrating_vue/dist/static folder where Webpack will store all the artifacts produced during serve or build. In addition to that we'll have, for each Django app, pre-processed templates files stored in the corresponding app_xxx/templates/app_xxx/ directory.

The pjs-converted-to-django-templates.html files in the application's template directories will get recreated every time there's a change in either the original template in /frontend/ or anything changes in the files related to the application and Webpack rebuilds them with a different hash. They can of course be called with any name, but if we stick to a standard name like ejs_index.html we can add it to .gitignore so they won't be added to the repository: they'll be recreated on deploy when npm run build is run.

In case something about what Webpack is doing isn't obvious, remember you can run vue inspect | less to see what Vue generates for Webpack to process (it'll contain, as a minimum, the names of the plugins for your applications, like html-app_one)

The templates will need to include all the CSS and JS files produced by Webpack and since we disable automatic injection we're going to add the necessary and <script> tags using the EJS templating language.

How to inject CSS and JS bundles in a HTML template file

The template will be valid for both Webpack HTML generation (EJS) and Django (Django template language or Jinja). To have more control about where and how our CSS and JS bundles are injected, we disabled automatic injection of those files above (controlled bt html-webpack-plugin, named "html" by Vue-cli) and we have EJS code to re-add them in the template blocks were they're needed, for example:

{% extends "base_site.html" %}
{% load static i18n %}{% get_current_language as LANGUAGE_CODE %}

{% block style %}
{# htmlWebpackPlugin.tags.headTags requires html-weback-plugin 4.0 we have 3.2 #}
{# with 3.2 the "way" is to loop over htmlWebpackPlugin.files.css for CSS #}
<% for (var item in htmlWebpackPlugin.files.css) { %>
    <link href="<%= htmlWebpackPlugin.files.css[item] %>" rel="preload" as="style">
    <link href="<%= htmlWebpackPlugin.files.css[item] %>" rel="stylesheet">
<% } %>
{# we can also loop over JS files here, marking them as preload #}
<% for (var chunk in htmlWebpackPlugin.files.chunks) { %>
    <link href="<%= htmlWebpackPlugin.files.chunks[chunk].entry %>" rel="preload" as="script">
<% } %>
{# include existing styles from the template we're extending #}
{{ block.super }}
{% endblock style %}

{% block extrahead %}
{% endblock extrahead %}

{% block content_header %}
    <div class="row">

        <div class="col-md-6 col-sm-6 col-xs-4">
            <div class="button-holder">
                <input class="fa-search textinput textInput" placeholder="Search..." />
            </div>
        </div>

        <div class="col-md-6 col-sm-6 col-xs-8">
            <div class="button-holder pull-right">
                    <button id="btn-do-something" class="btn btn-ghost btn-orange btn-pill pull-right btn-margin-right" >
                        {% trans 'Do something' %}
                    </button>
            </div>
        </div>
    </div>

{% endblock content_header %}

{% block content_body %}
    <noscript>
      <strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
    </noscript>
    <p>Some text from the base template? Doesn't get autoreloaded of course...</p>
    <p>Are we a user? {{ user }}</p>
    <p>Is user staff? {{ user.is_staff }}</p>
    <p>Is user a superuser? {{ user.is_superuser }}</p>
    {# we can then use a Vue app tag, the relevant (bundled) code is loaded at the end of the file #}
    <div id="app"></div>
{% endblock content_body %}

{% block extrascript %}
{# htmlWebpackPlugin.tags.bodyTags requires html-weback-plugin 4.0 we have 3.2 #}
{# with 3.2 the "way" is to loop over htmlWebpackPlugin.files.chunks for JS #}
<% for (var chunk in htmlWebpackPlugin.files.chunks) { %>
<script src="<%= htmlWebpackPlugin.files.chunks[chunk].entry %>"></script>
<% } %>
{% endblock extrascript %}

Keep in mind that there will be no CSS files from your application when running in dev mode (apparently due to some bug) and so the top tags will be filled only when running with code produced from npm run build

in case you need some debugging of what (and when... development != production) exactly is available from htmlWebpackPlugin you can add this code fragment somewhere in our source template files:

<hr />
<p>files object</p>
<ul>
    <% for (var obj in htmlWebpackPlugin.files) { %>
    <li>
        <%= obj %> (<%= typeof(htmlWebpackPlugin.files[obj]) %>): <%= htmlWebpackPlugin.files[obj] %>
    </li>
    <% } %>
</ul>
<hr />
<p>chunks object</p>
<ul>
    <% for (var obj in htmlWebpackPlugin.files.chunks) { %>
    <li>
        <%= obj %> (<%= typeof(htmlWebpackPlugin.files.chunks[obj]) %>): <%= htmlWebpackPlugin.files.chunks[obj] %>
    </li>
    <% } %>
</ul>
<hr />
<p>css object</p>
<ul>
    <% for (var obj in htmlWebpackPlugin.files.css) { %>
    <li>
        <%= obj %> (<%= typeof(htmlWebpackPlugin.files.css[obj]) %>): <%= htmlWebpackPlugin.files.css[obj] %>
    </li>
    <% } %>
</ul>
<hr />
<p>js object</p>
<ul>
    <% for (var obj in htmlWebpackPlugin.files.js) { %>
    <li>
        <%= obj %> (<%= typeof(htmlWebpackPlugin.files.js[obj]) %>): <%= htmlWebpackPlugin.files.js[obj] %>
    </li>
    <% } %>
</ul>
<hr />

Changes to Django settings.py

Add the two apps to INSTALLED_APPS


  37β”Š  36β”‚    'django.contrib.sessions',
  38β”Š  37β”‚    'django.contrib.messages',
  39β”Š  38β”‚    'django.contrib.staticfiles',
    β”Š  39β”‚
    β”Š  40β”‚    'app_one',
    β”Š  41β”‚    'app_two',
  40β”Š  42β”‚]
  41β”Š  43β”‚
  42β”Š  44β”‚MIDDLEWARE = [

At the end of the file, add STATICFILES_DIR to enable collecting static files from dist/static and the STATICFILE_FINDERS to explore both apps and directories for them

───────────────────┐
120: USE_TZ = True β”‚
β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
 118β”Š 120β”‚# https://docs.djangoproject.com/en/3.1/howto/static-files/
 119β”Š 121β”‚
 120β”Š 122β”‚STATIC_URL = '/static/'
    β”Š 123β”‚STATICFILES_DIRS = (
    β”Š 124β”‚    BASE_DIR / 'dist' / 'static',   # Vue assets static dir
    β”Š 125β”‚)
    β”Š 126β”‚
    β”Š 127β”‚# List of finder classes that know how to find static files in
    β”Š 128β”‚# various locations.
    β”Š 129β”‚STATICFILES_FINDERS = [
    β”Š 130β”‚    'django.contrib.staticfiles.finders.FileSystemFinder',
    β”Š 131β”‚    'django.contrib.staticfiles.finders.AppDirectoriesFinder',
    β”Š 132β”‚    # 'django.contrib.staticfiles.finders.DefaultStorageFinder',
    β”Š 133β”‚]


Another useful addition (unrelated with Vue) is to create a templates folder inside the project settings directory (the one where the settings.py file is) so that we can have "project wide templates" and "global template overrides".

  56β”Š  56β”‚TEMPLATES = [
  57β”Š  57β”‚    {
  58β”Š  58β”‚        'BACKEND': 'django.template.backends.django.DjangoTemplates',
  59β”Š    β”‚        'DIRS': [],
    β”Š  59β”‚        'DIRS': [
    β”Š  60β”‚            BASE_DIR / 'integrating_vue' / 'templates'
    β”Š  61β”‚        ],
  60β”Š  62β”‚        'APP_DIRS': True,
  61β”Š  63β”‚        'OPTIONS': {
  62β”Š  64β”‚            'context_processors': [

Other notes

Files like the application's favicon.ico need to be served from any static directory. We can use any from any application. Keep in mind that out of the box these files aren't managed by Webpack. This is probably a missing configuration that might be remedied in the future, but it's a minor thing, since using Django static we can't have more than one favicon.ico per project unless we call them with different filenames then load the appropriate one from the application template... and if we do that we don't need special support from Vue/Webpack.

About

A way to integrate Django and Vue 3 applications using vue-cli

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published