Makina Blog

Le blog Makina-corpus

Comment développer et intégrer un composant VueJS indépendant dans Django ?


Si vous avez un besoin précis et complexe de JavaScript dans une page ou pour un widget, il peut être intéressant de développer en VueJS sans pour autant recourir à une SPA.

Qui dit front-end JavaScript, dit souvent Single-Page Application (SPA), or une autre technique existe pour profiter de la versatilité du framework front-end tout en se passant du besoin d'un système de routing ou de store. Nous allons dans cet article vous montrer comment créer un module de toute pièce avec sa propre API pour gérer des entrées d'historique en ayant une manière pour les afficher basée sur un composant Vue.

Le code est accessible sur Github car celui-ci a été créé dans le cadre du partenariat sur le projet OSIS pour notre client l'Université Catholique de Louvain-La-Neuve (UCL).

La création de la structure

L'idée est de pouvoir réutiliser ce package pour plusieurs projets. Ainsi, nous ferons en sorte que notre projet soit non seulement réutilisable, mais aussi packagable pour que les fichiers statiques soient livrés avec le code. Pour cela, la structure suivante est adaptée :

├── MANIFEST.in
├── frontend/
├── osis_history/
├── package.json
├── setup.py
└── vue.config.js

Ainsi, le code Python de notre package (dossier osis_history) est soigneusement séparé de notre code JavaScript (dossier frontend). Les fichiers setup.py et MANIFEST.IN nous serviront à définir la manière de se comporter du package Python.

Les fichiers de configuration (package.json, vue.config.js) sont également dans la racine du projet et ne seront donc pas présents lorsque le package sera installé via pip.

L'app Django

Commençons par initialiser notre code Python avec les commandes :

$ ./manage.py startapp osis_history
$ mkdir tmp_osis_history
$ mv osis_history tmp_osis_history/osis_history
$ mv tmp_osis_history osis_history

Puis ajoutons un README.md et un setup.py pour décrire notre projet :

from setuptools import setup, find_packages

setup(
    name='OSIS History',
    version='0.1',
    description='History management API and UI',
    url='http://github.com/uclouvain/osis-history',
    author='Université catholique de Louvain',
    author_email='O365G-team-osis-dev@groupes.uclouvain.be',
    license='AGPLv3',
    packages=find_packages(exclude=('osis_history.tests',)),
    include_package_data=True,
)

Ici, nous excluons le package osis_history.tests lors de l'installation via pip. Ainsi, les tests ne seront présents que lorsque le dépôt sera cloné.

Le projet OSIS est affichable en anglais et en français, ce qui nous donne un modèle simple :

class HistoryEntry(models.Model):
    object_uuid = models.UUIDField(
        verbose_name=_("Registered object's UUID"), db_index=True
    )
    message_fr = models.TextField(verbose_name=_("Message in french"))
    message_en = models.TextField(verbose_name=_("Message in english"))
    created = models.DateTimeField(verbose_name=_("Created"), auto_now_add=True)
    author = models.CharField(verbose_name=_("Author"), max_length=255)
    tags = ArrayField(
        models.CharField(max_length=50),
        verbose_name=_("Tags"),
        blank=True,
        default=list,
    )

    class Meta:
        verbose_name = _("History entry")
        verbose_name_plural = _("History entries")
        ordering = ("-created",)

Celui-ci ne dépend d'aucun autre modèle (pas de ForeignKey), et ne référence que des UUID d'objets. Nous pouvons considérer ce choix comme plus judicieux que d'avoir une GenericForeignKey. En effet, l'intérêt est de récupérer en lecture des informations à partir d'un identifiant et non d'augmenter l'objet initial.

Le principe d'historiser toutes sortes d'actions n'interviendra que rarement dans le métier d'une application et aura très peu de couplage dans le reste de celle-ci.

Si des langues venaient à être ajoutées, il pourrait être intéressant de passer le message à stocker sur un HStoreField pour gérer les traductions.

Inutile d'aller plus loin dans la description du code Python, il suffit ensuite de créer des points d'entrées d'API pour lister les instances du modèle que nous venons de déclarer. En effet, si côté JavaScript nous faisons un appel Ajax, via fetch() par exemple, le navigateur utilisera les mêmes cookies que pour un appel d'une page normale. Si la vue est basée sur Django Rest Framework par exemple, Il suffit de spécifier une authentification par session et l'utilisateur courant sera automatiquement authentifié.

Le composant VueJS

Le côté le plus intéressant est sans doute le développement de la partie VueJS. L'idée est de fournir un composant VueJS tout en laissant le soin d'intégrer le framework Vue par un moyen classique (par exemple un CDN).

Commençons donc par initialiser notre package avec vue-cli :

$ vue create osis_history
Vue CLI v5.0.4
? Please pick a preset: Manually select features
? Check the features needed for your project: Babel, CSS Pre-processors, Linter, Unit
? Choose a version of Vue.js that you want to start the project with: 2.x
? Pick a CSS pre-processor (PostCSS, Autoprefixer and CSS Modules are supported by default): Sass/SCSS (with dart-sass)
? Pick a linter / formatter config: Airbnb
? Pick additional lint features: Lint on save
? Pick a unit testing solution: Jest
? Where do you prefer placing config for Babel, ESLint, etc.? In package.json
? Save this as a preset for future projects? No

En sélectionnant manuellement les fonctionnalités, cela nous permet de configurer plus finement la manière de fonctionner du composant.

Le mode composant

