Rexistros de desenvolvedores front-end de Habr: refactorización e reflexión

Rexistros de desenvolvedores front-end de Habr: refactorización e reflexión

Sempre me interesou como se estrutura Habr desde dentro, como se estrutura o fluxo de traballo, como se estruturan as comunicacións, que estándares se usan e como se escribe xeralmente o código aquí. Afortunadamente, tiven esa oportunidade, porque recentemente entrei a formar parte do equipo de habra. Usando o exemplo dunha pequena refactorización da versión móbil, tentarei responder á pregunta: como é traballar aquí á fronte. No programa: Node, Vue, Vuex e SSR con salsa de notas sobre a experiencia persoal en Habr.

O primeiro que debes saber sobre o equipo de desenvolvemento é que somos poucos. Non é suficiente - son tres dianteiros, dous atrás e o liderado técnico de todos os Habr - Baxley. Tamén hai, por suposto, un probador, un deseñador, tres Vadim, unha vasoira milagre, un especialista en mercadotecnia e outros Bumburums. Pero só hai seis colaboradores directos das fontes de Habr. Isto é bastante raro: un proxecto cunha audiencia multimillonaria, que desde fóra parece unha empresa xigante, en realidade parece máis unha startup acolledora coa estrutura organizativa máis plana posible.

Como moitas outras empresas de TI, Habr profesa ideas áxiles, prácticas de CI e iso é todo. Pero segundo os meus sentimentos, Habr como produto está a desenvolverse máis en ondas que continuamente. Entón, durante varios sprints seguidos, codificamos algo con dilixencia, deseñamos e redeseñamos, rompemos algo e arranxámolo, resolvemos tickets e creamos outros novos, pisamos un rastrillo e disparámonos nos pés, para finalmente lanzar a función en produción. E despois chega unha certa calma, un período de reurbanización, tempo de facer o que está no cuadrante "importante-non urxente".

É precisamente este sprint "fora de tempada" o que se comentará a continuación. Nesta ocasión incluíu unha refactorización da versión móbil de Habr. En xeral, a compañía ten moitas esperanzas niso e, no futuro, debería substituír todo o zoolóxico das encarnacións de Habr e converterse nunha solución multiplataforma universal. Algún día haberá deseño adaptativo, PWA, modo sen conexión, personalización do usuario e moitas outras cousas interesantes.

Imos poñer a tarefa

Unha vez, nun stand-up ordinario, un dos fronte falou de problemas na arquitectura do compoñente de comentarios da versión móbil. Con isto en mente, organizamos un micro encontro en formato de psicoterapia de grupo. Todos por quendas dicían onde doía, gravaban todo en papel, simpatizaban, entendían, agás que ninguén aplaudiu. O resultado foi unha lista de 20 problemas, que deixou claro que Habr móbil aínda tiña un longo e espiñento camiño cara ao éxito.

Preocupábame principalmente a eficiencia do uso dos recursos e o que se chama unha interface fluida. Todos os días, na ruta casa-traballo-casa, vía o meu vello teléfono tentando desesperadamente mostrar 20 titulares no feed. Parecía algo así:

Rexistros de desenvolvedores front-end de Habr: refactorización e reflexiónInterface Mobile Habr antes da refactorización

Que está pasando aquí? En resumo, o servidor serviu a páxina HTML a todos do mesmo xeito, independentemente de se o usuario estaba conectado ou non. A continuación, cárgase o cliente JS e solicita de novo os datos necesarios, pero axustado para a autorización. É dicir, en realidade fixemos o mesmo traballo dúas veces. A interface parpadeou e o usuario descargou cen kilobytes adicionais. En detalle todo parecía aínda máis arrepiante.

Rexistros de desenvolvedores front-end de Habr: refactorización e reflexiónAntigo esquema SSR-CSR. A autorización só é posible nas etapas C3 e C4, cando Node JS non está ocupado xerando HTML e pode enviar solicitudes de proxy á API.

A nosa arquitectura daquela época foi descrita con moita precisión por un dos usuarios de Habr:

A versión móbil é unha merda. Dígoo como é. Unha terrible combinación de SSR e RSE.

Tiñamos que admitilo, por moi triste que fose.

Avaliei as opcións, creei un ticket en Jira cunha descrición a nivel de "agora está mal, faino ben" e descompuxín a tarefa a grandes rasgos:

  • reutilizar datos,
  • minimizar o número de redeseños,
  • eliminar solicitudes duplicadas,
  • facer o proceso de carga máis evidente.

Reutilicemos os datos

En teoría, a representación no servidor está deseñada para resolver dous problemas: non sufrir limitacións do motor de busca en termos de Indización SPA e mellorar a métrica FMP (inevitablemente empeorando TTI). Nun escenario clásico que finalmente formulado en Airbnb en 2013 ano (aínda en Backbone.js), SSR é a mesma aplicación JS isomórfica que se executa no ambiente Node. O servidor simplemente envía o deseño xerado como resposta á solicitude. Despois, a rehidratación prodúcese no lado do cliente e, a continuación, todo funciona sen recargas de páxina. Para Habr, como para moitos outros recursos con contido de texto, a representación do servidor é un elemento crítico para construír relacións amigables cos buscadores.

A pesar de que pasaron máis de seis anos desde a aparición da tecnoloxía, e durante este tempo moita auga voou debaixo da ponte no mundo front-end, para moitos desenvolvedores esta idea aínda está envolta en segredo. Non nos deixamos de lado e lanzamos unha aplicación Vue con soporte SSR para a produción, faltando un pequeno detalle: non enviamos o estado inicial ao cliente.

Por que? Non hai unha resposta exacta a esta pregunta. Ou non querían aumentar o tamaño da resposta do servidor, ou por mor doutros problemas arquitectónicos, ou simplemente non despegou. Dun xeito ou doutro, tirar o estado e reutilizar todo o que fixo o servidor parece bastante apropiado e útil. A tarefa é realmente trivial - o estado é simplemente inxectado no contexto de execución, e Vue engádeo automaticamente ao deseño xerado como unha variable global: window.__INITIAL_STATE__.

Un dos problemas que xurdiu é a incapacidade de converter estruturas cíclicas en JSON (referencia circular); resolveuse simplemente substituíndo tales estruturas polas súas contrapartes planas.

Ademais, ao tratar con contido UGC, debes lembrar que os datos deben converterse en entidades HTML para non romper o HTML. Para estes fins utilizamos he.

Minimización de redeseños

Como podes ver no diagrama anterior, no noso caso, unha instancia de Node JS realiza dúas funcións: SSR e "proxy" na API, onde se produce a autorización do usuario. Esta circunstancia fai imposible autorizar mentres o código JS se está a executar no servidor, xa que o nodo ten un único fío e a función SSR é sincrónica. É dicir, o servidor simplemente non pode enviar solicitudes a si mesmo mentres a pila de chamadas está ocupada con algo. Resultou que actualizamos o estado, pero a interface non deixou de moverse, xa que os datos do cliente debían actualizarse tendo en conta a sesión do usuario. Necesitabamos ensinar á nosa aplicación a poñer os datos correctos no estado inicial, tendo en conta o inicio de sesión do usuario.

Só había dúas solucións ao problema:

  • anexar datos de autorización ás solicitudes entre servidores;
  • Divida as capas de Node JS en dúas instancias separadas.

A primeira solución requiría o uso de variables globais no servidor, e a segunda ampliaba o prazo para completar a tarefa polo menos un mes.

Como facer unha elección? Habr adoita moverse polo camiño de menor resistencia. Informalmente, hai un desexo xeral de reducir ao mínimo o ciclo da idea ao prototipo. O modelo de actitude cara ao produto lembra un pouco aos postulados de booking.com, coa única diferenza de que Habr toma moito máis en serio os comentarios dos usuarios e confía en ti, como desenvolvedor, para que tomes esas decisións.

