Bonjour, cher Habrazhiteli!
Aujourd'hui, DevOps est sur la bonne voie. Dans presque toutes les conférences sur l'automatisation, vous pouvez entendre le conférencier dire «nous avons mis en œuvre DevOps ici et là, appliqué ceci et cela, il est devenu beaucoup plus facile de mener des projets, etc., etc.». Et c'est louable. Mais, en règle générale, la mise en œuvre de DevOps dans de nombreuses entreprises se termine au stade de l'automatisation des opérations informatiques, et très peu de gens parlent de la mise en œuvre de DevOps directement dans le processus de développement lui-même.
Je voudrais corriger ce petit malentendu. DevOps peut entrer dans le développement grâce à la formalisation de la base de code, par exemple, lors de l'écriture d'une interface graphique pour l'API REST.
Dans cet article, je voudrais partager avec vous la solution au cas non standard que notre entreprise a rencontré - nous avons pu automatiser la formation de l'interface de l'application Web. Je vais vous expliquer comment nous en sommes arrivés à cette tâche et ce que nous avons utilisé pour la résoudre. Nous ne pensons pas que notre approche est la seule vraie, mais nous l'aimons vraiment.
J'espère que ce matériel vous sera intéressant et utile.
Eh bien, commençons!
Contexte
Cette histoire a commencé il y a environ un an: c'était une belle journée d'été et notre service de développement créait la prochaine application web. À l'ordre du jour était la tâche d'introduire une nouvelle fonctionnalité dans l'application - il était nécessaire d'ajouter la possibilité de créer des crochets personnalisés.

À cette époque, l'architecture de notre application Web a été construite de telle manière que pour implémenter une nouvelle fonctionnalité, nous devions faire ce qui suit:
- Au back-end: créer un modèle pour une nouvelle entité (hooks), décrire les champs de ce modèle, décrire toute la logique des actions que le modèle peut effectuer, etc.
- A l'avant-plan: créez une classe de présentation qui correspond au nouveau modèle dans l'API, décrivez manuellement tous les champs de ce modèle, ajoutez tous les types d'actions que cette vue peut exécuter, etc.
Il s'avère que nous avons simultanément à la fois à deux endroits, il a fallu faire des changements très similaires dans le code, d'une manière ou d'une autre, en se "dupliquant". Et cela, comme vous le savez, n'est pas bon, car avec d'autres modifications, les développeurs devraient apporter des modifications au même endroit, à deux endroits en même temps.
Supposons que nous devons changer le type du champ "nom" de "chaîne" en "zone de texte". Pour ce faire, nous devrons effectuer cette modification dans le code du modèle sur le serveur, puis apporter des modifications similaires au code de présentation sur le client.
C'est trop compliqué?
Auparavant, nous avons toléré ce fait, car de nombreuses applications n'étaient pas très volumineuses et il y avait une place pour «dupliquer» le code sur le serveur et le client. Mais ce même jour d'été, avant l'introduction de la nouvelle fonctionnalité, quelque chose a cliqué en nous, et nous avons réalisé que nous ne pouvions plus travailler comme ça. L'approche actuelle était très déraisonnable et exigeait beaucoup de temps et de travail. De plus, la «duplication» de code sur le back-end et le front-end pourrait conduire à des bugs inattendus à l'avenir: les développeurs pourraient apporter des modifications sur le serveur et oublier de faire des changements similaires sur le client, et alors tout ne se passerait pas bien selon plan.
Comment éviter la duplication de code? Rechercher une solution
Nous avons commencé à nous demander comment optimiser le processus d'introduction de nouvelles fonctionnalités.
Nous nous sommes posé la question: "Peut-on immédiatement éviter de dupliquer les changements de représentation du modèle en front-end'e, après tout changement de sa structure en back-end'e?"
Nous avons pensé et répondu: "Non, nous ne pouvons pas."
Puis nous nous sommes posé une autre question: "OK, quelle est donc la raison d'une telle duplication de code?"
Et puis cela nous est apparu: le problème, en fait, est que notre front-end ne reçoit pas de données sur la structure actuelle de l'API. Le front-end ne sait rien des modèles qui existent dans l'API jusqu'à ce que nous en informions nous-mêmes.
Et puis nous avons eu l'idée: que faire si nous construisons l'architecture d'application de telle manière que:
- Front-end reçu de l'API non seulement les données du modèle, mais aussi la structure de ces modèles;
- Représentations frontales formées dynamiquement basées sur la structure des modèles;
- Tout changement dans la structure de l'API était automatiquement affiché sur le front-end.
La mise en œuvre d'une nouvelle fonctionnalité prendra beaucoup moins de temps, car elle ne nécessitera des modifications que du côté back-end, et le front-end ramassera automatiquement tout et le présentera correctement à l'utilisateur.
La polyvalence de la nouvelle architecture
Et puis, nous avons décidé de penser un peu plus largement: la nouvelle architecture ne convient-elle qu'à notre application actuelle, ou pouvons-nous l'utiliser ailleurs?

