Registros de desarrollador front-end de Habr: refactorización y reflexión

Registros de desarrollador front-end de Habr: refactorización y reflexión

Siempre me ha interesado cómo se estructura Habr desde dentro, cómo se estructura el flujo de trabajo, cómo se estructuran las comunicaciones, qué estándares se utilizan y cómo se escribe el código aquí en general. Afortunadamente, tuve esa oportunidad porque recientemente me uní al equipo de habra. Usando el ejemplo de una pequeña refactorización de la versión móvil, intentaré responder la pregunta: ¿cómo es trabajar aquí en el frente? En el programa: Node, Vue, Vuex y SSR con salsa de notas sobre experiencia personal en Habr.

Lo primero que debes saber sobre el equipo de desarrollo es que somos pocos. No es suficiente: son tres delanteros, dos traseros y el líder técnico de todos los Habr: Baxley. Por supuesto, también hay un probador, un diseñador, tres Vadim, una escoba milagrosa, un especialista en marketing y otros Bumburums. Pero sólo hay seis contribuyentes directos a las fuentes de Habr. Esto es bastante raro: un proyecto con una audiencia multimillonaria, que desde fuera parece una empresa gigante, en realidad parece más bien una acogedora startup con la estructura organizativa más plana posible.

Como muchas otras empresas de TI, Habr profesa ideas ágiles, prácticas de CI y eso es todo. Pero en mi opinión, Habr como producto se está desarrollando más en oleadas que de forma continua. Entonces, durante varios sprints seguidos, codificamos algo diligentemente, diseñamos y rediseñamos, rompemos algo y lo arreglamos, solucionamos tickets y creamos otros nuevos, pisamos un rastrillo y nos disparamos en los pies, para finalmente lanzar la característica en producción. Y luego llega una cierta pausa, un período de reurbanización, tiempo para hacer lo que está en el cuadrante "importante-no urgente".

Es precisamente este sprint “fuera de temporada” el que se analizará a continuación. Esta vez incluyó una refactorización de la versión móvil de Habr. En general, la compañía tiene grandes esperanzas en él y en el futuro debería reemplazar todo el zoológico de encarnaciones de Habr y convertirse en una solución multiplataforma universal. Algún día habrá diseño adaptativo, PWA, modo fuera de línea, personalización del usuario y muchas otras cosas interesantes.

Establezcamos la tarea

Una vez, en una reunión ordinaria, uno de los frontales habló de problemas en la arquitectura del componente de comentarios de la versión móvil. Teniendo esto en cuenta, organizamos un microencuentro en formato de psicoterapia de grupo. Todos se turnaron para decir dónde les dolía, anotaron todo en un papel, se solidarizaron, entendieron, pero nadie aplaudió. El resultado fue una lista de 20 problemas, que dejaron claro que Habr móvil todavía tiene un camino largo y espinoso hacia el éxito.

Lo que más me preocupaba era la eficiencia del uso de los recursos y lo que se llama una interfaz fluida. Todos los días, en la ruta casa-trabajo-casa, veía mi viejo teléfono tratando desesperadamente de mostrar 20 titulares en el feed. Se parecía a esto:

Registros de desarrollador front-end de Habr: refactorización y reflexiónInterfaz móvil de Habr antes de la refactorización

¿Que está pasando aqui? En resumen, el servidor entregó la página HTML a todos de la misma manera, independientemente de si el usuario inició sesión o no. Luego se carga el cliente JS y solicita nuevamente los datos necesarios, pero ajustados para la autorización. Es decir, en realidad hicimos el mismo trabajo dos veces. La interfaz parpadeó y el usuario descargó unos cien kilobytes extra. En detalle todo parecía aún más espeluznante.

Registros de desarrollador front-end de Habr: refactorización y reflexiónAntiguo esquema RSS-RSE. La autorización solo es posible en las etapas C3 y C4, cuando Node JS no está ocupado generando HTML y puede enviar solicitudes a la API.

Nuestra arquitectura de esa época fue descrita con mucha precisión por uno de los usuarios de Habr:

La versión móvil es una mierda. Lo digo como es. Una terrible combinación de RSS y RSE.

Teníamos que admitirlo, por muy triste que fuera.

Evalué las opciones, creé un ticket en Jira con una descripción en el nivel "ahora está mal, hazlo bien" y descompuse la tarea a grandes rasgos:

  • reutilizar datos,
  • minimizar el número de redibujos,
  • eliminar solicitudes duplicadas,
  • hacer que el proceso de carga sea más obvio.

Reutilicemos los datos

En teoría, el renderizado del lado del servidor está diseñado para resolver dos problemas: no sufrir las limitaciones de los motores de búsqueda en términos de indexación de SPA y mejorar la métrica FMP (inevitablemente empeorando TTI). En un escenario clásico que finalmente formulado en Airbnb en 2013 año (todavía en Backbone.js), SSR es la misma aplicación JS isomórfica que se ejecuta en el entorno Node. El servidor simplemente envía el diseño generado como respuesta a la solicitud. Luego se produce la rehidratación en el lado del cliente y luego todo funciona sin recargas de página. Para Habr, como para muchos otros recursos con contenido de texto, la representación del servidor es un elemento crítico en la construcción de relaciones amistosas con los motores de búsqueda.

A pesar de que han pasado más de seis años desde la aparición de la tecnología, y durante este tiempo ha corrido mucha agua bajo el puente en el mundo front-end, para muchos desarrolladores esta idea todavía está envuelta en un velo de secreto. No nos quedamos al margen y lanzamos a producción una aplicación Vue con soporte SSR, olvidándonos de un pequeño detalle: no enviamos el estado inicial al cliente.

¿Por qué? No hay una respuesta exacta a esta pregunta. O no querían aumentar el tamaño de la respuesta del servidor, o debido a muchos otros problemas arquitectónicos, o simplemente no despegó. De una forma u otra, descartar el estado y reutilizar todo lo que hizo el servidor parece bastante apropiado y útil. La tarea es realmente trivial. El estado simplemente se inyecta. en el contexto de ejecución, y Vue lo agrega automáticamente al diseño generado como una variable global: window.__INITIAL_STATE__.

Uno de los problemas que ha surgido es la imposibilidad de convertir estructuras cíclicas a JSON (referencia circular); se resolvió simplemente reemplazando dichas estructuras con sus contrapartes planas.

Además, cuando se trata de contenido UGC, se debe recordar que los datos deben convertirse a entidades HTML para no romper el HTML. Para estos fines utilizamos he.

Minimizar redibujos

Como puede ver en el diagrama anterior, en nuestro caso, una instancia de Node JS realiza dos funciones: SSR y "proxy" en la API, donde se produce la autorización del usuario. Esta circunstancia imposibilita la autorización mientras el código JS se ejecuta en el servidor, ya que el nodo es de un solo subproceso y la función SSR es síncrona. Es decir, el servidor simplemente no puede enviarse solicitudes a sí mismo mientras la pila de llamadas está ocupada con algo. Resultó que actualizamos el estado, pero la interfaz no dejó de temblar, ya que los datos del cliente tuvieron que actualizarse teniendo en cuenta la sesión del usuario. Necesitábamos enseñar a nuestra aplicación a poner los datos correctos en el estado inicial, teniendo en cuenta el inicio de sesión del usuario.

Sólo había dos soluciones al problema:

  • adjuntar datos de autorización a solicitudes entre servidores;
  • divida las capas de Node JS en dos instancias separadas.

La primera solución requirió el uso de variables globales en el servidor y la segunda extendió el plazo para completar la tarea en al menos un mes.

¿Cómo hacer una elección? Habr a menudo avanza por el camino de menor resistencia. Informalmente, existe un deseo general de reducir al mínimo el ciclo de la idea al prototipo. El modelo de actitud hacia el producto recuerda un poco a los postulados de booking.com, con la única diferencia de que Habr se toma mucho más en serio los comentarios de los usuarios y confía en usted, como desarrollador, para tomar esas decisiones.

