Bom dia, querido Habrazhiteli!
Hoje, o DevOps está na onda de sucesso. Em quase todas as conferências dedicadas à automação, você pode ouvir do orador dizendo “implementamos o DevOps aqui e ali, aplicamos isso e aquilo, ficou muito mais fácil realizar projetos etc., etc.”. E é louvável. Mas, como regra, a implementação do DevOps em muitas empresas termina no estágio de automação das operações de TI, e poucas pessoas falam sobre a implementação do DevOps diretamente no próprio processo de desenvolvimento.
Eu gostaria de corrigir esse pequeno mal-entendido. O DevOps pode entrar em desenvolvimento através da formalização da base de código, por exemplo, ao escrever uma GUI para a API REST.
Neste artigo, gostaria de compartilhar com você a solução para o caso não padrão que nossa empresa encontrou - fomos capazes de automatizar a formação da interface do aplicativo da web. Vou falar sobre como chegamos a essa tarefa e o que costumávamos resolvê-la. Não acreditamos que nossa abordagem seja a única verdadeira, mas gostamos muito dela.
Espero que este material seja interessante e útil para você.
Bem, vamos começar!
Antecedentes
Essa história começou há cerca de um ano: era um lindo dia de verão e nosso departamento de desenvolvimento estava criando o próximo aplicativo da web. Na agenda estava a tarefa de introduzir um novo recurso no aplicativo - era necessário adicionar a capacidade de criar ganchos personalizados.

Naquela época, a arquitetura do nosso aplicativo da Web era construída de tal maneira que, para implementar um novo recurso, precisávamos fazer o seguinte:
- No back-end: crie um modelo para uma nova entidade (ganchos), descreva os campos desse modelo, descreva toda a lógica das ações que o modelo pode executar, etc.
- No front-end'e: crie uma classe de apresentação que corresponda ao novo modelo na API, descreva manualmente todos os campos que esse modelo possui, adicione todos os tipos de ações que essa visualização pode executar, etc.
Acontece que, simultaneamente, ao mesmo tempo em dois lugares, era necessário fazer alterações muito semelhantes no código, de uma maneira ou de outra, "duplicando" uma à outra. E isso, como você sabe, não é bom, porque com outras alterações, os desenvolvedores precisariam fazer correções da mesma maneira em dois locais ao mesmo tempo.
Suponha que precisamos alterar o tipo do campo "nome" de "string" para "textarea". Para fazer isso, precisaremos fazer essa edição no código do modelo no servidor e, em seguida, fazer alterações semelhantes ao código de apresentação no cliente.
Isso é muito complicado?
Anteriormente, defendemos esse fato, já que muitos aplicativos não eram muito grandes e havia um lugar para "duplicar" o código no servidor e no cliente. Mas naquele mesmo dia de verão, antes da introdução do novo recurso, algo clicou dentro de nós e percebemos que não podíamos mais trabalhar dessa maneira. A abordagem atual era muito irracional e exigia muito tempo e trabalho. Além disso, a “duplicação” de código no back-end e no front-end pode levar a erros inesperados no futuro: os desenvolvedores podem fazer alterações no servidor e esquecer de fazer alterações semelhantes no cliente, e então tudo não vai bem de acordo com o plano.
Como evitar a duplicação de código? Procure uma solução
Começamos a pensar em como otimizar o processo de introdução de novos recursos.
Fizemos a nós mesmos a pergunta: "Podemos evitar duplicar alterações na representação do modelo no front-end'e, após qualquer alteração em sua estrutura no back-end'e?"
Pensamos e respondemos: "Não, não podemos".
Então nos perguntamos outra pergunta: "OK, qual é o motivo dessa duplicação de código?"
E então ocorreu-nos: o problema, de fato, é que nosso front-end não recebe dados na estrutura atual da API. O front-end não sabe nada sobre os modelos que existem na API até que nós mesmos o informemos.
E então tivemos a ideia: e se construirmos a arquitetura do aplicativo de tal maneira que:
- O front-end recebido da API não apenas os dados do modelo, mas também a estrutura desses modelos;
- Representações formadas dinamicamente de front-end com base na estrutura dos modelos;
- Qualquer alteração na estrutura da API era exibida automaticamente no front-end.
A implementação de um novo recurso levará muito menos tempo, porque exigirá alterações apenas no lado de back-end, e o front-end coletará automaticamente tudo e o apresentará ao usuário corretamente.
A versatilidade da nova arquitetura
E então decidimos pensar um pouco mais amplamente: a nova arquitetura é adequada apenas para o nosso aplicativo atual ou podemos usá-lo em outro lugar?

