ReactJS, serverseitiges Rendering und einige Feinheiten bei der Verarbeitung von Seiten-Metatags

Eines der Probleme, die Sie beim Schreiben einer serverseitigen Rendering-Anwendung lösen müssen, besteht darin, mit den Meta-Tags zu arbeiten, die jede Seite haben sollte, damit sie von Suchmaschinen indiziert werden können.

Wenn Sie mit Google beginnen, ist die erste Lösung, zu der Sie geführt werden, höchstwahrscheinlich React Helmet .

Einer der Vorteile der Bibliothek besteht darin, dass sie in gewisser Weise als isomorph angesehen und sowohl auf der Client- als auch auf der Serverseite perfekt verwendet werden kann.

class Page extends Component { render() { return ( <div> <Helmet> <title>Turbo Todo</title> <meta name="theme-color" content="#008f68" /> </Helmet> {/* ... */} </div> ); } } 

Auf dem Server sieht der Router dann so aus:

 app.get('/*', (req, res) => { const html = renderToString(<App />); const helmet = Helmet.renderStatic(); res.send(` <!doctype html> <html ${helmet.htmlAttributes.toString()}> <head> ${helmet.title.toString()} ${helmet.meta.toString()} </head> <body ${helmet.bodyAttributes.toString()}> <div id="app">${html}</div> </body> </html> `); }); 

