Byteli

GraphQL With Apollo

Server Side

package.json

graphql: graphQL安装

apollo-server: GraphQL服务器基础包

apollo-datasource-rest: api交互

 1{
 2	"name": "catstronauts-server",
 3	"version": "1.0.0",
 4	"description": "back-end demo app for Apollo's lift-off III course",
 5	"main": "src/index.js",
 6	"scripts": {
 7		"start": "nodemon src/index"
 8	},
 9	"dependencies": {
10		"graphql": "^15.5.1",
11		"apollo-server": "^3.0.0",
12		"apollo-datasource-rest": "^0.11.0"
13	},
14	"devDependencies": {
15		"dotenv": "^8.2.0",
16		"nodemon": "^2.0.4"
17	},
18	"author": "Raphael Terrier @R4ph-t",
19	"license": "MIT",
20	"private": true
21}

Project Structure

 1.
 2├── README.md
 3├── package-lock.json
 4├── package.json
 5└── src
 6    ├── datasources
 7       └── track-api.js
 8    ├── index.js
 9    ├── resolvers.js
10    └── schema.js

schema.js

定义客户端可以获取的字段,及其类型。

类型除了常规的String、Int、ID类型,还可是其他字段。

除了表明返回一个值,还可以返回一个数组

符号约定

[]:返回一个数组

!:不为null,但数组可以为empty

 1// gql used for wrapping GraphQL strings
 2const { gql } = require('apollo-server')
 3
 4const typeDefs = gql`
 5	"""
 6	多行注释
 7	"""
 8	type Query {
 9		"Query to get tracks array for the homepage grid"
10		tracksForHome: [Track!]!
11		"Fetch a specific track, provided a track's ID"
12		track(id: ID!): Track
13		module(id: ID!): Module
14	}
15
16	"A track is a group of Modules that teaches about a specific topic"
17	type Track {
18		id: ID!
19		"The track's title"
20		title: String!
21		"The track's main Author"
22		author: Author!
23		"The track's illustration to display in track card or track page detail"
24		thumbnail: String
25		"The track's approximate length to complete, in minutes"
26		length: Int
27		"The number of modules this track contains"
28		modulesCount: Int
29		"The track's complete description, can be in Markdown format"
30		description: String
31		"The number of times a track has been viewed"
32		numberOfViews: Int
33		"The track's complete array of Modules"
34		modules: [Module!]!
35	}
36
37	type Module {
38		id: ID!
39		"The module's title"
40		title: String!
41		"The Module's length in minutes"
42		length: Int
43		"The module's video url"
44		videoUrl: String
45		"The video's content"
46		content: String
47	}
48
49	"Author of a complete Track or a Module"
50	type Author {
51		id: ID!
52		"Author's first and last name"
53		name: String!
54		"Author's profile picture"
55		photo: String
56	}
57`
58
59module.exports = typeDefs

track.js

利用RESTDataSource 实现和api的交互。

 1const { RESTDataSource } = require('apollo-datasource-rest')
 2
 3class TrackAPI extends RESTDataSource {
 4	constructor() {
 5		// 可以用RESTDataSource的方法
 6		super()
 7		// REST API address
 8		this.baseURL = 'https://odyssey-lift-off-rest-api.herokuapp.com/'
 9	}
10
11	getTracksForHome() {
12		return this.get('tracks')
13	}
14
15	getTrack(trackId) {
16		return this.get(`track/${trackId}`)
17	}
18
19	getModule(moduleId) {
20		return this.get(`module/${moduleId}`)
21	}
22
23	getTrackModules(trackId) {
24		return this.get(`track/${trackId}/modules`)
25	}
26
27	getAuthor(authorId) {
28		return this.get(`author/${authorId}`)
29	}
30}
31
32module.exports = TrackAPI

resolvers.js

定义客户端可以发起的查询类型。

查询有4个参数:

parent: 根据类型定义,首次返回的数据。使用场景为根据返回的Object ID,再进行查询。

args: 客户端查询时发送的参数。使用场景为查找某个单一项目。

context: 传入的其他方法或参数。如数据api,认证。

