将Apollo Client与NextJS一起使用时,服务器端呈现数据吗?

时间:2019-09-20 03:55:02

标签: javascript reactjs graphql next.js apollo-client

我的组件当前在浏览器中水合,我希望避免这种情况。当您访问链接时,我希望它与需要显示的所有数据预先混合在一起,即在服务器上呈现。当前,该组件如下所示:

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',
    }),
  ]),
  ssrMode: !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上的withwithApollo官方示例合并到我的项目中,但是它在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

2 个答案:

答案 0 :(得分:2)

在使用Next.js和Apollo时,您需要实现两个关键的功能:SSR和缓存的网络数据。 两者都很难平衡。但是有可能。

方法是:

  1. 在您的getInitialProps函数中,使用apolloclient获取应该在页面加载时可见的数据。如果操作正确,数据将与HTML一起(SSR)一起检索,并且初始页面加载中不会显示烦人的加载器。

现在,如果您需要编辑,添加或删除某些页面数据,并且希望在更改后更新页面而不刷新页面,那么上面的信息是不够的。 因为例如,如果您编辑数据,则典型/推荐的Apollo方法是什么也不做。阿波罗(Apollo)会为您神奇地处理一切。除了初始数据必须来自阿波罗缓存并且必须具有Id字段。 现在,当您直接从服务器加载初始数据时,很可能是您没有从以前缓存的数据中读取数据。

因此,需要执行下面的步骤2才能在数据更改时启用数据的自动刷新。

  1. 您知道要编辑的任何数据都必须来自缓存。因此,不要试图使用getInitialProps中的数据来填充此类数据。相反,您可以使用useQuery或其等效项,使用与用于getInitialProps查询相同的Graphql查询来查询相同的数据。现在发生的是,阿波罗这次没有从网络中获取数据,而是从服务器中的上一个查询中获取数据。然后突然之间,您看到的是即时加载的数据,而不是到处看到加载的信息。然后,在编辑数据的同时,它也会立即更新,因为首先是从缓存中填充了数据,现在更新会更改缓存的数据并自动刷新页面。

这样,您将继续使用所有您喜欢的最新工具,例如useQuery和getDataFromTree。

答案 1 :(得分:0)

一些研究可以帮助您

我相信您应该使用getMarkupFromTree,如本期https://github.com/apollographql/react-apollo/issues/3251所示,以及如何实现https://github.com/trojanowski/react-apollo-hooks/issues/52

看来,如果要使用钩子,则需要@trojanowski的react-apollo-hooks软件包。

有些人说此解决方案无效。有人认为它有一些不足之处,例如,它将整个标记渲染两次,一次一次,一次一次,以获取阿波罗查询。作为回应,他们建议做一些事情,例如在获取初始道具时直接调用查询,这比应该做的工作更多,因为ssr功能应该可以立即使用。