Diese beiden Snippets sind vollständig korrekt und effizient, es gibt jedoch einen ABER, der obige Code für den Server ist vollständig synchron und daher vollständig sicher. Wenn er jedoch asynchron wird, werden schwer zu debuggende Fehler in sich verborgen:

 app.get('/*', async (req, res) => { // .... await anyAsyncAction(); //.... const helmet = Helmet.renderStatic(); // ... }); 

Das Problem liegt in erster Linie in der React Helmet-Bibliothek selbst und insbesondere darin, dass sie alle Tags im React Tree sammelt und in eine globale Variable einfĂĽgt. Da der Code asynchron geworden ist, kann der Code gleichzeitig verarbeitete Anforderungen von verschiedenen Benutzern mischen.

Die gute Nachricht ist, dass auf der Basis dieser Bibliothek eine Gabelung hergestellt wurde und es jetzt besser ist, der Bibliothek fĂĽr Reaktionshelme und asynchrone Funktionen den Vorzug zu geben. Das Hauptparadigma dabei ist, dass in diesem Fall der Kontext der Reaktionshelme im Rahmen einer einzelnen Anforderung isoliert wird, indem die React Tree-Anwendung in HelmetProvider eingekapselt wird:

 import { Helmet, HelmetProvider } from 'react-helmet-async'; app.get('/*', async (req, res) => {​ // ... code may content any async actions const helmetContext = {}; const app = ( <HelmetProvider context={helmetContext}> <App/> </HelmetProvider> ); // ...code may content any async actions const html = renderToString(app); const { helmet } = helmetContext; // ...code may content any async actions }); 

Dies könnte abgeschlossen sein, aber vielleicht werden Sie noch weiter gehen, um die maximale Leistung herauszufiltern und einige SEO-Kennzahlen zu verbessern. Sie können beispielsweise die TTFB-Metrik (Time To First Byte) verbessern, wenn der Server das Seitenlayout mit berechneten Abschnitten senden kann, anstatt zu warten, bis die Berechnung abgeschlossen ist. Zu diesem Zweck werden Sie beginnen, renderToNodeStream anstelle von renderToString zu verwenden .

Hier stehen wir wieder vor einem kleinen Problem. Um alle Meta-Tags zu erhalten, die eine Seite benötigt, müssen wir den gesamten Reaktionsbaum der Anwendung durchgehen. Das Problem besteht jedoch darin, dass die Meta-Tags gesendet werden müssen, bevor wir mit dem Streaming von Inhalten mit renderToNodeStream beginnen. Tatsächlich müssen wir den React Tree dann zweimal berechnen und es sieht ungefähr so ​​aus:

 app.get('/*', async (req, res) => {​ const helmetContext = {}; let app = ( <HelmetProvider context={helmetContext}> <App/> </HelmetProvider> ); // do a first pass render so that react-helmet-async // can see what meta tags to render ReactDOMServer.renderToString(app); const { helmet } = helmetContext; response.write(` <html> <head> ${helmet.title.toString()}​ ${helmet.meta.toString()} </head> <body> `); const stream = ReactDOMServer.renderToNodeStream(app); stream.pipe(response, { end: false }); stream.on('end', () => response.end('</body></html>')); }); 

Mit einem solchen Ansatz wird die Notwendigkeit einer solchen Optimierung im Prinzip zu einer großen Frage, und es ist unwahrscheinlich, dass wir die TTFB-Metrik verbessern, die wir erreichen möchten.

Hier können wir ein wenig optimieren und es gibt mehrere Möglichkeiten

  • Verwenden Sie anstelle von renderToString renderToStaticMarkup. Dies wird wahrscheinlich dazu beitragen, dass Sie einige Zeit gewinnen
  • Anstatt die Renderer zu verwenden, die vom React from the Box angeboten werden, erstellen Sie eine eigene Light-Version des Durchgangs durch den React Tree, die auf der React Tree Walker- Bibliothek basiert, oder lehnen Sie es ab, den Baum vollständig zu rendern und sehen Sie sich nur die erste Ebene des Baums an, ohne auf die eingebetteten Komponenten zu achten sagen Sie flaches Rendering
  • Stellen Sie sich ein Caching-System vor, bei dem manchmal der erste Durchgang durch den Reaktionsbaum ĂĽbersprungen wird

Auf jeden Fall klingt alles, was beschrieben wird, zu raffiniert und wirft im Prinzip Zweifel an diesem Wettlauf um Effizienz auf, wenn eine ungewöhnlich komplexe Architektur in wenigen Millisekunden erstellt wird.

In diesem Fall scheint es mir fĂĽr diejenigen, die mit dem Extrahieren von Daten zum Rendern fĂĽr die SSR vertraut sind (und wenn jemand es nicht weiĂź, ist dies ein hervorragender Artikel zu diesem Thema), hilfreich zu sein, den gleichen Weg zu gehen, um Meta-Tags fĂĽr die Seite zu extrahieren.

Das allgemeine Konzept ist, dass wir eine Konfigurationsdatei für Router haben - dies ist eine gewöhnliche JS-Struktur, bei der es sich um ein Array von Objekten handelt, von denen jedes mehrere Felder des Typs component , path enthält . Basierend auf der Anforderungs-URL finden wir den Router und die dazugehörige Komponente in der Konfigurationsdatei. Für diese Komponenten definieren wir eine Reihe statischer Methoden wie loadData und beispielsweise createMetatags für unsere Metatags.

Somit wird die Seitenkomponente selbst folgendermaĂźen aussehen:

 class ProductPage extends React.Component { static createMetatags(store, request){ const item = selectItem(store, request.params.product_id); return [] .concat({property: 'og:description', content: item.desc}) .concat({property: 'og:title', content: item.title}) } static loadData(store, request){ // extract external data for SSR and return Promise } // the rest of component } 

Wir haben eine statische Methode createMetatags definiert, mit der die erforderlichen Metatags erstellt werden. In diesem Sinne sieht der Code auf dem Server folgendermaĂźen aus:

 app.get('/*', async (req, res) => {​​ const store = createStore(); const matchedRoutes = matchRoutes(routes, request.path); // load app state await Promise.all( matchedRoutes.reduce((promises, { route }) => { return route.component.loadData ? promises.concat(route.component.loadData(store, req)) : promises; }, []) ); // to get metatags const metaTags = matchedRoutes.reduce((tags, {route}) => { return route.component.createMetatags ? tags.concat(route.component.createMetatags(store, req)): tags }); res.write(`​ <html>​ <head>​ ${ReactDOMServer.renderToString(() => metaTags.map(tag => <meta {...tag}/>) )}​​ </head>​ <body>​ `);​ const stream = ReactDOMServer.renderToNodeStream(app);​ stream.pipe(response, { end: false });​ stream.on('end', () => response.end('</body></html>'));​ }); 

Das heißt Jetzt müssen wir den React-Baum nicht mehr zweimal rendern - wir können sofort alles, was wir brauchen, um zu arbeiten, aus einer isomorphen Anwendung extrahieren, analog zur Datenextraktion für eine Route.

Source: https://habr.com/ru/post/de484456/


All Articles