Guten Tag, lieber Habrazhiteli!
Heute ist DevOps auf der Erfolgswelle. In fast jeder Konferenz, die sich der Automatisierung widmet, können Sie vom Redner hören: „Wir haben hier und da DevOps implementiert, dies und das angewendet, es wurde viel einfacher, Projekte usw. usw. durchzuführen.“ Und es ist lobenswert. In der Regel endet die Implementierung von DevOps in vielen Unternehmen jedoch in der Phase der Automatisierung des IT-Betriebs, und nur sehr wenige Menschen sprechen davon, DevOps direkt im Entwicklungsprozess selbst zu implementieren.
Ich möchte dieses kleine Missverständnis korrigieren. DevOps kann durch die Formalisierung der Codebasis entwickelt werden, beispielsweise beim Schreiben einer GUI für die REST-API.
In diesem Artikel möchte ich Ihnen die Lösung für den nicht standardmäßigen Fall vorstellen, auf den unser Unternehmen gestoßen ist - wir konnten die Erstellung der Webanwendungsschnittstelle automatisieren. Ich werde Ihnen erzählen, wie wir zu dieser Aufgabe gekommen sind und wie wir sie gelöst haben. Wir glauben nicht, dass unser Ansatz der einzig wahre ist, aber wir mögen ihn wirklich.
Ich hoffe, dieses Material wird für Sie interessant und nützlich sein.
Nun, fangen wir an!
Hintergrund
Diese Geschichte begann vor ungefähr einem Jahr: Es war ein wunderschöner Sommertag und unsere Entwicklungsabteilung erstellte die nächste Webanwendung. Auf der Tagesordnung stand die Aufgabe, eine neue Funktion in die Anwendung einzuführen - es war notwendig, die Möglichkeit hinzuzufügen, benutzerdefinierte Hooks zu erstellen.

Zu diesem Zeitpunkt wurde die Architektur unserer Webanwendung so erstellt, dass wir zur Implementierung einer neuen Funktion Folgendes tun mussten:
- Im Back-End: Erstellen Sie ein Modell für eine neue Entität (Hooks), beschreiben Sie die Felder dieses Modells, beschreiben Sie die gesamte Logik der Aktionen, die das Modell ausführen kann usw.
- Am Frontend: Erstellen Sie eine Präsentationsklasse, die dem neuen Modell in der API entspricht, beschreiben Sie manuell alle Felder dieses Modells, fügen Sie alle Arten von Aktionen hinzu, die diese Ansicht ausführen kann usw.
Es stellt sich heraus, dass wir gleichzeitig an zwei Stellen gleichzeitig sehr ähnliche Änderungen am Code vornehmen mussten, auf die eine oder andere Weise, um uns gegenseitig zu "duplizieren". Und das ist, wie Sie wissen, nicht gut, da Entwickler bei weiteren Änderungen Änderungen an derselben Stelle an zwei Stellen gleichzeitig vornehmen müssten.
Angenommen, wir müssen den Typ des Felds "Name" von "Zeichenfolge" in "Textbereich" ändern. Dazu müssen wir diese Änderung im Modellcode auf dem Server vornehmen und dann ähnliche Änderungen am Präsentationscode auf dem Client vornehmen.
Ist es zu kompliziert?
Zuvor haben wir uns mit dieser Tatsache abgefunden, da viele Anwendungen nicht sehr groß waren und es einen Ort gab, an dem der Code auf dem Server und auf dem Client „dupliziert“ werden konnte. Aber an diesem Sommertag, vor der Einführung der neuen Funktion, klickte etwas in uns und wir stellten fest, dass wir so nicht mehr arbeiten konnten. Der derzeitige Ansatz war sehr unvernünftig und erforderte viel Zeit und Arbeit. Darüber hinaus könnte das „Duplizieren“ von Code im Back-End und Front-End in Zukunft zu unerwarteten Fehlern führen: Entwickler könnten Änderungen am Server vornehmen und vergessen, ähnliche Änderungen am Client vorzunehmen, und dann würde nicht alles gut gehen nach Plan.
Wie vermeide ich Codeduplizierungen? Suche nach einer Lösung
Wir fragten uns, wie wir den Prozess der Einführung neuer Funktionen optimieren können.
Wir haben uns die Frage gestellt: "Können wir sofort vermeiden, Änderungen in der Darstellung des Modells am Front-End zu duplizieren, nachdem sich die Struktur am Back-End geändert hat?"
Wir dachten und antworteten: "Nein, das können wir nicht."
Dann stellten wir uns eine andere Frage: "OK, was ist dann der Grund für eine solche Vervielfältigung von Code?"
Und dann wurde uns klar: Das Problem ist tatsächlich, dass unser Front-End keine Daten zur aktuellen API-Struktur empfängt. Das Front-End weiß nichts über die Modelle, die in der API vorhanden sind, bis wir es selbst darüber informieren.
Und dann kamen wir auf die Idee: Was ist, wenn wir die Anwendungsarchitektur so erstellen, dass:
- Von der API empfangenes Front-End nicht nur Modelldaten, sondern auch die Struktur dieser Modelle;
- Dynamisch geformte Front-End-Darstellungen basierend auf der Struktur von Modellen;
- Jede Änderung in der Struktur der API wurde automatisch im Frontend angezeigt.
Das Implementieren einer neuen Funktion nimmt viel weniger Zeit in Anspruch, da Änderungen nur auf der Back-End-Seite erforderlich sind und das Front-End automatisch alles aufnimmt und dem Benutzer ordnungsgemäß präsentiert.
Die Vielseitigkeit der neuen Architektur
Und dann haben wir uns entschlossen, etwas breiter zu denken: Ist die neue Architektur nur für unsere aktuelle Anwendung geeignet oder können wir sie woanders verwenden?