info: 暂未使用。

 1const resolvers = {
 2	Query: {
 3		// returns an array of Tracks that will be used to populate the homepage grid of our web client
 4		tracksForHome: (_, __, { dataSources }) => {
 5			return dataSources.trackAPI.getTracksForHome()
 6		},
 7		track: (_, { id }, { dataSources }) => {
 8			return dataSources.trackAPI.getTrack(id)
 9		},
10		module: (_, { id }, { dataSources }) => {
11			return dataSources.trackAPI.getModule(id)
12		}
13	},
14	Track: {
15		author: ({ authorId }, _, { dataSources }) => {
16			return dataSources.trackAPI.getAuthor(authorId)
17		},
18		modules: ({ id }, _, { dataSources }) => {
19			return dataSources.trackAPI.getTrackModules(id)
20		}
21	}
22}
23
24module.exports = resolvers

index.js

 1const { ApolloServer } = require('apollo-server')
 2const typeDefs = require('./schema')
 3const resolvers = require('./resolvers')
 4const TrackAPI = require('./datasources/track-api')
 5
 6const server = new ApolloServer({
 7	typeDefs,
 8	resolvers,
 9	// 对应resolvers中的定义
10	dataSources: () => {
11		return {
12			trackAPI: new TrackAPI()
13		}
14	}
15})
16
17server.listen().then(() => {
18	console.log(`
19    🚀  Server is running!
20    🔉  Listening on port 4000
21    📭  Query at https://studio.apollographql.com/dev
22  `)
23})

Client Side

package.json

based on React

graphql: GraphQL 前端

@apollo/client: apollo 前端

 1{
 2  "name": "catstronauts-client",
 3  "version": "1.0.0",
 4  "private": true,
 5  "description": "front-end demo app for Apollo's lift-off III course",
 6  "dependencies": {
 7    "@apollo/client": "^3.3.6",
 8    "@apollo/space-kit": "^9.3.1",
 9    "@emotion/cache": "^11.4.0",
10    "@emotion/core": "^10.1.1",
11    "@emotion/react": "^11.4.0",
12    "@emotion/styled": "^11.3.0",
13    "@reach/router": "^1.3.4",
14    "framer-motion": "^4.1.17",
15    "graphql": "^15.3.0",
16    "react": "^16.13.1",
17    "react-dom": "^16.13.1",
18    "react-emotion": "^10.0.0",
19    "react-markdown": "^6.0.2",
20    "react-player": "^2.6.0",
21    "react-scripts": "^4.0.3"
22  },
23  "scripts": {
24    "start": "react-scripts start",
25    "build": "react-scripts build",
26    "test": "react-scripts test",
27    "eject": "react-scripts eject"
28  },
29  "eslintConfig": {
30    "extends": "react-app"
31  },
32  "browserslist": {
33    "production": [
34      ">0.2%",
35      "not dead",
36      "not op_mini all"
37    ],
38    "development": [
39      "last 1 chrome version",
40      "last 1 firefox version",
41      "last 1 safari version"
42    ]
43  },
44  "devDependencies": {
45    "@testing-library/jest-dom": "^4.2.4",
46    "@testing-library/react": "^9.3.2",
47    "@testing-library/user-event": "^7.1.2",
48    "apollo": "^2.30.2"
49  },
50  "main": "src/index.js",
51  "author": "Raphael Terrier @R4ph-t",
52  "license": "MIT"
53}

Project Structure

 1.
 2├── README.md
 3├── package-lock.json
 4├── package.json
 5├── public
 6   ├── _redirects
 7   ├── favicon.ico
 8   ├── index.html
 9   ├── logo192.png
10   ├── logo512.png
11   ├── manifest.json
12   ├── robots.txt
13   └── space_kitty_pattern.png
14└── src
15    ├── assets
16       ├── cat_logo.png
17       ├── cat_logo@2x.png
18       ├── space_cat_logo.png
19       ├── space_cat_logo@2x.png
20       └── space_kitty_pattern.svg
21    ├── components
22       ├── __tests__
23          ├── module-detail.js
24          ├── modules-navigation.js
25          ├── query-result.js
26          └── track-detail.js
27       ├── content-section.js
28       ├── footer.js
29       ├── header.js
30       ├── index.js
31       ├── layout.js
32       ├── md-content.js
33       ├── module-detail.js
34       ├── modules-navigation.js
35       ├── query-result.js
36       └── track-detail.js
37    ├── containers
38       ├── __tests__
39          └── track-card.js
40       └── track-card.js
41    ├── index.js
42    ├── pages
43       ├── __tests__
44          └── tracks.js
45       ├── index.js
46       ├── module.js
47       ├── track.js
48       └── tracks.js
49    ├── styles.js
50    └── utils
51        ├── helpers.js
52        ├── test-utils.js
53        └── useWindowDimensions.js

