Buen día, querido Habrazhiteli!
Hoy DevOps está en la ola del éxito. En casi cualquier conferencia dedicada a la automatización, puede escuchar al orador que dice "implementamos DevOps aquí y allá, aplicamos esto y aquello, se hizo mucho más fácil llevar a cabo proyectos, etc., etc.". Y es encomiable. Pero, por regla general, la implementación de DevOps en muchas empresas termina en la etapa de automatización de las operaciones de TI, y muy pocas personas hablan sobre la implementación de DevOps directamente en el proceso de desarrollo.
Me gustaría corregir este pequeño malentendido. DevOps puede entrar en desarrollo a través de la formalización de la base del código, por ejemplo, al escribir una GUI para la API REST.
En este artículo, me gustaría compartir con ustedes la solución al caso no estándar que encontró nuestra empresa: pudimos automatizar la formación de la interfaz de la aplicación web. Te contaré cómo llegamos a esta tarea y qué utilizamos para resolverla. No creemos que nuestro enfoque sea el único verdadero, pero realmente nos gusta.
Espero que este material sea interesante y útil para usted.
Bueno, empecemos!
Antecedentes
Esta historia comenzó hace aproximadamente un año: era un hermoso día de verano y nuestro departamento de desarrollo estaba creando la próxima aplicación web. En la agenda estaba la tarea de introducir una nueva característica en la aplicación: era necesario agregar la capacidad de crear ganchos personalizados.

En ese momento, la arquitectura de nuestra aplicación web se creó de tal manera que para implementar una nueva característica, teníamos que hacer lo siguiente:
- En el back-end: cree un modelo para una nueva entidad (ganchos), describa los campos de este modelo, describa toda la lógica de acciones que el modelo puede realizar, etc.
- En el front-end: cree una clase de presentación que corresponda al nuevo modelo en la API, describa manualmente todos los campos que tiene este modelo, agregue todos los tipos de acciones que puede ejecutar esta vista, etc.
Resulta que simultáneamente en dos lugares a la vez, era necesario hacer cambios muy similares en el código, de una forma u otra, "duplicando" entre sí. Y esto, como saben, no es bueno, porque con más cambios, los desarrolladores tendrían que hacer cambios en el mismo lugar en dos lugares al mismo tiempo.
Supongamos que necesitamos cambiar el tipo del campo "nombre" de "cadena" a "área de texto". Para hacer esto, necesitaremos hacer esta enmienda en el código del modelo en el servidor, y luego hacer cambios similares al código de presentación en el cliente.
¿Es muy complicado?
Anteriormente, soportábamos este hecho, ya que muchas aplicaciones no eran muy grandes y había un lugar para "duplicar" el código en el servidor y el cliente. Pero ese mismo día de verano, antes de la introducción de la nueva función, algo hizo clic dentro de nosotros y nos dimos cuenta de que ya no podíamos trabajar así. El enfoque actual era muy irrazonable y requería mucho tiempo y trabajo. Además, la "duplicación" de código en el back-end y el front-end podría generar errores inesperados en el futuro: los desarrolladores podrían hacer cambios en el servidor y olvidarse de hacer cambios similares en el cliente, y entonces todo no iría bien De acuerdo al plan.
¿Cómo evitar la duplicación de código? Busca una solución
Comenzamos a preguntarnos cómo podemos optimizar el proceso de introducción de nuevas características.
Nos hicimos la pregunta: "¿Podemos evitar de inmediato la duplicación de cambios en la representación del modelo en el front-end, después de cualquier cambio en su estructura en el back-end?"
Pensamos y respondimos: "No, no podemos".
Luego nos hicimos otra pregunta: "Bien, ¿cuál es la razón de tal duplicación de código?"
Y luego nos dimos cuenta: el problema, de hecho, es que nuestro front-end no recibe datos sobre la estructura API actual. El front-end no sabe nada sobre los modelos que existen en la API hasta que nosotros mismos lo informemos.
Y entonces se nos ocurrió la idea: ¿qué pasaría si construimos la arquitectura de la aplicación de tal manera que:
- El front-end recibido de la API no solo los datos del modelo, sino también la estructura de estos modelos;
- Representaciones de front-end dinámicamente formadas basadas en la estructura de modelos;
- Cualquier cambio en la estructura de la API se muestra automáticamente en el front-end.
La implementación de una nueva característica llevará mucho menos tiempo, ya que requerirá cambios solo en el lado del back-end, y el front-end recogerá automáticamente todo y lo presentará al usuario correctamente.
La versatilidad de la nueva arquitectura.
Y luego, decidimos pensar un poco más ampliamente: ¿la nueva arquitectura es adecuada solo para nuestra aplicación actual, o podemos usarla en otro lugar?

