Journaux des développeurs front-end Habr : refactorisation et réflexion

Journaux des développeurs front-end Habr : refactorisation et réflexion

J'ai toujours été intéressé par la façon dont Habr est structuré de l'intérieur, comment le flux de travail est structuré, comment les communications sont structurées, quelles normes sont utilisées et comment le code est généralement écrit ici. Heureusement, j'ai eu une telle opportunité, car je suis récemment devenu membre de l'équipe Habra. En prenant l'exemple d'une petite refactorisation de la version mobile, je vais essayer de répondre à la question : qu'est-ce que ça fait de travailler ici au front. Au programme : Node, Vue, Vuex et SSR avec à la sauce des notes sur l'expérience personnelle dans Habr.

La première chose qu’il faut savoir sur l’équipe de développement, c’est que nous sommes peu nombreux. Pas assez - ce sont trois fronts, deux arrières et la avance technique de tous Habr - Baxley. Il y a bien sûr aussi un testeur, un designer, trois Vadim, un balai miracle, un spécialiste du marketing et d'autres Bumburums. Mais il n’y a que six contributeurs directs aux sources de Habr. C'est assez rare : un projet avec un public de plusieurs millions de dollars, qui de l'extérieur ressemble à une entreprise géante, ressemble en réalité davantage à une startup confortable avec la structure organisationnelle la plus plate possible.

Comme beaucoup d’autres sociétés informatiques, Habr professe des idées Agile, des pratiques CI, et c’est tout. Mais selon mon ressenti, Habr en tant que produit se développe plus par vagues que de manière continue. Ainsi, pendant plusieurs sprints d'affilée, nous codons diligemment quelque chose, concevons et redessinons, cassons quelque chose et le réparons, résolvons des tickets et en créons de nouveaux, montons sur un râteau et nous tirons une balle dans les pieds, afin de enfin publier la fonctionnalité dans production. Et puis vient une certaine accalmie, une période de réaménagement, le temps de faire ce qui est dans le quadrant « important-pas urgent ».

C’est précisément de ce sprint « hors saison » qui sera évoqué ci-dessous. Cette fois, cela incluait une refactorisation de la version mobile de Habr. En général, la société fonde de grands espoirs en cela et, à l’avenir, elle devrait remplacer l’ensemble du zoo des incarnations de Habr et devenir une solution multiplateforme universelle. Un jour, il y aura une mise en page adaptative, un PWA, un mode hors ligne, une personnalisation par l'utilisateur et bien d'autres choses intéressantes.

Fixons la tâche

Un jour, lors d'un stand-up ordinaire, l'un des membres du front a parlé de problèmes dans l'architecture du composant commentaires de la version mobile. Dans cette optique, nous avons organisé une micro-réunion sous forme de psychothérapie de groupe. Tout le monde disait à tour de rôle où ça faisait mal, ils notaient tout sur papier, ils sympathisaient, ils comprenaient, sauf que personne n'applaudissait. Le résultat fut une liste de 20 problèmes, qui montrait clairement que le Habr mobile avait encore un chemin long et épineux vers le succès.

J'étais principalement préoccupé par l'efficacité de l'utilisation des ressources et par ce qu'on appelle une interface fluide. Chaque jour, sur le trajet domicile-travail-domicile, je voyais mon ancien téléphone essayer désespérément d'afficher 20 titres dans le flux. Cela ressemblait à ceci :

Journaux des développeurs front-end Habr : refactorisation et réflexionInterface Mobile Habr avant la refactorisation

Que se passe t-il ici? En bref, le serveur a servi la page HTML à tout le monde de la même manière, que l'utilisateur soit connecté ou non. Ensuite, le client JS est chargé et demande à nouveau les données nécessaires, mais ajustées pour l'autorisation. Autrement dit, nous avons fait le même travail deux fois. L'interface a clignoté et l'utilisateur a téléchargé une bonne centaine de kilo-octets supplémentaires. Dans le détail, tout semblait encore plus effrayant.

Journaux des développeurs front-end Habr : refactorisation et réflexionAncien schéma RSS-RSE. L'autorisation n'est possible qu'aux étapes C3 et C4, lorsque Node JS n'est pas occupé à générer du HTML et peut envoyer des requêtes par proxy à l'API.