En effet, vue-cli génère un package.json de manière à démarrer le projet en SPA. Nous allons donc commencer par modifier la manière de construire (build) le paquet JS final.

  1. Renommer le dossier src en frontend, afin de ne pas confondre le code source Python de JS (mv src frontend)
  2. Modifier la section "scripts" du package.json afin d'utiliser le mode "library" :

    "build": "vue-cli-service build --target lib --name osis-history --entry frontend/main.js --mode production",
    "coverage": "jest --coverage",
    "lint": "vue-cli-service lint frontend",
    "test": "jest",
    "watch": "vue-cli-service build --target lib --name osis-history --entry frontend/main.js --mode production --watch"
    

    Comme expliqué dans la documentation, ce mode considère que Vue est disponible en global, et donc ne l'intègre pas à la compilation.

Nous supprimons également le script "serve" puisque qu'il n'y a pas de mode SPA, mais cela pourrait être remplacé par storybook pour un développement facilité du composant. Nous ajoutons également un mode watch pour faciliter le développement.

  1. Modifier la manière dont se comporte vue-cli via le fichier vue.config.json :
module.exports = {
  outputDir: "osis_history/static/osis_history",
  configureWebpack (config) {
    // Removes demo.html
    const index = config.plugins.findIndex(plugin => plugin.options?.filename === 'demo.html');
    if (index !== -1) {
      config.plugins.splice(index, 1);
    }
    return {
      externals: {
        'vue-i18n': 'VueI18n',
      },
    };
  },
  filenameHashing: false,
  chainWebpack: config => {
    config.plugins.delete('preload')
    config.plugins.delete('prefetch')
  },
}

Cette configuration désactive des fonctionnalités utilisées uniquement pour une SPA (page de démo, prefetch et preload), et considère d'autres bibliothèques (par exemple VueI18n) comme externes afin de ne pas les compiler. Pour se rapprocher au mieux d'un fichier statique tel que Django l'attend, ceux-ci sont placés dans le dossier static/ de l'application Django et le hashing est désactivé pour avoir des noms de fichier fixes.

L'intégration au HTML

Le code du composant VueJS ne sera pas expliqué en détail dans cet article (cf les fichiers concernés HistoryViewer.vue et ses sous-composants). En revanche, il est intéressant de détailler l'intégration avec le code HTML final.

Notre point d'entrée est en effet le fichier main.js qui traditionnellement initialise une SPA dans un conteneur #app à travers le code :

new Vue({
  render: (h) => h(App),
}).$mount('#app');

Dans notre cas, si plusieurs composants sont à initialiser sur la page, il vaut mieux préférer une classe CSS. Également, afin de configurer le composant différemment selon les emplacements, il est utile d'initialiser ses props par rapport à des données passées sous forme d'attribut HTML. Nous obtenons :

import Vue from 'vue';
import HistoryViewer from './HistoryViewer';
import { i18n } from './i18n';

document.querySelectorAll('.history-viewer').forEach((elem) => {
  new Vue({
    render: (h) => h(HistoryViewer, { props: elem.dataset }),
    i18n,
  }).$mount(elem);
});

NB: le principe de data-attribute ne prend pas en compte le type de la donnée, celle-ci sera toujours de type 'string'. Pour palier à cela, il convient de faire une conversion de type en amont, par exemple :

const props = { ...elem.dataset };
if (typeof props.limit !== 'undefined') {
  props.limit = Number.parseInt(props.limit);
}
new Vue({
  render: (h) => h(HistoryViewer, { props }),
  i18n,
}).$mount(elem);

Notre composant est maintenant utilisable (à condition que Vue, VueI18n et notre script soient présents), si nous affichons le HTML suivant :

{% load static %}
<html>
<head>
  <link href="{% static 'osis_history/osis-history.css' %}" rel="stylesheet"/>
</head>
<body>
<div class="history-viewer" data-url="{% url 'some-test' object.uuid %}"></div>

<script type="text/javascript" src="https://unpkg.com/vue@2/dist/vue.runtime.min.js"></script>
<script type="text/javascript" src="https://unpkg.com/vue-i18n@8.24.4/dist/vue-i18n.min.js"></script>
<script type="text/javascript" src="{% static 'osis_history/osis-history.umd.min.js' %}"></script>
</body>
</html>

Nous espérons que ce tutoriel vous aidera à développer et intégrer un composant VueJS indépendant dans Django facilement.

Formations associées

Front end

VueJS

Toulouse Du 23 au 25 mai 2023

Voir la formation

Django

Django Rest Framework

A distance (foad) Du 1er au 6 décembre 2022

Voir la formation

Django

Django avancé

A distance (foad) Du 7 au 9 novembre 2022

Voir la formation

Actualités en lien

Image
DjangoCon Europe 2022
21/09/2022 - 13:54

DjangoCon Porto 2022 : mise en œuvre du Domain-driven design (DDD) dans Django

Dans le cadre de la conférence DjangoCon Europe à Porto du 21 au 25 septembre 2022, nous présenterons notre retour d'expérience sur l'intégration de quelques concepts DDD dans OSIS, un projet open-source chez notre partenaire l'UCLouvain.

Voir l'article
Image
Randonnée
06/09/2022 - 09:43

Créer des vues SQL dans Django et les afficher dans un SIG

Nous allons décrire un processus via la mise en place de vues SQL qui permettent à l'utilisateur de lire de la donnée formatée, sans possibilité d'influer sur le contenu d'une base et tout en se connectant directement à celle-ci.

Voir l'article
02/08/2019 - 13:26

Créer un tag d'inclusion avec paramètres dans Django

La bibliothèque de tags interne permet d'enregistrer des tags avec paramètres ou des tags d'inclusion de template, voici comment faire les deux en même temps.

Voir l'article

Inscription à la newsletter

Nous vous avons convaincus