要点总结
如果您曾经制作过基本的 React 应用页面,它可能会存在 SEO 差和性能问题,尤其是在较慢的设备上。您可以添加传统的网页服务端渲染(通常使用 NodeJS),但这并非一个简单的过程,尤其是在处理异步 API 时。
服务端渲染代码的两个主要好处是:
请记住,Google 会等待您的 JavaScript 加载,因此标题内容等简单内容会无问题地更改。(不过,我无法说明其他搜索引擎的情况,或者这有多可靠。)
在这篇文章中,我将讨论在使用服务端渲染的 React 代码时如何从异步 API 获取数据。React 代码具有内置于 JavaScript 中的整个应用程序结构。这意味着,与具有控制器的传统 MVC 模式不同,您在应用程序渲染之前不知道需要什么数据。使用像 Create React App 这样的框架,您可以快速创建高质量的工作应用程序,但它要求您仅在客户端处理渲染。这存在性能问题,以及 SEO/数据问题,您可以在其中根据需要更改头部。
问题
React 主要同步渲染,因此如果您没有数据,则会渲染加载屏幕并等待数据到来。这在服务器端效果不佳,因为您在渲染之前不知道需要什么,或者您知道需要什么,但您已经渲染了。
查看此标准渲染方法:
ReactDOM.render( <provider> store={store}></provider> <browserrouter></browserrouter> <app></app> > > , document.getElementById('root') )
问题:
因此,我在这里使用了两个库,它们非常流行,因此希望它可以应用于您使用的其他库。
Redux:存储服务器和客户端同步的状态是一个噩梦般的问题。它非常昂贵,并且通常会导致复杂的错误。在服务器端,理想情况下,除了足以使事情正常工作并正确渲染之外,您不想使用 Redux 做任何事情。(您仍然可以照常使用它;只需设置足够的状态使其看起来像客户端。)如果您想尝试,请查看各种分布式系统指南作为起点。
React-Router:仅供参考,这是 v4 版本,这是默认安装的版本,但如果您有较旧的现有项目,则会有很大不同。您需要确保在服务器端和客户端处理路由,并且使用 v4——它在这方面非常出色。
毕竟,如果您需要进行数据库调用怎么办?这突然成为一个大问题,因为它是非同步的,并且位于您的组件内部。当然,这不是一个新问题:在官方 React 存储库中查看它。
您必须进行渲染才能确定需要哪些依赖项——这些依赖项需要在运行时确定——并在提供给客户端之前获取这些依赖项。
现有解决方案
下面,我将回顾当前提供的用于解决此问题的解决方案。
在我们开始之前,如果您想要生产环境的服务端渲染的 React 代码或通用应用程序,Next.js 是您的理想选择。它有效、简洁,并且有 Zeit 支持。
但是,它是有主见的,您必须使用他们的工具链,并且他们处理异步数据加载的方式不一定那么灵活。
查看 Next.js 存储库文档中的这段直接复制内容:
ReactDOM.render( <provider> store={store}></provider> <browserrouter></browserrouter> <app></app> > > , document.getElementById('root') )
getInitialProps
是关键,它返回一个 promise,该 promise 解析为一个填充 props 的对象,并且仅在页面上。最棒的是,这只是内置到他们的工具链中:添加它即可工作,无需任何工作!
那么如何获取数据库数据呢?您进行 API 调用。您不想?好吧,太糟糕了。(好的,您可以添加自定义内容,但您必须自己完全实现它。)但是,如果您考虑一下,这是一个非常合理且通常来说是良好的实践,因为否则,您的客户端仍然会进行相同的 API 调用,并且服务器上的延迟几乎可以忽略不计。
您还可以访问的内容受到限制——几乎只是请求对象;同样,这似乎是良好的实践,因为您无法访问您的状态,而您的状态在服务器和客户端上本来就不同。哦,如果您之前没有注意到,它只适用于顶级页面组件。
Redux Connect 是一个非常有主见的服务器端渲染器,具有不错的理念,但是如果您不使用他们描述的所有工具,这可能不适合您。此包有很多内容,但它非常复杂,尚未升级到 React Router v4。有很多设置,但让我们来看最重要的部分,只是为了学习一些经验教训:
ReactDOM.render( <provider> store={store}></provider> <browserrouter></browserrouter> <app></app> > > , document.getElementById('root') )
装饰器在 JavaScript 中不是标准的。在撰写本文时,它们处于第 2 阶段,因此请谨慎使用。这只是添加高阶组件的另一种方式。这个想法很简单:密钥是传递给您的 props 的内容,然后您有一系列 promise,它们会解析并传入。这看起来不错。也许另一种选择就是这个:
import React from 'react' export default class extends React.Component { static async getInitialProps ({ req }) { return req ? { userAgent: req.headers['user-agent'] } : { userAgent: navigator.userAgent } } render () { return <div> Hello World {this.props.userAgent} </div> } }
使用 JavaScript 可以做到这一点,而且不会出现太多问题。
react-frontload 存储库没有很多文档或解释,但我所能获得的最佳理解可能来自测试(例如这个测试)和阅读源代码。当某些内容被挂载时,它会被添加到 promise 队列中,当该队列解析时,它会被提供服务。它所做的事情非常好,尽管很难推荐一些没有良好文档、维护或使用的内容:
// 1. 连接您的数据,类似于 react-redux @connect @asyncConnect([{ key: 'lunch', promise: ({ params, helpers }) => Promise.resolve({ id: 1, name: 'Borsch' }) }]) class App extends React.Component { render() { // 2. 将数据作为 props 访问 const lunch = this.props.lunch return ( <div>{lunch.name}</div> ) } }
寻找更好的解决方案
以上解决方案都没有真正符合我对库的灵活性和简单性的期望,因此我现在将介绍我自己的实现。目标不是编写包,而是让您了解如何根据您的用例编写自己的包。
此示例解决方案的存储库位于此处。
其背后的想法相对简单,尽管最终会产生相当多的代码。这是为了概述我们正在讨论的想法。
服务器必须渲染 React 代码两次,我们只会为此使用 renderToString
。我们希望在第一次和第二次渲染之间保持上下文。在我们的第一次渲染中,我们试图消除任何 API 调用、promise 和异步操作。在我们的第二次渲染中,我们希望获取我们获得的所有数据并将其放回我们的上下文中,从而渲染我们的工作页面以进行分发。这也意味着应用程序代码需要根据上下文执行操作(或不执行操作),例如是否在服务器上或浏览器上,以及在任一情况下是否正在获取数据。
此外,我们可以根据需要自定义它。在本例中,我们根据上下文更改状态代码和头部。
在您的代码中,您需要知道您是在服务器上还是在浏览器上工作,理想情况下,您希望对它进行复杂控制。使用 React Router,您可以获得一个静态上下文 prop,这很棒,所以我们将使用它。目前,我们只是添加了一个数据对象和请求数据,正如我们从 Next.js中学到的那样。我们的 API 在服务器和客户端之间有所不同,因此您需要提供一个服务器 API,最好与您的客户端 API 具有相似的接口:
ReactDOM.render( <provider> store={store}></provider> <browserrouter></browserrouter> <app></app> > > , document.getElementById('root') )
在第一次渲染之后,我们将获取那些挂起的 promise 并等待这些 promise 完成,然后重新渲染,更新上下文:
import React from 'react' export default class extends React.Component { static async getInitialProps ({ req }) { return req ? { userAgent: req.headers['user-agent'] } : { userAgent: navigator.userAgent } } render () { return <div> Hello World {this.props.userAgent} </div> } }
快速从我们的服务器跳转到应用程序代码:在我们任何具有路由器连接的组件中,我们现在都可以获得它:
// 1. 连接您的数据,类似于 react-redux @connect @asyncConnect([{ key: 'lunch', promise: ({ params, helpers }) => Promise.resolve({ id: 1, name: 'Borsch' }) }]) class App extends React.Component { render() { // 2. 将数据作为 props 访问 const lunch = this.props.lunch return ( <div>{lunch.name}</div> ) } }
哇,这有很多复杂的代码。在这个阶段,您可能想要采用更具中继的方法,在该方法中,您将数据获取代码分离到另一个组件中。
此组件由您可能熟悉的组件构成——渲染步骤和 componentWillMount
步骤。四阶段 if 语句处理不同的状态——预取、后取、预渲染和后端渲染。我们还在数据加载后添加到头部。
最后,还有一个获取数据步骤。理想情况下,您的 API 和数据库具有相同的 API,这使得执行相同。您可能希望将这些放入 Thunk 或 Saga 中的操作中,以使其更具可扩展性。
查看文章“服务端 React 渲染”和存储库 React 服务端渲染以了解更多信息。请记住,您仍然需要处理数据未加载的状态!您只会在第一次加载时进行服务器端渲染,因此您将在后续页面上显示加载屏幕。
我们需要将任何预取数据作为页面请求的一部分发送,因此我们将添加一个脚本标签:
@asyncConnect([{ lunch: ({ params, helpers }) => Promise.resolve({ id: 1, name: 'Borsch' }) }])
然后我们需要将其添加到我们的搜索和替换中。但是,HTML 使用非常基本的脚本标签查找器,因此如果您有脚本标签,则需要对其进行 base-64 编码。此外,不要忘记我们的头部标签!
const App = () => ( <frontload>isServer</frontload> <component1> entityId='1' store={store}></component1> > ) return frontloadServerRender(() => ( render(<app></app>) )).then((serverRenderedMarkup) => { console.log(serverRenderedMarkup) })
我们还处理状态代码更改——例如,对于 404——因此,如果您有 404 页面,您可以这样做:
const context = {data: {}, head: [], req, api} const store = configureStore() renderToString( <provider> store={store}></provider> <staticrouter> location={req.url} context={context}> <app></app> > > )
总结
如果您不确定自己在做什么,只需使用 Next.js。它专为服务端渲染和通用应用程序而设计,或者如果您希望手动执行所有操作的灵活性,则可以按照您想要的方式进行。一个例子可能包括您在子组件中而不是在页面级别进行数据获取。
希望本文能帮助您入门!不要忘记查看 GitHub 存储库以获取可行的实现。
关于异步 API 和服务端渲染 React 的常见问题解答 (FAQ)
服务端渲染 (SSR) 和客户端渲染 (CSR) 是渲染网页的两种不同方法。在 SSR 中,服务器会响应请求生成页面的完整 HTML,然后将其发送到客户端。这会导致更快的初始页面加载时间,并且有利于 SEO。但是,这可能会导致页面转换速度变慢,因为每次请求都需要渲染整个页面。另一方面,CSR 意味着渲染是在浏览器中使用 JavaScript 进行的。这会导致初始页面加载时间变慢,但页面转换速度更快,因为只需要重新渲染必要的组件。
要在客户端渲染的 React 应用程序中发出服务器端请求,您可以使用 fetch API 或 axios 等库。您可以在 componentDidMount
生命周期方法中或在使用函数组件时在 useEffect
挂钩内发出请求。然后可以将响应设置为状态并在您的组件中使用。
这可能是由于 React 批处理状态更新的方式造成的。如果您在 React 组件内更新全局变量,则由于 setState
的异步性质,它可能会更新两次。为避免这种情况,您可以使用 setState
的函数形式,这可以确保状态更新基于先前状态,而不是当前状态。
要在服务端渲染的 React 中使用异步 API,您可以在服务器端代码中使用 async/await
语法。这允许您在渲染页面之前等待 API 响应。您可以使用 axios 等库来发出 API 请求。
服务端渲染在 React 中有很多好处。它提高了初始页面加载时间,这可以带来更好的用户体验。它还提高了 SEO,因为搜索引擎爬虫可以更容易地索引服务端渲染的内容。此外,它允许更一致的初始状态,因为相同的代码在服务器和客户端上运行。
您可以使用 try/catch
块在异步函数中处理错误。这允许您捕获在发出 API 请求时发生的任何错误并适当地处理它们,例如通过渲染错误消息。
是的,您可以在服务端渲染的 React 中使用钩子。但是,请记住,钩子只能在函数组件中使用,而不能在类组件中使用。此外,某些钩子(例如 useEffect
)不会在服务器上运行,因此您需要确保您的代码可以处理这种情况。
提高服务端渲染的 React 应用程序性能的方法有很多。您可以使用代码分割,只为每个页面加载必要的代码。您还可以使用缓存来避免重新渲染未更改的页面。此外,优化服务器端代码可以帮助提高性能。
您可以使用 Jest 和 React Testing Library 等测试库来测试您的服务端渲染的 React 应用程序。这些库允许您隔离测试组件并确保它们正确渲染。
是的,Next.js 是一个用于 React 的框架,它开箱即用地支持服务端渲染。它提供了一个简单的服务端渲染 API,还支持静态站点生成和客户端渲染。
以上是处理服务器渲染的反应中的异步API的详细内容。更多信息请关注PHP中文网其他相关文章!