🤖 pattern

This commit is contained in:
Sergey Yarkov 2020-03-16 20:52:06 +03:00
parent 143d6dc399
commit d845bf1672
20 changed files with 1183 additions and 263 deletions

3
.gitignore vendored
View File

@ -23,3 +23,6 @@
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# cms
cms

View File

@ -1,80 +1,6 @@
### This is a next-apollo-graphql setup to start develop your web site. This setup uses server side rendering.
## 🚀 Getting Started
To start the development server, you need to run the following commands below.
### Using Node
```bash
npm install
npm run dev
```
Open http://localhost:3000 with your browser to see the result.
Our application runs on port `3000`, and GraphQL server also runs on `localhost:3000/api/graphql`.
### Using Nginx
```bash
docker-compose build && docker-compose up
```
Our application runs on port `80`, and GraphQL server also runs on `localhost:80/api/graphql`.
## ⚡ Using Query
You can easily use `Query` component to send queries to the GraphQL server. Just pass your query to the `Query` component.
```jsx
<Query query={ARTICLES_QUERY}>
{({ data: { articles } }) => {
return <Articles articles={articles} />;
}}
</Query>
```
Use the `withData` HOC to wrap your pages.
```jsx
// ./frontend/pages/articles.js
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';
const Articles = () => {
return (
<>
<SEO pageTitle='Articles page' />
<Layout>
<h1>Articles page</h1>
<div>
<Query query={ARTICLES_QUERY}>
{({ data: { articles } }) => {
return articles.map(art => {
return (
<div key={art.id}>
<p>
title: {art.title} <br />
id: {art.id}
</p>
</div>
)
})
}}
</Query>
</div>
</Layout>
</>
)
}
export default withData(Articles)
```
## License:
[MIT](https://github.com/sergeyyarkov/next-apollo-graphql-setup/blob/master/LICENSE)
### This is a next-strapi-mongo setup to start develop your web site. This setup uses server side rendering.

18
apolloClient.js Normal file
View File

@ -0,0 +1,18 @@
import { ApolloClient } from 'apollo-client'
import { InMemoryCache } from 'apollo-cache-inmemory'
import { HttpLink } from 'apollo-link-http'
import fetch from 'isomorphic-unfetch'
export default function createApolloClient (initialState, ctx) {
// The `ctx` (NextPageContext) will only be present on the server.
// use it to extract auth headers (ctx.req) or similar.
return new ApolloClient({
ssrMode: Boolean(ctx),
link: new HttpLink({
uri: 'https://next-blog-example.herokuapp.com/graphql', // Server URL (must be absolute)
credentials: 'same-origin', // Additional fetch() options like `credentials` or `headers`
fetch
}),
cache: new InMemoryCache().restore(initialState)
})
}

View File

@ -12,3 +12,29 @@ services:
build: nginx
ports:
- '80:80'
strapi:
container_name: strapi
image: strapi/strapi
environment:
- DATABASE_CLIENT=mongo
- DATABASE_HOST=db
- DATABASE_PORT=27017
- DATABASE_NAME=strapi
- DATABASE_USERNAME=
- DATABASE_PASSWORD=
- DATABASE_AUTHENTICATION_DATABASE=strapi
ports:
- '1337:1337'
volumes:
- ./cms:/srv/app
depends_on:
- db
db:
container_name: mongo
image: mongo
environment:
- MONGO_INITDB_DATABASE=strapi
ports:
- '27017'
restart: always

View File

@ -1,11 +0,0 @@
const articles = [
{ id: 1, title: 'Article 1' },
{ id: 2, title: 'Article 2' },
{ id: 3, title: 'Article 3' },
{ id: 4, title: 'Article 4' },
{ id: 5, title: 'Article 5' }
]
module.exports = {
articles
}

View File

@ -1,8 +0,0 @@
import { makeExecutableSchema } from 'graphql-tools'
import { typeDefs } from './typeDefs'
import { resolvers } from './resolvers'
export const schema = makeExecutableSchema({
typeDefs,
resolvers
})

View File

@ -1,11 +0,0 @@
const { articles } = require('../data')
class Article {
static all () {
return articles
}
}
module.exports = {
Article
}

View File

@ -1,5 +0,0 @@
const { Article } = require('./Article')
module.exports = {
Article
}

View File

@ -1,11 +0,0 @@
const { Article } = require('../models')
const articlesResolvers = {
Query: {
articles: () => Article.all()
}
}
module.exports = {
articlesResolvers
}

View File

@ -1,7 +0,0 @@
const { articlesResolvers } = require('./articlesResolvers')
const resolvers = [articlesResolvers]
module.exports = {
resolvers
}

View File

@ -1,8 +0,0 @@
const { query } = require('./query')
const { articleType } = require('./types')
const typeDefs = [query, articleType]
module.exports = {
typeDefs
}

View File

@ -1,11 +0,0 @@
const { gql } = require('apollo-boost')
const query = gql`
type Query {
articles: [Article]
}
`
module.exports = {
query
}

View File

@ -1,12 +0,0 @@
const { gql } = require('apollo-boost')
const articleType = gql`
type Article {
id: ID
title: String
}
`
module.exports = {
articleType
}

View File

@ -1,5 +0,0 @@
const { articleType } = require('./articleType')
module.exports = {
articleType
}

View File

@ -1,22 +1,95 @@
import React from 'react'
import App from 'next/app'
import Head from 'next/head'
import { ApolloProvider } from '@apollo/react-hooks'
import { ApolloClient } from 'apollo-client'
import { InMemoryCache } from 'apollo-cache-inmemory'
import createApolloClient from '../apolloClient'
// On the client, we store the Apollo Client in the following variable.
// This prevents the client from reinitializing between page transitions.
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]
* Installs the Apollo Client on NextPageContext
* or NextAppContext. Useful if you want to use apolloClient
* inside getStaticProps, getStaticPaths or getServerSideProps
* @param {NextPageContext | NextAppContext} ctx
*/
export function withApollo (PageComponent, { ssr = true } = {}) {
export const initOnContext = ctx => {
const inAppContext = Boolean(ctx.ctx)
// We consider installing `withApollo({ ssr: true })` on global App level
// as antipattern since it disables project wide Automatic Static Optimization.
if (process.env.NODE_ENV === 'development') {
if (inAppContext) {
console.warn(
'Warning: You have opted-out of Automatic Static Optimization due to `withApollo` in `pages/_app`.\n' +
'Read more: https://err.sh/next.js/opt-out-auto-static-optimization\n'
)
}
}
// Initialize ApolloClient if not already done
const apolloClient =
ctx.apolloClient ||
initApolloClient(ctx.apolloState || {}, inAppContext ? ctx.ctx : ctx)
// We send the Apollo Client as a prop to the component to avoid calling initApollo() twice in the server.
// Otherwise, the component would have to call initApollo() again but this
// time without the context. Once that happens, the following code will make sure we send
// the prop as `null` to the browser.
apolloClient.toJSON = () => null
// Add apolloClient to NextPageContext & NextAppContext.
// This allows us to consume the apolloClient inside our
// custom `getInitialProps({ apolloClient })`.
ctx.apolloClient = apolloClient
if (inAppContext) {
ctx.ctx.apolloClient = apolloClient
}
return ctx
}
/**
* Always creates a new apollo client on the server
* Creates or reuses apollo client in the browser.
* @param {NormalizedCacheObject} initialState
* @param {NextPageContext} ctx
*/
const initApolloClient = (initialState, ctx) => {
// 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, ctx)
}
// Reuse client on the client-side
if (!globalApolloClient) {
globalApolloClient = createApolloClient(initialState, ctx)
}
return globalApolloClient
}
/**
* Creates a withApollo HOC
* that provides the apolloContext
* to a next.js Page or AppTree.
* @param {Object} withApolloOptions
* @param {Boolean} [withApolloOptions.ssr=false]
* @returns {(PageComponent: ReactNode) => ReactNode}
*/
export const withApollo = ({ ssr = false } = {}) => PageComponent => {
const WithApollo = ({ apolloClient, apolloState, ...pageProps }) => {
const client = apolloClient || initApolloClient(apolloState)
let client
if (apolloClient) {
// Happens on: getDataFromTree & next.js ssr
client = apolloClient
} else {
// Happens on: next.js csr
client = initApolloClient(apolloState, undefined)
}
return (
<ApolloProvider client={client}>
<PageComponent {...pageProps} />
@ -28,49 +101,53 @@ export function withApollo (PageComponent, { ssr = true } = {}) {
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())
const inAppContext = Boolean(ctx.ctx)
const { apolloClient } = initOnContext(ctx)
// Run wrapped getInitialProps methods
let pageProps = {}
if (PageComponent.getInitialProps) {
pageProps = await PageComponent.getInitialProps(ctx)
} else if (inAppContext) {
pageProps = await App.getInitialProps(ctx)
}
// Only on the server:
if (typeof window === 'undefined') {
const { AppTree } = ctx
// 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) {
// Only if dataFromTree is enabled
if (ssr && AppTree) {
try {
// Run all GraphQL queries
// Import `@apollo/react-ssr` dynamically.
// We don't want to have this in our client bundle.
const { getDataFromTree } = await import('@apollo/react-ssr')
await getDataFromTree(
<AppTree
pageProps={{
...pageProps,
apolloClient
}}
/>
)
// Since AppComponents and PageComponents have different context types
// we need to modify their props a little.
let props
if (inAppContext) {
props = { ...pageProps, apolloClient }
} else {
props = { pageProps: { ...pageProps, apolloClient } }
}
// Take the Next.js AppTree, determine which queries are needed to render,
// and fetch them. This method can be pretty slow since it renders
// your entire AppTree once for every query. Check out apollo fragments
// if you want to reduce the number of rerenders.
// https://www.apollographql.com/docs/react/data/fragments/
await getDataFromTree(<AppTree {...props} />)
} catch (error) {
// Prevent Apollo Client GraphQL errors from crashing SSR.
// Handle them in components via the data.error prop:
@ -84,65 +161,16 @@ export function withApollo (PageComponent, { ssr = true } = {}) {
}
}
// Extract query data from the Apollo store
const apolloState = apolloClient.cache.extract()
return {
...pageProps,
apolloState
// Extract query data from the Apollo store
apolloState: apolloClient.cache.extract(),
// Provide the client for ssr. As soon as this payload
// gets JSON.stringified it will remove itself.
apolloClient: ctx.apolloClient
}
}
}
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
})
}
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'
})
}
}