Notre architecture de cette époque a été décrite très précisément par l'un des utilisateurs de Habr :

La version mobile est de la merde. Je le dis tel quel. Une terrible combinaison de RSS et de RSE.

Nous avons dû l'admettre, même si c'était triste.

J'ai évalué les options, créé un ticket dans Jira avec une description du niveau « c'est mauvais maintenant, faites-le bien » et décomposé la tâche en grandes lignes :

  • réutiliser les données,
  • minimiser le nombre de redessins,
  • éliminer les demandes en double,
  • rendre le processus de chargement plus évident.

Réutilisons les données

En théorie, le rendu côté serveur est conçu pour résoudre deux problèmes : ne pas souffrir des limitations des moteurs de recherche en termes de Indexation SPA et améliorer la métrique FMP (inévitablement aggravation TTI). Dans un scénario classique qui finalement formulé chez Airbnb en 2013 année (toujours sur Backbone.js), SSR est la même application JS isomorphe exécutée dans l'environnement Node. Le serveur envoie simplement la mise en page générée en réponse à la demande. Ensuite, la réhydratation se produit côté client, puis tout fonctionne sans rechargement de page. Pour Habr, comme pour de nombreuses autres ressources à contenu textuel, le rendu du serveur est un élément essentiel pour établir des relations amicales avec les moteurs de recherche.

Malgré le fait que plus de six ans se sont écoulés depuis l'avènement de la technologie et que pendant ce temps beaucoup d'eau a vraiment coulé sous les ponts dans le monde front-end, pour de nombreux développeurs, cette idée est encore entourée de secret. Nous ne sommes pas restés à l'écart et avons déployé une application Vue avec support SSR en production, il manquait un petit détail : nous n'avons pas envoyé l'état initial au client.

Pourquoi? Il n’y a pas de réponse exacte à cette question. Soit ils ne voulaient pas augmenter la taille de la réponse du serveur, soit à cause de nombreux autres problèmes architecturaux, soit cela n’a tout simplement pas décollé. D'une manière ou d'une autre, supprimer l'état et réutiliser tout ce que le serveur a fait semble tout à fait approprié et utile. La tâche est en réalité triviale - l'état est simplement injecté dans le contexte d'exécution, et Vue l'ajoute automatiquement à la mise en page générée en tant que variable globale : window.__INITIAL_STATE__.

L'un des problèmes apparus est l'incapacité de convertir les structures cycliques en JSON (référence circulaire); a été résolu en remplaçant simplement ces structures par leurs homologues plats.

De plus, lorsque vous traitez du contenu UGC, vous devez vous rappeler que les données doivent être converties en entités HTML afin de ne pas casser le code HTML. À ces fins, nous utilisons he.

Minimiser les redessins

Comme vous pouvez le voir sur le diagramme ci-dessus, dans notre cas, une instance Node JS remplit deux fonctions : SSR et « proxy » dans l'API, où l'autorisation de l'utilisateur se produit. Cette circonstance rend impossible l'autorisation pendant l'exécution du code JS sur le serveur, car le nœud est monothread et la fonction SSR est synchrone. Autrement dit, le serveur ne peut tout simplement pas s'envoyer des requêtes pendant que la pile d'appels est occupée par quelque chose. Il s'est avéré que nous avons mis à jour l'état, mais l'interface n'a pas cessé de trembler, puisque les données sur le client devaient être mises à jour en tenant compte de la session utilisateur. Nous devions apprendre à notre application à mettre les bonnes données dans l’état initial, en tenant compte du login de l’utilisateur.

Il n'y avait que deux solutions au problème :

  • joindre des données d'autorisation aux demandes inter-serveurs ;
  • divisez les couches Node JS en deux instances distinctes.

La première solution nécessitait l'utilisation de variables globales sur le serveur, et la seconde prolongeait d'au moins un mois le délai de réalisation de la tâche.

Comment faire un choix ? Habr emprunte souvent la voie de la moindre résistance. De manière informelle, il existe une volonté générale de réduire au minimum le cycle allant de l’idée au prototype. Le modèle d'attitude envers le produit rappelle quelque peu les postulats de booking.com, la seule différence étant que Habr prend beaucoup plus au sérieux les commentaires des utilisateurs et vous fait confiance, en tant que développeur, pour prendre de telles décisions.

