mirror of
https://github.com/terribleplan/next.js.git
synced 2024-01-19 02:48:18 +00:00
Added a new example with relay modern and a graphql server with express. (#4670)
* Added a new example with relay modern and a graphql server with express. * removed .graphqlconfig file from with-relay-modern-server-express example
This commit is contained in:
parent
2fdd43c307
commit
a996fba09c
8
examples/with-relay-modern-server-express/.babelrc
Normal file
8
examples/with-relay-modern-server-express/.babelrc
Normal file
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"presets": [
|
||||
"next/babel"
|
||||
],
|
||||
"plugins": [
|
||||
"relay"
|
||||
]
|
||||
}
|
1
examples/with-relay-modern-server-express/.env
Normal file
1
examples/with-relay-modern-server-express/.env
Normal file
|
@ -0,0 +1 @@
|
|||
RELAY_SERVER=http://localhost:3000
|
2
examples/with-relay-modern-server-express/.gitignore
vendored
Normal file
2
examples/with-relay-modern-server-express/.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
__generated__/
|
||||
schema/schema.graphql
|
54
examples/with-relay-modern-server-express/README.md
Normal file
54
examples/with-relay-modern-server-express/README.md
Normal file
|
@ -0,0 +1,54 @@
|
|||
[![Deploy to now](https://deploy.now.sh/static/button.svg)](https://deploy.now.sh/?repo=https://github.com/zeit/next.js/tree/master/examples/with-relay-modern)
|
||||
|
||||
# Relay Modern Server Express Example
|
||||
|
||||
## How to use
|
||||
|
||||
### Using `create-next-app`
|
||||
|
||||
Execute [`create-next-app`](https://github.com/segmentio/create-next-app) with [Yarn](https://yarnpkg.com/lang/en/docs/cli/create/) or [npx](https://github.com/zkat/npx#readme) to bootstrap the example:
|
||||
|
||||
```bash
|
||||
npx create-next-app --example with-relay-modern-server-express with-relay-modern-server-express-app
|
||||
# or
|
||||
yarn create next-app --example with-relay-modern-server-express with-relay-modern-server-express-app
|
||||
```
|
||||
|
||||
### Download manually
|
||||
|
||||
Download the example [or clone the repo](https://github.com/zeit/next.js):
|
||||
|
||||
```bash
|
||||
curl https://codeload.github.com/zeit/next.js/tar.gz/canary | tar -xz --strip=2 next.js-canary/examples/with-relay-modern-server-express
|
||||
cd with-relay-modern-server-express
|
||||
```
|
||||
|
||||
Install it:
|
||||
|
||||
```bash
|
||||
npm install
|
||||
# or
|
||||
yarn
|
||||
```
|
||||
|
||||
Run the project (it runs automatically the Relay ahead-of-time compilation)
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
# or
|
||||
yarn dev
|
||||
```
|
||||
|
||||
Deploy it to the cloud with [now](https://zeit.co/now) ([download](https://zeit.co/download)):
|
||||
|
||||
```bash
|
||||
now
|
||||
```
|
||||
|
||||
## The idea behind the example
|
||||
|
||||
[Relay Modern](https://facebook.github.io/relay/docs/relay-modern.html) is a new version of Relay designed from the ground up to be easier to use, more extensible and, most of all, able to improve performance on mobile devices. Relay Modern accomplishes this with static queries and ahead-of-time code generation.
|
||||
|
||||
In this simple example, we integrate Relay Modern seamlessly with Next by wrapping our _pages_ inside a [higher-order component (HOC)](https://facebook.github.io/react/docs/higher-order-components.html). Using the HOC pattern we're able to pass down a query result data created by Relay into our React component hierarchy defined inside each page of our Next application. The HOC takes `options` argument that allows to specify a `query` that will be executed on the server when a page is being loaded.
|
||||
|
||||
This example implements a simple graphql server using express, showing a custom graphql server integrated to next.js and relay modern.
|
|
@ -0,0 +1,22 @@
|
|||
import React from 'react'
|
||||
import { createFragmentContainer, graphql } from 'react-relay'
|
||||
|
||||
const BlogPostPreview = props => {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<div>{props.post.title}</div>
|
||||
<div>{props.post.content}</div>
|
||||
<br />
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
|
||||
export default createFragmentContainer(BlogPostPreview, {
|
||||
post: graphql`
|
||||
fragment BlogPostPreview_post on BlogPost {
|
||||
id
|
||||
title
|
||||
content
|
||||
}
|
||||
`
|
||||
})
|
|
@ -0,0 +1,59 @@
|
|||
import React from 'react'
|
||||
import { createFragmentContainer, graphql } from 'react-relay'
|
||||
import BlogPostPreview from './BlogPostPreview'
|
||||
import Router from 'next/router'
|
||||
|
||||
const BlogPosts = props => {
|
||||
let afterParam = props.viewer.allBlogPosts.pageInfo.endCursor
|
||||
afterParam = afterParam ? `&after=${afterParam}` : ''
|
||||
|
||||
let hasNextPage = props.viewer.allBlogPosts.pageInfo.hasNextPage
|
||||
hasNextPage = hasNextPage || props.relayVariables.before
|
||||
|
||||
let hasPrevPage = props.viewer.allBlogPosts.pageInfo.hasPreviousPage
|
||||
hasPrevPage = hasPrevPage || props.relayVariables.after
|
||||
|
||||
let beforeParam = props.viewer.allBlogPosts.pageInfo.startCursor
|
||||
beforeParam = beforeParam ? `&before=${beforeParam}` : ''
|
||||
|
||||
const nextOnClick = () => Router.push(`/?first=2${afterParam}`)
|
||||
const prevOnClick = () => Router.push(`/?last=2${beforeParam}`)
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1>Blog posts</h1>
|
||||
{props.viewer.allBlogPosts.edges.map(({ node }) =>
|
||||
<BlogPostPreview key={node.id} post={node} />
|
||||
)}
|
||||
<br />
|
||||
<button disabled={!hasPrevPage} onClick={prevOnClick}>
|
||||
Previous Page
|
||||
</button>
|
||||
|
||||
<button disabled={!hasNextPage} onClick={nextOnClick}>
|
||||
Next Page
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default createFragmentContainer(BlogPosts, {
|
||||
viewer: graphql`
|
||||
fragment BlogPosts_viewer on Viewer {
|
||||
allBlogPosts {
|
||||
pageInfo {
|
||||
hasNextPage
|
||||
hasPreviousPage
|
||||
startCursor
|
||||
endCursor
|
||||
}
|
||||
edges {
|
||||
node {
|
||||
...BlogPostPreview_post
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
})
|
|
@ -0,0 +1,31 @@
|
|||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
// Thank you https://github.com/robrichard
|
||||
// https://github.com/robrichard/relay-context-provider
|
||||
|
||||
class RelayProvider extends React.Component {
|
||||
getChildContext () {
|
||||
return {
|
||||
relay: {
|
||||
environment: this.props.environment,
|
||||
variables: this.props.variables
|
||||
}
|
||||
}
|
||||
}
|
||||
render () {
|
||||
return this.props.children
|
||||
}
|
||||
}
|
||||
|
||||
RelayProvider.childContextTypes = {
|
||||
relay: PropTypes.object.isRequired
|
||||
}
|
||||
|
||||
RelayProvider.propTypes = {
|
||||
environment: PropTypes.object.isRequired,
|
||||
variables: PropTypes.object.isRequired,
|
||||
children: PropTypes.node
|
||||
}
|
||||
|
||||
export default RelayProvider
|
|
@ -0,0 +1,52 @@
|
|||
import { Environment, Network, RecordSource, Store } from 'relay-runtime'
|
||||
import fetch from 'isomorphic-unfetch'
|
||||
|
||||
let relayEnvironment = null
|
||||
|
||||
// Define a function that fetches the results of an operation (query/mutation/etc)
|
||||
// and returns its results as a Promise:
|
||||
function fetchQuery (
|
||||
operation,
|
||||
variables,
|
||||
cacheConfig,
|
||||
uploadables,
|
||||
) {
|
||||
// Because we implement the graphql server, the client must to point to the same host
|
||||
const relayServer = process.browser ? '' : process.env.RELAY_SERVER
|
||||
return fetch(`${relayServer}/graphql`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json'
|
||||
}, // Add authentication and other headers here
|
||||
body: JSON.stringify({
|
||||
query: operation.text, // GraphQL text from input
|
||||
variables
|
||||
})
|
||||
}).then(response => response.json())
|
||||
}
|
||||
|
||||
export default function initEnvironment ({ records = {} } = {}) {
|
||||
// Create a network layer from the fetch function
|
||||
const network = Network.create(fetchQuery)
|
||||
const store = new Store(new RecordSource(records))
|
||||
|
||||
// Make sure to create a new Relay environment for every server-side request so that data
|
||||
// isn't shared between connections (which would be bad)
|
||||
if (!process.browser) {
|
||||
return new Environment({
|
||||
network,
|
||||
store
|
||||
})
|
||||
}
|
||||
|
||||
// reuse Relay environment on client-side
|
||||
if (!relayEnvironment) {
|
||||
relayEnvironment = new Environment({
|
||||
network,
|
||||
store
|
||||
})
|
||||
}
|
||||
|
||||
return relayEnvironment
|
||||
}
|
51
examples/with-relay-modern-server-express/lib/withData.js
Normal file
51
examples/with-relay-modern-server-express/lib/withData.js
Normal file
|
@ -0,0 +1,51 @@
|
|||
import React from 'react'
|
||||
import initEnvironment from './createRelayEnvironment'
|
||||
import { fetchQuery } from 'react-relay'
|
||||
import RelayProvider from './RelayProvider'
|
||||
|
||||
export default (ComposedComponent, options = {}) => {
|
||||
return class WithData extends React.Component {
|
||||
static displayName = `WithData(${ComposedComponent.displayName})`
|
||||
|
||||
static async getInitialProps (ctx) {
|
||||
// Evaluate the composed component's getInitialProps()
|
||||
let composedInitialProps = {}
|
||||
if (ComposedComponent.getInitialProps) {
|
||||
composedInitialProps = await ComposedComponent.getInitialProps(ctx)
|
||||
}
|
||||
|
||||
let queryProps = {}
|
||||
let queryRecords = {}
|
||||
const environment = initEnvironment()
|
||||
|
||||
if (options.query) {
|
||||
const variables = composedInitialProps.relayVariables || {}
|
||||
// TODO: Consider RelayQueryResponseCache
|
||||
// https://github.com/facebook/relay/issues/1687#issuecomment-302931855
|
||||
queryProps = await fetchQuery(environment, options.query, variables)
|
||||
queryRecords = environment.getStore().getSource().toJSON()
|
||||
}
|
||||
|
||||
return {
|
||||
...composedInitialProps,
|
||||
...queryProps,
|
||||
queryRecords
|
||||
}
|
||||
}
|
||||
|
||||
constructor (props) {
|
||||
super(props)
|
||||
this.environment = initEnvironment({
|
||||
records: props.queryRecords
|
||||
})
|
||||
}
|
||||
|
||||
render () {
|
||||
return (
|
||||
<RelayProvider environment={this.environment} variables={{}}>
|
||||
<ComposedComponent {...this.props} />
|
||||
</RelayProvider>
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
22
examples/with-relay-modern-server-express/next.config.js
Normal file
22
examples/with-relay-modern-server-express/next.config.js
Normal file
|
@ -0,0 +1,22 @@
|
|||
require('dotenv').config()
|
||||
|
||||
const path = require('path')
|
||||
const Dotenv = require('dotenv-webpack')
|
||||
|
||||
module.exports = {
|
||||
webpack: (config) => {
|
||||
config.plugins = config.plugins || []
|
||||
|
||||
config.plugins = [
|
||||
...config.plugins,
|
||||
|
||||
// Read the .env file
|
||||
new Dotenv({
|
||||
path: path.join(__dirname, '.env'),
|
||||
systemvars: true
|
||||
})
|
||||
]
|
||||
|
||||
return config
|
||||
}
|
||||
}
|
32
examples/with-relay-modern-server-express/package.json
Normal file
32
examples/with-relay-modern-server-express/package.json
Normal file
|
@ -0,0 +1,32 @@
|
|||
{
|
||||
"name": "with-relay-modern-server-express",
|
||||
"version": "1.0.0",
|
||||
"description": "Example of Next.js with Relay Modern SSR and a Graphql Server with Express",
|
||||
"scripts": {
|
||||
"predev": "npm run relay",
|
||||
"dev": "node server",
|
||||
"build": "next build",
|
||||
"prestart": "npm run build",
|
||||
"start": "NODE_ENV=production node server",
|
||||
"relay": "relay-compiler --src ./ --exclude '**/.next/**' '**/node_modules/**' '**/test/**' '**/__generated__/**' '**/server/**' --schema ./server/schema.graphql --verbose"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"dotenv": "^4.0.0",
|
||||
"dotenv-webpack": "^1.5.4",
|
||||
"express-graphql": "^0.6.12",
|
||||
"graphql": "^0.13.2",
|
||||
"graphql-relay": "^0.5.5",
|
||||
"isomorphic-unfetch": "^2.0.0",
|
||||
"next": "latest",
|
||||
"react": "^16.2.0",
|
||||
"react-dom": "^16.2.0",
|
||||
"react-relay": "^1.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"babel-plugin-relay": "^1.4.1",
|
||||
"graphql-cli": "^1.0.0-beta.4",
|
||||
"relay-compiler": "^1.5.0"
|
||||
}
|
||||
}
|
38
examples/with-relay-modern-server-express/pages/index.js
Normal file
38
examples/with-relay-modern-server-express/pages/index.js
Normal file
|
@ -0,0 +1,38 @@
|
|||
import React, { Component } from 'react'
|
||||
import { graphql } from 'react-relay'
|
||||
import withData from '../lib/withData'
|
||||
import BlogPosts from '../components/BlogPosts'
|
||||
|
||||
class Index extends Component {
|
||||
static displayName = `Index`
|
||||
|
||||
static async getInitialProps (context) {
|
||||
let { after, before, first, last } = context.query
|
||||
|
||||
if (last === undefined) {
|
||||
first = 2
|
||||
}
|
||||
|
||||
return {
|
||||
relayVariables: { after, before, first, last }
|
||||
}
|
||||
}
|
||||
|
||||
render (props) {
|
||||
return (
|
||||
<div>
|
||||
<BlogPosts viewer={this.props.viewer} relayVariables={this.props.relayVariables} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default withData(Index, {
|
||||
query: graphql`
|
||||
query pages_indexQuery($after: String, $before: String, $first: Int, $last: Int) {
|
||||
viewer(after: $after, before: $before, first: $first, last: $last) {
|
||||
...BlogPosts_viewer
|
||||
}
|
||||
}
|
||||
`
|
||||
})
|
34
examples/with-relay-modern-server-express/server/db.js
Normal file
34
examples/with-relay-modern-server-express/server/db.js
Normal file
|
@ -0,0 +1,34 @@
|
|||
module.exports = {
|
||||
blogPosts: [
|
||||
{
|
||||
id: 'p1',
|
||||
title: 'Title 1',
|
||||
content: 'Lorem Ipsum 1'
|
||||
},
|
||||
{
|
||||
id: 'p2',
|
||||
title: 'Title 2',
|
||||
content: 'Lorem Ipsum 2'
|
||||
},
|
||||
{
|
||||
id: 'p3',
|
||||
title: 'Title 3',
|
||||
content: 'Lorem Ipsum 3'
|
||||
},
|
||||
{
|
||||
id: 'p4',
|
||||
title: 'Title 4',
|
||||
content: 'Lorem Ipsum 4'
|
||||
},
|
||||
{
|
||||
id: 'p5',
|
||||
title: 'Title 5',
|
||||
content: 'Lorem Ipsum 5'
|
||||
},
|
||||
{
|
||||
id: 'p6',
|
||||
title: 'Title 6',
|
||||
content: 'Lorem Ipsum 6'
|
||||
}
|
||||
]
|
||||
}
|
34
examples/with-relay-modern-server-express/server/index.js
Normal file
34
examples/with-relay-modern-server-express/server/index.js
Normal file
|
@ -0,0 +1,34 @@
|
|||
const express = require('express')
|
||||
const graphqlHTTP = require('express-graphql')
|
||||
const next = require('next')
|
||||
const path = require('path')
|
||||
const fs = require('fs')
|
||||
const { buildSchema } = require('graphql')
|
||||
const rootValue = require('./rootValue')
|
||||
const port = parseInt(process.env.PORT, 10) || 3000
|
||||
const dev = process.env.NODE_ENV !== 'production'
|
||||
const app = next({ dev })
|
||||
const handle = app.getRequestHandler()
|
||||
const schemaPath = path.join(__dirname, '/schema.graphql')
|
||||
const schemaContent = fs.readFileSync(schemaPath).toString()
|
||||
const graphqlSchema = buildSchema(schemaContent)
|
||||
|
||||
app.prepare()
|
||||
.then(() => {
|
||||
const server = express()
|
||||
|
||||
server.use('/graphql', graphqlHTTP({
|
||||
schema: graphqlSchema,
|
||||
graphiql: false,
|
||||
rootValue: rootValue
|
||||
}))
|
||||
|
||||
server.get('*', (req, res) => {
|
||||
return handle(req, res)
|
||||
})
|
||||
|
||||
server.listen(port, (err) => {
|
||||
if (err) throw err
|
||||
console.log(`> Ready on http://localhost:${port}`)
|
||||
})
|
||||
})
|
|
@ -0,0 +1,7 @@
|
|||
const gqr = require('graphql-relay')
|
||||
const db = require('./db')
|
||||
const viewer = variables => ({
|
||||
allBlogPosts: gqr.connectionFromArray(db.blogPosts, variables)
|
||||
})
|
||||
|
||||
module.exports = { viewer }
|
|
@ -0,0 +1,35 @@
|
|||
type Query {
|
||||
viewer(after: String, before: String, first: Int, last: Int): Viewer!
|
||||
node(id: ID!): Node
|
||||
}
|
||||
|
||||
type Viewer {
|
||||
allBlogPosts(after: String, before: String, first: Int, last: Int): BlogPostConnection!
|
||||
}
|
||||
|
||||
interface Node {
|
||||
id: ID!
|
||||
}
|
||||
|
||||
type BlogPostConnection {
|
||||
pageInfo: PageInfo!
|
||||
edges: [BlogPostEdge]
|
||||
}
|
||||
|
||||
type PageInfo {
|
||||
hasNextPage: Boolean!
|
||||
hasPreviousPage: Boolean!
|
||||
startCursor: String
|
||||
endCursor: String
|
||||
}
|
||||
|
||||
type BlogPostEdge {
|
||||
node: BlogPost!
|
||||
cursor: String!
|
||||
}
|
||||
|
||||
type BlogPost implements Node {
|
||||
content: String!
|
||||
id: ID!
|
||||
title: String!
|
||||
}
|
Loading…
Reference in a new issue