index.js

ApolloClient: create apollo client

InMemoryCache: enable cache utility

ApolloProvider: makes apollo client accessible for all React components

 1import React from 'react'
 2import ReactDOM from 'react-dom'
 3import GlobalStyles from './styles'
 4import Pages from './pages'
 5import { ApolloProvider, ApolloClient, InMemoryCache } from '@apollo/client'
 6
 7// initialize apollo client
 8const client = new ApolloClient({
 9	uri: 'http://localhost:4000',
10	// use cache to store queried data, speed up the next same query
11	cache: new InMemoryCache()
12})
13
14ReactDOM.render(
15	// so the apollo client can be used in all components
16	<ApolloProvider client={client}>
17		<GlobalStyles />
18		<Pages />
19	</ApolloProvider>,
20	document.getElementById('root')
21)

Page.js(App.js)

@reach/router: router for reract, enable multiple pages’ app. Components like Track or Module could access use the patameter.

 1import React, { Fragment } from 'react'
 2import { Router } from '@reach/router'
 3/** importing our pages */
 4import Tracks from './tracks'
 5import Track from './track'
 6import Module from './module'
 7
 8export default function Pages() {
 9	return (
10		<Router primary={false} component={Fragment}>
11			<Tracks path='/' />
12			<Track path='/track/:trackId' />
13			<Module path={'/track/:trackId/module/:moduleId'} />
14		</Router>
15	)
16}

module.js

notice that you can get data from different resolvers.

 1import React from 'react'
 2// QueryResult ensure that the data handling process will not mess up the layout.
 3import { Layout, QueryResult } from '../components'
 4// gql: transform the Query to GraphQl-friendly
 5// useQuery: Hook that enable components executes the GraphQL queries, and get server feedback.
 6import { gql, useQuery } from '@apollo/client'
 7import ModuleDetail from '../components/module-detail'
 8
 9export const GET_MODULE = gql`
10	query getModule($moduleId: ID!, $trackId: ID!) {
11		#    get the query variables
12		module(id: $moduleId) {
13			id
14			title
15			videoUrl
16			content
17		}
18
19		track(id: $trackId) {
20			id
21			title
22			modules {
23				id
24				title
25				length
26			}
27		}
28	}
29`
30
31const Module = ({ trackId, moduleId }) => {
32	const { loading, error, data } = useQuery(GET_MODULE, {
33		variables: {
34			// use the data in router's url
35			trackId: trackId,
36			moduleId: moduleId
37		}
38	})
39
40	return (
41		<Layout fullWidth>
42			<QueryResult loading={loading} error={error} data={data}>
43				<ModuleDetail track={data?.track} module={data?.module} />
44			</QueryResult>
45		</Layout>
46	)
47}
48
49export default Module

QueryResult.js

 1import React from 'react'
 2import styled from '@emotion/styled'
 3import { LoadingSpinner } from '@apollo/space-kit/Loaders/LoadingSpinner'
 4
 5/**
 6 * Query Results conditionally renders Apollo useQuery hooks states:
 7 * loading, error or its children when data is ready
 8 * won't overwrite the style in outer box
 9 */
10const QueryResult = ({ loading, error, data, children }) => {
11	if (error) {
12		return <p>ERROR: {error.message}</p>
13	}
14	if (loading) {
15		return (
16			<SpinnerContainer>
17				<LoadingSpinner data-testid='spinner' size='large' theme='grayscale' />
18			</SpinnerContainer>
19		)
20	}
21	if (!data) {
22		return <p>Nothing to show...</p>
23	}
24	if (data) {
25		return children
26	}
27}
28
29export default QueryResult
30
31/** Query Result styled components */
32const SpinnerContainer = styled.div({
33	display: 'flex',
34	justifyContent: 'center',
35	alignItems: 'center',
36	width: '100%',
37	height: '100vh'
38})

Apollo Studio Explorer

default address: https://studio.apollographql.com/dev

  1. write & run the query in ASE
  2. check the results, modify schema and resolvers if necessary.
  3. when everything is fine, just copy the query into the project and get what you want.

#graphql #react


Reply to this post by email ↪