Siguiendo esta lógica y mi propio deseo de resolver rápidamente el problema, elegí variables globales. Y, como suele ocurrir, tarde o temprano tendrás que pagar por ellos. Pagamos casi de inmediato: trabajamos el fin de semana, aclaramos las consecuencias, escribió autopsia y comenzó a dividir el servidor en dos partes. El error fue muy estúpido y el error que lo involucraba no fue fácil de reproducir. Y sí, es una pena, pero de una forma u otra, tropezando y gimiendo, mi PoC con variables globales entró en producción y está funcionando con bastante éxito mientras espera el cambio a una nueva arquitectura de "dos nodos". Este fue un paso importante, porque formalmente se logró el objetivo: SSR aprendió a entregar una página completamente lista para usar y la interfaz de usuario se volvió mucho más tranquila.

Registros de desarrollador front-end de Habr: refactorización y reflexiónInterfaz móvil Habr después de la primera etapa de refactorización

En última instancia, la arquitectura SSR-CSR de la versión móvil conduce a esta imagen:

Registros de desarrollador front-end de Habr: refactorización y reflexiónCircuito SSR-CSR de “dos nodos”. La API de Node JS siempre está lista para E/S asíncronas y no está bloqueada por la función SSR, ya que esta última se encuentra en una instancia separada. La cadena de consulta n.º 3 no es necesaria.

Eliminando solicitudes duplicadas

Una vez realizadas las manipulaciones, la representación inicial de la página ya no provocó epilepsia. Pero el uso posterior de Habr en modo SPA aún causó confusión.

Dado que la base del flujo de usuarios son las transiciones de la forma lista de artículos → artículo → comentarios y viceversa, era importante optimizar el consumo de recursos de esta cadena en primer lugar.

Registros de desarrollador front-end de Habr: refactorización y reflexiónVolver al feed de publicaciones provoca una nueva solicitud de datos

No había necesidad de profundizar más. En el screencast de arriba puedes ver que la aplicación vuelve a solicitar la lista de artículos al deslizar el dedo hacia atrás, y durante la solicitud no vemos los artículos, lo que significa que los datos anteriores desaparecen en alguna parte. Parece que el componente de lista de artículos usa un estado local y lo pierde al destruirlo. De hecho, la aplicación utilizó un estado global, pero la arquitectura Vuex se construyó de frente: los módulos están vinculados a páginas, que a su vez están vinculadas a rutas. Además, todos los módulos son "desechables": cada visita posterior a la página reescribe el módulo completo:

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

En total, teníamos un módulo. Lista de artículos, que contiene objetos de tipo Artículo y módulo PáginaArtículo, que era una versión extendida del objeto Artículo, un poco Artículo completo. En general, esta implementación no conlleva nada terrible: es muy simple, incluso se podría decir ingenua, pero extremadamente comprensible. Si reinicia el módulo cada vez que cambia la ruta, incluso podrá vivir con él. Sin embargo, moverse entre fuentes de artículos, por ejemplo /feed → /todos, está garantizado que tiraremos a la basura todo lo relacionado con el feed personal, ya que solo tenemos uno Lista de artículos, en el que necesitas poner nuevos datos. Esto nos lleva nuevamente a la duplicación de solicitudes.

Habiendo reunido todo lo que pude desenterrar sobre el tema, formulé una nueva estructura estatal y se la presenté a mis colegas. Las discusiones fueron largas, pero al final los argumentos a favor superaron las dudas y comencé la implementación.

La lógica de una solución se revela mejor en dos pasos. Primero intentamos desacoplar el módulo Vuex de las páginas y vincularlo directamente a las rutas. Sí, habrá un poco más de datos en la tienda, los captadores se volverán un poco más complejos, pero no cargaremos artículos dos veces. Para la versión móvil, este es quizás el argumento más fuerte. Se verá algo como esto:

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

Pero, ¿qué pasa si las listas de artículos pueden superponerse entre múltiples rutas y si queremos reutilizar datos de objetos? Artículo para renderizar la página de publicación, convirtiéndola en Artículo completo? En este caso, sería más lógico utilizar esta estructura:

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