De hecho, de una forma u otra, casi todas las aplicaciones tienen parte de una funcionalidad similar:
- casi todas las aplicaciones tienen usuarios, y en este sentido, es necesario tener una funcionalidad asociada con el registro y la autorización del usuario;
- Casi todas las aplicaciones tienen varios tipos de vistas: hay una vista para ver una lista de objetos de un modelo, hay una vista para ver un registro detallado de un solo objeto modelo individual;
- Casi todos los modelos tienen atributos similares en tipo: datos de cadena, números, etc., y en este sentido, debe poder trabajar con ellos tanto en el back-end como en el front-end.
Y dado que nuestra compañía a menudo desarrolla aplicaciones web personalizadas, pensamos: ¿por qué necesitamos reinventar la rueda cada vez y desarrollar una funcionalidad similar cada vez desde cero, si podemos escribir un marco una vez que describa todo lo básico, común para muchos aplicaciones, cosas, y luego, creando un nuevo proyecto, use desarrollos listos para usar como dependencias y, si es necesario, cámbielos declarativamente en un nuevo proyecto.
Por lo tanto, en el transcurso de una larga discusión, tuvimos la idea de crear VSTUtils, un marco que:
- Contenía la funcionalidad básica, más similar a la mayoría de las aplicaciones;
- Se permite generar front-end sobre la marcha, en función de la estructura de la API.
¿Cómo hacer amigos back-end y front-end?
Bueno, entonces tenemos que hacer, pensamos. Ya teníamos un back-end, también un front-end, pero ni el servidor ni el cliente tenían una herramienta que pudiera informar o recibir datos sobre la estructura de la API.
En la búsqueda de una solución a este problema, nos fijamos en la especificación
OpenAPI , que, según la descripción de los modelos y las relaciones entre ellos, genera un gran JSON que contiene toda esta información.
Y pensamos que, en teoría, al inicializar la aplicación en el cliente, el front-end puede recibir este JSON de la API y construir todas las vistas necesarias sobre la base. Solo queda enseñar a nuestro front-end a hacer todo esto.
Y después de un tiempo le enseñamos.
Versión 1.0: lo que salió de ella
La arquitectura del marco VSTUtils de las primeras versiones constaba de 3 partes condicionales y se parecía a esto:
- Back end:
- Django y Python son todos lógicos relacionados con el modelo. Basado en el modelo base de Django, hemos creado varias clases de modelos principales de VSTUtils. Todas las acciones que estos modelos pueden realizar las implementamos usando Python;
- Django REST Framework : generación de API REST. Según la descripción de los modelos, se forma una API REST, gracias a la cual se comunican el servidor y el cliente;
- Capa intermedia entre back-end y front-end:
- OpenAPI - Generación JSON con una descripción de la estructura API. Una vez que se han descrito todos los modelos en el back-end, se crean vistas para ellos. Agregar cada una de las vistas introduce la información necesaria en el JSON resultante:
Ejemplo JSON - Esquema 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 es un mecanismo que analiza un esquema OpenAPI y genera vistas. Este mecanismo se inicia una vez, cuando la aplicación se inicializa en el cliente. Al enviar una solicitud a la API, recibe el JSON solicitado en respuesta con una descripción de la estructura de la API y, al analizarlo, crea todos los objetos JS necesarios que contienen los parámetros de las representaciones del modelo. Esta solicitud de API es bastante pesada, por lo que la almacenamos en caché y la solicitamos nuevamente solo al actualizar la versión de la aplicación;
- Libs JavaScript SPA : representación de vistas y enrutamiento entre ellas. Estas bibliotecas fueron escritas por uno de nuestros desarrolladores front-end. Cuando un usuario accede a una página en particular, el motor de representación dibuja la página en función de los parámetros almacenados en los objetos de representación JS.
Por lo tanto, lo que tenemos: tenemos un back-end que describe toda la lógica asociada con los modelos. Luego, OpenAPI ingresa al juego, que, según la descripción de los modelos, genera JSON con una descripción de la estructura API. A continuación, el testigo se transmite al cliente, que, al analizar el JAP OpenAPI generado, genera automáticamente una interfaz web.
Incorporación de características en la aplicación en la nueva arquitectura: cómo funciona
¿Recuerdas la tarea de agregar ganchos personalizados? Así es como lo implementaríamos en una aplicación basada en VSTUtils:

Ahora, gracias a VSTUtils, no necesitamos escribir nada desde cero. Esto es lo que hacemos para agregar la capacidad de crear ganchos personalizados:
- En el back-end: tomamos y heredamos de la clase más adecuada en VSTUtils, agregamos nuevas funcionalidades específicas al nuevo modelo;
- En el frente:
- Si la vista para este modelo no es diferente de la vista básica de VSTUtils, entonces no hacemos nada, todo se muestra automáticamente correctamente;
- Si necesita cambiar de alguna manera el comportamiento de la vista, utilizando el mecanismo de señal, expandimos declarativamente o cambiamos completamente el comportamiento básico de la vista.
Como resultado, obtuvimos una solución bastante buena, logramos nuestro objetivo, nuestro front-end se generó automáticamente. El proceso de introducción de nuevas funciones en los proyectos existentes se ha acelerado notablemente: las versiones comenzaron a lanzarse cada 2 semanas, mientras que anteriormente lanzamos versiones cada 2-3 meses con un número mucho menor de nuevas características. Me gustaría señalar que el equipo de desarrollo se ha mantenido igual, fue la nueva arquitectura de la aplicación la que nos dio los frutos.
Versión 1.0: nuestros corazones exigen un cambio
Pero, como saben, no hay límite para la perfección, y VSTUtils no fue la excepción.
A pesar de que pudimos automatizar la formación del front-end, el resultado no fue la solución directa que originalmente queríamos.
La arquitectura de la aplicación del lado del cliente no fue pensada a fondo, y resultó no tan flexible como podría ser:
- el proceso de introducir sobrecargas funcionales no siempre fue conveniente;
- El mecanismo de análisis de OpenAPI no fue óptimo;
- La representación de las representaciones y el enrutamiento entre ellas se realizó mediante bibliotecas autoescritas, que tampoco nos convenían por una serie de razones:
- Estas bibliotecas no estaban cubiertas por pruebas;
- no había documentación para estas bibliotecas;
- no tenían ninguna comunidad; en caso de detección de errores en ellos o la partida del empleado que los escribió, el soporte para dicho código sería muy difícil.
Y dado que en nuestra empresa nos adherimos al enfoque de DevOps y tratamos de estandarizar y formalizar nuestro código tanto como sea posible, en febrero de este año decidimos llevar a cabo una refactorización global del marco front-end VSTUtils. Tuvimos varias tareas:
- para formar no solo clases de presentación en el front-end, sino también clases de modelos, nos dimos cuenta de que sería más correcto separar los datos (y su estructura) de su presentación. Además, la presencia de varias abstracciones en forma de representación y modelo facilitaría enormemente la adición de sobrecargas de la funcionalidad básica en proyectos basados en VSTUtils;
- use un marco probado con una gran comunidad (Angular, React, Vue) para renderizar y enrutar; esto nos permitirá regalar todo el dolor de cabeza con soporte para el código relacionado con el renderizado y el enrutamiento dentro de nuestra aplicación.
Refactorización: elección del marco JS
Entre los frameworks JS más populares: Angular, React, Vue, nuestra elección recayó en Vue porque:
- La base del código de Vue pesa menos que React y Angular;
Cuadro comparativo de tamaño de marco comprimido
- El proceso de representación de la página de Vue lleva menos tiempo que React y Angular;

- El umbral de entrada en Vue es mucho más bajo que en React y Angular;
- Sintaxis de plantillas nativamente comprensible;
- Documentación elegante y detallada disponible en varios idiomas, incluido el ruso;
- Un ecosistema desarrollado que proporciona, además de la biblioteca central de Vue, bibliotecas para enrutamiento y para crear un almacén de datos reactivo.
Versión 2.0: el resultado de la refactorización frontal
El proceso de refactorización global del front-end de VSTUtils tomó aproximadamente 4 meses y esto es lo que terminamos con:

El marco front-end de VSTUtils todavía consta de dos bloques grandes: el primero analiza el esquema OpenAPI, el segundo representa las vistas y el enrutamiento entre ellos, pero ambos bloques han sufrido una serie de cambios significativos.
El mecanismo que analiza el esquema OpenAPI ha sido completamente reescrito. El enfoque para analizar este esquema ha cambiado. Intentamos hacer que la arquitectura front-end sea lo más similar posible a la arquitectura back-end. Ahora, en el lado del cliente, no solo tenemos una única abstracción en forma de representaciones, ahora también tenemos abstracciones en forma de modelos y conjuntos de consultas:
- Los objetos de la clase Modelo y sus descendientes son objetos correspondientes a las abstracciones del lado del servidor de los Modelos Django. Los objetos de este tipo contienen datos sobre la estructura del modelo (nombre del modelo, campos del modelo, etc.);
- Los objetos de la clase QuerySet y sus descendientes son objetos correspondientes a la abstracción Django QuerySets del lado del servidor. Los objetos de este tipo contienen métodos que le permiten realizar solicitudes de API (agregar, modificar, recibir, eliminar datos de objetos modelo);
- objetos de la clase Ver: objetos que almacenan datos sobre cómo representar el modelo en una página en particular, qué plantilla usar para "renderizar" la página, qué otras representaciones de los modelos puede vincular esta página, etc.
La unidad responsable de la representación y el enrutamiento también ha cambiado significativamente. Abandonamos las bibliotecas JS SPA auto-escritas a favor de Vue.js. Hemos desarrollado nuestros propios componentes Vue que componen todas las páginas de nuestra aplicación web. El enrutamiento entre vistas se realiza utilizando la biblioteca vue-router, y usamos vuex como almacenamiento reactivo del estado de la aplicación.
También me gustaría señalar que, en el lado frontal, la implementación de las clases Model, QuerySet y View no depende de los medios de representación y enrutamiento, es decir, si de repente queremos cambiar de Vue a otro marco, por ejemplo, React o algo nuevo, entonces todo lo que tenemos que hacer es reescribir los componentes Vue a los componentes del nuevo marco, reescribir el enrutador, el repositorio, y eso es todo: el marco VSTUtils volverá a funcionar. La implementación de las clases Model, QuerySet y View seguirá siendo la misma, ya que no depende de Vue.js. Creemos que esta es una muy buena ayuda para posibles cambios futuros.
Para resumir
Por lo tanto, la renuencia a escribir código "duplicado" resultó en la tarea de automatizar la formación del front-end de una aplicación web, que se resolvió creando el marco VSTUtils. Logramos construir la arquitectura de la aplicación web para que el back-end y el front-end se complementen armoniosamente y cualquier cambio en la estructura de la API se detecte y se muestre correctamente en el cliente.
Los beneficios que hemos recibido al formalizar la arquitectura de la aplicación web:
- Los lanzamientos de aplicaciones que se ejecutan sobre la base de VSTUtils comenzaron a aparecer 2 veces más a menudo. Esto se debe al hecho de que ahora para introducir una nueva característica, a menudo, necesitamos agregar código solo en el back-end, el front-end se generará automáticamente, lo que ahorra tiempo;
- Actualización simplificada de la funcionalidad básica. Dado que ahora toda la funcionalidad básica se ensambla en un marco, para actualizar algunas dependencias importantes o mejorar la funcionalidad básica, necesitamos realizar cambios en un solo lugar: en la base de código VSTUtils. Al actualizar la versión de VSTUtils en proyectos secundarios, todas las innovaciones se recogerán automáticamente;
- Encontrar nuevos empleados se ha vuelto más fácil. De acuerdo, es mucho más fácil encontrar un desarrollador para una pila de tecnología formalizada (Django, Vue) que buscar una persona que acepte trabajar con una grabadora desconocida. Resultados de búsqueda para desarrolladores que mencionaron Django o Vue en HeadHunter en sus CV (en todas las regiones):
- Django: se encontraron 3.454 hojas de vida para 3.136 solicitantes;
- Vue: se encontraron 4.092 currículums para 3.747 solicitantes de empleo.
Las desventajas de tal formalización de la arquitectura de una aplicación web incluyen lo siguiente:
- Debido al análisis del esquema OpenAPI, la inicialización de la aplicación en el cliente tarda un poco más que antes (aproximadamente 20-30 milisegundos más);
- Indización de búsqueda sin importancia. El hecho es que en este momento no estamos utilizando la representación del servidor en el marco de VSTUtils, y todo el contenido de la aplicación se forma en la forma final que ya está en el cliente. Pero para nuestros proyectos, a menudo no se necesitan resultados de búsqueda altos y para nosotros no es tan crítico.
En esto mi historia llega a su fin, ¡gracias por su atención!
Enlaces utiles