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
age: Int!
:Int
类型的年龄不能为nullhobbies:[Hobby]!
:数组类型的Hoby
不能为null,但是可以为空。
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
- write & run the query in ASE
- check the results, modify schema and resolvers if necessary.
- when everything is fine, just copy the query into the project and get what you want.