Lista de artículos aquí es sólo una especie de depósito de artículos. Todos los artículos que se descargaron durante la sesión del usuario. Los tratamos con sumo cuidado, porque se trata de tráfico que puede haber sido descargado a través de un dolor en algún lugar del metro entre las estaciones, y definitivamente no queremos causarle este dolor al usuario nuevamente obligándolo a cargar datos que ya tiene. descargado. Un objeto ArtículosIds es simplemente una serie de ID (como si fueran "enlaces") a objetos Artículo. Esta estructura le permite evitar duplicar datos comunes a las rutas y reutilizar el objeto. Artículo al representar una página de publicación fusionando datos extendidos en ella.

La salida de la lista de artículos también se ha vuelto más transparente: el componente iterador recorre en iteración la matriz con los ID de los artículos y dibuja el componente teaser del artículo, pasando el Id como accesorio, y el componente secundario, a su vez, recupera los datos necesarios de Lista de artículos. Cuando vas a la página de publicación, obtenemos la fecha ya existente de Lista de artículos, hacemos una solicitud para obtener los datos que faltan y simplemente los agregamos al objeto existente.

¿Por qué es mejor este enfoque? Como escribí anteriormente, este enfoque es más cuidadoso con los datos descargados y le permite reutilizarlos. Pero además de esto, abre el camino a nuevas posibilidades que encajan perfectamente en dicha arquitectura. Por ejemplo, sondear y cargar artículos en el feed a medida que aparecen. Simplemente podemos poner las últimas publicaciones en un "almacenamiento". Lista de artículos, guarde una lista separada de nuevos ID en ArtículosIds y notificar al usuario al respecto. Cuando hacemos clic en el botón "Mostrar nuevas publicaciones", simplemente insertaremos nuevos ID al comienzo de la matriz de la lista actual de artículos y todo funcionará casi mágicamente.

Hacer que la descarga sea más agradable

La guinda del pastel de refactorización es el concepto de esqueletos, que hace que el proceso de descarga de contenido en una Internet lenta sea un poco menos repugnante. No hubo discusiones sobre este tema; el camino desde la idea hasta el prototipo tomó literalmente dos horas. El diseño prácticamente se dibujó solo y enseñamos a nuestros componentes a renderizar bloques div simples que apenas parpadeaban mientras esperábamos datos. Subjetivamente, este enfoque de carga en realidad reduce la cantidad de hormonas del estrés en el cuerpo del usuario. El esqueleto se ve así:

Registros de desarrollador front-end de Habr: refactorización y reflexión
habracargando

Reflexionando

Llevo seis meses trabajando en Habré y mis amigos todavía me preguntan: bueno, ¿te gusta estar allí? Está bien, cómodo, sí. Pero hay algo que hace que este trabajo sea diferente a otros. Trabajé en equipos que eran completamente indiferentes a su producto, no sabían ni entendían quiénes eran sus usuarios. Pero aquí todo es diferente. Aquí te sientes responsable de lo que haces. En el proceso de desarrollo de una función, usted se convierte parcialmente en su propietario, participa en todas las reuniones del producto relacionadas con su funcionalidad, hace sugerencias y toma decisiones usted mismo. Hacer un producto que usas todos los días es genial, pero escribir código para personas que probablemente sean mejores que tú en eso es simplemente una sensación increíble (sin sarcasmo).

Después del lanzamiento de todos estos cambios, recibimos comentarios positivos y fueron muy, muy agradables. Es inspirador. ¡Gracias! Escribe más.

Permítanme recordarles que después de las variables globales decidimos cambiar la arquitectura y asignar la capa de proxy a una instancia separada. La arquitectura de “dos nodos” ya se lanzó en forma de prueba beta pública. Ahora cualquiera puede cambiarlo y ayudarnos a mejorar Habr móvil. Eso es todo por hoy. Estaré encantado de responder a todas tus preguntas en los comentarios.

Fuente: habr.com

Añadir un comentario