除了 Apollo Server 外,Apollo 另外提供了 Apollo Server Express,可搭配 Express 與其他 Middleware 做進階應用。
Version
macOS Catalina 10.15.1
WebStorm 2019.2.4
Node 10.16.3
Apollo Server Express 2.9.7
Introduction
Apollo 官方提供了 Apollo Server 與 Apollo Server Express,兩者有以下差異:
Apollo Server
- 不可使用 Express 的 middleware
- Server 的任意 path 都可執行 GraphQL API 與 GraphQL Playground
- 已經設定好 WebSocket
- 適合一般使用
Apollo Server Express
- 可自行使用 Express 的 middleware
- 預設以
/graphql
為 path - WebSocket 須另外設定
- 適合進階使用
可發現大部分場景 Apollo Server 就很夠用,若需使用 middleware 預期他進階功能,則要使用 Apollo Server Express。
Install Package
$ yarn add express apollo-server-express graphql
$ yarn add @babel/core @babel/cli @babel/preset-env @babel/node --dev
$ yarn add nodemon rimraf cross-env --dev
安裝 Express、Apollo Server Express、GraphQL 與 Babel 所需 package。
$ yarn add express apollo-server-express graphql
安裝 Express、Apollo Server Express 與 GraphQL。
Apollo Server Express 只是 Express 的 middleware,因此必須安裝 Express
$ yarn add @babel/core @babel/cli @babel/preset-env @babel/node --dev
安裝 Babel 相關 package,由於只是轉譯用,安裝成 devDependency
即可。
其中 @babel/node
負責將 ES6+ 轉譯成 Node 能執行的 js。
$ yarn add nodemon rimraf cross-env --dev
安裝 nodemon
可監控檔案修改重新 Babel 轉譯與重啟 Node。
安裝 rimraf
,稍後 yarn clean
時刪除 dist
目錄。
安裝 cross-env 可跨平台設定 NODE_ENV
環境變數。
server/package.json
Babel Configuration
server/.babelrc
{
"presets": [
"@babel/preset-env"
]
}
在 project 根目錄下建立 .babelrc
,使用剛剛安裝的 @babel/preset-env
的設定轉譯 ES6+。
Nodemon Configuration
server/nodemon.json
{
"exec": "NODE_ENV=development yarn dev",
"watch": ["server/src/*"],
"ext": "js, json"
}
- exec:當有變動時,將執行
yarn dev
- watch:Nodemon 將持續觀察的目錄
- ext:Nodemon 將持續觀察的 extension
watch
與ext
可視實際需求加以修改
Node
src/index.js
import { ApolloServer, gql } from 'apollo-server-express'
import express from 'express'
let data = [
{ title: 'FP in JavaScript', category: 'FP'},
{ title: 'RxJS in Action', category: 'FRP'},
{ title: 'Speaking JavaScript', category: 'JS'}
]
let typeDefs = gql`
type Query {
books(category: BookCategory!): [Book]
}
type Book {
title: String
category: BookCategory
}
enum BookCategory {
FP
FRP
JS
}
`
let books = (_, { category }) => data.filter(x => x.category === category)
let resolvers = {
Query: {
books
}
}
let app = express()
let apolloServer = new ApolloServer({ typeDefs, resolvers })
apolloServer.applyMiddleware({ app })
app.listen({ port: 4000 },
() => console.log(`GraphQL Server ready at http://localhost:4000${ apolloServer.graphqlPath }`)
)
在 server/src
目錄下建立 index.js
。
第 1 行
import { ApolloServer, gql } from 'apollo-server-express'
從 apollo-server-express
import 進 ApolloServer
與 gql
。
注意是
apollo-server-express
不是apollo-server
第 2 行
import express from 'express'
從 express
import 進 express。
第 4 行
let data = [
{ title: 'FP in JavaScript', category: 'FP'},
{ title: 'RxJS in Action', category: 'FRP'},
{ title: 'Speaking JavaScript', category: 'JS'}
]
原始資料為 array of object,包含 title
與 category
兩個 property。
10 行
let typeDefs = gql`
...
`
在 typeDefs
內定義 GraphQL schema。
11 行
type Query {
books(category: BookCategory!): [Book]
}
定義 books
query,其 category
argument 型別為 BookCategory
enum 且不可為 null,回傳為 Book
type array。
15 行
type Book {
title: String
category: BookCategory
}
定義 Book
type,包含 object 內的 title
與 category
,其中 category
型別為 BookCategory
enum。
20 行
enum BookCategory {
FP
FRP
JS
}
定義 BookCategory
enum,包含 FP
、FRP
與 JS
三個 enumeration。
29 行
let resolvers = {
Query: {
books
}
}
在 resolvers
內定義 books
query。
27 行
let books = (_, { category }) => data.filter(x => x.category === category)
實現 books
query,resolver 依序有 4 個 argument: parent
、args
、context
與 info
,讀取 argument 只會用到第二個 args
,而 parent
目前用不到可用 _
表示, context
與 info
目前可忽略。
直接從 args
destructure 出 category
使用。
34 行
let app = express()
使用 express()
建立 app
object。
36 行
let apolloServer = new ApolloServer({ typeDefs, resolvers })
使用 ApolloServer
建立 apollo
object,將 typeDefs
與 resolvers
組合成 object 傳進其 constructor。
38 行
apolloServer.applyMiddleware({ app })
將 Apollo Server Express 以 middleware 套用到 Express 上。
43 行
app.listen({ port: 4000 },
() => console.log(`GraphQL Server ready at http://localhost:4000${ apollo.graphqlPath }`)
)
Apollo Server Express 預設會以 /graphql
為 path。
僅管不使用 Express 設定
/
,Apollo Server Express 預設也只使用/graphql
為 path
Yarn Script
server/package.json
"scripts": {
"serve": "nodemon",
"start": "node ./dist/index.js",
"dev": "babel-node ./src/index.js",
"prod": "yarn clean && cross_env NODE_ENV=production yarn build && yarn start",
"clean": "rimraf dist",
"build": "babel ./src --out-dir dist",
"docker:build": "yarn clean && cross_env NODE_ENV=production yarn build && docker build -t $npm_package_name:$npm_package_version ."
},
設定 Apollo Server Express 的 Yarn script。
"serve": "nodemon",
yarn server
執行 nodemon
。
"start": "node ./dist/index.js",
yarn start
使用 node
執行 Babel 轉譯後的 js 並啟動 Express 與 Apollo Server Express。
"dev": "babel-node ./src/index.js",
yarn dev
使用 babel-node
將 ES6+ 轉譯成 Node 能執行的 js 在記憶體內,並啟動 Express 與 Apollo Server Express。
"prod": "yarn clean && cross_env NODE_ENV=production yarn build && yarn start",
yarn prod
設定 NODE_ENV
為 production
,且依序執行:
yarn clean
刪除dist
目錄yarn build
使用 Babel 轉譯成 Node 能執行的 jsyarn start
使用node
執行 Babel 轉譯後的 js 並啟動 Express 與 Apollo Server Express
"clean": "rimraf dist",
yarn clean
刪除 dist
目錄。
"build": "babel ./src --out-dir dist"
yarn build
使用 Babel 將 src
目錄下的 ES6+ 轉譯成 Node 能執行的 js 到 dist
目錄下。
將來要包進 Docker 的 js 也是
dist
目錄,而非src
目錄
"docker:build": "yarn clean && cross_env NODE_ENV=production yarn build && docker build -t $npm_package_name:$npm_package_version ."
yarn docker:build
與 yarn prod
類似,差異只在最後 docker:build
包成 image 而非啟動 Apollo Server Express。
Summary
雖然 Yarn script 內分的很細,但其實會用到的 script 只有 3 個:
yarn serve
:development 時使用,存檔後 Nodemon 會自動 Babel 轉譯,以babel-node
重新啟動 Express 與 Apollo Server Expressyarn build
:development 時使用,以babel
將src
目錄下所有 js 重新轉譯到dist
目錄下yarn docker:build
:產生 Apollo Server Express 的 Docker image
Dockerfile
server/dockerfile
FROM node:lts-alpine
WORKDIR /usr/app
COPY package.json .
RUN yarn install --production
COPY dist/* ./
CMD [ "node", "index.js" ]
在 server
目錄下建立 dockerfile
,由於我們要另外安裝 Express 與 Apollo Server Express,因此要另外寫 dockerfile
安裝 package,不可直接使用 docker-compose.yml
。
第 1 行
FROM node:lts-alpine
使用最新 LTS 版的 node:lts-alpine
為基底建立 image。
建議使用
alpine
為 production image,size 會小很多
第 2 行
WORKDIR /usr/app
將 working directory 切換到 /usr/app
,相當於:
mkdir /usr/app
cd /usr/app
稍後 COPY
、RUN
與 CMD
都會在此目錄下。
第 3 行
COPY package.json .
將根目錄的 package.json
複製到 Docker 內的 /usr/app
,因為要使用 yarn install
安裝 Express 與 Apollo Server Express。
第 4 行
RUN yarn install --production
執行 yarn install
安裝 dependencies
下的 package。
第 5 行
COPY dist/* ./
將 Babel 編譯過的 js 複製進 Docker 內的 /usr/app
。
第 6 行
CMD [ "node", "index.js" ]
執行 Docker 內的 /usr/app/index.js
啟動 Apollo Server Express。
Start Apollo Express
$ yarn serve
使用 yarn all
同時啟動 Apollo Server Express 與 Vue。
Vue 一同往常啟動在 http://localhost:8080
。
若訪問 http://localhost:4000
,不再顯示 GraphQL Playground,而是由 Express 接管。
不再如 Apollo Server 隨便打 http://localhost:4000
下的網址都可啟動 GraphQL Playground。
必須明確訪問 http://localhost:4000/graphql
才能使用 GraphQL Playground。
Conclusion
- 若要使用其他 middleware,就要使用 Apollo Server Express,但預設 path 為
/graphql
而不是/
Sample Code
完整範例可在我的 GitHub 上找到