View File

@ -1,3 +1,4 @@
# Main
server {
listen 80;
server_name localhost;
@ -15,6 +16,46 @@ server {
location = /50x.html {
root /usr/share/nginx/html;
}
}
# CMS
server {
listen 80;
server_name dashboard.localhost;
location / {
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
proxy_pass http://strapi:1337;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
}
# GraphQL
server {
listen 80;
server_name api.localhost;
location /graphql {
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
proxy_pass http://strapi:1337/graphql;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
}

972
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -14,8 +14,10 @@
"apollo-boost": "^0.4.7",
"apollo-link-schema": "^1.2.4",
"apollo-server-micro": "^2.10.1",
"graphql-import": "^1.0.0-beta.2",
"graphql-tag": "^2.10.3",
"graphql-tools": "^4.0.7",
"isomorphic-unfetch": "^3.0.0",
"next": "9.2.2",
"node-sass": "^4.13.1",
"optimize-css-assets-webpack-plugin": "^5.0.3",

View File

@ -1,7 +0,0 @@
import { ApolloServer } from 'apollo-server-micro'
import { schema } from '../../graphql'
const apolloServer = new ApolloServer({ schema })
export const config = { api: { bodyParser: false } }
export default apolloServer.createHandler({ path: '/api/graphql' })

View File

@ -31,4 +31,4 @@ const Articles = () => {
)
}
export default withApollo(Articles)
export default withApollo({ ssr: true })(Articles)