
Eu sempre fiquei interessado em como o Habr é organizado por dentro, como o fluxo de trabalho é construído, como as comunicações são construídas, quais padrões são aplicados e como o código é escrito aqui. Felizmente, essa oportunidade me apareceu, porque recentemente me tornei parte da equipe habrac. Usando o exemplo de uma pequena refatoração da versão para celular, tentarei responder à pergunta: como é trabalhar aqui na frente. No programa: Node, Vue, Vuex e SSR com molho de anotações sobre a experiência pessoal em Habré.
A primeira coisa que você precisa saber sobre a equipe de desenvolvimento é que somos poucos. Poucas são três frentes, duas costas e técnicos de toda a Habr - Bucksley. Claro, também há um testador, designer, três Vadim, uma vassoura milagrosa, um profissional de marketing e outros Bumburums. Mas existem apenas seis colaboradores diretos para os tipos Habra. Isso é bastante raro - um projeto com um público multimilionário que se parece com uma empresa gigantesca de fora é na verdade mais como uma startup aconchegante com a estrutura organizacional mais plana.
Como muitas outras empresas de TI, Habr professa as idéias do Agile, a prática do IC, e isso é tudo. Mas, de acordo com meus sentimentos, Habr, como produto, se desenvolve de forma mais ondulante do que contínua. Portanto, para vários sprints consecutivos, trabalhamos duro para codificar, projetar e redesenhar, quebrar algo e consertar, resolver tickets e iniciar novos, pisar no ancinho e dar um tiro nas pernas para finalmente liberar o recurso para o prod. E então chega uma pausa, um período de reconstrução, o tempo para fazer o que está no quadrante “importante e não urgente”.
Quase um sprint "fora de temporada" será discutido abaixo. Dessa vez, refatorou a versão móvel do Habr. Em geral, a empresa tem grandes esperanças e, no futuro, deve substituir todo o zoológico de encarnações da Habr e se tornar uma solução universal de plataforma cruzada. Algum dia, aparecerá o layout adaptável, o PWA, o modo offline e a personalização do usuário, além de muitas coisas interessantes.
Nós definimos a tarefa
Certa vez, em um stand-up comum, uma das frentes falou sobre problemas na arquitetura do componente de comentário da versão móvel. A partir desta apresentação, organizamos uma micro-reunião no formato de psicoterapia de grupo. Cada um, por sua vez, disse onde dói, tudo foi fixado no papel, simpatizado, compreendido, exceto que ninguém aplaudiu. O resultado foi uma lista de 20 problemas, que deixaram claro que o Habr móvel deve percorrer um longo e espinhoso caminho para o sucesso.
Minha principal preocupação era a eficiência de recursos e o que é chamado de interface suave. Todos os dias, na rota "casa-trabalho-casa", via meu telefone antigo tentando desesperadamente exibir 20 títulos no fluxo. Parecia algo assim:
Interface móvel Habr antes de refatorarO que está acontecendo aqui? Em resumo, o servidor forneceu a página HTML para todos da mesma maneira, independentemente de o usuário estar conectado ou não. Em seguida, o JS do cliente é carregado e solicita novamente os dados necessários, mas com uma alteração para autorização. Na verdade, fizemos o mesmo trabalho duas vezes. A interface tremeluziu e o usuário baixou centenas de kilobytes extras. Em detalhes, tudo parecia ainda mais assustador.
Antigo circuito SSR-CSR. A autorização é possível apenas nos estágios C3 e C4, quando o Nó JS não está ocupado gerando HTML e pode fazer proxy de solicitações de API.Nossa arquitetura da época foi descrita com muita precisão por um dos usuários de Habr:
A versão móvel é uma merda. Eu falo como é. Uma terrível combinação de SSR junto com CSR.
Tínhamos que admitir, por mais triste que seja.
Eu descobri as opções, coloquei um ticket no "Jira" com uma descrição no nível de "Agora é ruim, faça as regras" e, com movimentos largos, decompus a tarefa:
- reutilizar dados
- minimizar o número de redesenhos,
- excluir solicitações duplicadas
- torne o processo de carregamento mais óbvio.
Reutilizar dados
Em teoria, a renderização do lado do servidor foi projetada para resolver dois problemas: não sofrer com as limitações dos mecanismos de pesquisa em relação à
indexação do SPA e melhorar a métrica do
FMP (inevitavelmente piorando o
TTI ). No cenário clássico, que foi finalmente
formulado no Airbnb em 2013 (em Backbone.js), o SSR é o mesmo aplicativo JS isomórfico em execução no ambiente Node. O servidor simplesmente retorna o layout gerado como uma resposta à solicitação. Em seguida, a reidratação ocorre no lado do cliente e tudo funciona sem recarregamentos de página. Para Habr, assim como para muitos outros recursos preenchidos por 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 fluiu no mundo frontend, para muitos desenvolvedores essa idéia ainda é coberta por um véu de sigilo. Não nos afastamos e lançamos um aplicativo Vue com suporte SSR ao produto, com um pequeno detalhe: não lançamos o estado inicial para o cliente.
Porque Não há resposta exata para esta pergunta. Eles não queriam aumentar o tamanho da resposta do servidor ou por causa de vários outros problemas de arquitetura ou simplesmente não decolaram. De uma maneira ou de outra, jogar o estado e reutilizar tudo o que o servidor fez parece ser bastante apropriado e útil. A tarefa é realmente trivial - o
estado simplesmente se injeta 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 surgiram foi a incapacidade de converter estruturas
circulares em JSON; foi resolvido simplesmente substituindo essas estruturas por seus análogos planos.
Além disso, ao lidar com conteúdo UGC, lembre-se de que os dados devem ser convertidos em entidades HTML para não quebrar o HTML. Para esses propósitos, usamos
ele .
Minimizar redesenhos
Como pode ser visto no diagrama acima, no nosso caso, uma instância de Node JS executa duas funções: SSR e "proxy" na API, onde o usuário está autorizado. Essa circunstância impossibilita a autorização no momento da execução do código JS 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 está ocupada com alguma coisa. Acabamos pulando o estado, mas a interface não parou de se mexer, pois os dados no cliente devem ser atualizados levando em consideração a sessão do usuário. Foi necessário ensinar nosso aplicativo 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 JS do nó em duas instâncias separadas.
A primeira solução exigiu o uso de variáveis globais no servidor e a segunda estendeu o tempo necessário para concluir a tarefa em pelo menos um mês.
Como fazer uma escolha? Habr geralmente se move pelo caminho de menor resistência. Informalmente, existe um certo desejo geral de minimizar o ciclo da ideia ao protótipo. O modelo de atitude em relação ao produto lembra um pouco os postulados de booking.com, com a única diferença de que Habr é muito mais sério sobre o feedback do usuário e confia na adoção de tais decisões para você como desenvolvedor.
Seguindo essa lógica e meu próprio desejo de resolver rapidamente o problema, escolhi variáveis globais. E, como isso costuma acontecer, mais cedo ou mais tarde eles têm que pagar por eles. Pagamos quase imediatamente: trabalhamos no fim de semana, captamos as consequências, escrevemos um
post mortem e começamos a dividir o servidor em duas partes. O erro foi muito estúpido, e o bug com a participação dela não foi fácil de reproduzir. E sim, para uma vergonha, mas de alguma forma, tropeçando e grunhindo, meu PoC com variáveis globais ainda entrou em produção e trabalha com bastante sucesso na expectativa de mudar para uma nova arquitetura de "dois dias". Esse foi um passo importante, porque formalmente o objetivo foi alcançado - o SSR aprendeu a fornecer uma página completamente pronta para uso e a interface do usuário ficou muito mais calma.
Interface Habr móvel após o primeiro estágio de refatoraçãoPor fim, a arquitetura SSR-CSR da versão móvel leva a esta imagem:

Esquema SSR-CSR de "dois dias". A API JS do nó está sempre pronta para E / S assíncrona e não é bloqueada pela função SSR, pois a última está em uma instância separada. A cadeia de consulta nº 3 não é necessária.Excluir solicitações duplicadas
Após as manipulações, a renderização inicial da página deixou de provocar epilepsia. Mas o uso posterior do Habr no modo SPA ainda causou perplexidade.
Como o fluxo do usuário é baseado nas transições da
lista de artigos → artigo → comentários e vice-versa, era importante otimizar o consumo de recursos dessa cadeia em primeiro lugar.
Um retorno ao feed de postagem provoca uma nova solicitação de dadosEu não tive que cavar fundo. No screencast acima, pode ser visto que o aplicativo consulta novamente a lista de artigos ao retornar e, durante a solicitação, não vemos o artigo, portanto os dados anteriores desaparecem em algum lugar. Parece que o componente da lista de artigos usa um estado local e o perde ao destruir. De fato, o aplicativo usava o estado global, mas a arquitetura Vuex foi construída na testa: os módulos estão vinculados a páginas, que por sua vez estão vinculadas a rotas. Além disso, todos os módulos são "únicos" - cada visita subsequente à página reescreveu o módulo inteiro:
ArticlesList: [ { Article1 }, ... ], PageArticle: { ArticleFull1 },
No total, tínhamos o módulo
ArticlesList , que contém objetos do tipo
Article e o módulo
PageArticle , que era uma versão estendida do objeto
Article , uma espécie de
ArticleFull . De maneira geral, essa implementação não traz nada de terrível em si mesma - é muito simples, pode-se até dizer ingenuamente, mas é extremamente clara. Se você cortar o zeramento do módulo a cada mudança de rota, poderá até conviver com ele. No entanto, a transição entre feeds de artigos, por exemplo
/ feed → / all , é garantida para jogar fora tudo relacionado ao feed pessoal, já que temos apenas uma
ArticleList na qual colocar novos dados. Isso novamente leva a consultas duplicadas.
Reunindo tudo o que consegui descobrir sobre o assunto, formulei uma nova estrutura de estado e a apresentei aos meus colegas. As discussões foram longas, mas no final, os argumentos "a favor" superaram as dúvidas, e eu comecei a implementação.
A lógica da solução é melhor divulgada em duas etapas. Primeiro, tentamos desatar o módulo Vuex das páginas e vincular diretamente às rotas. Sim, haverá um pouco mais de dados na loja, os getters se tornarão um pouco mais complicados, mas não carregaremos os artigos duas vezes. Para a versão móvel, esse talvez seja o argumento mais forte. Será algo parecido com isto:
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 os dados de um objeto
Article para renderizar uma página de postagem, transformando-a em
ArticleFull ? Nesse caso, seria mais lógico usar essa estrutura:
ArticlesIds: { ROUTE_FEED: [ '1', ... ], ROUTE_ALL: [ '1', '2', ... ], }, ArticlesList: { '1': { Article1 }, '2': { Article2 }, ... }
ArticlesList aqui é apenas algum tipo de repositório de artigos. Todos os artigos que foram carregados durante a sessão do usuário. Nós os tratamos da maneira mais cuidadosa possível, porque esse é um tráfego que pode ter sido carregado por meio da dor em algum lugar do metrô entre estações, e definitivamente não queremos causar ao usuário essa dor novamente, forçando-o a carregar os dados que ele já baixou. O objeto
ArticlesIds é apenas uma matriz de identificadores (como "links") para objetos de
artigo . Essa estrutura permite que você não duplique os dados comuns às rotas e reutilize o objeto
Artigo ao renderizar uma página de postagem mesclando dados estendidos a ela.
A saída da lista de artigos também se tornou mais transparente: o componente iterador itera sobre a matriz com IDs de artigo e desenha o componente teaser do artigo, passando a Id como adereços, e o componente filho, por sua vez, recupera os dados necessários a partir da
ArticlesList . Quando você vai para a página de publicação, obtemos a data existente na Lista de
artigos , solicitamos os dados ausentes e simplesmente a adicionamos ao objeto existente.
Por que essa abordagem é melhor? Como escrevi acima, essa abordagem é mais cuidadosa em relação aos dados baixados e permite reutilizá-los. Além disso, abre caminho para novas oportunidades que se encaixam perfeitamente nessa arquitetura. Por exemplo, polling e upload de artigos para o feed conforme eles aparecem. Podemos simplesmente adicionar novas postagens à “loja” da
ArticlesList , salvar uma lista separada de novos IDs no
ArticlesIds e notificar o usuário sobre isso. Quando você clica no botão "Mostrar novas publicações", simplesmente inserimos um novo ID no início da matriz da lista atual de artigos e tudo funcionará quase que de forma mágica.
Tornando o download mais agradável
A cereja no bolo da refatoração era o conceito de esqueletos, o que torna o processo de download de conteúdo na Internet lenta um pouco menos nojento. Não houve discussões sobre esse assunto, a jornada da idéia ao protótipo levou literalmente duas horas. O design foi desenhado quase por nós mesmos, e ensinamos nossos componentes a renderizar blocos div despretensiosos e quase tremeluzentes enquanto aguardamos os dados. Subjetivamente, essa abordagem de carregamento realmente reduz a quantidade de hormônios do estresse no corpo do usuário. O esqueleto fica assim:
HabraloadingRefletir
Trabalho em Habré há seis meses e os amigos ainda perguntam: bem, como você gosta? Bom, confortável - sim. Mas há algo que distingue esse trabalho dos outros. Trabalhei em equipes que eram completamente indiferentes ao seu produto, não sabiam e não entendiam quem eram seus usuários. Mas aqui tudo é diferente. Aqui você se sente responsável pelo que está fazendo. No processo de desenvolvimento de um recurso, você se torna parcialmente seu fornecedor, participa de todas as reuniões de produtos relacionadas à sua funcionalidade, faz sugestões e toma você mesmo. Fazer um produto que você usa diariamente é muito legal, e escrever código para pessoas que podem ser melhores nisso é apenas uma sensação incrível (sem sarcasmo).
Após o lançamento de todas essas alterações, recebemos um feedback positivo e foi muito, muito bom. É inspirador. Obrigada Escreva mais.
Deixe-me lembrá-lo que, depois das variáveis globais, decidimos mudar a arquitetura e separar a camada proxy em uma instância separada. A arquitetura de "dois dias" já chegou ao lançamento 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. Ficarei feliz em responder a todas as suas perguntas nos comentários.