
Ich war immer daran interessiert, wie der Habr von innen angeordnet ist, wie der Workflow aufgebaut ist, wie die Kommunikation aufgebaut ist, welche Standards angewendet werden und wie der Code hier geschrieben wird. Glücklicherweise erschien mir eine solche Gelegenheit, weil ich kürzlich Teil des Habracommand wurde. Am Beispiel eines kleinen Refactorings der mobilen Version werde ich versuchen, die Frage zu beantworten: Wie ist es, hier vorne zu arbeiten? Im Programm: Node, Vue, Vuex und SSR mit Sauce aus Notizen zu persönlichen Erfahrungen in Habré.
Das erste, was Sie über das Entwicklungsteam wissen müssen, ist, dass wir nur wenige sind. Nur wenige sind drei Fronten, zwei Rückseiten und technische Daten aller Habr - Bucksley. Natürlich gibt es auch einen Tester, Designer, drei Vadim, einen Wunderbesen, einen Vermarkter und andere Bumburums. Es gibt jedoch nur sechs direkte Mitwirkende an den Habra-Sorten. Dies ist ziemlich selten - ein Projekt mit einem Multimillionen-Dollar-Publikum, das von außen wie ein gigantisches Unternehmen aussieht, ist eigentlich eher ein gemütliches Startup mit der flachsten Organisationsstruktur.
Wie viele andere IT-Unternehmen bekennt sich Habr zu den Ideen von Agile, der Praxis von CI und das ist alles. Aber nach meinen Gefühlen entwickelt sich Habr als Produkt eher wellig als kontinuierlich. Für mehrere Sprints hintereinander arbeiten wir hart daran, zu programmieren, zu entwerfen und neu zu gestalten, etwas zu zerbrechen und zu reparieren, Tickets aufzulösen und neue zu starten, auf den Rechen zu treten und uns in die Beine zu schießen, um die Funktion endlich für das Produkt freizugeben. Und dann kommt eine Pause, eine Phase der Sanierung, die Zeit, das zu tun, was sich im Quadranten „wichtig-nicht dringend“ befindet.
Ein solcher Sprint außerhalb der Saison wird weiter unten diskutiert. Diesmal erhielt er das Refactoring der mobilen Version von Habr. Im Allgemeinen setzt das Unternehmen große Hoffnungen darauf und sollte in Zukunft den gesamten Habr-Inkarnationszoo ersetzen und eine universelle plattformübergreifende Lösung werden. Eines Tages werden adaptives Layout, PWA, Offline-Modus, Benutzeranpassung und viele interessante Dinge angezeigt.
Wir stellen die Aufgabe
Einmal sprach eine der Fronten bei einem normalen Stand-up über Probleme in der Architektur der Kommentarkomponente der mobilen Version. Aus dieser Präsentation heraus organisierten wir ein Mikrotreffen im Format der Gruppenpsychotherapie. Jeder sagte der Reihe nach, wo er Schmerzen hatte, alles war auf Papier fixiert, sympathisiert, verstanden, außer dass niemand klatschte. Die Ausgabe war eine Liste von 20 Problemen, die deutlich machten, dass der mobile Habr einen langen und dornigen Weg zum Erfolg gehen muss.
Mein Hauptanliegen war die Ressourceneffizienz und eine so genannte reibungslose Schnittstelle. Jeden Tag auf der Route „Heim-Arbeit-Zuhause“ sah ich mein altes Telefon verzweifelt versuchen, 20 Überschriften im Stream anzuzeigen. Es sah ungefähr so aus:
Mobile Habr-Schnittstelle vor dem RefactoringWas ist hier los? Kurz gesagt, der Server hat die HTML-Seite allen auf dieselbe Weise zur Verfügung gestellt, unabhängig davon, ob der Benutzer angemeldet ist oder nicht. Dann wird der Client JS geladen und fordert erneut die erforderlichen Daten an, jedoch mit einer Änderung zur Autorisierung. Das heißt, wir haben die gleiche Arbeit zweimal gemacht. Die Benutzeroberfläche flackerte und der Benutzer lud gut hundert zusätzliche Kilobyte herunter. Im Detail sah alles noch gruseliger aus.
Alte SSR-CSR-Schaltung. Die Autorisierung ist nur in den Phasen C3 und C4 möglich, in denen Node JS nicht mit der Generierung von HTML beschäftigt ist und API-Anforderungen proxy kann.Unsere damalige Architektur wurde von einem von Habrs Benutzern sehr genau beschrieben:
Die mobile Version ist Scheiße. Ich spreche so wie es ist. Eine schreckliche Kombination von SSR und CSR.
Wir mussten es zugeben, wie traurig es auch sein mag.
Ich habe die Optionen herausgefunden, mir ein Ticket im "Jira" mit einer Beschreibung auf der Ebene "Jetzt ist es schlecht, mach die Regeln" gesetzt und mit großen Strichen die Aufgabe zerlegt:
- Daten wiederverwenden
- Minimieren Sie die Anzahl der Neuzeichnungen.
- doppelte Anfragen ausschließen
- Machen Sie den Ladevorgang deutlicher.
Daten wiederverwenden
Theoretisch soll das serverseitige Rendern zwei Probleme lösen: nicht unter den Einschränkungen von Suchmaschinen hinsichtlich der
SPA-Indizierung zu leiden und die
FMP- Metrik zu verbessern (was den
TTI zwangsläufig verschlechtert). In dem klassischen Szenario, das
2013 in Airbnb formuliert wurde (zurück bei Backbone.js), ist SSR dieselbe isomorphe JS-Anwendung, die in der Node-Umgebung ausgeführt wird. Der Server gibt das generierte Layout einfach als Antwort auf die Anforderung zurück. Dann erfolgt auf der Client-Seite eine Rehydratisierung, und dann funktioniert alles ohne erneutes Laden der Seite. Für Habr und viele andere mit Text gefüllte Ressourcen ist das Server-Rendering ein entscheidendes Element beim Aufbau freundschaftlicher Beziehungen zu Suchmaschinen.
Trotz der Tatsache, dass seit dem Aufkommen der Technologie mehr als sechs Jahre vergangen sind und in dieser Zeit wirklich viel Wasser in die Frontend-Welt geflossen ist, ist diese Idee für viele Entwickler immer noch geheim. Wir sind nicht beiseite getreten und haben eine Vue-Anwendung mit SSR-Unterstützung für das Produkt bereitgestellt, wobei ein kleines Detail fehlte: Wir haben den Anfangszustand nicht an den Client übergeben.
Warum? Es gibt keine genaue Antwort auf diese Frage. Entweder wollten sie die Antwort vom Server nicht vergrößern oder wegen einer Reihe anderer Architekturprobleme oder sie starteten einfach nicht. Auf die eine oder andere Weise scheint es durchaus angemessen und nützlich zu sein, den Status zu werfen und alles wiederzuverwenden, was der Server getan hat. Die Aufgabe ist eigentlich trivial -
state fügt sich
einfach in den Ausführungskontext ein und Vue fügt sie automatisch als globale Variable zum generierten Layout hinzu:
window.__INITIAL_STATE__
.
Eines der auftretenden Probleme war die Unfähigkeit,
kreisförmige Strukturen in JSON umzuwandeln. wurde gelöst, indem solche Strukturen einfach durch ihre flachen Analoga ersetzt wurden.
Denken Sie beim Umgang mit UGC-Inhalten außerdem daran, dass Daten in HTML-Entitäten konvertiert werden sollten, um den HTML-Code nicht zu beschädigen. Für diese Zwecke verwenden wir
ihn .
Minimierungen neu zeichnen
Wie aus dem obigen Diagramm ersichtlich ist, führt in unserem Fall eine Node JS-Instanz zwei Funktionen aus: SSR und "Proxy" in der API, in der der Benutzer autorisiert ist. Dieser Umstand macht eine Autorisierung zum Zeitpunkt der Ausführung des JS-Codes auf dem Server unmöglich, da der Knoten Single-Threaded ist und die SSR-Funktion synchron ist. Das heißt, der Server kann einfach keine Anforderungen an sich selbst senden, während der Aufrufstapel mit etwas beschäftigt ist. Es stellte sich heraus, dass wir den Status übersprungen haben, aber die Schnittstelle hat nicht aufgehört zu zucken, da die Daten auf dem Client unter Berücksichtigung der Benutzersitzung aktualisiert werden sollten. Es war notwendig, unserer Anwendung beizubringen, die korrekten Daten unter Berücksichtigung des Logins des Benutzers in den Ausgangszustand zu versetzen.
Es gab nur zwei Lösungen für das Problem:
- Autorisierungsdaten an Interserver-Anfragen zu klammern;
- Teilen Sie Node JS-Ebenen in zwei separate Instanzen auf.
Die erste Lösung erforderte die Verwendung globaler Variablen auf dem Server, und die zweite verlängerte die für die Ausführung der Aufgabe erforderliche Zeit um mindestens einen Monat.
Wie treffe ich eine Wahl? Habr bewegt sich oft auf dem Weg des geringsten Widerstands. Informell besteht ein gewisser allgemeiner Wunsch, den Zyklus von der Idee zum Prototyp zu minimieren. Das Modell der Einstellung zum Produkt erinnert ein wenig an die Postulate von booking.com, mit dem einzigen Unterschied, dass Habr das Feedback der Benutzer viel ernst nimmt und darauf vertraut, dass Sie als Entwickler solche Entscheidungen treffen.
Nach dieser Logik und meinem eigenen Wunsch, das Problem schnell zu lösen, habe ich globale Variablen ausgewählt. Und wie so oft müssen sie früher oder später dafür bezahlen. Wir haben fast sofort bezahlt: Wir haben am Wochenende gearbeitet, die Konsequenzen aufgegriffen, ein
Post-Mortem geschrieben und den Server in zwei Teile geteilt. Der Fehler war sehr dumm und der Fehler mit ihrer Teilnahme war nicht leicht zu reproduzieren. Und ja, für solch eine Schande, aber irgendwie, stolpernd und grunzend, ging mein PoC mit globalen Variablen immer noch in Produktion und arbeitet ziemlich erfolgreich in Erwartung des Übergangs zu einer neuen "zweitägigen" Architektur. Dies war ein wichtiger Schritt, da das Ziel formal erreicht wurde - die SSR lernte, eine Seite zu erstellen, die vollständig einsatzbereit war, und die Benutzeroberfläche wurde viel ruhiger.
Mobile Habr-Schnittstelle nach der ersten Phase des RefactoringsLetztendlich führt die SSR-CSR-Architektur der mobilen Version zu diesem Bild:

"Zweitägiges" SSR-CSR-Schema. Die Knoten-JS-API ist immer für asynchrone E / A bereit und wird von der SSR-Funktion nicht blockiert, da sich diese in einer separaten Instanz befindet. Abfragekette Nr. 3 wird nicht benötigt.Doppelte Anfragen ausschließen
Nach den Manipulationen löste das erste Rendern der Seite keine Epilepsie mehr aus. Die weitere Verwendung von Habr im SPA-Modus sorgte jedoch immer noch für Verwirrung.
Da der Benutzerfluss auf Übergängen der Formularliste
von Artikeln → Artikel → Kommentare und umgekehrt basiert, war es zunächst wichtig, den Ressourcenverbrauch dieser Kette zu optimieren.
Eine Rückkehr zum Post-Feed führt zu einer neuen DatenanforderungIch musste nicht tief graben. Der obige Screencast zeigt, dass die Anwendung die Liste der Artikel beim Zurückwischen erneut abfragt. Während der Anforderung wird der Artikel nicht angezeigt, was bedeutet, dass die vorherigen Daten irgendwo verschwinden. Es sieht so aus, als ob die Artikellistenkomponente einen lokalen Status verwendet und diesen bei Zerstörung verliert. Tatsächlich verwendete die Anwendung den globalen Status, aber die Vuex-Architektur wurde auf der Stirn aufgebaut: Die Module sind an Seiten gebunden, die wiederum an Routen gebunden sind. Darüber hinaus sind alle Module „einmalig“ - bei jedem weiteren Besuch der Seite wurde das gesamte Modul neu geschrieben:
ArticlesList: [ { Article1 }, ... ], PageArticle: { ArticleFull1 },
Insgesamt hatten wir das
ArticlesList- Modul, das Objekte vom Typ
Article enthält, und das
PageArticle- Modul, das eine erweiterte Version des
Article- Objekts war, eine Art
ArticleFull . Im Großen und Ganzen trägt diese Implementierung nichts Schreckliches an sich - es ist sehr einfach, man könnte sogar naiv sagen, aber es ist äußerst klar. Wenn Sie die Nullstellung des Moduls bei jeder Änderung der Route ausschalten, können Sie sogar damit leben. Der Übergang zwischen Artikel-Feeds, z. B.
/ feed → / all , wirft jedoch garantiert alles aus, was mit dem persönlichen Feed zusammenhängt, da wir nur eine
Artikelliste haben, in die neue Daten eingefügt werden können. Dies führt wiederum zu doppelten Abfragen.
Ich stellte alles zusammen, was ich zu diesem Thema herausfinden konnte, formulierte eine neue Staatsstruktur und präsentierte sie meinen Kollegen. Die Diskussionen waren langwierig, aber am Ende überwogen die Argumente „für“ die Zweifel, und ich begann mit der Umsetzung.
Die Logik der Lösung lässt sich am besten in zwei Schritten offenbaren. Zuerst versuchen wir, das Vuex-Modul von den Seiten zu lösen und direkt an die Routen zu binden. Ja, es werden etwas mehr Daten im Geschäft sein, Getter werden etwas komplizierter, aber wir werden die Artikel nicht zweimal laden. Für die mobile Version ist dies vielleicht das stärkste Argument. Es wird ungefähr so aussehen:
ArticlesList: { ROUTE_FEED: [ { Article1 }, ... ], ROUTE_ALL: [ { Article2 }, ... ], }
Was aber, wenn sich Artikellisten zwischen mehreren Routen überschneiden können, und was ist, wenn wir die Daten eines
Artikelobjekts wiederverwenden möchten, um eine
Postseite zu rendern und sie in
ArticleFull umzuwandeln ? In diesem Fall wäre es logischer, eine solche Struktur zu verwenden:
ArticlesIds: { ROUTE_FEED: [ '1', ... ], ROUTE_ALL: [ '1', '2', ... ], }, ArticlesList: { '1': { Article1 }, '2': { Article2 }, ... }
ArticlesList hier ist nur eine Art Artikel-Repository. Alle Artikel, die während der Benutzersitzung hochgeladen wurden. Wir behandeln sie so sorgfältig wie möglich, da dies Verkehr ist, der möglicherweise durch Schmerzen irgendwo in der U-Bahn zwischen den Stationen geladen wurde, und wir möchten den Benutzer definitiv nicht erneut mit diesen Schmerzen belasten, sodass er die bereits heruntergeladenen Daten laden muss. Das
ArticlesIds- Objekt ist nur ein Array von Bezeichnern (wie „Links“) zu Artikelobjekten. Mit dieser Struktur können Sie die für Routen üblichen Daten nicht duplizieren und das Artikelobjekt beim Rendern einer Postseite wiederverwenden, indem Sie erweiterte Daten darin zusammenführen.
Die Ausgabe der Artikelliste ist auch transparenter geworden: Die Iteratorkomponente durchläuft das Array mit Artikel-IDs und zeichnet die Artikel-Teaser-Komponente, wobei die ID als Requisiten übergeben wird, und die untergeordnete Komponente ruft wiederum die erforderlichen Daten aus der
Artikelliste ab . Wenn Sie zur Veröffentlichungsseite gehen, erhalten wir das vorhandene Datum aus der
Artikelliste , fordern die fehlenden Daten an und fügen sie einfach dem vorhandenen Objekt hinzu.
Warum ist dieser Ansatz besser? Wie ich oben geschrieben habe, ist dieser Ansatz in Bezug auf die heruntergeladenen Daten vorsichtiger und ermöglicht es Ihnen, sie wiederzuverwenden. Abgesehen davon eröffnet es den Weg für einige neue Möglichkeiten, die perfekt in eine solche Architektur passen. Zum Beispiel das Abrufen und Hochladen von Artikeln in den Feed, sobald sie angezeigt werden. Wir können einfach neue Beiträge zum
ArticlesList- Store hinzufügen, eine separate Liste neuer IDs in
ArticlesIds speichern und den Benutzer darüber informieren. Wenn Sie auf die Schaltfläche "Neue Veröffentlichungen anzeigen" klicken, fügen Sie einfach eine neue ID am Anfang des Arrays der aktuellen Artikelliste ein, und alles funktioniert fast auf magische Weise.
Den Download angenehmer gestalten
Die Kirsche auf dem Refactoring-Kuchen war das Konzept der Skelette, was das Herunterladen von Inhalten im langsamen Internet etwas weniger ekelhaft macht. Es gab keine Diskussionen zu diesem Thema, die Reise von der Idee zum Prototyp dauerte buchstäblich zwei Stunden. Das Design wurde fast von uns selbst gezeichnet und wir haben unseren Komponenten beigebracht, wie man unprätentiöse, kaum flackernde Div-Blöcke rendert, während man auf Daten wartet. Subjektiv reduziert dieser Ansatz zum Laden wirklich die Menge an Stresshormonen im Körper des Benutzers. Das Skelett sieht so aus:
HabraloadingReflektieren
Ich arbeite seit sechs Monaten in Habré und Freunde fragen immer noch: Nun, wie gefällt es dir? Gut, bequem - ja. Aber es gibt etwas, das diese Arbeit von anderen unterscheidet. Ich arbeitete in Teams, die ihrem Produkt völlig gleichgültig gegenüberstanden, nicht wussten und nicht verstanden, wer ihre Benutzer waren. Aber hier ist alles anders. Hier fühlen Sie sich verantwortlich für das, was Sie tun. Bei der Entwicklung eines Features werden Sie teilweise dessen Eigentümer, nehmen an allen Produktbesprechungen zu Ihrer Funktionalität teil, machen Vorschläge und treffen selbst Entscheidungen. Ein Produkt herzustellen, das Sie täglich selbst verwenden, ist sehr cool, und Code für Leute zu schreiben, die es vielleicht besser können, ist einfach ein unglaubliches Gefühl (kein Sarkasmus).
Nach der Veröffentlichung all dieser Änderungen erhielten wir ein positives Feedback und es war sehr, sehr schön. Es ist inspirierend. Vielen Dank! Schreiben Sie mehr.
Ich möchte Sie daran erinnern, dass wir nach globalen Variablen beschlossen haben, die Architektur zu ändern und die Proxy-Schicht in eine separate Instanz aufzuteilen. Die "zweitägige" Architektur hat die Veröffentlichung bereits in Form von öffentlichen Betatests erreicht. Jetzt kann jeder darauf umsteigen und uns helfen, das mobile Habr besser zu machen. Das ist alles für heute. Gerne beantworte ich alle Ihre Fragen in den Kommentaren.