Logs do desenvolvedor front-end Habr: refatoração e reflexão

Logs do desenvolvedor front-end Habr: refatoração e reflexão

Sempre me interessei em saber como o Habr é estruturado por dentro, como o fluxo de trabalho é estruturado, como as comunicações são estruturadas, quais padrões são usados ​​e como o código geralmente é escrito aqui. Felizmente tive essa oportunidade, pois recentemente passei a fazer parte da equipe habra. Usando o exemplo de uma pequena refatoração da versão mobile, tentarei responder à pergunta: como é trabalhar aqui na frente. No programa: Node, Vue, Vuex e SSR com molho de notas sobre experiência pessoal em Habr.

A primeira coisa que você precisa saber sobre a equipe de desenvolvimento é que somos poucos. Não o suficiente - são três frentes, duas costas e o líder técnico de todos Habr - Baxley. Há, claro, também um testador, um designer, três Vadim, uma vassoura milagrosa, um especialista em marketing e outros Bumburums. Mas existem apenas seis contribuintes diretos para as fontes de Habr. Isso é bastante raro - um projeto com um público multimilionário, que visto de fora parece uma empresa gigante, na realidade parece mais uma startup aconchegante com a estrutura organizacional mais plana possível.

Como muitas outras empresas de TI, a Habr professa ideias ágeis, práticas de CI e isso é tudo. Mas, na minha opinião, o produto Habr está se desenvolvendo mais em ondas do que continuamente. Então, por vários sprints seguidos, codificamos algo diligentemente, projetamos e redesenhamos, quebramos algo e consertamos, resolvemos tickets e criamos novos, pisamos em um ancinho e damos um tiro nos pés, para finalmente liberar o recurso em Produção. E então chega uma certa calmaria, um período de redesenvolvimento, hora de fazer o que está no quadrante “importante-não urgente”.

É justamente esse sprint “fora de temporada” que será discutido a seguir. Desta vez incluiu uma refatoração da versão móvel do Habr. Em geral, a empresa tem grandes esperanças nisso e, no futuro, deverá substituir todo o zoológico das encarnações de Habr e se tornar uma solução universal multiplataforma. Algum dia haverá layout adaptativo, PWA, modo offline, personalização do usuário e muitas outras coisas interessantes.

Vamos definir a tarefa

Certa vez, em um stand-up comum, um dos frontais falou sobre problemas na arquitetura do componente de comentários da versão mobile. Pensando nisso, organizamos um microencontro em formato de psicoterapia de grupo. Todos se revezaram dizendo onde doeu, registraram tudo no papel, se solidarizaram, entenderam, só que ninguém bateu palmas. O resultado foi uma lista de 20 problemas, que deixou claro que o Habr móvel ainda tinha um longo e espinhoso caminho para o sucesso.

Eu estava principalmente preocupado com a eficiência do uso de recursos e com o que é chamado de interface suave. Todos os dias, no trajeto casa-trabalho-casa, eu via meu telefone antigo tentando desesperadamente exibir 20 manchetes no feed. Parecia algo assim:

Logs do desenvolvedor front-end Habr: refatoração e reflexãoInterface Mobile Habr antes da refatoração

O que está acontecendo aqui? Resumindo, o servidor servia a página HTML para todos da mesma forma, independentemente de o usuário estar logado ou não. Em seguida, o JS do cliente é carregado e solicita novamente os dados necessários, mas ajustados para autorização. Ou seja, na verdade fizemos o mesmo trabalho duas vezes. A interface piscou e o usuário baixou uns bons cem quilobytes extras. Nos detalhes tudo parecia ainda mais assustador.

Logs do desenvolvedor front-end Habr: refatoração e reflexãoAntigo esquema SSR-CSR. A autorização só é possível nos estágios C3 e C4, quando o Node JS não está ocupado gerando HTML e pode fazer proxy de solicitações para a API.

Nossa arquitetura daquela época foi descrita com muita precisão por um dos usuários do Habr:

A versão mobile é uma porcaria. Estou contando como é. Uma terrível combinação de SSR e CSR.

Tínhamos que admitir, por mais triste que fosse.

