使用带有 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 订阅?

NextJS 中的 Apollo 分页

使用 NextJs 将 JWT 令牌传递到 Apollo 上下文

NextJS、Apollo、WPGraphQL & 组合或检索超过 100 条记录