add nextjs-8-serverless sample app (#1640)

This commit is contained in:
Praveen Durairaj 2019-02-21 15:27:20 +05:30 committed by Shahidh K Muhammed
parent d3b994885e
commit 5fc2df2766
15 changed files with 6920 additions and 0 deletions

View File

@ -0,0 +1,83 @@
### Node ###
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# TypeScript v1 declaration files
typings/
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
.env.test
# parcel-bundler cache (https://parceljs.org/)
.cache
# next.js build output
.next
# nuxt.js build output
.nuxt
# vuepress build output
.vuepress/dist
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/

View File

@ -0,0 +1,125 @@
# nextjs-8-serverless
> Boilerplate to get started with Next.js 8 Serverless Mode, Hasura GraphQL engine as CMS and postgres as database.
# Tutorial
- Deploy Postgres and GraphQL Engine on Heroku:
[![Deploy to heroku](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy?template=https://github.com/hasura/graphql-engine-heroku)
Please checkout our [docs](https://docs.hasura.io/1.0/graphql/manual/deployment/index.html) for other deployment methods
- Get the Heroku app URL (say `my-app.herokuapp.com`)
- Create `author` table:
Open Hasura console: visit https://my-app.herokuapp.com on a browser
Navigate to `Data` section in the top nav bar and create a table as follows:
![Create author table](../gatsby-postgres-graphql/assets/add_table.jpg)
- Insert sample data into `author` table:
![Insert data into author table](../gatsby-postgres-graphql/assets/insert_data.jpg)
Verify if the row is inserted successfully
![Insert data into author table](../gatsby-postgres-graphql/assets/browse_rows.jpg)
- Clone this repo:
```bash
git clone https://github.com/hasura/graphql-engine
cd graphql-engine/community/sample-apps/nextjs-8-serverless
```
- Install npm modules:
```bash
npm install
```
- Open `lib/init-apollo.js` and configure Hasura's GraphQL Endpoint as follows:
```js
function create (initialState) {
return new ApolloClient({
connectToDevTools: process.browser,
ssrMode: !process.browser, // Disables forceFetch on the server (so queries are only run once)
link: new HttpLink({
uri: 'https://myapp.herokuapp.com/v1alpha1/graphql', // Server URL (must be absolute)
credentials: 'same-origin' // Additional fetch() options like `credentials` or `headers`
}),
cache: new InMemoryCache().restore(initialState || {})
})
}
```
Replace the `uri` with your own Hasura GraphQL endpoint.
In this example, we integrate Apollo with Next by wrapping our *pages/_app.js* inside a higher-order component HOC. Using the HOC pattern we're able to pass down a central store of query result data created by Apollo into our React component hierarchy defined inside each page of our Next application.
On initial page load, while on the server and inside `getInitialProps`, we invoke the Apollo method, [`getDataFromTree`](https://www.apollographql.com/docs/react/features/server-side-rendering.html#getDataFromTree). This method returns a promise; at the point in which the promise resolves, our Apollo Client store is completely initialized.
- We have defined the graphql query in `components/AuthorList.js`.
```graphql
query author($skip: Int!) {
author(offset: $skip, limit: 5) {
id
name
}
author_aggregate {
aggregate {
count
}
}
}
```
- Run the app:
```bash
npm run dev
```
- Test the app
Visit [http://localhost:3000](http://localhost:3000) to view the app
# Serverless Mode
With Next.js 8, each page in the `pages` directory becomes a serverless lambda. To enable `serverless` mode, we add the `serverless` build `target` in `next.config.js`.
```
module.exports = {
target: "serverless",
};
```
That's it! Now build the serverless app by running the following command:
```
npm run build
```
In the `.next` folder, you will see a `serverless` folder generated after the build. Inside that there is a `pages` folder, which will have outputs of lambda per page.
```
pages/index.js => .next/serverless/pages/index.js
pages/about.js => .next/serverless/pages/about.js
```
# Deploy to now.sh
Deploy it to the cloud with [now](https://zeit.co/now) ([download](https://zeit.co/download)):
```bash
npm install -g now
now
```
Note: Older versions of now-cli doesn't support serverless mode.
Once the deployment is successful, you will be able to navigate to pages `/` and `/about`, with each one internally being a lambda function which `now` manages.

View File

@ -0,0 +1,42 @@
export default ({ children }) => (
<main>
{children}
<style jsx global>{`
* {
font-family: Menlo, Monaco, 'Lucida Console', 'Liberation Mono',
'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', 'Courier New',
monospace, serif;
}
body {
margin: 0;
padding: 25px 50px;
}
a {
color: #22bad9;
}
p {
font-size: 14px;
line-height: 24px;
}
article {
margin: 0 auto;
max-width: 650px;
}
button {
align-items: center;
background-color: #22bad9;
border: 0;
color: white;
display: flex;
padding: 5px 7px;
}
button:active {
background-color: #1b9db7;
transition: background-color 0.3s;
}
button:focus {
outline: none;
}
`}</style>
</main>
)

View File

@ -0,0 +1,109 @@
import { Query } from 'react-apollo'
import gql from 'graphql-tag'
export const authorQuery = gql`
query author($skip: Int!) {
author(offset: $skip, limit: 5) {
id
name
}
author_aggregate {
aggregate {
count
}
}
}
`
export const authorQueryVars = {
skip: 0,
}
export default function AuthorList () {
return (
<Query query={authorQuery} variables={authorQueryVars}>
{({ loading, error, data: { author, author_aggregate }, fetchMore }) => {
if (error) return <ErrorMessage message='Error loading authors.' />
if (loading) return <div>Loading</div>
const areMoreAuthors = author.length < author_aggregate.aggregate.count
return (
<section>
<ul>
{author.map((a, index) => (
<li key={a.id}>
<div>
<span>{index + 1}. </span>
<a>{a.name}</a>
</div>
</li>
))}
</ul>
{areMoreAuthors ? (
<button onClick={() => loadMoreAuthors(author, fetchMore)}>
{' '}
{loading ? 'Loading...' : 'Show More'}{' '}
</button>
) : (
''
)}
<style jsx>{`
section {
padding-bottom: 20px;
}
li {
display: block;
margin-bottom: 10px;
}
div {
align-items: center;
display: flex;
}
a {
font-size: 14px;
margin-right: 10px;
text-decoration: none;
padding-bottom: 0;
border: 0;
}
span {
font-size: 14px;
margin-right: 5px;
}
ul {
margin: 0;
padding: 0;
}
button:before {
align-self: center;
border-style: solid;
border-width: 6px 4px 0 4px;
border-color: #ffffff transparent transparent transparent;
content: '';
height: 0;
margin-right: 5px;
width: 0;
}
`}</style>
</section>
)
}}
</Query>
)
}
function loadMoreAuthors (author, fetchMore) {
fetchMore({
variables: {
skip: author.length
},
updateQuery: (previousResult, { fetchMoreResult }) => {
if (!fetchMoreResult) {
return previousResult
}
return Object.assign({}, previousResult, {
// Append the new results to the old one
author: [...previousResult.author, ...fetchMoreResult.author]
})
}
})
}

View File

@ -0,0 +1,13 @@
export default ({ message }) => (
<aside>
{message}
<style jsx>{`
aside {
padding: 1.5em;
font-size: 14px;
color: white;
background-color: red;
}
`}</style>
</aside>
)

View File

@ -0,0 +1,28 @@
import Link from 'next/link'
import { withRouter } from 'next/router'
const Header = ({ router: { pathname } }) => (
<header>
<Link prefetch href='/'>
<a className={pathname === '/' ? 'is-active' : ''}>Home</a>
</Link>
<Link prefetch href='/about'>
<a className={pathname === '/about' ? 'is-active' : ''}>About</a>
</Link>
<style jsx>{`
header {
margin-bottom: 25px;
}
a {
font-size: 14px;
margin-right: 15px;
text-decoration: none;
}
.is-active {
text-decoration: underline;
}
`}</style>
</header>
)
export default withRouter(Header)

View File

@ -0,0 +1,36 @@
import { ApolloClient, InMemoryCache, HttpLink } from 'apollo-boost'
import fetch from 'isomorphic-unfetch'
let apolloClient = null
// Polyfill fetch() on the server (used by apollo-client)
if (!process.browser) {
global.fetch = fetch
}
function create (initialState) {
return new ApolloClient({
connectToDevTools: process.browser,
ssrMode: !process.browser, // Disables forceFetch on the server (so queries are only run once)
link: new HttpLink({
uri: 'https://myapp.herokuapp.com/v1alpha1/graphql', // Server URL (must be absolute)
credentials: 'same-origin' // Additional fetch() options like `credentials` or `headers`
}),
cache: new InMemoryCache().restore(initialState || {})
})
}
export default function initApollo (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(initialState)
}
// Reuse client on the client-side
if (!apolloClient) {
apolloClient = create(initialState)
}
return apolloClient
}

View File

@ -0,0 +1,61 @@
import React from 'react'
import initApollo from './init-apollo'
import Head from 'next/head'
import { getDataFromTree } from 'react-apollo'
export default App => {
return class Apollo extends React.Component {
static displayName = 'withApollo(App)'
static async getInitialProps (ctx) {
const { Component, router } = ctx
let appProps = {}
if (App.getInitialProps) {
appProps = await App.getInitialProps(ctx)
}
// Run all GraphQL queries in the component tree
// and extract the resulting data
const apollo = initApollo()
if (!process.browser) {
try {
// Run all GraphQL queries
await getDataFromTree(
<App
{...appProps}
Component={Component}
router={router}
apolloClient={apollo}
/>
)
} 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 = apollo.cache.extract()
return {
...appProps,
apolloState
}
}
constructor (props) {
super(props)
this.apolloClient = initApollo(props.apolloState)
}
render () {
return <App {...this.props} apolloClient={this.apolloClient} />
}
}
}

View File

@ -0,0 +1,3 @@
module.exports = {
target: "serverless"
}

View File

@ -0,0 +1,6 @@
{
"version": 2,
"builds": [
{ "src": "next.config.js", "use": "@now/next" }
]
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,24 @@
{
"name": "hasura-nextjs-serverless",
"version": "1.0.0",
"scripts": {
"now-build": "next build",
"dev": "next",
"build": "next build",
"start": "next start"
},
"dependencies": {
"apollo-boost": "^0.1.16",
"express": "^4.16.4",
"graphql": "^14.0.2",
"isomorphic-unfetch": "^3.0.0",
"next": "latest",
"prop-types": "^15.6.2",
"react": "^16.7.0",
"react-apollo": "^2.1.11",
"react-dom": "^16.7.0",
"serverless-http": "^1.9.0"
},
"author": "Praveen <praveen@hasura.io>",
"license": "MIT"
}

View File

@ -0,0 +1,19 @@
import App, { Container } from 'next/app'
import React from 'react'
import withApolloClient from '../lib/with-apollo-client'
import { ApolloProvider } from 'react-apollo'
class MyApp extends App {
render () {
const { Component, pageProps, apolloClient } = this.props
return (
<Container>
<ApolloProvider client={apolloClient}>
<Component {...pageProps} />
</ApolloProvider>
</Container>
)
}
}
export default withApolloClient(MyApp)

View File

@ -0,0 +1,39 @@
import App from '../components/App'
import Header from '../components/Header'
export default () => (
<App>
<Header />
<article>
<h1>The Idea Behind This Example</h1>
<p>
<a href='https://www.apollographql.com/client/'>Apollo</a> is a GraphQL
client that allows you to easily query the exact data you need from a
GraphQL server. In addition to fetching and mutating data, Apollo
analyzes your queries and their results to construct a client-side cache
of your data, which is kept up to date as further queries and mutations
are run, fetching more results from the server.
</p>
<p>
In this simple example, we integrate Apollo seamlessly with{' '}
<a href='https://github.com/zeit/next.js'>Next</a> by wrapping our pages
inside a{' '}
<a href='https://facebook.github.io/react/docs/higher-order-components.html'>
higher-order component (HOC)
</a>
. Using the HOC pattern we're able to pass down a central store of query
result data created by Apollo into our React component hierarchy defined
inside each page of our Next application.
</p>
<p>
On initial page load, while on the server and inside getInitialProps, we
invoke the Apollo method,{' '}
<a href='https://www.apollographql.com/docs/react/features/server-side-rendering.html#getDataFromTree'>
getDataFromTree
</a>
. This method returns a promise; at the point in which the promise
resolves, our Apollo Client store is completely initialized.
</p>
</article>
</App>
)

View File

@ -0,0 +1,10 @@
import App from '../components/App'
import Header from '../components/Header'
import AuthorList from '../components/AuthorList'
export default () => (
<App>
<Header />
<AuthorList />
</App>
)