In der Tat haben fast alle Anwendungen auf die eine oder andere Weise einen Teil einer ähnlichen Funktionalität:
- Fast alle Anwendungen haben Benutzer, und in dieser Hinsicht ist es erforderlich, über Funktionen zu verfügen, die mit der Registrierung und Autorisierung von Benutzern verbunden sind.
- Fast alle Anwendungen haben verschiedene Arten von Ansichten: Es gibt eine Ansicht zum Anzeigen einer Liste von Objekten eines Modells, es gibt eine Ansicht zum Anzeigen einer detaillierten Aufzeichnung eines einzelnen Modellobjekts;
- Fast alle Modelle haben ähnliche Typattribute: Zeichenfolgendaten, Zahlen usw. In dieser Hinsicht müssen Sie in der Lage sein, sowohl im Backend als auch im Frontend mit ihnen zu arbeiten.
Und da unser Unternehmen häufig benutzerdefinierte Webanwendungen entwickelt, haben wir uns gedacht: Warum müssen wir das Rad jedes Mal neu erfinden und jedes Mal ähnliche Funktionen von Grund auf neu entwickeln, wenn wir einmal ein Framework schreiben können, das alle für viele gemeinsamen Grundlagen beschreibt Anwendungen, Dinge und dann, wenn Sie ein neues Projekt erstellen, verwenden Sie vorgefertigte Entwicklungen als Abhängigkeiten und ändern Sie sie gegebenenfalls deklarativ in einem neuen Projekt.
So hatten wir im Verlauf einer langen Diskussion die Idee, VSTUtils zu erstellen - ein Framework, das:
- Es enthielt die Grundfunktionen, die den meisten Anwendungen am ähnlichsten waren.
- Erlaubt das Front-End im laufenden Betrieb basierend auf der Struktur der API.
Wie finde ich Freunde im Backend und Frontend?
Na dann müssen wir machen, dachten wir. Wir hatten bereits ein Back-End, ein Front-End auch, aber weder der Server noch der Client verfügten über ein Tool, mit dem Daten zur Struktur der API gemeldet oder empfangen werden konnten.
Bei der Suche nach einer Lösung für dieses Problem fiel unser Blick auf die
OpenAPI- Spezifikation, die basierend auf der Beschreibung der Modelle und den Beziehungen zwischen ihnen einen riesigen JSON generiert, der all diese Informationen enthält.
Und wir dachten, dass das Front-End beim Initialisieren der Anwendung auf dem Client theoretisch diesen JSON von der API empfangen und alle erforderlichen Ansichten auf seiner Basis erstellen kann. Es bleibt nur, unserem Front-End beizubringen, all dies zu tun.
Und nach einiger Zeit haben wir ihn unterrichtet.
Version 1.0 - was dabei herauskam
Die Architektur des VSTUtils-Frameworks der ersten Versionen bestand aus 3 bedingten Teilen und sah ungefähr so aus:
- Backend:
- Django und Python sind alle modellbezogene Logik. Basierend auf dem Basis-Django-Modell haben wir mehrere Klassen von VSTUtils-Kernmodellen erstellt. Alle Aktionen, die diese Modelle ausführen können, wurden mit Python implementiert.
- Django REST Framework - REST-API-Generierung. Basierend auf der Beschreibung der Modelle wird eine REST-API gebildet, dank derer Server und Client kommunizieren.
- Zwischenschicht zwischen Backend und Frontend:
- OpenAPI - JSON-Generierung mit einer Beschreibung der API-Struktur. Nachdem alle Modelle im Backend beschrieben wurden, werden Ansichten für sie erstellt. Durch Hinzufügen jeder der Ansichten werden die erforderlichen Informationen in den resultierenden JSON eingefügt:
JSON-Beispiel - OpenAPI-Schema{ // , (, ), // - , // - . 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"], }, } } }
- Frontend:
- JavaScript ist ein Mechanismus, der ein OpenAPI-Schema analysiert und Ansichten generiert. Dieser Mechanismus wird einmal gestartet, wenn die Anwendung auf dem Client initialisiert wird. Durch das Senden einer Anforderung an die API erhält sie den angeforderten JSON als Antwort mit einer Beschreibung der API-Struktur und erstellt bei ihrer Analyse alle erforderlichen JS-Objekte, die die Parameter der Modelldarstellungen enthalten. Diese API-Anforderung ist ziemlich umfangreich, daher werden sie zwischengespeichert und erst beim Aktualisieren der Anwendungsversion erneut angefordert.
- JavaScript SPA-Bibliotheken - Rendern von Ansichten und Weiterleiten zwischen ihnen. Diese Bibliotheken wurden von einem unserer Front-End-Entwickler geschrieben. Wenn ein Benutzer auf eine bestimmte Seite zugreift, zeichnet die Rendering-Engine die Seite basierend auf den in JS-Darstellungsobjekten gespeicherten Parametern.
Was wir also haben: Wir haben ein Back-End, das die gesamte mit Modellen verbundene Logik beschreibt. Dann betritt OpenAPI das Spiel, das basierend auf der Modellbeschreibung JSON mit einer Beschreibung der API-Struktur generiert. Als nächstes wird der Staffelstab an den Client übertragen, der bei der Analyse des generierten OpenAPI JSON automatisch eine Webschnittstelle generiert.
Einbetten von Funktionen in die Anwendung in die neue Architektur - wie es funktioniert
Erinnern Sie sich an die Aufgabe, benutzerdefinierte Hooks hinzuzufügen? So würden wir es in einer auf VSTUtils basierenden Anwendung implementieren:

Dank VSTUtils müssen wir jetzt nichts mehr von Grund auf neu schreiben. Folgendes tun wir, um die Möglichkeit zum Erstellen benutzerdefinierter Hooks hinzuzufügen:
- Am Backend: Wir übernehmen und erben von der am besten geeigneten Klasse in VSTUtils und fügen neue Funktionen hinzu, die für das neue Modell spezifisch sind.
- Am vorderen Ende:
- Wenn sich die Ansicht für dieses Modell nicht von der Basisansicht von VSTUtils unterscheidet, tun wir nichts, alles wird automatisch richtig angezeigt.
- Wenn Sie das Verhalten der Ansicht mithilfe des Signalmechanismus irgendwie ändern müssen, erweitern wir das grundlegende Verhalten der Ansicht deklarativ oder ändern es vollständig.
Als Ergebnis haben wir eine ziemlich gute Lösung erhalten, wir haben unser Ziel erreicht, unser Front-End wurde automatisch generiert. Der Prozess der Einführung neuer Funktionen in bestehende Projekte hat sich spürbar beschleunigt: Alle zwei Wochen wurden Veröffentlichungen veröffentlicht, während wir zuvor alle zwei bis drei Monate Veröffentlichungen mit einer viel geringeren Anzahl neuer Funktionen veröffentlicht haben. Ich möchte darauf hinweisen, dass das Entwicklungsteam das gleiche geblieben ist. Es war die neue Anwendungsarchitektur, die uns die Früchte getragen hat.
Version 1.0 - unsere Herzen fordern Veränderung
Aber wie Sie wissen, gibt es keine Grenzen für die Perfektion, und VSTUtils war keine Ausnahme.
Trotz der Tatsache, dass wir die Bildung des Frontends automatisieren konnten, war das Ergebnis nicht die direkte Lösung, die wir ursprünglich wollten.
Die clientseitige Anwendungsarchitektur wurde nicht gründlich durchdacht und erwies sich als nicht so flexibel wie möglich:
- Der Prozess der Einführung funktionaler Überlastungen war nicht immer bequem.
- Der OpenAPI-Parsing-Mechanismus war nicht optimal.
- Das Rendern von Darstellungen und das Weiterleiten zwischen ihnen wurde mit selbstgeschriebenen Bibliotheken durchgeführt, was uns aus mehreren Gründen ebenfalls nicht zusagte:
- Diese Bibliotheken wurden nicht durch Tests abgedeckt;
- Für diese Bibliotheken gab es keine Dokumentation.
- Sie hatten keine Community - im Falle der Entdeckung von Fehlern in ihnen oder der Abreise des Mitarbeiters, der sie geschrieben hat, wäre die Unterstützung für einen solchen Code sehr schwierig.
Und da wir in unserem Unternehmen am DevOps-Ansatz festhalten und versuchen, unseren Code so weit wie möglich zu standardisieren und zu formalisieren, haben wir uns im Februar dieses Jahres entschlossen, ein globales Refactoring des VSTUtils-Front-End-Frameworks durchzuführen. Wir hatten mehrere Aufgaben:
- Um nicht nur Präsentationsklassen am Frontend, sondern auch Modellklassen zu bilden, haben wir erkannt, dass es korrekter ist, Daten (und ihre Struktur) von ihrer Präsentation zu trennen. Darüber hinaus würde das Vorhandensein mehrerer Abstraktionen in Form einer Darstellung und eines Modells das Hinzufügen von Überladungen der Grundfunktionalität in Projekten, die auf VSTUtils basieren, erheblich erleichtern.
- Verwenden Sie ein getestetes Framework mit einer großen Community (Angular, React, Vue) zum Rendern und Weiterleiten. Auf diese Weise können wir alle Kopfschmerzen beseitigen und Code für das Rendern und Weiterleiten in unserer Anwendung unterstützen.
Refactoring - Auswahl des JS-Frameworks
Unter den beliebtesten JS-Frameworks: Angular, React, Vue fiel unsere Wahl auf Vue, weil:
- Die Codebasis von Vue wiegt weniger als React and Angular.
Gzipped Framework Größenvergleichstabelle
- Der Seitenrenderprozess von Vue dauert weniger lange als der von React und Angular.

- Die Eintrittsschwelle in Vue ist viel niedriger als in React und Angular.
- Nativ verständliche Syntax von Vorlagen;
- Elegante, detaillierte Dokumentation in mehreren Sprachen, einschließlich Russisch;
- Ein entwickeltes Ökosystem, das neben der Vue-Kernbibliothek Bibliotheken für das Routing und die Erstellung eines reaktiven Data Warehouse bereitstellt.
Version 2.0 - das Ergebnis des Front-End-Refactorings
Der Prozess der globalen Umgestaltung des Frontends von VSTUtils dauerte ungefähr 4 Monate, und am Ende haben wir Folgendes erreicht:

Das Front-End-Framework von VSTUtils besteht immer noch aus zwei großen Blöcken: Der erste befasst sich mit dem Parsen des OpenAPI-Schemas, der zweite mit dem Rendern von Ansichten und dem Routing zwischen ihnen, aber beide Blöcke haben eine Reihe bedeutender Änderungen erfahren.
Der Mechanismus, der das OpenAPI-Schema analysiert, wurde vollständig neu geschrieben. Der Ansatz zum Parsen dieses Schemas hat sich geändert. Wir haben versucht, die Front-End-Architektur der Back-End-Architektur so ähnlich wie möglich zu gestalten. Jetzt haben wir auf der Client-Seite nicht nur eine einzige Abstraktion in Form von Darstellungen, sondern auch Abstraktionen in Form von Modellen und QuerySets:
- Objekte der Model-Klasse und ihrer Nachkommen sind Objekte, die den serverseitigen Abstraktionen von Django Models entsprechen. Objekte dieses Typs enthalten Daten zur Struktur des Modells (Modellname, Modellfelder usw.);
- Objekte der QuerySet-Klasse und ihrer Nachkommen sind Objekte, die der serverseitigen Django QuerySets-Abstraktion entsprechen. Objekte dieses Typs enthalten Methoden, mit denen Sie API-Anforderungen ausführen können (Hinzufügen, Ändern, Empfangen, Löschen von Daten von Modellobjekten).
- Objekte der View-Klasse - Objekte, die Daten darüber speichern, wie das Modell auf einer bestimmten Seite dargestellt werden soll, welche Vorlage zum „Rendern“ der Seite verwendet werden soll, mit welchen anderen Darstellungen der Modelle diese Seite verknüpft werden kann usw.
Die für das Rendern und Routing zuständige Einheit hat sich ebenfalls erheblich geändert. Wir haben die selbstgeschriebenen JS SPA-Bibliotheken zugunsten von Vue.js aufgegeben. Wir haben unsere eigenen Vue-Komponenten entwickelt, aus denen alle Seiten unserer Webanwendung bestehen. Das Routing zwischen Ansichten erfolgt mithilfe der Vue-Router-Bibliothek, und wir verwenden Vuex als reaktiven Speicher für den Anwendungsstatus.
Ich möchte auch darauf hinweisen, dass die Implementierung der Klassen Model, QuerySet und View auf der Front-End-Seite nicht von den Render- und Routing-Mitteln abhängt, dh wenn wir plötzlich von Vue zu einem anderen Framework wechseln möchten, z. B. React oder Etwas Neues, dann müssen wir nur noch die Vue-Komponenten in die Komponenten des neuen Frameworks umschreiben, den Router und das Repository neu schreiben und das ist alles - das VSTUtils-Framework funktioniert wieder. Die Implementierung der Klassen Model, QuerySet und View bleibt unverändert, da sie nicht von Vue.js abhängt. Wir glauben, dass dies eine sehr gute Hilfe für mögliche zukünftige Änderungen ist.
Zusammenfassend
Die Zurückhaltung, „doppelten“ Code zu schreiben, führte daher zu der Aufgabe, die Bildung des Frontends einer Webanwendung zu automatisieren, was durch die Erstellung des VSTUtils-Frameworks gelöst wurde. Wir haben es geschafft, die Architektur der Webanwendung so zu gestalten, dass sich Back-End und Front-End harmonisch ergänzen und jede Änderung der API-Struktur automatisch erfasst und ordnungsgemäß auf dem Client angezeigt wird.
Die Vorteile, die wir durch die Formalisierung der Architektur der Webanwendung erhalten haben:
- Veröffentlichungen von Anwendungen, die auf der Basis von VSTUtils ausgeführt wurden, wurden zweimal häufiger veröffentlicht. Dies liegt an der Tatsache, dass wir für die Einführung einer neuen Funktion häufig nur Code im Back-End hinzufügen müssen. Das Front-End wird automatisch generiert. Dies spart Zeit.
- Vereinfachte Aktualisierung der Grundfunktionalität. Da jetzt alle grundlegenden Funktionen in einem Framework zusammengefasst sind, müssen einige Änderungen nur an einer Stelle vorgenommen werden, um einige wichtige Abhängigkeiten zu aktualisieren oder die grundlegenden Funktionen zu verbessern - in der VSTUtils-Codebasis. Beim Aktualisieren der Version von VSTUtils in untergeordneten Projekten werden alle Innovationen automatisch übernommen.
- Die Suche nach neuen Mitarbeitern ist einfacher geworden. Stimmen Sie zu, es ist viel einfacher, einen Entwickler für einen formalisierten Technologie-Stack (Django, Vue) zu finden, als nach einer Person zu suchen, die sich bereit erklärt, mit einem unbekannten Rekorder zu arbeiten. Suchergebnisse für Entwickler, die Django oder Vue in HeadHunter in ihren Lebensläufen erwähnt haben (über alle Regionen hinweg):
- Django - 3.454 Lebensläufe wurden für 3.136 Bewerber gefunden;
- Vue - 4.092 Lebensläufe wurden für 3.747 Arbeitssuchende gefunden.
Die Nachteile einer solchen Formalisierung der Architektur einer Webanwendung umfassen Folgendes:
- Aufgrund des Parsens des OpenAPI-Schemas dauert die Initialisierung der Anwendung auf dem Client etwas länger als zuvor (etwa 20 bis 30 Millisekunden länger).
- Unwichtige Suchindizierung. Tatsache ist, dass wir derzeit kein Server-Rendering im Rahmen von VSTUtils verwenden und der gesamte Inhalt der Anwendung in der endgültigen Form bereits auf dem Client erstellt wird. Für unsere Projekte werden jedoch häufig keine hohen Suchergebnisse benötigt, und für uns ist dies nicht so kritisch.
Damit endet meine Geschichte, danke für Ihre Aufmerksamkeit!
Nützliche Links