
J'ai toujours été intéressé par la façon dont le Habr est organisé de l'intérieur, comment le flux de travail est construit, comment les communications sont construites, quelles normes sont appliquées et comment le code est écrit ici. Heureusement, une telle opportunité m'est apparue, car récemment je suis devenu membre de la habracommand. En utilisant l'exemple d'un petit refactoring de la version mobile, je vais essayer de répondre à la question: à quoi ça ressemble de travailler ici en front-end. Au programme: Node, Vue, Vuex et SSR avec sauce à partir de notes sur l'expérience personnelle à Habré.
La première chose que vous devez savoir sur l'équipe de développement est que nous sommes peu nombreux. Peu sont trois fronts, deux dos et techniques de tous les Habr - Bucksley. Bien sûr, il y a aussi un testeur, un designer, trois Vadim, un balai miracle, un marketeur et d'autres Bumburums. Mais il n'y a que six contributeurs directs aux types Habra. C'est assez rare - un projet avec un public de plusieurs millions de dollars qui ressemble à une gigantesque entreprise de l'extérieur ressemble en fait plus à une startup confortable avec la structure organisationnelle la plus plate.
Comme de nombreuses autres sociétés informatiques, Habr professe les idées d'Agile, la pratique de CI, et c'est tout. Mais selon mes sentiments, Habr en tant que produit se développe de manière plus ondulante que continue. Donc, pendant plusieurs sprints d'affilée, nous travaillons dur pour coder, concevoir et repenser, casser quelque chose et réparer, résoudre des tickets et en commencer de nouveaux, monter sur le râteau et nous tirer dans les jambes pour enfin publier la fonctionnalité au prod. Et puis il y a une accalmie, une période de réaménagement, le temps de faire ce qui est dans le quadrant «important-non urgent».
À peu près un tel sprint «hors saison» sera discuté ci-dessous. Cette fois, il a fallu refactoriser la version mobile de Habr. En général, l'entreprise a de grands espoirs pour elle, et à l'avenir, elle devrait remplacer l'intégralité du zoo d'incarnation Habr et devenir une solution multi-plateforme universelle. Un jour, il apparaîtra une mise en page adaptative, et PWA, et le mode hors ligne, et la personnalisation de l'utilisateur, et beaucoup de choses intéressantes.
Nous fixons la tâche
Une fois, lors d'un stand-up ordinaire, l'un des fronts a parlé de problèmes dans l'architecture du composant de commentaire de la version mobile. A partir de cette présentation, nous avons organisé une micro-rencontre sous forme de psychothérapie de groupe. Chacun à son tour a dit où il avait mal, tout était fixé sur du papier, sympathisé, compris, sauf que personne n'applaudissait. Le résultat était une liste de 20 problèmes, qui indiquait clairement que le mobile Habr devait emprunter un chemin long et épineux vers le succès.
Ma principale préoccupation était l'efficacité des ressources et ce que l'on appelle une interface fluide. Chaque jour sur l'itinéraire «domicile-travail-maison», je voyais mon ancien téléphone essayer désespérément d'afficher 20 titres dans le flux. Cela ressemblait à ceci:
Interface Mobile Habr avant refactoringQue se passe-t-il ici? En bref, le serveur a donné 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 avec une modification d'autorisation. En fait, nous avons fait le même travail deux fois. L'interface a vacillé et l'utilisateur a téléchargé une bonne centaine de kilo-octets supplémentaires. Dans les détails, tout semblait encore plus effrayant.
Ancien circuit SSR-CSR. 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 proxy des requêtes 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 parle tel quel. Une terrible combinaison de SSR et de RSE.
Nous avons dû l'admettre, aussi triste que cela puisse être.
J'ai trouvé les options, je me suis fixé un ticket dans la "Jira" avec une description au niveau de "Maintenant c'est mauvais, fais les règles" et à grands coups j'ai décomposé la tâche:
- réutiliser les données
- minimiser le nombre de redessins,
- exclure les demandes en double
- rendre le processus de chargement plus évident.
Réutiliser 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 concernant l'
indexation SPA et améliorer la métrique
FMP (aggravant inévitablement
TTI ). Dans le scénario classique, qui a finalement été
formulé dans Airbnb en 2013 (de retour sur Backbone.js), SSR est la même application JS isomorphe exécutée dans l'environnement Node. Le serveur renvoie simplement la disposition générée en réponse à la demande. Ensuite, la réhydratation se produit du côté client, puis tout fonctionne sans rechargement de page. Pour Habr, ainsi que pour de nombreuses autres ressources textuelles, le rendu du serveur est un élément essentiel dans l'établissement de relations amicales avec les moteurs de recherche.
Malgré le fait que plus de six ans se soient écoulés depuis l'avènement de la technologie, et pendant ce temps, beaucoup d'eau a coulé dans le monde frontal, pour de nombreux développeurs, cette idée est toujours couverte d'un voile de secret. Nous ne sommes pas restés à l'écart et avons déployé une application Vue avec prise en charge SSR pour le prod, manquant un petit détail: nous n'avons pas jeté 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 d'un tas d'autres problèmes architecturaux, soit ils n'ont tout simplement pas décollé. D'une manière ou d'une autre, jeter l'état et réutiliser tout ce que le serveur a fait semble être tout à fait approprié et utile. La tâche est en fait triviale - l'
état s'injecte simplement dans le contexte d'exécution et Vue l'ajoute automatiquement à la présentation générée en tant que variable globale:
window.__INITIAL_STATE__
.
L'un des problèmes qui s'est posé est l'incapacité de convertir
les structures
circulaires en JSON; a été résolu en remplaçant simplement ces structures par leurs analogues plats.
De plus, lorsque vous traitez du contenu UGC, n'oubliez pas que les données doivent être converties en entités HTML afin de ne pas casser le code HTML. À ces fins, nous l'utilisons.
Minimisez les redessins
Comme le montre le diagramme ci-dessus, dans notre cas, une instance Node JS remplit deux fonctions: SSR et "proxy" dans l'API, où l'utilisateur est autorisé. Cette circonstance rend l'autorisation impossible au moment de 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 envoyer de demandes à lui-même pendant que la pile d'appels est occupée par quelque chose. Il s'est avéré que nous avons sauté l'état, mais l'interface n'a pas cessé de se contracter, car les données sur le client doivent être mises à jour en tenant compte de la session utilisateur. Il était nécessaire d'apprendre à notre application à mettre les données correctes dans l'état initial, en tenant compte de la connexion de l'utilisateur.
Il n'y avait que deux solutions au problème:
- pour accrocher les données d'autorisation aux requêtes interserveurs;
- Scinder les couches JS du nœud en deux instances distinctes.
La première solution nécessitait l'utilisation de variables globales sur le serveur, et la seconde allongeait le temps nécessaire pour terminer la tâche d'au moins un mois.
Comment faire un choix? Habr se déplace souvent sur le chemin de la moindre résistance. De manière informelle, il existe une certaine volonté générale de minimiser le cycle 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 que Habr est beaucoup plus sérieux au sujet des commentaires des utilisateurs et fait confiance à l'adoption de telles décisions pour vous en tant que développeur.
En suivant cette logique et mon propre désir de résoudre rapidement le problème, j'ai choisi des variables globales. Et, comme cela arrive souvent, tôt ou tard, ils doivent payer pour eux. Nous avons payé presque immédiatement: nous avons travaillé le week-end, nous avons mesuré les conséquences, écrit un
post mortem et commencé à diviser le serveur en deux parties. L'erreur était très stupide et le bogue avec sa participation n'était pas facile à reproduire. Et oui, pour une telle honte, mais en quelque sorte, trébuchant et grognant, mon PoC avec des variables globales est toujours entré en production et fonctionne avec succès en prévision du passage à une nouvelle architecture "de deux jours". C'était une étape importante, car formellement, l'objectif a été atteint - la SSR a appris à donner une page complètement prête à l'emploi, et l'interface utilisateur est devenue beaucoup plus calme.
Interface Habr mobile après la première étape de refactoringEn fin de compte, l'architecture SSR-CSR de la version mobile conduit à cette image:

Schéma SSR-CSR "deux jours". L'API Jode Node est toujours prête pour les E / S asynchrones et n'est pas bloquée par la fonction SSR, car cette dernière est dans une instance distincte. La chaîne de requête n ° 3 n'est pas nécessaire.Exclure les demandes en double
Après les manipulations, le rendu de page initial a cessé de provoquer l'épilepsie. Mais la poursuite de l'utilisation de Habr en mode SPA a toujours provoqué la perplexité.
Étant donné que le flux d'utilisateurs est basé sur des transitions de la
liste des articles → article → commentaires et vice versa, il était important d'optimiser la consommation des ressources de cette chaîne en premier lieu.
Un retour au post-flux provoque une nouvelle demande de donnéesJe n'ai pas eu à creuser profondément. Sur le screencast ci-dessus, on peut voir que l'application interroge à nouveau la liste des articles lors du balayage, et pendant la demande, nous ne voyons pas l'article, de sorte que les données précédentes disparaissent quelque part. Il semble que le composant de liste d'articles utilise un état local et le perd lors de la destruction. En fait, l'application utilisait l'état global, mais l'architecture Vuex a été construite sur le front: les modules sont liés aux pages, qui à leur tour sont liées aux routes. De plus, tous les modules sont «ponctuels» - chaque visite suivante sur la page réécrit le module entier:
ArticlesList: [ { Article1 }, ... ], PageArticle: { ArticleFull1 },
Au total, nous avions le module
ArticlesList , qui contient des objets du type
Article et le module
PageArticle , qui était une version étendue de l'objet
Article , une sorte d'
ArticleFull . Dans l'ensemble, cette implémentation n'a rien de terrible en soi - elle est très simple, pourrait-on même dire naïvement, mais elle est extrêmement claire. Si vous supprimez la mise à zéro du module à chaque changement de route, vous pouvez même vivre avec. Cependant, la transition entre les flux d'articles, par exemple
/ feed → / all , est garantie de jeter tout ce qui concerne le flux personnel, car nous n'avons qu'une seule liste d'
articles dans laquelle mettre de nouvelles données. Cela conduit à nouveau à des requêtes en double.
Réunissant tout ce que j'ai réussi à dénicher 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 au final, les arguments «pour» ont emporté sur les doutes, et j'ai commencé la mise en œuvre.
La logique de la solution est mieux divulguée en deux étapes. Tout d'abord, nous essayons de délier le module Vuex des pages et de nous lier directement aux routes. Oui, il y aura un peu plus de données dans le magasin, les getters deviendront un peu plus compliqués, mais nous ne chargerons 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 si nous voulons réutiliser les données d'un objet
Article pour rendre une page de publication, la transformant en
ArticleFull ? 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 }, ... }
ArticlesList ici est juste une sorte de référentiel d'articles. Tous les articles qui ont été téléchargés pendant la session utilisateur. Nous les traitons aussi soigneusement que possible, car il s'agit d'un trafic qui peut avoir été chargé à travers la douleur quelque part dans le métro entre les stations, et nous ne voulons certainement pas causer à nouveau cette douleur à l'utilisateur, le forçant à charger les données qu'il a déjà téléchargées. L'objet
ArticlesIds n'est qu'un tableau d'identifiants (comme des «liens») vers des objets
Article . Cette structure vous permet de ne pas 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 des ID d'article et dessine le composant d'accroche d'article, en passant Id comme accessoires, et le composant enfant récupère à son tour les données nécessaires de la liste d'
articles . Lorsque vous accédez à la page de publication, nous obtenons la date existante de la liste des
articles , faisons une demande pour les données manquantes et l'ajoutons simplement à l'objet existant.
Pourquoi cette approche est-elle meilleure? Comme je l'ai écrit ci-dessus, cette approche est plus prudente par rapport aux données téléchargées et vous permet de les réutiliser. Mais en plus de cela, il ouvre la voie à de nouvelles opportunités qui s'intègrent parfaitement dans une telle architecture. Par exemple, interroger et télécharger des articles dans le flux tels qu'ils apparaissent. Nous pouvons simplement ajouter de nouveaux articles à la «boutique»
ArticlesList , enregistrer une liste séparée de nouveaux ID dans
ArticlesIds et en informer l'utilisateur. Lorsque vous cliquez sur le bouton «Afficher les nouvelles publications», nous insérons simplement un nouvel identifiant 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 de refactoring était le concept de squelettes, ce qui rend le processus de téléchargement de contenu sur Internet lent un peu moins dégoûtant. Il n'y a pas eu de discussions sur ce sujet, le voyage de l'idée au prototype a pris littéralement deux heures. Le design a été dessiné presque par nous-mêmes, et nous avons appris à nos composants comment rendre des blocs div sans prétention et à peine vacillants en attendant les données. Subjectivement, cette approche du chargement réduit vraiment la quantité d'hormones de stress dans le corps de l'utilisateur. Le squelette ressemble à ceci:
HabraloadingRéfléchir
Je travaille à Habré depuis six mois et mes amis me demandent toujours: eh bien, ça vous plaît? Bon, confortable - oui. Mais il y a quelque chose qui distingue ce travail des autres. J'ai travaillé dans des équipes totalement indifférentes à leur produit, ne connaissant pas et ne comprenant pas 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 le responsable, participez à toutes les réunions de produits liées à votre fonctionnalité, faites des suggestions et prenez des décisions vous-même. Faire un produit que vous utilisez vous-même quotidiennement est très cool, et écrire du code pour les gens qui peuvent être meilleurs est juste un sentiment incroyable (pas de sarcasme).
Après la publication de tous ces changements, nous avons reçu un retour positif, et c'était très, très agréable. C'est inspirant. Je vous remercie! Écrivez plus.
Permettez-moi de vous rappeler qu'après les variables globales, nous avons décidé de changer l'architecture et de séparer la couche proxy en une instance distincte. L'architecture "de deux jours" a déjà atteint la sortie sous la forme de tests bêta publics. Désormais, tout le monde peut y basculer 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.