Apollo 提供了两种技术,可让你的应用快速加载,为用户避免不必要的延迟:
- Store 覆水,它将允许你的初始查询集立即返回数据,而无需服务端请求。
- 服务端渲染,它将服务端渲染好的初始 HTML 视图发送给客户端。
你可以使用这些技术中的一种或两种来提供更好的用户体验。
Store 覆水
对于能在客户端渲染 UI 之前于服务端执行查询的应用,Apollo 运行你为其设置数据的初始状态。这有时被称为覆水,因为数据被序列化并被包含在初始 HTML 代码中时,称数据被“脱水”。
例如,一个典型的方法是包括一个脚本标签,如下:
1 2 3 4 5 6 7
| <script> window.__APOLLO_STATE__ = initialState; </script>
|
然后可以使用从服务器传来的初始状态来为客户端执行覆水操作:
1 2 3
| const client = new ApolloClient({ initialState: window.__APOLLO_STATE__, });
|
我们将在下面演示如何使用 Node 和 react-apollo
的服务端渲染功能生成 HTML 和 Apollo store 的状态。但是,如果你通过其他方式渲染 HTML,则必须手动生成该状态。
如果你在 Apollo 外部使用 Redux,并且已经为 store 覆水,则应将 store 状态传递到 Store
构造函数。
然后,当客户端运行第一组查询时,数据将立即返回,因为它已经在 store 中!
如果你在某些初始查询中使用 forceFetch
,则可以在初始化期间传递 ssrForceFetchDelay
选项来跳过强制获取,以便即使是这些查询也将使用缓存的数据源:
1 2 3 4
| const client = new ApolloClient({ initialState: window.__APOLLO_STATE__, ssrForceFetchDelay: 100, });
|
服务端渲染
你可以使用 react-apollo
中内置的渲染函数在 Node 服务器上渲染整个基于 React 的 Apollo 应用。这些函数负责获取渲染组件树所需的所有查询。通常,你可以在 HTTP 服务器(如 Express)中使用这些功能。
不需要更改客户端查询来支持服务端渲染,因此你的基于 Apollo 的 React UI 支持开箱即用的 SSR。
服务端初始化
为了在服务器上渲染应用程序,你需要处理 HTTP 请求(使用像 Express 这样的服务端框架和支持服务端运行的路由库,如 React-Router),然后将应用渲染为一个字符串以传回响应。
我们将在下一部分中看到如何使用组件树并将其转换为一个字符串,但是你需要对如何在服务器上构建 Apollo 客户端实例保持谨慎,以确保一切都能工作完好:
当在服务器上创建 Apollo 客户端实例时,需要设置网络接口才能正确连接到 API 服务器。这可能与你在客户端上的操作看起来有所不同,因为如果你在客户端上使用相对 URL,则可能需要在服务端使用绝对 URL。
由于你只需获取每个查询的结果一次,请将 ssrMode: true
选项传递给 Apollo 客户端构造函数,以避免重复强制请求。
你需要确保为每个请求创建一个新的客户端或 store 实例,而不是为多个请求重复使用同一个客户端。否则,用户界面将变得过时,而且还会遇到认证的问题。
一旦你把它们放在一起,你会得到如下初始化代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
| import { ApolloClient, createNetworkInterface, ApolloProvider } from 'react-apollo'; import Express from 'express'; import { StaticRouter } from 'react-router'; import Layout from './routes/Layout'; const app = new Express(); app.use((req, res) => { const client = new ApolloClient({ ssrMode: true, networkInterface: createNetworkInterface({ uri: 'http://localhost:3010', opts: { credentials: 'same-origin', headers: { cookie: req.header('Cookie'), }, }, }), }); const context = {}; const app = ( <ApolloProvider client={client}> <StaticRouter location={req.url} context={context}> <Layout /> </StaticRouter> </ApolloProvider> ); }); app.listen(basePort, () => console.log( `App Server is now running on http://localhost:${basePort}` ));
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| // ./routes/Layout.js import { Route, Switch } from 'react-router'; import { Link } from 'react-router-dom'; import React from 'react'; // 路由单独定义在一个文件中利于客户端和服务端共用 import routes from './routes'; const Layout = () => <div> <nav> <ul> <li> <Link to="/">Home</Link> </li> <li> <Link to="/another">Another page</Link> </li> </ul> </nav> {/* React Router v4 中引入的新的 <Switch> https://reacttraining.com/react-router/web/api/Switch */} <Switch> {routes.map(route => <Route key={route.name} {...route} />)} </Switch> </div>; export default Layout;
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| import MainPage from './MainPage'; import AnotherPage from './AnotherPage'; const routes = [ { path: '/', name: 'home', exact: true, component: MainPage, }, { path: '/another', name: 'another', component: AnotherPage, }, ]; export default routes;
|
你可以查看 GitHunt 应用的 ui/server.js
以获取完整的代码示例。
接下来我们将看看渲染代码实际上是什么。
使用 getDataFromTree
getDataFromTree
方法使用 React 树,确定需要哪些查询来渲染它们,然后将其全部获取。如果你有嵌套查询,它会递归地放在整个树上。它返回一个 promise,当你的Apollo 客户端 store 中的数据准备就绪时可以被解析。
在 promise 解析之前,你的 Apollo 客户端 store 将被完全初始化,这意味着你的应用将立即呈现(因为所有查询都被预取),并且你可以在响应中返回字符串结果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| import { getDataFromTree } from "react-apollo" const client = new ApolloClient(....); getDataFromTree(app).then(() => { const content = ReactDOM.renderToString(app); const initialState = {[client.reduxRootKey]: client.getInitialState() }; const html = <Html content={content} state={initialState} />; res.status(200); res.send(`<!doctype html>\n${ReactDOM.renderToStaticMarkup(html)}`); res.end(); });
|
在这种情况下,你的 HTML 可能看起来像这样:
1 2 3 4 5 6 7 8 9 10 11 12
| function Html({ content, state }) { return ( <html> <body> <div id="content" dangerouslySetInnerHTML={{ __html: content }} /> <script dangerouslySetInnerHTML={{ __html: `window.__APOLLO_STATE__=${JSON.stringify(state).replace(/</g, '\\u003c')};`, }} /> </body> </html> ); }
|
避免本地查询的使用外网
如果你的 GraphQL 节点与你做服务端渲染的是在同一台服务器上,则可能希望在进行 SSR 查询时避免使用外网。特别是,如果本地主机在你的生产环境(例如 Heroku )上配置了防火墙,则这些查询的网络请求将不起作用。该问题的一个解决方案是 apollo-local-query 模块,它允许你为 Apollo 创建一个实际不使用外网的 networkInterface
。
跳过SSR查询
如果要在 SSR 期间有意地跳过查询,可以在查询选项中设置 ssr: false
。通常,这将意味着该组件在服务器上将处于加载状态。例如:
1 2 3
| const withClientOnlyUser = graphql(GET_USER_WITH_ID, { options: { ssr: false }, });
|
使用 renderToStringWithData
renderToStringWithData
方法简化了上面的内容,只需返回需要渲染的内容字符串即可。所以它略微减少了步骤:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| import { renderToStringWithData } from "react-apollo" const client = new ApolloClient(....); renderToStringWithData(app).then((content) => { const initialState = {[client.reduxRootKey]: client.getInitialState() }; const html = <Html content={content} state={initialState} />; res.status(200); res.send(`<!doctype html>\n${ReactDOM.renderToStaticMarkup(html)}`); res.end(); });
|