From 63ddaa7f6ee88c0cfb8b4eddc06da1b849bd292d Mon Sep 17 00:00:00 2001 From: Sergey Yarkov Date: Sun, 8 Mar 2020 15:23:52 +0300 Subject: [PATCH] :zap: fixed SSR --- docker-compose.yml | 14 ++-- graphql/index.js | 8 +++ lib/apollo.js | 151 ++++++++++++++++++++++++++++++++++++++++--- next.config.js | 3 - nginx/Dockerfile | 2 +- package-lock.json | 38 +++-------- package.json | 5 +- pages/api/graphql.js | 10 ++- pages/articles.js | 4 +- 9 files changed, 176 insertions(+), 59 deletions(-) create mode 100644 graphql/index.js diff --git a/docker-compose.yml b/docker-compose.yml index 808f631..e086e7a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -5,10 +5,10 @@ services: restart: always build: . ports: - - '3000:3000' - # webserver: - # container_name: webserver - # restart: unless-stopped - # build: nginx - # ports: - # - '80:80' \ No newline at end of file + - '3000' + webserver: + container_name: webserver + restart: unless-stopped + build: nginx + ports: + - '80:80' \ No newline at end of file diff --git a/graphql/index.js b/graphql/index.js new file mode 100644 index 0000000..5b24eb2 --- /dev/null +++ b/graphql/index.js @@ -0,0 +1,8 @@ +import { makeExecutableSchema } from 'graphql-tools' +import { typeDefs } from './typeDefs' +import { resolvers } from './resolvers' + +export const schema = makeExecutableSchema({ + typeDefs, + resolvers +}) diff --git a/lib/apollo.js b/lib/apollo.js index e72d87b..b8b95b9 100644 --- a/lib/apollo.js +++ b/lib/apollo.js @@ -1,13 +1,148 @@ -import { withData } from 'next-apollo' -import { HttpLink } from 'apollo-boost' +import React from 'react' +import Head from 'next/head' +import { ApolloProvider } from '@apollo/react-hooks' +import { ApolloClient } from 'apollo-client' +import { InMemoryCache } from 'apollo-cache-inmemory' -const config = { - link: new HttpLink({ - uri: process.env.GRAPHQL_API_URI, - opts: { - credentials: 'same-origin' +let globalApolloClient = null + +/** + * Creates and provides the apolloContext + * to a next.js PageTree. Use it by wrapping + * your PageComponent via HOC pattern. + * @param {Function|Class} PageComponent + * @param {Object} [config] + * @param {Boolean} [config.ssr=true] + */ +export function withApollo (PageComponent, { ssr = true } = {}) { + const WithApollo = ({ apolloClient, apolloState, ...pageProps }) => { + const client = apolloClient || initApolloClient(apolloState) + return ( + + + + ) + } + + // Set the correct displayName in development + if (process.env.NODE_ENV !== 'production') { + const displayName = + PageComponent.displayName || PageComponent.name || 'Component' + + if (displayName === 'App') { + console.warn('This withApollo HOC only works with PageComponents.') } + + WithApollo.displayName = `withApollo(${displayName})` + } + + if (ssr || PageComponent.getInitialProps) { + WithApollo.getInitialProps = async ctx => { + const { AppTree } = ctx + + // Initialize ApolloClient, add it to the ctx object so + // we can use it in `PageComponent.getInitialProp`. + const apolloClient = (ctx.apolloClient = initApolloClient()) + + // Run wrapped getInitialProps methods + let pageProps = {} + if (PageComponent.getInitialProps) { + pageProps = await PageComponent.getInitialProps(ctx) + } + + // Only on the server: + if (typeof window === 'undefined') { + // When redirecting, the response is finished. + // No point in continuing to render + if (ctx.res && ctx.res.finished) { + return pageProps + } + + // Only if ssr is enabled + if (ssr) { + try { + // Run all GraphQL queries + const { getDataFromTree } = await import('@apollo/react-ssr') + await getDataFromTree( + + ) + } catch (error) { + // Prevent Apollo Client GraphQL errors from crashing SSR. + // Handle them in components via the data.error prop: + // https://www.apollographql.com/docs/react/api/react-apollo.html#graphql-query-data-error + console.error('Error while running `getDataFromTree`', error) + } + + // getDataFromTree does not call componentWillUnmount + // head side effect therefore need to be cleared manually + Head.rewind() + } + } + + // Extract query data from the Apollo store + const apolloState = apolloClient.cache.extract() + + return { + ...pageProps, + apolloState + } + } + } + + return WithApollo +} + +/** + * Always creates a new apollo client on the server + * Creates or reuses apollo client in the browser. + * @param {Object} initialState + */ +function initApolloClient (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 (typeof window === 'undefined') { + return createApolloClient(initialState) + } + + // Reuse client on the client-side + if (!globalApolloClient) { + globalApolloClient = createApolloClient(initialState) + } + + return globalApolloClient +} + +/** + * Creates and configures the ApolloClient + * @param {Object} [initialState={}] + */ +function createApolloClient (initialState = {}) { + const ssrMode = typeof window === 'undefined' + const cache = new InMemoryCache().restore(initialState) + + // Check out https://github.com/zeit/next.js/pull/4611 if you want to use the AWSAppSyncClient + return new ApolloClient({ + ssrMode, + link: createIsomorphLink(), + cache }) } -export default withData(config) +function createIsomorphLink () { + if (typeof window === 'undefined') { + const { SchemaLink } = require('apollo-link-schema') + const { schema } = require('../graphql') + return new SchemaLink({ schema }) + } else { + const { HttpLink } = require('apollo-link-http') + return new HttpLink({ + uri: '/api/graphql', + credentials: 'same-origin' + }) + } +} diff --git a/next.config.js b/next.config.js index 5a3fc12..d6ef8db 100644 --- a/next.config.js +++ b/next.config.js @@ -9,8 +9,5 @@ module.exports = withSass({ config.optimization.minimizer.push(new OptimizeCSSAssetsPlugin({})) return config - }, - env: { - GRAPHQL_API_URI: 'http://localhost:3000/api/graphql' } }) diff --git a/nginx/Dockerfile b/nginx/Dockerfile index 8dc07a4..e65ef81 100644 --- a/nginx/Dockerfile +++ b/nginx/Dockerfile @@ -2,4 +2,4 @@ FROM nginx RUN rm /etc/nginx/conf.d/default.conf -COPY default.conf /etc/nginx/conf.d/ \ No newline at end of file +COPY default.conf /etc/nginx/conf.d/ diff --git a/package-lock.json b/package-lock.json index aed6789..62c7468 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1911,6 +1911,15 @@ "tslib": "^1.9.3" } }, + "apollo-link-schema": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/apollo-link-schema/-/apollo-link-schema-1.2.4.tgz", + "integrity": "sha512-tLpqd2PSddXJDqA25aERh1U6SsZZBX1isPXSa8FbxvU4Dd8n/DmCOE4mqHyFasgjqLN1zFWL7jgf964LPrAp7A==", + "requires": { + "apollo-link": "^1.2.13", + "tslib": "^1.9.3" + } + }, "apollo-server-caching": { "version": "0.5.1", "resolved": "https://registry.npmjs.org/apollo-server-caching/-/apollo-server-caching-0.5.1.tgz", @@ -6932,15 +6941,6 @@ "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" }, - "isomorphic-unfetch": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/isomorphic-unfetch/-/isomorphic-unfetch-3.0.0.tgz", - "integrity": "sha512-V0tmJSYfkKokZ5mgl0cmfQMTb7MLHsBMngTkbLY0eXvKqiVRRoZP04Ly+KhKrJfKtzC9E6Pp15Jo+bwh7Vi2XQ==", - "requires": { - "node-fetch": "^2.2.0", - "unfetch": "^4.0.0" - } - }, "isstream": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", @@ -8214,31 +8214,11 @@ } } }, - "next-apollo": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/next-apollo/-/next-apollo-3.1.10.tgz", - "integrity": "sha512-VX64IW7h6Rdg+Ma0y8ryAIz7IfyLaz6Nd4VITuhfXFZ3nj51blcjRR6kc4E945SPk2WdrIMlLRZubgPRAgh9Ug==", - "requires": { - "@babel/runtime": "^7.4.5", - "isomorphic-unfetch": "^3.0.0", - "prop-types": "15.7.2", - "prop-types-exact": "1.2.0", - "url": "0.11.0" - } - }, "next-tick": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.0.0.tgz", "integrity": "sha1-yobR/ogoFpsBICCOPchCS524NCw=" }, - "next-with-apollo": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/next-with-apollo/-/next-with-apollo-5.0.0.tgz", - "integrity": "sha512-JcMecKv6XtzsLf1hPKrbxM6g9sygnGdQugglmCDkfWhHZt1yziPa+g515qtw7in8A7Ed5W9PdaaaUn7CWWQ71w==", - "requires": { - "isomorphic-unfetch": "^3.0.0" - } - }, "nice-try": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", diff --git a/package.json b/package.json index b0c915a..c694a20 100644 --- a/package.json +++ b/package.json @@ -12,12 +12,11 @@ "@apollo/react-ssr": "^3.1.3", "@zeit/next-sass": "^1.0.1", "apollo-boost": "^0.4.7", + "apollo-link-schema": "^1.2.4", "apollo-server-micro": "^2.10.1", "graphql-tag": "^2.10.3", - "isomorphic-unfetch": "^3.0.0", + "graphql-tools": "^4.0.7", "next": "9.2.2", - "next-apollo": "^3.1.10", - "next-with-apollo": "^5.0.0", "node-sass": "^4.13.1", "optimize-css-assets-webpack-plugin": "^5.0.3", "react": "16.13.0", diff --git a/pages/api/graphql.js b/pages/api/graphql.js index adf10a3..c1759bc 100644 --- a/pages/api/graphql.js +++ b/pages/api/graphql.js @@ -1,9 +1,7 @@ -const { ApolloServer, gql } = require('apollo-server-micro') -const { typeDefs } = require('../../graphql/typeDefs') -const { resolvers } = require('../../graphql/resolvers') +import { ApolloServer } from 'apollo-server-micro' +import { schema } from '../../graphql' -const apolloServer = new ApolloServer({ typeDefs, resolvers }) -const handler = apolloServer.createHandler({ path: '/api/graphql' }) +const apolloServer = new ApolloServer({ schema }) export const config = { api: { bodyParser: false } } -export default handler +export default apolloServer.createHandler({ path: '/api/graphql' }) diff --git a/pages/articles.js b/pages/articles.js index 31e70f9..5560b3d 100644 --- a/pages/articles.js +++ b/pages/articles.js @@ -2,7 +2,7 @@ import Layout from '../components/Layout/Layout' import SEO from '../components/SEO/SEO' import Query from '../components/Query/query' import ARTICLES_QUERY from '../apollo/queries/articles/articles' -import withData from '../lib/apollo' +import { withApollo } from '../lib/apollo' const Articles = () => { return ( @@ -31,4 +31,4 @@ const Articles = () => { ) } -export default withData(Articles) +export default withApollo(Articles)