Avaliei as opções, criei um ticket no Jira com uma descrição no nível “está ruim agora, faça certo” e decompus a tarefa em traços gerais:

  • reutilizar dados,
  • minimizar o número de redesenhos,
  • eliminar solicitações duplicadas,
  • tornar o processo de carregamento mais óbvio.

Vamos reutilizar os dados

Em teoria, a renderização no lado do servidor foi projetada para resolver dois problemas: não sofrer com as limitações dos mecanismos de busca em termos de Indexação de SPA e melhorar a métrica FMP (inevitavelmente piorando TTI). Num cenário clássico que finalmente formulado no Airbnb em 2013 ano (ainda em Backbone.js), SSR é o mesmo aplicativo JS isomórfico em execução no ambiente Node. O servidor simplesmente envia o layout gerado como resposta à solicitação. Aí ocorre a reidratação do lado do cliente e tudo funciona sem recarregamento de página. Para Habr, como para muitos outros recursos com conteúdo de texto, a renderização do servidor é um elemento crítico na construção de relações amigáveis ​​com os mecanismos de busca.

Apesar do fato de que mais de seis anos se passaram desde o advento da tecnologia, e durante esse tempo muita água realmente passou por baixo da ponte no mundo front-end, para muitos desenvolvedores essa ideia ainda está envolta em segredo. Não ficamos de lado e lançamos uma aplicação Vue com suporte SSR para produção, faltando um pequeno detalhe: não enviamos o estado inicial ao cliente.

Por que? Não há uma resposta exata para esta pergunta. Ou eles não queriam aumentar o tamanho da resposta do servidor, ou por causa de vários outros problemas arquitetônicos, ou simplesmente não decolou. De uma forma ou de outra, descartar o estado e reutilizar tudo o que o servidor fez parece bastante apropriado e útil. A tarefa é realmente trivial - estado é simplesmente injetado no contexto de execução, e o Vue o adiciona automaticamente ao layout gerado como uma variável global: window.__INITIAL_STATE__.

Um dos problemas que surgiu é a incapacidade de converter estruturas cíclicas em JSON (referencia circular); foi resolvido simplesmente substituindo essas estruturas por suas contrapartes planas.

Além disso, ao lidar com conteúdo UGC, lembre-se que os dados devem ser convertidos em entidades HTML para não quebrar o HTML. Para esses fins usamos he.

Minimizando redesenhos

Como você pode ver no diagrama acima, em nosso caso, uma instância do Node JS executa duas funções: SSR e “proxy” na API, onde ocorre a autorização do usuário. Essa circunstância impossibilita a autorização enquanto o código JS estiver em execução no servidor, pois o nó é de thread único e a função SSR é síncrona. Ou seja, o servidor simplesmente não pode enviar solicitações para si mesmo enquanto a pilha de chamadas estiver ocupada com alguma coisa. Acontece que atualizamos o estado, mas a interface não parava de tremer, pois os dados do cliente precisavam ser atualizados levando em consideração a sessão do usuário. Precisávamos ensinar nossa aplicação a colocar os dados corretos no estado inicial, levando em consideração o login do usuário.

Havia apenas duas soluções para o problema:

  • anexar dados de autorização a solicitações entre servidores;
  • divida as camadas do Node JS em duas instâncias separadas.

A primeira solução exigia a utilização de variáveis ​​globais no servidor, e a segunda estendeu o prazo para conclusão da tarefa em pelo menos um mês.

Como fazer uma escolha? Habr frequentemente segue o caminho de menor resistência. Informalmente, existe um desejo geral de reduzir ao mínimo o ciclo da ideia ao protótipo. O modelo de atitude em relação ao produto lembra um pouco os postulados do booking.com, com a única diferença de que Habr leva muito mais a sério o feedback do usuário e confia em você, como desenvolvedor, para tomar tais decisões.

