Apollo 提供了两种技术,可让你的应用快速加载,为用户避免不必要的延迟:

  • Store 覆水,它将允许你的初始查询集立即返回数据,而无需服务端请求。
  • 服务端渲染,它将服务端渲染好的初始 HTML 视图发送给客户端。

你可以使用这些技术中的一种或两种来提供更好的用户体验。

Store 覆水

对于能在客户端渲染 UI 之前于服务端执行查询的应用,Apollo 运行你为其设置数据的初始状态。这有时被称为覆水,因为数据被序列化并被包含在初始 HTML 代码中时,称数据被“脱水”。

例如,一个典型的方法是包括一个脚本标签,如下:

1
2
3
4
5
6
7
<script>
// `initialState` 应该具有 Apollo store 状态的形状。确保只包括必要数据,例如:
// const initialState = {[client.reduxRootKey]: {
// data: client.store.getState()[client.reduxRootKey].data
// }};
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 客户端实例保持谨慎,以确保一切都能工作完好:

  1. 当在服务器上创建 Apollo 客户端实例时,需要设置网络接口才能正确连接到 API 服务器。这可能与你在客户端上的操作看起来有所不同,因为如果你在客户端上使用相对 URL,则可能需要在服务端使用绝对 URL。

  2. 由于你只需获取每个查询的结果一次,请将 ssrMode: true 选项传递给 Apollo 客户端构造函数,以避免重复强制请求。

  3. 你需要确保为每个请求创建一个新的客户端或 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
// 此示例使用 React Router v4,尽管它也能与支持 SSR 的其他路由库一块工作
import { ApolloClient, createNetworkInterface, ApolloProvider } from 'react-apollo';
import Express from 'express';
import { StaticRouter } from 'react-router';
import Layout from './routes/Layout';
// 请注意,您不必使用任何特定的 http 服务器,但我们在此示例中使用 Express
const app = new Express();
app.use((req, res) => {
const client = new ApolloClient({
ssrMode: true,
// 请注意,这是 SSR 服务器用于连接到 API 服务器的接口,因此我们需要确保它不被防火墙屏蔽
networkInterface: createNetworkInterface({
uri: 'http://localhost:3010',
opts: {
credentials: 'same-origin',
headers: {
cookie: req.header('Cookie'),
},
},
}),
});
const context = {};
// 客户端应用将会替换为 <BrowserRouter>
const app = (
<ApolloProvider client={client}>
<StaticRouter location={req.url} context={context}>
<Layout />
</StaticRouter>
</ApolloProvider>
);
// 渲染代码(下面可见)
});
app.listen(basePort, () => console.log( // eslint-disable-line no-console
`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
// ./routes/index.js
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(() => {
// 我们准备好渲染真实的DOM
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 }, // 不会在 SSR 期间调用
});

使用 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();
});
Edit on GitHub