服务器端渲染
服务器端渲染(SSR)是现代Web应用的性能优化。它允许您在服务器上渲染应用程序的初始状态为原始HTML和CSS之后再将其发送到浏览器。这意味着用户不必等到他们的浏览器下载和初始化React(或Angular,Vue等)内容才可用:
Apollo客户端提供了一种方便的API来支持服务器端渲染,包括一个执行所有用于渲染组件树的GraphQL查询的函数。您不需要对查询进行任何修改即可支持此API。
与客户端渲染的区别
当您在服务器端渲染您的React应用程序时,大部分代码与其客户端对应版本相同,但也存在一些重要的例外:
(在React的Router的情况下,您需要用
静态路由器(StaticRouter)组件代替在客户端使用的浏览器路由器(BrowserRouter)。)您需要在适用的任何地方将相对URL替换为绝对URL。
初始化Apollo客户端
以下是一个服务器端初始化Apollo客户端的示例:
import {ApolloClient,createHttpLink,InMemoryCache} from '@apollo/client';const client = new ApolloClient({ssrMode: true,link: createHttpLink({uri: 'https://:3010',credentials: 'same-origin',headers: {cookie: req.header('Cookie'),},}),cache: new InMemoryCache(),});
您将会注意到与典型客户端初始化的区别有几点
您需要提供
ssrMode: true。这可以防止 Apollo Client 无谓地重新查询查询,同时它还使您能够使用下面的getDataFromTree函数。而不是提供一个
uri选项,您需要将HttpLink实例提供给link选项。这使得您可以在服务器端向 GraphQL 端点发送请求时指定任何所需的认证详情。请注意,您还可能需要确保您的 GraphQL 端点被配置为接受来自 SSR 服务器的 GraphQL 操作(例如,通过白名单它的域名或 IP)。
您的 GraphQL 端点由执行 SSR 的同一服务器托管是可能的,也是有效的。在这种情况下,Apollo Client 不需要通过网络请求来执行查询。有关详细信息,请参阅下面的 避免为本地查询使用网络。
示例
让我们看看在 Node.js 应用中 SSR 的一个示例。此示例使用 Express 和 React Router v4,尽管它可以与任何服务器中间件以及支持 SSR 的任何路由器一起工作。
首先,这是一个示例 app.js 文件,不包括将 React 渲染为 HTML 和 CSS 的代码:
直到现在,每当这个示例服务器收到请求时,它首先初始化 Apollo Client,然后创建一个 React 树,该树被包裹在 ApolloProvider 和 StaticRouter 组件中。该树的 内容取决于请求的路径和 StaticRouter 定义的路线。
⚠️ 注意事项
为每个请求创建一个新的Apollo Client实例非常重要。否则,您对请求的响应可能包括来自先前请求的敏感缓存查询结果。
使用 getDataFromTree 执行查询
由于我们的应用程序使用 Apollo Client,React 树中的某些组件可能使用 useQuery 钩子执行 GraphQL 查询。我们可以通过 getDataFromTree 函数指导 Apollo Client 执行树组件所需的全部查询。
此函数遍历整个树并执行它遇到的每个必需的 查询(包括嵌套查询)。它返回一个 Promise。当Apollo Client缓存中所有结果数据都准备好时,Promise得到解决。
当 Promise 解决时,您就准备好渲染React树并返回它,以及当前的Apollo Client缓存状态。
注意,如果您将React树直接渲染为字符串(而不是下面的基于组件的示例),则需要使用 renderToStringWithData 而不是 getDataFromTree。这将确保客户端React水解通过使用 ReactDOMServer.renderToString 来生成字符串。
以下代码替换了示例中 app.use 调用中的 TODO 注释:
// Add this import to the top of the fileimport { getDataFromTree } from "@apollo/client/react/ssr";// Replace the TODO with thisgetDataFromTree(App).then((content) => {// Extract the entirety of the Apollo Client cache's current stateconst initialState = client.extract();// Add both the page content and the cache state to a top-level componentconst html = <Html content={content} state={initialState} />;// Render the component to static markup and return itres.status(200);res.send(`<!doctype html>\n${ReactDOM.renderToStaticMarkup(html)}`);res.end();});
渲染为静态标记的顶级 Html 组件的定义可能如下所示:
export function Html({ content, state }) {return (<html><body><div id="root" dangerouslySetInnerHTML={{ __html: content }} /><script dangerouslySetInnerHTML={{__html: `window.__APOLLO_STATE__=${JSON.stringify(state).replace(/</g, '\\u003c')};`,}} /></body></html>);}
这将导致渲染的React树作为 root div 的子项,并将初始缓存状态分配给 __APOLLO_STATE__ 全局对象。
此示例中的 replace 调用是通过转义字符串字面量中的 < 字符来执行的,以防止通过在字符串字面量中存在 </script> 进行的跨站脚本攻击。
重新激活客户端缓存
尽管服务器端缓存的状?态可供 __APOLLO_STATE__ 使用,但它还不在 客户端 缓存中。 InMemoryCache提供了一个有用的 restore 函数,可以用来响应从其他缓存实例获取的数据恢复其状态。
您可以在Apollo Client客户端初始化中这样重新激活缓存:
const client = new ApolloClient({cache: new InMemoryCache().restore(window.__APOLLO_STATE__),uri: 'https://example.com/graphql'});
现在当客户端应用程序运行其初始查询时,数据被立即返回,因为它们已经在缓存中啦!
初始化期间覆盖获取策略
如果你的初始查询使用了 网络优先 或 缓存和网络 获取策略,你可以向 Apollo Client 提供一个 ssrForceFetchDelay 选项,以在初始化期间跳过强制获取这些查询。这样,即使这些查询最初也只使用缓存:
const client = new ApolloClient({cache: new InMemoryCache().restore(window.__APOLLO_STATE__),link,ssrForceFetchDelay: 100, // in milliseconds});
避免本地查询使用网络
如果你的 GraphQL 端点是由你渲染的同一服务器托管,你可以在执行 SSR 查询时选择性地避免使用网络。如果服务器环境中(例如在 Heroku 上)localhost 被防火墙阻止,这将非常有用。
一个选项是使用 Apollo Link 通过使用本地 GraphQL 模式 来获取数据,而不是进行网络请求。为了实现这一点,在服务器上创建 Apollo Client 时,你应使用 SchemaLink 而不是使用 createHttpLink。 SchemaLink 使用您的模式和上下文立即运行查询,而不进行任何额外的网络请求:
import { ApolloClient, InMemoryCache } from '@apollo/client'import { SchemaLink } from '@apollo/client/link/schema';// ...const client = new ApolloClient({ssrMode: true,// Instead of "createHttpLink" use SchemaLink herelink: new SchemaLink({ schema }),cache: new InMemoryCache(),});
跳过一个查询
如果您想要在 SSR 期间故意跳过某个特定的 查询,可以在该查询的选项中包含 ssr: false。通常这意味着该组件在服务器上以“正在加载”状态渲染。例如:
function withClientOnlyUser() {useQuery(GET_USER_WITH_ID, { ssr: false });return <span>My query won't be run on the server</span>;}