En effet, d'une manière ou d'une autre, presque toutes les applications ont une fonctionnalité similaire:
- presque toutes les applications ont des utilisateurs, et à cet égard, il est nécessaire d'avoir des fonctionnalités associées à l'enregistrement et à l'autorisation des utilisateurs;
- presque toutes les applications ont plusieurs types de vues: il y a une vue pour visualiser une liste d'objets d'un modèle, il y a une vue pour voir un enregistrement détaillé d'un seul objet de modèle individuel;
- presque tous les modèles ont des attributs similaires dans le type: données de chaîne, nombres, etc., et à cet égard, vous devez être en mesure de travailler avec eux à la fois sur le back-end et sur le front-end.
Et puisque notre entreprise réalise souvent le développement d'applications Web personnalisées, nous avons pensé: pourquoi devons-nous réinventer la roue à chaque fois et développer à chaque fois des fonctionnalités similaires, si vous pouvez écrire une fois un cadre qui décrirait toutes les bases, communes à beaucoup applications, choses, puis, en créant un nouveau projet, utilisez des développements prêts à l'emploi comme dépendances et, si nécessaire, modifiez-les de manière déclarative dans un nouveau projet.
Ainsi, au cours d'une longue discussion, nous avons eu l'idée de créer des VSTUtils - un cadre qui:
- Il contenait les fonctionnalités de base, les plus similaires à la plupart des applications;
- Autorisé à générer un frontal à la volée, en fonction de la structure de l'API.
Comment se faire des amis back-end et front-end?
Eh bien, nous devons le faire, nous avons pensé. Nous avions déjà un back-end, un front-end aussi, mais ni le serveur ni le client ne disposaient d'un outil qui pouvait rapporter ou recevoir des données sur la structure de l'API.
Dans la recherche d'une solution à ce problème, notre regard est tombé sur la spécification
OpenAPI , qui, basée sur la description des modèles et les relations entre eux, génère un énorme JSON contenant toutes ces informations.
Et nous pensions que, en théorie, lors de l'initialisation de l'application sur le client, le frontal peut recevoir ce JSON de l'API et créer toutes les vues nécessaires sur sa base. Il ne reste plus qu'à apprendre à notre front-end à faire tout cela.
Et après un certain temps, nous lui avons appris.
Version 1.0 - ce qui en est sorti
L'architecture du framework VSTUtils des premières versions se composait de 3 parties conditionnelles et ressemblait à ceci:
- Arrière:
- Django et Python sont tous des logiques liées au modèle. Sur la base du modèle Django de base, nous avons créé plusieurs classes de modèles VSTUtils principaux. Toutes les actions que ces modèles peuvent effectuer sont implémentées à l'aide de Python;
- Django REST Framework - Génération d'API REST. Sur la base de la description des modèles, une API REST est formée, grâce à laquelle le serveur et le client communiquent;
- Intercouche entre back-end et front-end:
- OpenAPI - Génération JSON avec une description de la structure de l'API. Une fois que tous les modèles ont été décrits sur le serveur principal, des vues sont créées pour eux. L'ajout de chacune des vues introduit les informations nécessaires dans le JSON résultant:
Exemple JSON - Schéma OpenAPI{ // , (, ), // - , // - . definitions: { // Hook. Hook: { // , (, ), // - , // - (, ..). properties: { id: { title: "Id", type: "integer", readOnly: true, }, name: { title: "Name", type: "string", minLength:1, maxLength: 512, }, type: { title: "Type", type: "string", enum: ["HTTP","SCRIPT"], }, when: { title: "When", type: "string", enum: ["on_object_add","on_object_upd","on_object_del"], }, enable: { title:"Enable", type:"boolean", }, recipients: { title: "Recipients", type: "string", minLength: 1, } }, // , , . required: ["type","recipients"], } }, // , (, ), // - ( URL), // - . paths: { // '/hook/'. '/hook/': { // get /hook/. // , Hook. get: { operationId: "hook_list", description: "Return all hooks.", // , , . parameters: [ { name: "id", in: "query", description: "A unique integer value (or comma separated list) identifying this instance.", required: false, type: "string", }, { name: "name", in: "query", description: "A name string value (or comma separated list) of instance.", required: false, type: "string", }, { name: "type", in: "query", description: "Instance type.", required: false, type: "string", }, ], // , (, ), // - ; // - . responses: { 200: { description: "Action accepted.", schema: { properties: { results: { type: "array", items: { // , . $ref: "#/definitions/Hook", }, }, }, }, }, 400: { description: "Validation error or some data error.", schema: { $ref: "#/definitions/Error", }, }, 401: { // ... }, 403: { // ... }, 404: { // ... }, }, tags: ["hook"], }, // post /hook/. // , Hook. post: { operationId: "hook_add", description: "Create a new hook.", parameters: [ { name: "data", in: "body", required: true, schema: { $ref: "#/definitions/Hook", }, }, ], responses: { 201: { description: "Action accepted.", schema: { $ref: "#/definitions/Hook", }, }, 400: { description: "Validation error or some data error.", schema: { $ref: "#/definitions/Error", }, }, 401: { // ... }, 403: { // ... }, 404: { // ... }, }, tags: ["hook"], }, } } }
- Front-end:
- JavaScript est un mécanisme qui analyse un schéma OpenAPI et génère des vues. Ce mécanisme est lancé une fois, lorsque l'application est initialisée sur le client. En envoyant une demande à l'API, il reçoit le JSON demandé en réponse avec une description de la structure de l'API et, en l'analysant, crée tous les objets JS nécessaires contenant les paramètres des représentations du modèle. Cette demande d'API est assez lourde, nous la mettons en cache et la demandons à nouveau uniquement lors de la mise à jour de la version de l'application;
- Librairies JavaScript SPA - rendu des vues et routage entre elles. Ces bibliothèques ont été écrites par l'un de nos développeurs front-end. Lorsqu'un utilisateur accède à une page particulière, le moteur de rendu dessine la page en fonction des paramètres stockés dans les objets de représentation JS.
Ainsi, ce que nous avons: nous avons un back-end qui décrit toute la logique associée aux modèles. OpenAPI entre ensuite dans le jeu qui, sur la base de la description des modèles, génère du JSON avec une description de la structure de l'API. Ensuite, le bâton est transmis au client qui, en analysant l'OpenAPI JSON généré, génère automatiquement une interface Web.
Intégration de fonctionnalités dans l'application sur la nouvelle architecture - comment cela fonctionne
Rappelez-vous la tâche d'ajouter des crochets personnalisés? Voici comment nous l'implémenterions dans une application basée sur VSTUtils:

Maintenant, grâce à VSTUtils, nous n'avons plus besoin d'écrire quoi que ce soit à partir de zéro. Voici ce que nous faisons pour ajouter la possibilité de créer des crochets personnalisés:
- À l'arrière-plan: nous prenons et héritons de la classe la plus appropriée dans VSTUtils, ajoutons de nouvelles fonctionnalités spécifiques au nouveau modèle;
- À l'avant:
- si la vue de ce modèle n'est pas différente de la vue de base de VSTUtils, alors nous ne faisons rien, tout s'affiche automatiquement correctement;
- si vous avez besoin de changer le comportement de la vue d'une manière ou d'une autre, en utilisant le mécanisme du signal, nous développons ou modifions complètement le comportement de base de la vue.
En conséquence, nous avons obtenu une assez bonne solution, nous avons atteint notre objectif, notre front-end est devenu auto-généré. Le processus d'introduction de nouvelles fonctionnalités dans les projets existants s'est sensiblement accéléré: les versions ont commencé à être publiées toutes les 2 semaines, alors que nous publions auparavant des versions tous les 2 à 3 mois avec un nombre beaucoup plus réduit de nouvelles fonctionnalités. Je voudrais noter que l'équipe de développement est restée la même, c'est la nouvelle architecture d'application qui nous a donné les fruits.
Version 1.0 - nos coeurs exigent le changement
Mais, comme vous le savez, il n'y a pas de limite à la perfection, et VSTUtils n'a pas fait exception.
Malgré le fait que nous ayons pu automatiser la formation du front-end, le résultat n'a pas été la solution directe que nous souhaitions à l'origine.
L'architecture d'application côté client n'a pas été bien pensée et s'est avérée moins flexible que possible:
- le processus d'introduction de surcharges fonctionnelles n'était pas toujours commode;
- Le mécanisme d'analyse d'OpenAPI n'était pas optimal;
- le rendu des représentations et le routage entre elles ont été effectués à l'aide de bibliothèques auto-écrites, ce qui ne nous convenait pas non plus pour plusieurs raisons:
- Ces bibliothèques n'étaient pas couvertes par des tests;
- il n'y avait aucune documentation pour ces bibliothèques;
- ils n’avaient pas de communauté - en cas de détection de bugs ou de départ de l’employé qui les avait écrit, le support de ce code serait très difficile.
Et comme dans notre entreprise, nous adhérons à l'approche DevOps et essayons de standardiser et de formaliser notre code autant que possible, en février de cette année, nous avons décidé de procéder à une refactorisation globale du framework frontal VSTUtils. Nous avions plusieurs tâches:
- pour former non seulement des classes de présentation au début, mais aussi des classes modèles - nous avons réalisé qu'il serait plus correct de séparer les données (et leur structure) de leur présentation. De plus, la présence de plusieurs abstractions sous la forme d'une représentation et d'un modèle faciliterait grandement l'ajout de surcharges de la fonctionnalité de base dans les projets basés sur VSTUtils;
- utiliser un framework testé avec une grande communauté (Angular, React, Vue) pour le rendu et le routage - cela nous permettra de donner tout le mal de tête avec le support du code lié au rendu et au routage à l'intérieur de notre application.
Refactoring - choix du framework JS
Parmi les frameworks JS les plus populaires: Angular, React, Vue, notre choix s'est porté sur Vue car:
- La base de code de Vue pèse moins que React et Angular;
Tableau de comparaison de la taille du framework Gzipped
- Le processus de rendu de page de Vue prend moins de temps que React et Angular;

- Le seuil d'entrée dans Vue est beaucoup plus bas que dans React et Angular;
- Syntaxe nativement compréhensible des modèles;
- Documentation élégante et détaillée disponible en plusieurs langues, dont le russe;
- Un écosystème développé qui fournit, en plus de la bibliothèque principale Vue, des bibliothèques de routage et de création d'un entrepôt de données réactif.
Version 2.0 - le résultat du refactoring frontal
Le processus de refactoring global du front-end de VSTUtils a pris environ 4 mois et c'est ce que nous avons fini avec:

Le framework frontal VSTUtils se compose toujours de deux grands blocs: le premier traite de l'analyse du schéma OpenAPI, le second du rendu des vues et du routage entre eux, mais ces deux blocs ont subi un certain nombre de changements importants.
Le mécanisme qui analyse le schéma OpenAPI a été complètement réécrit. L'approche pour analyser ce schéma a changé. Nous avons essayé de rendre l'architecture frontale aussi similaire que possible à l'architecture back-end. Maintenant, côté client, nous n'avons pas seulement une seule abstraction sous forme de représentations, maintenant nous avons également des abstractions sous forme de modèles et de QuerySets:
- les objets de la classe Model et ses descendants sont des objets correspondant aux abstractions côté serveur des modèles Django. Les objets de ce type contiennent des données sur la structure du modèle (nom du modèle, champs du modèle, etc.);
- les objets de la classe QuerySet et ses descendants sont des objets correspondant à l'abstraction Django QuerySets côté serveur. Les objets de ce type contiennent des méthodes qui vous permettent d'effectuer des requêtes API (ajouter, modifier, recevoir, supprimer des données d'objets de modèle);
- objets de la classe View - objets qui stockent des informations sur la façon de représenter le modèle sur une page particulière, quel modèle utiliser pour «rendre» la page, à quelles autres représentations des modèles cette page peut se lier, etc.
L'unité responsable du rendu et du routage a également considérablement changé. Nous avons abandonné les bibliothèques JS SPA auto-écrites au profit de Vue.js. Nous avons développé nos propres composants Vue qui composent toutes les pages de notre application web. Le routage entre les vues se fait à l'aide de la bibliothèque vue-router, et nous utilisons vuex comme stockage réactif de l'état de l'application.
Je voudrais également noter que du côté frontal, l'implémentation des classes Model, QuerySet et View ne dépend pas des moyens de rendu et de routage, c'est-à-dire si nous voulons soudainement passer de Vue à un autre cadre, par exemple, React ou quelque chose de nouveau, alors tout ce que nous devons faire est de réécrire les composants Vue dans les composants du nouveau framework, réécrire le routeur, le référentiel, et c'est tout - le framework VSTUtils fonctionnera à nouveau. L'implémentation des classes Model, QuerySet et View restera la même, car elle ne dépend pas de Vue.js. Nous pensons que c'est une très bonne aide pour d'éventuels changements futurs.
Pour résumer
Ainsi, la réticence à écrire du code «en double» a entraîné la tâche d'automatiser la formation du front-end d'une application Web, qui a été résolue en créant le framework VSTUtils. Nous avons réussi à construire l'architecture de l'application Web afin que le back-end et le front-end se complètent harmonieusement et que tout changement dans la structure de l'API soit automatiquement détecté et affiché correctement sur le client.
Les avantages que nous avons retirés de la formalisation de l'architecture de l'application Web:
- Les versions d'applications fonctionnant sur la base de VSTUtils ont commencé à sortir 2 fois plus souvent. Cela est dû au fait que maintenant pour introduire une nouvelle fonctionnalité, souvent, nous devons ajouter du code uniquement sur le back-end, le front-end sera généré automatiquement - cela fait gagner du temps;
- Mise à jour simplifiée des fonctionnalités de base. Puisque maintenant toutes les fonctionnalités de base sont assemblées dans un cadre, afin de mettre à jour certaines dépendances importantes ou d'apporter une amélioration aux fonctionnalités de base, nous devons effectuer des corrections uniquement en un seul endroit - dans la base de code VSTUtils. Lors de la mise à jour de la version de VSTUtils dans les projets enfants, toutes les innovations seront automatiquement récupérées;
- Trouver de nouveaux employés est devenu plus facile. D'accord, il est beaucoup plus facile de trouver un développeur pour une pile technologique formalisée (Django, Vue) que de rechercher une personne qui accepte de travailler avec un enregistreur inconnu. Résultats de recherche pour les développeurs qui ont mentionné Django ou Vue sur HeadHunter dans leurs CV (dans toutes les régions):
- Django - 3 454 curriculum vitae ont été trouvés pour 3 136 candidats;
- Vue - 4 092 CV ont été trouvés pour 3 747 demandeurs d'emploi.
Les inconvénients d'une telle formalisation de l'architecture d'une application Web sont les suivants:
- En raison de l'analyse du schéma OpenAPI, l'initialisation de l'application sur le client prend un peu plus de temps qu'auparavant (environ 20 à 30 millisecondes de plus);
- Indexation de recherche sans importance. Le fait est que pour le moment nous n'utilisons pas le rendu de serveur dans le cadre de VSTUtils, et tout le contenu de l'application est formé dans la forme finale déjà sur le client. Mais pour nos projets, des résultats de recherche souvent élevés ne sont pas nécessaires et pour nous, ce n'est pas si critique.
Sur ce mon histoire se termine, merci de votre attention!
Liens utiles