De fato, de um jeito ou de outro, quase todos os aplicativos têm parte de uma funcionalidade semelhante:
- quase todos os aplicativos possuem usuários e, nesse sentido, é necessário ter funcionalidades associadas ao registro e autorização do usuário;
- quase todos os aplicativos têm vários tipos de visualizações: existe uma visualização para exibir uma lista de objetos de um modelo, existe uma visualização para exibir um registro detalhado de um único objeto de modelo individual;
- quase todos os modelos têm atributos semelhantes no tipo: dados de seqüência, números etc., e, nesse sentido, você precisa trabalhar com eles no back-end e no front-end.
E como nossa empresa geralmente realiza o desenvolvimento de aplicativos Web personalizados, pensamos: por que precisamos reinventar a roda todas as vezes e desenvolver funcionalidades semelhantes todas as vezes do zero, se você pode escrever uma estrutura uma vez que descreva tudo o básico, comum para muitos aplicativos, coisas e, em seguida, criando um novo projeto, use desenvolvimentos prontos como dependências e, se necessário, altere-os declarativamente em um novo projeto.
Assim, durante uma longa discussão, tivemos a ideia de criar VSTUtils - uma estrutura que:
- Continha a funcionalidade básica, mais semelhante à maioria dos aplicativos;
- Permitido gerar front-end em tempo real, com base na estrutura da API.
Como fazer amigos back-end e front-end?
Bem, então temos que fazer, pensamos. Já tínhamos um back-end, um front-end também, mas nem o servidor nem o cliente tinham uma ferramenta que pudesse relatar ou receber dados sobre a estrutura da API.
Na busca de uma solução para esse problema, nossos olhos se
voltaram para a especificação
OpenAPI , que, com base na descrição dos modelos e nos relacionamentos entre eles, gera um enorme JSON contendo todas essas informações.
E pensamos que, em teoria, ao inicializar o aplicativo no cliente, o front-end pode receber esse JSON da API e criar todas as visualizações necessárias com base. Resta apenas ensinar nosso front-end a fazer tudo isso.
E depois de algum tempo nós o ensinamos.
Versão 1.0 - o que saiu dela
A arquitetura da estrutura VSTUtils das primeiras versões consistia em 3 partes condicionais e era algo assim:
- Back end:
- Django e Python são todos modelos relacionados à lógica. Com base no modelo básico do Django, criamos várias classes de modelos principais do VSTUtils. Todas as ações que esses modelos podem executar foram implementadas usando Python;
- Django REST Framework - geração de API REST. Com base na descrição dos modelos, uma API REST é formada, graças à qual o servidor e o cliente se comunicam;
- Intercamada entre back-end e front-end:
- Geração OpenAPI - JSON com uma descrição da estrutura da API. Depois que todos os modelos foram descritos no backend, são criadas visualizações para eles. A adição de cada uma das visualizações introduz as informações necessárias no JSON resultante:
Exemplo 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 é um mecanismo que analisa um esquema OpenAPI e gera visualizações. Esse mecanismo é iniciado uma vez, quando o aplicativo é inicializado no cliente. Ao enviar uma solicitação para a API, ela recebe o JSON solicitado em resposta com uma descrição da estrutura da API e, analisando-a, cria todos os objetos JS necessários que contêm os parâmetros das representações do modelo. Como a solicitação da API é bastante pesada, nós a armazenamos em cache e solicitamos novamente somente ao atualizar a versão do aplicativo;
- Bibliotecas JavaScript SPA - exibindo visualizações e roteando entre elas. Essas bibliotecas foram escritas por um de nossos desenvolvedores de front-end. Quando um usuário acessa uma página específica, o mecanismo de renderização desenha a página com base nos parâmetros armazenados nos objetos de representação JS.
Assim, o que temos: temos um back-end que descreve toda a lógica associada aos modelos. Em seguida, o OpenAPI entra no jogo, que, com base na descrição do modelo, gera JSON com uma descrição da estrutura da API. Em seguida, o bastão é transmitido ao cliente, que, analisando o OpenAPI JSON gerado, gera automaticamente uma interface da web.
Incorporando recursos no aplicativo à nova arquitetura - como funciona
Lembre-se da tarefa de adicionar ganchos personalizados? Veja como implementá-lo em um aplicativo baseado em VSTUtils:

Agora, graças ao VSTUtils, não precisamos escrever nada do zero. Aqui está o que fazemos para adicionar a capacidade de criar ganchos personalizados:
- No back-end: pegamos e herdamos da classe mais adequada no VSTUtils, adicionamos novas funcionalidades específicas ao novo modelo;
- No front-end:
- se a visualização deste modelo não for diferente da visualização básica do VSTUtils, não faremos nada, tudo será automaticamente exibido corretamente;
- se você precisar, de alguma forma, alterar o comportamento da visualização, usando o mecanismo de sinal, declaramos expandir ou alterar completamente o comportamento básico da visualização.
Como resultado, obtivemos uma solução muito boa, alcançamos nosso objetivo, nosso front-end foi gerado automaticamente. O processo de introdução de novos recursos em projetos existentes acelerou-se notavelmente: lançamentos começaram a ser lançados a cada 2 semanas, enquanto anteriormente lançávamos lançamentos a cada 2-3 meses com um número muito menor de novos recursos. Gostaria de observar que a equipe de desenvolvimento permaneceu a mesma, foi a nova arquitetura de aplicativos que nos deu os frutos.
Versão 1.0 - nossos corações exigem mudança
Mas, como você sabe, não há limite para a perfeição, e o VSTUtils não foi exceção.
Apesar de termos conseguido automatizar a formação do front-end, o resultado não foi a solução direta que originalmente queríamos.
A arquitetura do aplicativo do lado do cliente não foi totalmente pensada e acabou não sendo tão flexível quanto poderia ser:
- o processo de introdução de sobrecargas funcionais nem sempre foi conveniente;
- O mecanismo de análise OpenAPI não era o ideal;
- a renderização de representações e o roteamento entre elas foram realizadas usando bibliotecas auto-escritas, o que também não nos convinha por várias razões:
- Essas bibliotecas não foram cobertas por testes;
- não havia documentação para essas bibliotecas;
- eles não tinham comunidade - no caso de detectar bugs ou sair do funcionário que os escreveu, o suporte a esse código seria muito difícil.
E como em nossa empresa aderimos à abordagem DevOps e tentamos padronizar e formalizar nosso código o máximo possível, em fevereiro deste ano, decidimos realizar uma refatoração global da estrutura de front-end do VSTUtils. Tivemos várias tarefas:
- para formar não apenas classes de apresentação no front-end, mas também classes de modelo - percebemos que seria mais correto separar os dados (e sua estrutura) da apresentação. Além disso, a presença de várias abstrações na forma de uma representação e um modelo facilitaria bastante a adição de sobrecargas da funcionalidade básica em projetos baseados em VSTUtils;
- use uma estrutura testada com uma grande comunidade (Angular, React, Vue) para renderização e roteamento - isso nos permitirá liberar toda a dor de cabeça com suporte ao código relacionado à renderização e roteamento dentro de nosso aplicativo.
Refatoração - escolha da estrutura JS
Entre os frameworks JS mais populares: Angular, React, Vue, nossa escolha ficou com o Vue porque:
- A base de código do Vue pesa menos que React e Angular;
Tabela de comparação de tamanho de estrutura compactada com Gzip
- O processo de renderização da página do Vue leva menos tempo que React e Angular;

- O limite de entrada no Vue é muito menor que no React e Angular;
- Sintaxe nativamente compreensível de modelos;
- Documentação elegante e detalhada disponível em vários idiomas, incluindo russo;
- Um ecossistema desenvolvido que fornece, além da biblioteca principal do Vue, bibliotecas para roteamento e criação de um armazém de dados reativo.
Versão 2.0 - o resultado da refatoração de front-end
O processo de refatoração global do front-end do VSTUtils levou cerca de 4 meses e foi assim que terminamos:

A estrutura front-end do VSTUtils ainda consiste em dois grandes blocos: o primeiro está envolvido na análise do esquema OpenAPI, o segundo está renderizando visualizações e roteamento entre eles, mas esses dois blocos sofreram várias alterações significativas.
O mecanismo que analisa o esquema OpenAPI foi completamente reescrito. A abordagem para analisar esse esquema mudou. Tentamos tornar a arquitetura front-end o mais semelhante possível à arquitetura back-end. Agora, no lado do cliente, não temos apenas uma única abstração na forma de representações, agora também temos abstrações na forma de modelos e QuerySets:
- objetos da classe Model e seus descendentes são objetos correspondentes às abstrações do servidor do Django Models no lado do servidor. Objetos desse tipo contêm dados sobre a estrutura do modelo (nome do modelo, campos do modelo etc.);
- objetos da classe QuerySet e seus descendentes são objetos correspondentes à abstração do Django QuerySets do servidor. Objetos desse tipo contêm métodos que permitem executar solicitações de API (adicionar, modificar, receber, excluir dados de objetos de modelo);
- objetos da classe View - objetos que armazenam dados sobre como representar o modelo em uma página específica, qual modelo usar para "renderizar" a página, quais outras representações dos modelos aos quais esta página pode vincular etc.
A unidade responsável pela renderização e roteamento também mudou significativamente. Abandonamos as bibliotecas JS SPA auto-escritas em favor dos Vue.js. Desenvolvemos nossos próprios componentes Vue que compõem todas as páginas de nosso aplicativo da web. O roteamento entre visualizações é feito usando a biblioteca vue-router, e usamos vuex como o armazenamento reativo do estado do aplicativo.
Também gostaria de observar que, no front-end, a implementação das classes Model, QuerySet e View não depende dos meios de renderização e roteamento, ou seja, se queremos repentinamente mudar do Vue para outra estrutura, por exemplo, React ou algo novo, tudo o que precisamos fazer é reescrever os componentes do Vue nos componentes da nova estrutura, reescrever o roteador, o repositório e isso é tudo - a estrutura do VSTUtils funcionará novamente. A implementação das classes Model, QuerySet e View permanecerá a mesma, pois não depende do Vue.js. Acreditamos que esta é uma ajuda muito boa para possíveis mudanças futuras.
Resumir
Assim, a relutância em escrever código "duplicado" resultou na tarefa de automatizar a formação do front-end de um aplicativo da Web, resolvido com a criação da estrutura do VSTUtils. Conseguimos construir a arquitetura do aplicativo da Web para que o back-end e o front-end se complementem harmoniosamente e qualquer alteração na estrutura da API seja automaticamente captada e exibida corretamente no cliente.
Os benefícios que recebemos da formalização da arquitetura do aplicativo Web:
- As liberações de aplicativos executados com base no VSTUtils começaram a sair duas vezes mais frequentemente. Isso se deve ao fato de que agora, para introduzir um novo recurso, geralmente precisamos adicionar código apenas no back-end, o front-end será gerado automaticamente - isso economiza tempo;
- Atualização simplificada da funcionalidade básica. Como agora toda a funcionalidade básica é montada em uma estrutura, para atualizar algumas dependências importantes ou melhorar a funcionalidade básica, precisamos fazer alterações em apenas um local - na base de código do VSTUtils. Ao atualizar a versão do VSTUtils em projetos filhos, todas as inovações serão selecionadas automaticamente;
- Encontrar novos funcionários ficou mais fácil. Concordo, é muito mais fácil encontrar um desenvolvedor para uma pilha de tecnologia formalizada (Django, Vue) do que procurar uma pessoa que concorda em trabalhar com um gravador desconhecido. Resultados de pesquisa para desenvolvedores que mencionaram Django ou Vue no HeadHunter em seus currículos (em todas as regiões):
- Django - 3.454 currículos foram encontrados para 3.136 candidatos;
- Vue - 4.092 currículos foram encontrados para 3.747 candidatos a emprego.
As desvantagens dessa formalização da arquitetura de um aplicativo Web incluem o seguinte:
- Devido à análise do esquema OpenAPI, a inicialização do aplicativo no cliente demora um pouco mais do que antes (cerca de 20 a 30 milissegundos a mais);
- Indexação de pesquisa sem importância. O fato é que, no momento, não estamos usando a renderização do servidor na estrutura do VSTUtils, e todo o conteúdo do aplicativo é formado na forma final já existente no cliente. Porém, para nossos projetos, geralmente não são necessários resultados altos de pesquisa e, para nós, isso não é tão crítico.
Nisto minha história chega ao fim, muito obrigado pela atenção!
Links úteis