Seguindo esta lóxica e o meu propio desexo de resolver rapidamente o problema, escollín variables globais. E, como adoita suceder, hai que pagalos tarde ou cedo. Pagamos case de inmediato: traballamos a fin de semana, aclaramos as consecuencias, escribimos post mortem e comezou a dividir o servidor en dúas partes. O erro foi moi estúpido, e o erro que o implicaba non era doado de reproducir. E si, é unha mágoa por isto, pero dun xeito ou doutro, tropezando e xemendo, o meu PoC con variables globais entrou en produción e está a funcionar con bastante éxito mentres agardaba o paso a unha nova arquitectura de "dous nodos". Este foi un paso importante, porque formalmente o obxectivo foi alcanzado: SSR aprendeu a entregar unha páxina completamente lista para usar e a IU tornouse moito máis tranquila.

Rexistros de desenvolvedores front-end de Habr: refactorización e reflexiónInterface Mobile Habr despois da primeira fase de refactorización

En definitiva, a arquitectura SSR-CSR da versión móbil leva a esta imaxe:

Rexistros de desenvolvedores front-end de Habr: refactorización e reflexiónCircuíto SSR-CSR de "dous nodos". A API Node JS sempre está lista para E/S asíncrona e non está bloqueada pola función SSR, xa que esta última está situada nunha instancia separada. A cadea de consulta número 3 non é necesaria.

Eliminación de solicitudes duplicadas

Despois de realizar as manipulacións, a representación inicial da páxina xa non provocaba epilepsia. Pero o uso posterior de Habr no modo SPA aínda causou confusión.

Xa que a base do fluxo de usuarios son as transicións do formulario lista de artigos → artigo → comentarios e viceversa, foi importante optimizar o consumo de recursos desta cadea en primeiro lugar.

Rexistros de desenvolvedores front-end de Habr: refactorización e reflexiónVolver ao feed de publicacións provoca unha nova solicitude de datos

Non había necesidade de afondar. No screencast anterior podes ver que a aplicación volve solicitar a lista de artigos ao pasar o dedo cara atrás e durante a solicitude non vemos os artigos, o que significa que os datos anteriores desaparecen nalgún lugar. Parece que o compoñente da lista de artigos usa un estado local e pérdeo ao destruír. De feito, a aplicación utilizaba un estado global, pero a arquitectura Vuex construíuse de forma frontal: os módulos están ligados a páxinas, que á súa vez están vinculadas a rutas. Ademais, todos os módulos son "desbotables": cada visita posterior á páxina reescribía todo o módulo:

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

En total, tivemos un módulo Lista de artigos, que contén obxectos de tipo Artigo e módulo PáxinaArtigo, que era unha versión estendida do obxecto Artigo, tipo Artigo completo. En xeral, esta implementación non leva nada terrible en si mesma: é moi sinxelo, ata pódese dicir inxenuo, pero moi comprensible. Se restableces o módulo cada vez que cambias a ruta, incluso podes vivir con el. Non obstante, moverse entre fontes de artigos, por exemplo /feed → /todos, está garantido para tirar todo o relacionado co feed persoal, xa que só temos un Lista de artigos, no que cómpre poñer novos datos. Isto volve a levarnos á duplicación de solicitudes.

Despois de recoller todo o que puiden desenterrar sobre o tema, formulei unha nova estrutura estatal e presenteina aos meus compañeiros. As discusións foron longas, pero ao final os argumentos a favor superaron as dúbidas e comecei a implementar.

A lóxica dunha solución é mellor revelada en dous pasos. Primeiro tentamos desvincular o módulo Vuex das páxinas e vincular directamente ás rutas. Si, haberá un pouco máis de datos na tenda, os getters serán un pouco máis complexos, pero non cargaremos artigos dúas veces. Para a versión móbil, este é quizais o argumento máis forte. Será algo así:

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

Pero que pasa se as listas de artigos poden solaparse entre varias rutas e que pasa se queremos reutilizar os datos de obxectos Artigo para renderizar a páxina da publicación, converténdoa en Artigo completo? Neste caso, sería máis lóxico usar tal estrutura:

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