Suivant cette logique et ma propre volonté de résoudre rapidement le problème, j'ai choisi des variables globales. Et comme cela arrive souvent, il faudra les payer tôt ou tard. Nous avons payé presque immédiatement : nous avons travaillé le week-end, réglé les conséquences, a écrit post-mortem et a commencé à diviser le serveur en deux parties. L'erreur était très stupide et le bug qui l'impliquait n'était pas facile à reproduire. Et oui, c'est dommage pour cela, mais d'une manière ou d'une autre, trébuchant et gémissant, mon PoC avec des variables globales est néanmoins entré en production et fonctionne assez bien en attendant le passage à une nouvelle architecture « à deux nœuds ». Il s'agissait d'une étape importante, car formellement, l'objectif était atteint : SSR a appris à fournir une page entièrement prête à l'emploi et l'interface utilisateur est devenue beaucoup plus calme.

Journaux des développeurs front-end Habr : refactorisation et réflexionInterface mobile Habr après la première étape de refactoring

Au final, l’architecture SSR-CSR de la version mobile conduit à ce constat :

Journaux des développeurs front-end Habr : refactorisation et réflexionCircuit SSR-CSR « à deux nœuds ». L'API Node JS est toujours prête pour les E/S asynchrones et n'est pas bloquée par la fonction SSR, puisque cette dernière se trouve dans une instance distincte. La chaîne de requête n°3 n’est pas nécessaire.

Éliminer les demandes en double

Une fois les manipulations effectuées, le rendu initial de la page ne provoquait plus l'épilepsie. Mais l'utilisation ultérieure de Habr en mode SPA a encore semé la confusion.

Puisque la base du flux d'utilisateurs est constituée de transitions de la forme liste des articles → article → commentaires et vice versa, il était important en premier lieu d’optimiser la consommation des ressources de cette chaîne.

Journaux des développeurs front-end Habr : refactorisation et réflexionLe retour au fil de publication provoque une nouvelle demande de données

Il n’était pas nécessaire de creuser profondément. Dans le screencast ci-dessus, vous pouvez voir que l'application demande à nouveau la liste des articles lors du balayage arrière, et lors de la demande, nous ne voyons pas les articles, ce qui signifie que les données précédentes disparaissent quelque part. Il semble que le composant liste d'articles utilise un état local et le perd lors de la destruction. En fait, l'application utilisait un état global, mais l'architecture Vuex a été construite de front : les modules sont liés à des pages, qui à leur tour sont liées à des routes. De plus, tous les modules sont « jetables » - chaque visite ultérieure de la page réécrit l'intégralité du module :

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

Au total, nous avions un module Liste des articles, qui contient des objets de type Article et module PageArticle, qui était une version étendue de l'objet Article, type de ArticleComplet. Dans l'ensemble, cette implémentation n'a rien de terrible en soi - elle est très simple, on pourrait même dire naïve, mais extrêmement compréhensible. Si vous réinitialisez le module à chaque fois que vous modifiez l'itinéraire, vous pouvez même vivre avec. Cependant, se déplacer entre les flux d'articles, par exemple /flux → /tous, est assuré de jeter tout ce qui concerne le flux personnel, puisque nous n'en avons qu'un Liste des articles, dans lequel vous devez mettre de nouvelles données. Cela nous conduit encore une fois à une duplication des demandes.

Après avoir rassemblé tout ce que j'ai pu trouver sur le sujet, j'ai formulé une nouvelle structure étatique et l'ai présentée à mes collègues. Les discussions ont été longues, mais finalement les arguments en faveur ont emporté les doutes et j’ai commencé à la mettre en œuvre.

La logique d’une solution se révèle mieux en deux étapes. Nous essayons d'abord de découpler le module Vuex des pages et de le lier directement aux routes. Oui, il y aura un peu plus de données dans le store, les getters deviendront un peu plus complexes, mais on ne chargera pas les articles deux fois. Pour la version mobile, c’est peut-être l’argument le plus fort. Cela ressemblera à ceci :

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

Mais que se passe-t-il si les listes d'articles peuvent se chevaucher entre plusieurs itinéraires et que se passe-t-il si nous souhaitons réutiliser les données d'objets ? Article pour afficher la page de publication, en la transformant en ArticleComplet? Dans ce cas, il serait plus logique d'utiliser une telle structure :

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

