
Siempre me interesó cómo se organiza el Habr desde adentro, cómo se construye el flujo de trabajo, cómo se construyen las comunicaciones, qué estándares se aplican y cómo se escribe el código aquí. Afortunadamente, tal oportunidad se me apareció, porque recientemente me convertí en parte del comando habra. Usando el ejemplo de una pequeña refactorización de la versión móvil, trataré de 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 debe saber sobre el equipo de desarrollo es que somos pocos. Pocos son tres frentes, dos respaldos y técnicas de todo Habr - Bucksley. Por supuesto, también hay un probador, diseñador, tres Vadim, una escoba milagrosa, un vendedor y otros Bumburums. Pero solo hay seis contribuyentes directos al género Habra. Esto es bastante raro: un proyecto con una audiencia multimillonaria que parece una empresa gigantesca desde el exterior es en realidad más como una startup acogedora con la estructura organizativa más plana.
Al igual que muchas otras empresas de TI, Habr profesa las ideas de Agile, la práctica de CI y eso es todo. Pero según mis sentimientos, Habr, como producto, se desarrolla de manera más ondulante que continua. Entonces, durante varios sprints seguidos, estamos trabajando arduamente para codificar, diseñar y rediseñar, romper algo y arreglar, resolver boletos y comenzar nuevos, pisar el rastrillo y dispararnos en las piernas para finalmente lanzar la función en producción. Y luego viene una pausa, un período de reurbanización, el tiempo para hacer lo que está en el cuadrante "importante-no urgente".
A continuación se tratará sobre un sprint fuera de temporada. Esta vez consiguió refactorizar la versión móvil de Habr. En general, la compañía tiene grandes esperanzas y, en el futuro, debería reemplazar todo el zoológico de encarnación Habr y convertirse en una solución universal multiplataforma. Algún día aparecerá un diseño adaptativo y PWA, y el modo fuera de línea, y la personalización del usuario, y muchas cosas interesantes.
Nosotros establecemos la tarea
Una vez, en un stand-up ordinario, uno de los frentes habló sobre problemas en la arquitectura del componente de comentarios de la versión móvil. A partir de esta presentación, organizamos una micro reunión en el formato de psicoterapia grupal. A su vez, cada uno decía dónde tenía dolor, todo estaba arreglado en papel, simpatizaba, entendía, excepto que nadie aplaudía. El resultado fue una lista de 20 problemas, lo que dejó en claro que el Habr móvil debe recorrer un camino largo y espinoso hacia el éxito.
Mi principal preocupación era la eficiencia de los recursos y lo que se llama una interfaz fluida. Todos los días, en la ruta "hogar-trabajo-hogar", veía mi viejo teléfono tratando desesperadamente de mostrar 20 títulos en la secuencia. Se parecía a esto:
Interfaz móvil Habr antes de refactorizar¿Qué está pasando aquí? En resumen, el servidor proporcionó la página HTML a todos de la misma manera, independientemente de si el usuario inició sesión o no. Luego, el cliente JS se carga y nuevamente solicita los datos necesarios, pero con una enmienda para la autorización. Es decir, de hecho, hicimos el mismo trabajo dos veces. La interfaz parpadeó y el usuario descargó unos cien kilobytes adicionales. En detalles, todo parecía aún más espeluznante.
Antiguo circuito SSR-CSR. La autorización solo es posible en las etapas C3 y C4, cuando el Nodo JS no está ocupado generando HTML y puede proxy de solicitudes de 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. Yo hablo tal como es. Una terrible combinación de SSR junto con CSR.
Tuvimos que admitirlo, por triste que sea.
Descubrí las opciones, me puse un boleto en el "Jira" con una descripción en el nivel de "Ahora está mal, haz las reglas" y con trazos amplios descompuse la tarea:
- reutilizar datos
- minimizar el número de redibujos,
- excluir solicitudes duplicadas
- hacer que el proceso de carga sea más obvio.
Reutilizar datos
En teoría, la representación del lado del servidor está diseñada para resolver dos problemas: no sufrir las limitaciones de los motores de búsqueda con respecto a la
indexación de SPA y mejorar la métrica de
FMP (inevitablemente empeoramiento de
TTI ). En el escenario clásico, que finalmente se
formuló en Airbnb en 2013 (en Backbone.js), SSR es la misma aplicación JS isomorfa que se ejecuta en el entorno Node. El servidor simplemente devuelve 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 recargar la página. Para Habr, así como para muchos otros recursos llenos de texto, la representación del servidor es un elemento crítico en la construcción de relaciones amigables con los motores de búsqueda.
A pesar del hecho de que han pasado más de seis años desde el advenimiento de la tecnología, y durante este tiempo, realmente ha fluido mucha agua en el mundo frontend, para muchos desarrolladores esta idea todavía está cubierta por un velo de secreto. No nos hicimos a un lado y lanzamos una aplicación Vue con soporte SSR para el producto, sin un pequeño detalle: no lanzamos 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 un montón de otros problemas arquitectónicos, o simplemente no despegaron. De una forma u otra, arrojar estado y reutilizar todo lo que hizo el servidor parece ser 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 surgió fue la incapacidad de convertir estructuras
circulares a JSON; se resolvió simplemente reemplazando tales estructuras con sus análogos planos.
Además, al tratar con contenido UGC, recuerde que los datos deben convertirse a entidades HTML para no romper el HTML. Para estos fines usamos
él .
Minimizar redibujos
Como se puede ver en el diagrama anterior, en nuestro caso, una instancia de Node JS realiza dos funciones: SSR y "proxy" en la API, donde el usuario está autorizado. Esta circunstancia hace que la autorización sea imposible en el momento de la ejecución del código JS en el servidor, ya que el nodo tiene 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 omitimos el estado, pero la interfaz no dejó de contraerse, ya que los datos del cliente deberían actualizarse teniendo en cuenta la sesión del usuario. Era necesario 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.
Solo había dos soluciones al problema:
- para aferrar datos de autorización a solicitudes entre servidores;
- Divida las capas de Nodo JS en dos instancias separadas.
La primera solución requirió el uso de variables globales en el servidor, y la segunda extendió el tiempo necesario para completar la tarea al menos un mes.
¿Cómo hacer una elección? Habr a menudo se mueve por el camino de menor resistencia. Informalmente, existe un cierto deseo general de minimizar 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 es mucho más serio acerca de los comentarios de los usuarios y confía en la adopción de tales decisiones como desarrollador.
Siguiendo esta lógica y mi propio deseo de resolver rápidamente el problema, elegí variables globales. Y, como esto sucede a menudo, tarde o temprano tienen que pagar por ellos. Pagamos casi de inmediato: trabajamos el fin de semana, recogimos las consecuencias, escribimos un
post mortem y comenzamos a dividir el servidor en dos partes. El error fue muy estúpido, y el error con su participación no fue fácil de reproducir. Y sí, por vergüenza, pero de alguna manera, tropezando y gruñendo, mi PoC con variables globales todavía entró en producción y funciona con bastante éxito en previsión de pasar a una nueva arquitectura de "dos días". Este fue un paso importante, porque formalmente se logró el objetivo: el SSR aprendió a dar una página que estaba completamente lista para usar, y la interfaz de usuario se volvió mucho más tranquila.
Interfaz móvil Habr después de la primera etapa de refactorizaciónFinalmente, la arquitectura de la versión móvil SSR-CSR lleva a esta imagen:

Esquema SSR-CSR de "dos días". Nodo JS API siempre está listo para E / S asíncronas y no está bloqueado por la función SSR, ya que este último se encuentra en una instancia separada. La cadena de consulta # 3 no es necesaria.Excluir solicitudes duplicadas
Después de las manipulaciones, la presentación inicial de la página dejó de provocar epilepsia. Pero el uso posterior de Habr en el modo SPA todavía causó desconcierto.
Dado que el flujo de usuarios se basa en las transiciones de la
lista de artículos → artículo → comentarios y viceversa, fue importante optimizar el consumo de recursos de esta cadena en primer lugar.
Un regreso al feed posterior provoca una nueva solicitud de datosNo tuve que cavar profundo. En el screencast anterior, se puede ver que la aplicación vuelve a consultar la lista de artículos cuando se desliza hacia atrás, y durante la solicitud no vemos el artículo, por lo que los datos anteriores desaparecen en algún lugar. Parece que el componente de la lista de artículos usa un estado local y lo pierde al destruir. De hecho, la aplicación utilizaba el estado global, pero la arquitectura Vuex se creó en la 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 "únicos": cada visita posterior a la página reescribió todo el módulo:
ArticlesList: [ { Article1 }, ... ], PageArticle: { ArticleFull1 },
En total, teníamos el módulo
ArticlesList , que contiene objetos del tipo
Article y el módulo
PageArticle , que era una versión extendida del objeto
Article , una especie de
ArticleFull . En general, esta implementación no conlleva nada terrible en sí misma: es muy simple, incluso se podría decir ingenuamente, pero es extremadamente clara. Si recorta la puesta a cero del módulo con cada cambio de la ruta, incluso puede vivir con él. Sin embargo, la transición entre feeds de artículos, por ejemplo
/ feed → / all , garantiza descartar todo lo relacionado con el feed personal, ya que solo tenemos una lista de
artículos en la que colocar nuevos datos. Esto nuevamente conduce a consultas duplicadas.
Al reunir todo lo que logré descubrir 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 la solución se revela mejor en dos etapas. Primero, intentamos desatar 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 complicados, pero no cargaremos los artículos dos veces. Para la versión móvil, este es quizás el argumento más fuerte. Se verá más o menos así:
ArticlesList: { ROUTE_FEED: [ { Article1 }, ... ], ROUTE_ALL: [ { Article2 }, ... ], }
Pero, ¿qué sucede si las listas de artículos pueden superponerse entre múltiples rutas, y qué sucede si queremos reutilizar los datos de un objeto
Artículo para representar una página de publicación, convirtiéndola en
Artículo Completo ? En este caso, sería más lógico utilizar dicha estructura:
ArticlesIds: { ROUTE_FEED: [ '1', ... ], ROUTE_ALL: [ '1', '2', ... ], }, ArticlesList: { '1': { Article1 }, '2': { Article2 }, ... }
La lista de artículos aquí es solo una especie de repositorio de artículos. Todos los artículos que se cargaron durante la sesión del usuario. Los tratamos con el mayor cuidado posible, porque este es el tráfico que puede haber sido cargado a través del dolor en algún lugar del metro entre estaciones, y definitivamente no queremos causarle al usuario este dolor nuevamente, forzándolo a cargar los datos que ya ha descargado. El objeto
ArticlesIds es solo una matriz de identificadores (como "enlaces") para objetos
Article . Esta estructura le permite no duplicar los datos comunes a las rutas y reutilizar el objeto
Artículo al representar una página de publicación al fusionar datos extendidos en ella.
El resultado de la lista de artículos también se ha vuelto más transparente: el componente iterador itera sobre la matriz con ID de artículo y dibuja el componente teaser del artículo, pasando Id como accesorios, y el componente hijo a su vez recupera los datos necesarios de la
Lista de artículos . Cuando va a la página de publicación, obtenemos la fecha existente de la lista de
artículos , solicitamos 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 en relación con los datos descargados y le permite reutilizarlos. Pero además de esto, abre el camino a nuevas oportunidades que encajan perfectamente en dicha arquitectura. Por ejemplo, sondear y subir artículos al feed tal como aparecen. Simplemente podemos agregar nuevas publicaciones a la "tienda" de
ArticlesList , guardar una lista separada de nuevos ID en
ArticlesIds y notificar al usuario sobre esto. Cuando hace clic en el botón "Mostrar nuevas publicaciones", simplemente insertamos un nuevo Id al comienzo de la matriz de la lista actual de artículos y todo funcionará casi mágicamente.
Hacer la descarga más agradable
La guinda del pastel de refactorización fue el concepto de esqueletos, lo que hace que el proceso de descarga de contenido en Internet sea un poco menos desagradable. No hubo discusiones sobre este tema, el viaje de la idea al prototipo tomó literalmente dos horas. El diseño fue dibujado casi por nosotros mismos, y enseñamos a nuestros componentes cómo renderizar bloques div sin pretensiones, apenas parpadeantes mientras esperamos datos. Subjetivamente, este enfoque de carga realmente reduce la cantidad de hormonas del estrés en el cuerpo del usuario. El esqueleto se ve así:
HabraloadingReflexionar
Llevo seis meses trabajando en Habré y mis amigos todavía me preguntan: bueno, ¿qué te parece? Bien, cómodo, sí. Pero hay algo que distingue este trabajo de los demás. Trabajé en equipos que eran completamente indiferentes a su producto, no sabían y no entendían quiénes eran sus usuarios. Pero aquí todo es diferente. Aquí te sientes responsable de lo que estás haciendo. En el proceso de desarrollo de una función, se convierte parcialmente en su propietario, participa en todas las reuniones de productos relacionadas con su funcionalidad, hace sugerencias y toma decisiones usted mismo. Hacer un producto que usa usted mismo a diario es muy bueno, y escribir código para las personas que pueden ser mejores es simplemente una sensación increíble (sin sarcasmo).
Después del lanzamiento de todos estos cambios, recibimos una respuesta positiva, y fue muy, muy agradable. Es inspirador. Gracias Escribe más
Permítame recordarle que después de las variables globales, decidimos cambiar la arquitectura y separar la capa proxy en una instancia separada. La arquitectura de "dos días" ya ha llegado al lanzamiento en forma de pruebas beta públicas. Ahora cualquiera puede cambiar y ayudarnos a mejorar el Habr móvil. Eso es todo por hoy. Estaré encantado de responder todas sus preguntas en los comentarios.