Lista de artigos aquí só é unha especie de repositorio de artigos. Todos os artigos que se descargaron durante a sesión do usuario. Tratámolos co máximo coidado, porque se trata de tráfico que puido ser descargado pola dor nalgún lugar do metro entre estacións, e definitivamente non queremos volver causarlle esta dor ao usuario obrigándoo a cargar datos que xa ten. descargado. Un obxecto ArtigosIds é simplemente unha matriz de ID (como se "enlazase") a obxectos Artigo. Esta estrutura permite evitar duplicar datos comúns ás rutas e reutilizar o obxecto Artigo ao renderizar unha páxina de publicación combinando datos estendidos nela.

A saída da lista de artigos tamén se fixo máis transparente: o compoñente iterador itera a través da matriz con ID de artigo e debuxa o compoñente teaser do artigo, pasando o Id como un accesorio, e o compoñente fillo, á súa vez, recupera os datos necesarios de Lista de artigos. Cando accedes á páxina de publicación, obtemos a data xa existente de Lista de artigos, realizamos unha solicitude para obter os datos que faltan e simplemente engadímolos ao obxecto existente.

Por que é mellor este enfoque? Como escribín anteriormente, este enfoque é máis suave con respecto aos datos descargados e permíteche reutilizalos. Pero ademais disto, abre o camiño a algunhas novas posibilidades que encaixan perfectamente a tal arquitectura. Por exemplo, sondeando e cargando artigos no feed tal e como aparecen. Podemos simplemente poñer as últimas publicacións nun "almacenamento" Lista de artigos, garda unha lista separada de novos ID ArtigosIds e notificarlle ao usuario. Cando facemos clic no botón "Mostrar novas publicacións", simplemente inseriremos novos ID ao comezo da matriz da lista actual de artigos e todo funcionará case de xeito máxico.

Facendo a descarga máis agradable

A guinda do bolo de refactorización é o concepto de esqueletos, que fai que o proceso de descarga de contidos nunha Internet lenta sexa un pouco menos noxento. Non houbo discusións sobre este asunto; o camiño da idea ao prototipo levou literalmente dúas horas. O deseño practicamente debuxou por si mesmo e ensinamos aos nosos compoñentes a renderizar bloques div sinxelos e apenas parpadeantes mentres agardamos polos datos. Subxectivamente, este enfoque para a carga reduce realmente a cantidade de hormonas do estrés no corpo do usuario. O esqueleto ten o seguinte aspecto:

Rexistros de desenvolvedores front-end de Habr: refactorización e reflexión
Habraloading

Reflexionando

Levo seis meses traballando en Habré e os meus amigos aínda me preguntan: bueno, que che parece aí? Está ben, cómodo - si. Pero hai algo que fai que este traballo sexa diferente dos outros. Traballei en equipos totalmente indiferentes ao seu produto, que non sabían nin entendían quen eran os seus usuarios. Pero aquí todo é diferente. Aquí séntese responsable do que fas. No proceso de desenvolvemento dunha función, convértete parcialmente no seu propietario, participas en todas as reunións de produtos relacionadas coa túa funcionalidade, fai suxestións e toma decisións ti mesmo. Crear un produto que uses todos os días é moi xenial, pero escribir código para persoas que probablemente sexan mellores ca ti é unha sensación incrible (sen sarcasmo).

Despois do lanzamento de todos estes cambios, recibimos comentarios positivos, e foi moi, moi agradable. É inspirador. Grazas! Escribe máis.

Permíteme lembrarche que despois das variables globais decidimos cambiar a arquitectura e asignar a capa proxy nunha instancia separada. A arquitectura de "dous nodos" xa chegou ao lanzamento en forma de probas beta públicas. Agora calquera pode cambiar a el e axudarnos a mellorar Habr móbil. Iso é todo por hoxe. Estarei encantado de responder a todas as túas preguntas nos comentarios.

Fonte: www.habr.com

Engadir un comentario