Liste des articles ici, c'est juste une sorte de référentiel d'articles. Tous les articles téléchargés pendant la session utilisateur. Nous les traitons avec le plus grand soin, car il s'agit d'un trafic qui peut avoir été téléchargé par une douleur quelque part dans le métro entre les stations, et nous ne voulons certainement pas causer à nouveau cette douleur à l'utilisateur en l'obligeant à charger des données qu'il a déjà téléchargé. Un objet Identifiants des articles est simplement un tableau d'identifiants (comme s'il s'agissait de « liens ») vers des objets Article. Cette structure permet d'éviter de dupliquer les données communes aux itinéraires et de réutiliser l'objet Article lors du rendu d'une page de publication en y fusionnant des données étendues.

La sortie de la liste d'articles est également devenue plus transparente : le composant itérateur parcourt le tableau avec les ID d'article et dessine le composant teaser d'article, en passant l'ID comme accessoire, et le composant enfant, à son tour, récupère les données nécessaires de Liste des articles. Lorsque vous accédez à la page de publication, nous obtenons la date déjà existante de Liste des articles, nous faisons une demande pour obtenir les données manquantes et les ajoutons simplement à l'objet existant.

Pourquoi cette approche est-elle meilleure ? Comme je l'ai écrit plus haut, cette approche est plus douce vis-à-vis des données téléchargées et permet de les réutiliser. Mais au-delà de cela, cela ouvre la voie à de nouvelles possibilités qui s’intègrent parfaitement dans une telle architecture. Par exemple, interroger et charger des articles dans le flux au fur et à mesure de leur apparition. Nous pouvons simplement mettre les derniers messages dans un « stockage » Liste des articles, enregistrez une liste distincte de nouveaux identifiants dans Identifiants des articles et en informer l'utilisateur. Lorsque nous cliquons sur le bouton « Afficher les nouvelles publications », nous insérerons simplement de nouveaux identifiants au début du tableau de la liste actuelle des articles et tout fonctionnera presque comme par magie.

Rendre le téléchargement plus agréable

La cerise sur le gâteau du refactoring est le concept de squelettes, qui rend le processus de téléchargement de contenu sur un Internet lent un peu moins dégoûtant. Il n'y a eu aucune discussion à ce sujet, le chemin de l'idée au prototype a pris littéralement deux heures. La conception s'est pratiquement dessinée toute seule et nous avons appris à nos composants à restituer des blocs div simples et à peine scintillants en attendant les données. Subjectivement, cette approche du chargement réduit en fait la quantité d’hormones de stress dans le corps de l’utilisateur. Le squelette ressemble à ceci :

Journaux des développeurs front-end Habr : refactorisation et réflexion
Habraloading

Réflexion

Cela fait six mois que je travaille à Habré et mes amis me demandent encore : eh bien, comment te sens-tu là-bas ? D'accord, confortable - oui. Mais il y a quelque chose qui différencie ce travail des autres. J'ai travaillé dans des équipes complètement indifférentes à leur produit, qui ne connaissaient ni ne comprenaient qui étaient leurs utilisateurs. Mais ici, tout est différent. Ici, vous vous sentez responsable de ce que vous faites. Dans le processus de développement d'une fonctionnalité, vous en devenez partiellement propriétaire, participez à toutes les réunions produits liées à votre fonctionnalité, faites des suggestions et prenez vous-même des décisions. Créer soi-même un produit que vous utilisez tous les jours est très cool, mais écrire du code pour des personnes qui sont probablement meilleures que vous dans ce domaine est tout simplement un sentiment incroyable (pas de sarcasme).

Après la sortie de tous ces changements, nous avons reçu des retours positifs, et c'était très, très sympa. C'est inspirant. Merci! Écrivez davantage.

Permettez-moi de vous rappeler qu'après les variables globales, nous avons décidé de modifier l'architecture et d'attribuer la couche proxy à une instance distincte. L'architecture « à deux nœuds » a déjà été publiée sous la forme de tests bêta publics. Désormais, tout le monde peut y accéder et nous aider à améliorer Habr mobile. C'est tout pour aujourd'hui. Je me ferai un plaisir de répondre à toutes vos questions dans les commentaires.

Source: habr.com

Ajouter un commentaire