Seguindo essa lógica e meu próprio desejo de resolver o problema rapidamente, optei por variáveis ​​globais. E, como sempre acontece, você terá que pagar por eles mais cedo ou mais tarde. Pagamos quase imediatamente: trabalhamos no fim de semana, esclarecemos as consequências, escrevemos post mortem e comecei a dividir o servidor em duas partes. O erro foi muito estúpido e o bug que o envolveu não foi fácil de reproduzir. E sim, é uma pena isso, mas de uma forma ou de outra, tropeçando e gemendo, meu PoC com variáveis ​​globais entrou em produção e está funcionando com bastante sucesso enquanto aguarda a mudança para uma nova arquitetura de “dois nós”. Este foi um passo importante, pois formalmente o objetivo foi alcançado - o SSR aprendeu a entregar uma página totalmente pronta para uso e a UI ficou muito mais tranquila.

Logs do desenvolvedor front-end Habr: refatoração e reflexãoInterface Mobile Habr após a primeira etapa de refatoração

Em última análise, a arquitetura SSR-CSR da versão móvel leva a esta imagem:

Logs do desenvolvedor front-end Habr: refatoração e reflexãoCircuito SSR-CSR de “dois nós”. A API Node JS está sempre pronta para E/S assíncrona e não é bloqueada pela função SSR, pois esta está localizada em uma instância separada. A cadeia de consulta nº 3 não é necessária.

Eliminando solicitações duplicadas

Depois de realizadas as manipulações, a renderização inicial da página não provocava mais epilepsia. Mas o uso posterior do Habr no modo SPA ainda causou confusão.

Como a base do fluxo do usuário são as transições do formulário lista de artigos → artigo → comentários e vice-versa, era importante, em primeiro lugar, otimizar o consumo de recursos desta cadeia.

Logs do desenvolvedor front-end Habr: refatoração e reflexãoVoltar ao feed de postagem provoca uma nova solicitação de dados

Não houve necessidade de cavar fundo. No screencast acima você pode ver que o aplicativo solicita novamente a lista de artigos ao deslizar para trás, e durante a solicitação não vemos os artigos, o que significa que os dados anteriores desaparecem em algum lugar. Parece que o componente da lista de artigos usa um estado local e o perde ao ser destruído. Na verdade, a aplicação utilizava um estado global, mas a arquitetura Vuex foi construída de frente: os módulos estão vinculados às páginas, que por sua vez estão vinculadas às rotas. Além disso, todos os módulos são “descartáveis” - cada visita subsequente à página reescreveu todo o módulo:

ArticlesList: [
  { Article1 },
  ...
],
PageArticle: { ArticleFull1 },

No total, tivemos um módulo Lista de artigos, que contém objetos do tipo Artigo e módulo Artigo da página, que era uma versão estendida do objeto Artigo, tipo de Artigo Completo. Em geral, esta implementação não traz nada de terrível em si mesma - é muito simples, pode-se até dizer ingênua, mas extremamente compreensível. Se você redefinir o módulo toda vez que alterar a rota, poderá até conviver com ele. No entanto, mover-se entre feeds de artigos, por exemplo /feed → /todos, tem a garantia de jogar fora tudo relacionado ao feed pessoal, já que só temos um Lista de artigos, no qual você precisa colocar novos dados. Isso novamente nos leva à duplicação de solicitações.

Depois de reunir tudo o que consegui desenterrar sobre o tema, formulei uma nova estrutura estatal e apresentei-a aos meus colegas. As discussões foram demoradas, mas no final os argumentos a favor superaram as dúvidas e iniciei a implementação.

A lógica de uma solução é melhor revelada em duas etapas. Primeiro tentamos desacoplar o módulo Vuex das páginas e vincular diretamente às rotas. Sim, haverá um pouco mais de dados na loja, os getters ficarão um pouco mais complexos, mas não carregaremos os artigos duas vezes. Para a versão mobile, este talvez seja o argumento mais forte. Vai parecer algo assim:

ArticlesList: {
  ROUTE_FEED: [ 
    { Article1 },
    ...
  ],
  ROUTE_ALL: [ 
    { Article2 },
    ...
  ],
}

Mas e se as listas de artigos puderem se sobrepor entre várias rotas e se quisermos reutilizar dados de objetos Artigo para renderizar a página de postagem, transformando-a em Artigo Completo? Neste caso, seria mais lógico utilizar tal estrutura:

ArticlesIds: {
  ROUTE_FEED: [ '1', ... ],
  ROUTE_ALL: [ '1', '2', ... ],
},
ArticlesList: {
  '1': { Article1 }, 
  '2': { Article2 },
  ...
}

Lista de artigos aqui é apenas uma espécie de repositório de artigos. Todos os artigos que foram baixados durante a sessão do usuário. Nós os tratamos com o máximo cuidado, pois se trata de um tráfego que pode ter sido baixado através de dor em algum lugar do metrô entre as estações, e definitivamente não queremos causar esse sofrimento novamente ao usuário, forçando-o a carregar dados que ele já possui. baixado. Um objeto ArtigosIds é simplesmente uma matriz de IDs (como se fossem “links”) para objetos Artigo. Esta estrutura permite evitar a duplicação de dados comuns às rotas e reutilizar o objeto Artigo ao renderizar uma página de postagem mesclando dados estendidos nela.

A saída da lista de artigos também se tornou mais transparente: o componente iterador percorre o array com os IDs dos artigos e desenha o componente teaser do artigo, passando o Id como um suporte, e o componente filho, por sua vez, recupera os dados necessários de Lista de artigos. Quando você vai para a página de publicação, obtemos a data já existente de Lista de artigos, fazemos uma solicitação para obter os dados ausentes e simplesmente os adicionamos ao objeto existente.

Por que essa abordagem é melhor? Como escrevi acima, essa abordagem é mais suave em relação aos dados baixados e permite reutilizá-los. Mas, além disso, abre caminho para algumas novas possibilidades que se encaixam perfeitamente em tal arquitetura. Por exemplo, pesquisar e carregar artigos no feed conforme eles aparecem. Podemos simplesmente colocar as últimas postagens em um “armazenamento” Lista de artigos, salve uma lista separada de novos IDs em ArtigosIds e notificar o usuário sobre isso. Ao clicar no botão “Mostrar novas publicações”, simplesmente inseriremos novos Ids no início do array da lista atual de artigos e tudo funcionará quase que magicamente.

Tornando o download mais agradável

A cereja do bolo da refatoração é o conceito de esqueletos, que torna o processo de download de conteúdo em uma Internet lenta um pouco menos nojento. Não houve discussões sobre esse assunto: o caminho da ideia ao protótipo levou literalmente duas horas. O design praticamente se desenhou sozinho e ensinamos nossos componentes a renderizar blocos div simples e quase imperceptíveis enquanto aguardamos pelos dados. Subjetivamente, esta abordagem de carga na verdade reduz a quantidade de hormônios do estresse no corpo do usuário. O esqueleto fica assim:

Logs do desenvolvedor front-end Habr: refatoração e reflexão
Habraloading

Refletindo

Trabalho em Habré há seis meses e meus amigos ainda perguntam: bem, você gosta de lá? Ok, confortável - sim. Mas há algo que torna este trabalho diferente dos outros. Trabalhei em equipes que eram completamente indiferentes ao seu produto, não sabiam nem entendiam quem eram seus usuários. Mas aqui tudo é diferente. Aqui você se sente responsável pelo que faz. No processo de desenvolvimento de uma feature, você se torna parcialmente seu proprietário, participa de todas as reuniões de produto relacionadas à sua funcionalidade, faz sugestões e toma decisões por conta própria. Criar um produto que você usa todos os dias é muito legal, mas escrever código para pessoas que provavelmente são melhores nisso do que você é uma sensação incrível (sem sarcasmo).

Após o lançamento de todas essas mudanças, recebemos um feedback positivo e foi muito, muito bom. É inspirador. Obrigado! Escreva mais.

Deixe-me lembrá-lo de que, após as variáveis ​​globais, decidimos mudar a arquitetura e alocar a camada proxy em uma instância separada. A arquitetura de “dois nós” já foi lançada na forma de testes beta públicos. Agora qualquer um pode mudar para ele e nos ajudar a melhorar o Habr móvel. Isso é tudo por hoje. Terei prazer em responder todas as suas perguntas nos comentários.

Fonte: habr.com

Adicionar um comentário