ReactJS,服务器端渲染和处理页面元标记的一些技巧

编写Server Side渲染应用程序时必须解决的问题之一是处理每个页面应具有的meta标签,这有助于搜索引擎为它们建立索引。

开始使用Google时,最有可能找到您的解决方案是React Helmet

该库的优点之一是,它可以以某种方式被认为是同构的,并且可以在客户端和服务器端完美使用。

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

在服务器上,路由器将如下所示:

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

这两个片段都是完全正确和高效的,但是有一个BUT,上面的服务器代码是完全同步的,因此是完全安全的,但是,如果变成异步,它将自身隐藏难以调试的错误:

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

这里的问题主要出在React Helmet库本身中,尤其是它收集了React Tree内的所有标签并将其实际上放入一个全局变量中,并且由于代码已变为异步代码,因此代码可以混合来自不同用户的同时处理的请求。

这里的好消息是,在此库的基础上创建了一个fork,现在最好优先选择react-helmet-async库。 它的主要范例是,在这种情况下,通过在HelmetProvider中封装React Tree应用程序,将在单个请求的框架内隔离react-helmet上下文:

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

可以完成此操作,但是也许您会尝试进一步以最大限度地提高性能并改善某些SEO指标。 例如,您可以改进“第一个字节的时间”(TTFB)度量标准-服务器可以发送带有大块的页面布局,而不必等到完全计算出来。 为此,您将开始考虑使用renderToNodeStream而不是renderToString

在这里,我们再次面临一个小问题。 为了获得页面所需的所有元标记,我们必须遍历整个应用程序的反应树,但是问题是必须在开始使用renderToNodeStream传输流内容之前发送元标记。 实际上,然后我们需要计算两次反应树,它看起来像这样:

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

通过这种方法,原则上对这种优化的需求成为一个大问题,并且我们不太可能会改善我们想要实现的TTFB指标。

在这里我们可以进行一些优化,有几种选择

  • 代替renderToString使用renderToStaticMarkup,这可能会在某种程度上帮助赢得一段时间
  • 与其使用盒中的react提供的渲染器,不如通过react-tree-walker库提供您自己通过React树的段落的浅色版本,或者拒绝完全渲染树并仅查看树的第一层,而不关注嵌入式组件,因此说浅渲染
  • 考虑一个可能有时会跳过反应树的第一步的缓存系统

但是无论如何,当几毫秒内构建了一些异常复杂的体系结构时,所描述的一切听起来都太复杂了,从原理上讲,人们对这种争夺效率的态度产生了怀疑。

在这种情况下,对我来说,对于那些熟悉如何为SSR提取数据进行渲染的人来说(如果有人不知道,那么这是本主题的出色文章 ),我们将以相同的方式为页面提取元标记。

一般概念是,我们有一个用于路由器的配置文件-这是一个普通的JS结构,它是一个对象数组,每个对象包含类型为path的几个字段。 根据请求url,我们从配置文件中找到路由器和与其关联的组件。 对于这些组件,我们定义了一组静态方法,例如loadData和元标记的createMetatags

因此,页面组件本身将变为:

 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 } 

我们定义了一个静态的createMetatags方法,该方法创建所需的元标记集。 考虑到这一点,服务器上的代码将如下所示:

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

即 现在,我们不需要渲染两次React树-我们可以立即将同构应用程序中需要的所有内容提取出来,这类似于对路线的数据提取。

Source: https://habr.com/ru/post/zh-CN484456/


All Articles