ReactJS, rendu côté serveur et quelques subtilités du traitement des balises META de la page

L'un des problèmes que vous devrez résoudre lors de l'écriture d'une application de rendu côté serveur est de travailler avec les balises META que chaque page devrait avoir, ce qui aide à les indexer par les moteurs de recherche.

À partir de Google, la première solution à laquelle vous serez amené est probablement React Helmet .

L'un des avantages de la bibliothèque est qu'elle peut être considérée comme isomorphe d'une certaine manière et peut être parfaitement utilisée à la fois côté client et côté serveur.

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

Sur le serveur, le routeur ressemblera alors Ă  ceci:

 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> `); }); 

Ces deux extraits sont complètement corrects et fonctionnels, mais il y a un MAIS, le code ci-dessus pour le serveur est complètement synchrone et donc complètement sûr, mais s'il devient asynchrone, il masquera en lui-même les bogues difficiles à déboguer:

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

Le problème ici est principalement dans la bibliothèque React Helmet elle-même et, en particulier, en ce qu'elle recueille toutes les balises à l'intérieur de l'arborescence React et les place en fait dans une variable globale, et puisque le code est devenu asynchrone, le code peut mélanger simultanément les demandes traitées de différents utilisateurs.

La bonne nouvelle ici est qu'un fork a été créé sur la base de cette bibliothèque et maintenant il vaut mieux privilégier la bibliothèque react-casque-async . Le principal paradigme est que dans ce cas, le contexte React-Casque sera isolé dans le cadre d'une seule requête en encapsulant l'application React Tree dans HelmetProvider:

 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 }); 

Cela pourrait être terminé, mais peut-être irez-vous plus loin dans le but de réduire les performances maximales et d'améliorer certaines mesures de référencement. Par exemple, vous pouvez améliorer la métrique Time To First Byte (TTFB) - lorsque le serveur peut envoyer la mise en page avec des morceaux au fur et à mesure qu'ils sont calculés, plutôt que d'attendre qu'elle soit entièrement calculée. Pour ce faire, vous commencerez à chercher à utiliser renderToNodeStream au lieu de renderToString .

Ici, nous sommes à nouveau confrontés à un petit problème. Pour obtenir toutes les balises META dont une page a besoin, nous devons parcourir toute l'arborescence de réaction de l'application, mais le problème est que les balises META doivent être envoyées avant le moment où nous commençons à diffuser du contenu à l'aide de renderToNodeStream. En fait, nous devons ensuite calculer l'arbre de réaction deux fois et il ressemble à ceci:

 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>')); }); 

Avec une telle approche, le besoin d'une telle optimisation devient en principe une grande question et il est peu probable que nous améliorions la métrique TTFB que nous voulons atteindre.

Ici, nous pouvons jouer un peu d'optimisation et il y a plusieurs options

  • au lieu de renderToString, utilisez renderToStaticMarkup, ce qui contribuera probablement dans une certaine mesure Ă  gagner du temps
  • au lieu d'utiliser les moteurs de rendu proposĂ©s par la rĂ©action de la boĂ®te, crĂ©ez votre propre version allĂ©gĂ©e du passage Ă  travers l'arbre de rĂ©action, par exemple, basĂ© sur la bibliothèque react-tree-walker , ou refusez de rendre complètement l'arbre et de ne regarder que le premier niveau de l'arbre, sans prĂŞter attention aux composants intĂ©grĂ©s, donc dire un rendu superficiel
  • envisager un système de mise en cache qui peut parfois ignorer la première promenade dans l'arbre de rĂ©action

Mais en tout cas, tout ce qui est décrit semble trop sophistiqué et, en principe, met en doute cette course à l'efficacité, quand une architecture anormalement complexe est construite en quelques millisecondes.

Il me semble dans ce cas, pour ceux qui savent comment extraire des données pour le rendu pour le SSR (et si quelqu'un ne le sait pas, alors c'est un excellent article sur ce sujet), nous aiderons à procéder de la même manière pour extraire les méta balises de la page.

Le concept général est que nous avons un fichier de configuration pour les routeurs - il s'agit d'une structure JS ordinaire, qui est un tableau d'objets, chacun contenant plusieurs champs du composant de type, path . Sur la base de l'URL de demande, nous trouvons le routeur et le composant qui lui sont associés dans le fichier de configuration. Pour ces composants, nous définissons un ensemble de méthodes statiques telles que loadData et, par exemple, createMetatags pour nos balises META.

Ainsi, le composant de page lui-mĂŞme deviendra comme ceci:

 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 } 

Nous avons défini une méthode statique createMetatags qui crée l'ensemble requis de balises META. Dans cet esprit, le code sur le serveur deviendra comme ceci:

 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>'));​ }); 

C'est-à-dire Désormais, nous n'avons plus besoin de rendre l'arbre React deux fois - nous pouvons immédiatement extraire tout ce dont nous avons besoin pour travailler à partir d'une application isomorphe, par analogie avec l'extraction de données pour un itinéraire.

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


All Articles