服务器端渲染
服务器端渲染(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://127.0.0.1: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>;}