使用带有 NextJS 的 Apollo Client 时服务器端渲染数据?
Posted
技术标签:
【中文标题】使用带有 NextJS 的 Apollo Client 时服务器端渲染数据?【英文标题】:Server-side render data when using Apollo Client with NextJS? 【发布时间】:2020-01-21 02:39:29 【问题描述】:我的组件目前在浏览器上水合,这是我想避免的。当您访问该链接时,我希望它预先包含它需要显示的所有数据,即在服务器上呈现。目前,该组件如下所示:
import graphql from "react-apollo";
import gql from 'graphql-tag';
import withData from "../../apollo/with-data";
import getPostsQuery from '../../apollo/schemas/getPostsQuery.graphql';
const renderers =
paragraph: (props) => <Typography variant="body2" gutterBottom ...props />,
;
const GET_POSTS = gql`$getPostsQuery`;
const PostList = (data: error, loading, posts) =>
let payload;
if(error)
payload = (<div>There was an error!</div>);
else if(loading)
payload = (<div>Loading...</div>);
else
payload = (
<>
posts.map((post) => (
<div>
<div>post.title</div>
<div>post.body</div>
</div>
))
</>
);
return payload;
;
export default withData(graphql(GET_POSTS)(PostList));
如您所见,它首先在后台获取帖子时显示文本Loading...
。我不想要那个。我希望它已经与获取的数据一起预水合。
作为参考,我的 Apollo 初始化如下所示:
// apollo/with-data.js
import React from "react";
import PropTypes from "prop-types";
import ApolloProvider, getDataFromTree from "react-apollo";
import initApollo from "./init-apollo";
export default ComposedComponent =>
return class WithData extends React.Component
static displayName = `WithData($ComposedComponent.displayName)`;
static propTypes =
serverState: PropTypes.object.isRequired
;
static async getInitialProps(ctx)
const headers = ctx.req ? ctx.req.headers : ;
let serverState = ;
// Evaluate the composed component's getInitialProps()
let composedInitialProps = ;
if (ComposedComponent.getInitialProps)
composedInitialProps = await ComposedComponent.getInitialProps(ctx);
// Run all graphql queries in the component tree
// and extract the resulting data
if (!process.browser)
const apollo = initApollo(headers);
// Provide the `url` prop data in case a graphql query uses it
const url = query: ctx.query, pathname: ctx.pathname ;
// Run all graphql queries
const app = (
<ApolloProvider client=apollo>
<ComposedComponent url=url ...composedInitialProps />
</ApolloProvider>
);
await getDataFromTree(app);
// Extract query data from the Apollo's store
const state = apollo.getInitialState();
serverState =
apollo:
// Make sure to only include Apollo's data state
data: state.data
;
return
serverState,
headers,
...composedInitialProps
;
constructor(props)
super(props);
this.apollo = initApollo(this.props.headers, this.props.serverState);
render()
return (
<ApolloProvider client=this.apollo>
<ComposedComponent ...this.props />
</ApolloProvider>
);
;
;
// apollo/init-apollo.js
import InMemoryCache from 'apollo-cache-inmemory';
import ApolloClient from 'apollo-client';
import ApolloLink from 'apollo-link';
import onError from 'apollo-link-error';
import HttpLink from 'apollo-link-http';
import fetch from 'isomorphic-fetch';
let apolloClient = null;
// Polyfill fetch() on the server (used by apollo-client)
if (!process.browser)
global.fetch = fetch;
const create = (headers, initialState) => new ApolloClient(
initialState,
link: ApolloLink.from([
onError(( graphQLErrors, networkError ) =>
if (graphQLErrors)
graphQLErrors.forEach(( message, locations, path ) => console.log(
`[GraphQL error]: Message: $message, Location: $locations, Path: $path`,
));
if (networkError) console.log(`[Network error]: $networkError`);
),
new HttpLink(
// uri: 'https://dev.schandillia.com/graphql',
uri: process.env.CMS,
credentials: 'same-origin',
),
]),
s-s-rMode: !process.browser, // Disables forceFetch on the server (so queries are only run once)
cache: new InMemoryCache(),
);
export default function initApollo(headers, initialState = )
// Make sure to create a new client for every server-side request so that data
// isn't shared between connections (which would be bad)
if (!process.browser)
return create(headers, initialState);
// Reuse client on the client-side
if (!apolloClient)
apolloClient = create(headers, initialState);
return apolloClient;
更新:我尝试将https://github.com/zeit/next.js/tree/canary/examples/with-apollo 的官方withApollo 示例合并到我的项目中,但它在getDataFromTree()
上抛出了一个不变的错误:
元素类型无效:应为字符串(用于内置组件)或类/函数(用于复合组件)但得到:未定义。
对于/init/apollo.js
、/components/blog/PostList.jsx
和/pages/Blog/jsx
文件,我使用了与示例存储库中完全相同的代码。在我的具体情况下,唯一的区别是我有一个明确的_app.jsx
,内容如下:
/* eslint-disable max-len */
import '../static/styles/fonts.scss';
import '../static/styles/style.scss';
import '../static/styles/some.css';
import CssBaseline from '@material-ui/core/CssBaseline';
import ThemeProvider from '@material-ui/styles';
import jwt from 'jsonwebtoken';
import withRedux from 'next-redux-wrapper';
import App,
Container,
from 'next/app';
import Head from 'next/head';
import React from 'react';
import Provider from 'react-redux';
import makeStore from '../reducers';
import mainTheme from '../themes/main-theme';
import getSessIDFromCookies from '../utils/get-sessid-from-cookies';
import getLanguageFromCookies from '../utils/get-language-from-cookies';
import getUserTokenFromCookies from '../utils/get-user-token-from-cookies';
import removeFbHash from '../utils/remove-fb-hash';
class MyApp extends App
static async getInitialProps( Component, ctx )
let userToken;
let sessID;
let language;
if (ctx.isServer)
ctx.store.dispatch( type: 'UPDATEIP', payload: ctx.req.headers['x-real-ip'] );
userToken = getUserTokenFromCookies(ctx.req);
sessID = getSessIDFromCookies(ctx.req);
language = getLanguageFromCookies(ctx.req);
const dictionary = require(`../dictionaries/$language`);
ctx.store.dispatch( type: 'SETLANGUAGE', payload: dictionary );
if(ctx.res)
if(ctx.res.locals)
if(!ctx.res.locals.authenticated)
userToken = null;
sessID = null;
if (userToken && sessID) // TBD: validate integrity of sessID
const userInfo = jwt.verify(userToken, process.env.JWT_SECRET);
ctx.store.dispatch( type: 'ADDUSERINFO', payload: userInfo );
ctx.store.dispatch( type: 'ADDSESSION', payload: sessID ); // component will be able to read from store's state when rendered
const pageProps = Component.getInitialProps ? await Component.getInitialProps(ctx) : ;
return pageProps ;
componentDidMount()
// Remove the server-side injected CSS.
const jssStyles = document.querySelector('#jss-server-side');
if (jssStyles)
jssStyles.parentNode.removeChild(jssStyles);
// Register serviceWorker
if ('serviceWorker' in navigator) navigator.serviceWorker.register('/serviceWorker.js');
// Handle FB's ugly redirect URL hash
removeFbHash(window, document);
render()
const Component, pageProps, store = this.props;
return (
<Container>
<Head>
<meta name="viewport" content="user-scalable=0, initial-scale=1, minimum-scale=1, width=device-width, height=device-height, shrink-to-fit=no" />
<meta httpEquiv="X-UA-Compatible" content="IE=edge,chrome=1" />
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
<link rel="icon" type="image/png" sizes="194x194" href="/favicon-194x194.png" />
<link rel="icon" type="image/png" sizes="192x192" href="/android-chrome-192x192.png" />
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
<link rel="manifest" href="/site.webmanifest" />
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#663300" />
<meta name="msapplication-TileColor" content="#da532c" />
<meta name="msapplication-TileImage" content="/mstile-144x144.png" />
</Head>
<ThemeProvider theme=mainTheme>
/* CssBaseline kickstart an elegant, consistent, and simple baseline to build upon. */
<CssBaseline />
<Provider store=store>
<Component ...pageProps />
</Provider>
</ThemeProvider>
</Container>
);
export default withRedux(makeStore)(MyApp);
删除这个文件不是一个选项,因为这是我处理一些预加载 cookie 逻辑的地方。
供参考的回购位于https://github.com/amitschandillia/proost/tree/master/web
【问题讨论】:
您查看过 s-s-r 文档吗?可以使用来自服务器的数据恢复内存缓存:apollographql.com/docs/react/features/server-side-rendering。不确定在使用 next.js 时效果如何。 没错。我确实在这里浏览了他们的文档:apollographql.com/docs/react/features/server-side-rendering。但没有找到任何与 Next 等同构设置有关的内容。 只想说我最近(2020 年 2 月 28 日)成功使用了 github.com/zeit/next.js/tree/canary/examples/with-apollo 参考实现。我的版本:apollo-client@2.6.8、@apollo/react-s-s-r@3.1.3、next@9.2.2。建议使用 getMarkupFromTree 的 github 问题对我来说是浪费时间。 【参考方案1】:在使用 Next.js 和 Apollo 时,您需要实现两个关键目标:s-s-r 和缓存网络数据。 两者兼顾是一个艰难的平衡。但这是可能的。
方法是:
-
在您的 getInitialProps 函数中,使用 apolloclient 获取在页面加载时应该可见的数据。如果操作正确,数据将与 html ie (s-s-r) 一起检索,并且在您的初始页面加载时不会显示烦人的加载程序。
现在,如果您有一些页面数据需要编辑、添加或删除,并且您希望在更改后更新页面而不刷新页面,那么以上是不够的。 因为例如,如果您编辑数据,典型/推荐的 Apollo 方法是不做任何事情。阿波罗为您神奇地处理这一切。除了初始数据必须来自 apollo 缓存并且它必须有一个 Id 字段。 现在您已经直接从服务器加载了初始数据,很可能您没有从之前缓存的数据中读取数据。
因此需要下面的第 2 步来启用数据更改时数据的自动刷新。
-
您知道要编辑的任何数据都必须来自缓存。所以不要试图使用来自 getInitialProps 的数据来填充这些数据。相反,您可以使用 useQuery 或其等效项来使用与 getInitialProps 查询相同的 Graphql 查询来查询相同的数据。现在发生的情况是,阿波罗这次没有从网络中获取数据,而是从服务器上一次查询中获取数据。然后突然之间,你看到的不是加载......到处都是,而是立即加载数据。然后在您编辑数据的同时,它也会立即更新,因为数据首先是从缓存中填充的,现在更新会更改缓存的数据并自动刷新您的页面。
这样,您可以毫不费力地继续使用所有您喜欢的最新工具,例如 useQuery 和 getDataFromTree。
【讨论】:
【参考方案2】:一些有用的研究:
我相信你应该使用getMarkupFromTree
,正如本期建议的https://github.com/apollographql/react-apollo/issues/3251 以及如何实现https://github.com/***owski/react-apollo-hooks/issues/52。
看来,如果你想使用钩子,你需要 @***owski 的 react-apollo-hooks
包。
有些人说这个解决方案不起作用。一些人认为它有一些低效率,例如,它将整个标记呈现两次,一次由下一次,一次是为了获取阿波罗查询。作为回应,他们建议做一些事情,比如直接在 get initial props 中调用查询,这只是比应该做的更多的工作,因为 s-s-r 功能应该是开箱即用的。
【讨论】:
以上是关于使用带有 NextJS 的 Apollo Client 时服务器端渲染数据?的主要内容,如果未能解决你的问题,请参考以下文章
如何从 NextJS 服务器为 Apollo 客户端补充水分
Next JS 与 Apollo 和 MongoDB 的连接
如何在客户端 NextJS 中使用 Apollo GraphQL 订阅?