Vue CLI 已經基於 Babel 建立,但 Apollo GraphQL 則使用原生 Node,本文將以 Vue Apollo + Apollo GraphQL + Babel 打造 GraphQL Fullstack 開發環境。
Version
macOS Catalina 10.15.1
WebStorm 2019.2.4
Node 10.16.3
Vue CLI 4.0.5
Vue 2.6.10
Vue Apollo 3.0.0-beta.11
Apollo GraphQL 2.9.6
Babel 7.6.4
Vue CLI
$ vue create vue-gql
使用 Vue CLI 建立 vue-gql
project。
Vue Apollo
$ vue add apollo
安裝 apollo
plugin 自動設定 Vue Apollo 。
是否安裝 sample code ? 直接按 ↩
選擇 N
不安裝。
是否安裝 GraphQL Server ? 直接按 ↩
選擇 N
不安裝。
是否設定 Apollo Engine ? 直接按 ↩
選擇 N
不設定。
apollo
plugin 除了安裝 Vue Apollo 外,還幫我們設定了以上檔案。
Apollo Server
$ mkdir server
$ cd server
$ yarn init --yes
$ yarn add apollo-server graphql
$ yarn add @babel/core @babel/cli @babel/preset-env @babel/node --dev
$ yarn add nodemon rimraf cross-env --dev
安裝 Apollo GraphQL 與 Babel 所需 package。
$ mkdir server
$ cd server
在 Vue project 內建立 server
目錄,專門放置 Apollo GraphQL 相關檔案。
$ yarn init --yes
建立 package.json
。
$ yarn add apollo-server graphql
安裝 apollo-server
與 graphql
。
$ 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"
]
}
在 server
目錄下建立 .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
server/src/index.js
import { ApolloServer, gql } from 'apollo-server'
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 apolloServer = new ApolloServer({ typeDefs, resolvers })
apolloServer.listen()
.then(({ url }) => `GraphQL Server ready at ${ url }`)
.then(console.log)
在 server
目錄下建立 src
,所有 Apollo GraphQL 的 js 都在 src
目錄下。
第 1 行
import { ApolloServer, gql } from 'apollo-server'
從 apollo-server
import 進 ApolloServer
與 gql
。
第 3 行
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。
第 9 行
let typeDefs = gql`
...
`
在 typeDefs
內定義 GraphQL schema。
10 行
type Query {
books(category: BookCategory!): [Book]
}
定義 books
query,其 category
argument 型別為 BookCategory
enum 且不可為 null,回傳為 Book
array。
14 行
type Book {
title: String
category: BookCategory
}
定義 Book
type,包含 object 內的 title
與 category
,其中 category
型別為 BookCategory
enum。
19 行
enum BookCategory {
FP
FRP
JS
}
定義 BookCategory
enum,包含 FP
、FRP
與 JS
三個 enumeration。
28 行
let resolvers = {
Query: {
books
}
}
在 resolvers
內定義 books
query。
26 行
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 apolloServer = new ApolloServer({ typeDefs, resolvers })
使用 ApolloServer
建立 apollo
object,將 typeDefs
與 resolvers
組合成 object 傳進其 constructor。
36 行
apolloServer.listen()
.then(({ url }) => `GraphQL Server ready at ${ url }`)
.then(console.log)
使用 listen()
啟動 Apollo GraphQL,由於 listen()
回傳為 promise,因此不用如 callback 般要一次做完,可以使用多次 then()
處理,維持每個 then()
單一職責只做一件事情。
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 GraphQL 的 Yarn script。
"serve": "nodemon",
yarn server
執行 nodemon
。
"start": "node ./dist/index.js",
yarn start
使用 node
執行 Babel 轉譯後的 js 並啟動 Apollo GraphQL。
"dev": "babel-node ./src/index.js",
yarn dev
使用 babel-node
將 ES6+ 轉譯成 Node 能執行的 js 在記憶體內,並啟動 Apollo GraphQL。
"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 並啟動 Apollo GraphQL
"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 GraphQL。
Summary
雖然 Yarn script 內分的很細,但其實會用到的 script 只有 3 個:
yarn serve
:development 時使用,存檔後 Nodemon 會自動 Babel 轉譯,以babel-node
重新啟動 Apollo GraphQLyarn build
:development 時使用,以babel
將src
目錄下所有 js 重新轉譯到dist
目錄下yarn docker:build
:產生 Apollo GraphQL 的 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
,由於我們要另外安裝 Apollo GraphQL,且將自己寫的 js 包進 Docker image,因此不可直接使用 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
安裝 Apollo Server。
第 4 行
RUN yarn install --production
執行 yarn install
安裝 dependencies
下的 package。
不需安裝 Babel 所需套件進 Docker,因此加上
--production
第 5 行
COPY dist/* ./
將 Babel 編譯過的 js 複製進 Docker 內的 /usr/app
。
第 6 行
CMD [ "node", "index.js" ]
執行 Docker 內的 /usr/app/index.js
啟動 Apollo Server。
Vue Apollo
Vue Apollo Configuration
src/.env
VUE_APP_GRAPHQL_HTTP=http://localhost:4000
在 Vue project 的 src
目錄下建立 .env
,設定 Apollo Server 位置。
Vue
App.vue
<template>
<div>
<ul>
<li v-for="(item, index) in books" :key="index">
{{ item.title }}
</li>
</ul>
</div>
</template>
<script>
import gql from 'graphql-tag'
let books = {
query: gql`
query ($category: BookCategory!) {
books(category: $category) {
title
category
}
}`,
variables: {
category: 'FP'
}
}
export default {
name: 'app',
apollo: {
books
}
}
</script>
12 行
import gql from 'graphql-tag'
從 Graphql-tag
import 進 gql
。
21 行
export default {
name: 'app',
apollo: {
books
}
}
在 Vue instance 的 apollo
property 內宣告 books
query。
14 行
let books = {
query:gql`query ($category: BookCategory!) {
books(category: $category) {
title
category
}
}`,
variables:{
category:'FP'
}
}
定義 books
query 所使用的 GraphQL,將 GraphQL Playground 的 GraphQL 複製貼上即可。
第 1 行
<template>
<div>
<ul>
<li v-for="(item, index) in books" :key="index">
{{ item.title }}
</li>
</ul>
</div>
</template>
由於 books
query 回傳為 array,因此一樣使用 v-for
顯示所有資料。
Yarn Script
package.json
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint",
"gql": "cd server && yarn serve",
"all": "yarn serve & yarn gql",
"docker:gql-build": "cd server && yarn docker:build",
"docker:vue-build": "yarn build && docker build -t vue-gql:$npm_package_version .",
"docker:all": "yarn docker:gql-build && yarn docker:vue-build",
"docker:up": "docker-compose up -d",
"docker:down": "docker-compose down"
},
Vue 的 package.json
,將以此統一管理 Vue 與 Apollo Server。
"gql": "cd server && yarn serve",
yarn gql
啟動 Apollo GraphQL,由於 yarn serve
定義在 server/package.json
,因此要先 cd server
。
"all": "yarn serve & yarn gql"
yarn all
同時啟動 Vue 與 Apollo Server。
"docker:gql-build": "cd server && yarn docker:build",
yarn docker:gql-build
產生 Apollo Server 的 Docker image。
"docker:vue-build": "yarn build && docker build -t vue-gql:$npm_package_version .",
yarn docker:vue-build
產生 Vue 的 Docker image。
"docker:all": "yarn docker:gql-build && yarn docker:vue-build",
yarn docker:all
一次產生 Apollo GraphQL 與 Vue 的 Docker image。
"docker:up": "docker-compose up -d",
yarn docker:up
同時啟動 Apollo GraphQL 與 Vue 兩個 container。
"docker:down": "docker-compose down"
yarn docker:down
同時結束 Apollo GraphQL 與 Vue 兩個 container。
Dockerfile
dockerfile
FROM nginx:alpine
COPY dist /usr/share/nginx/html
將 yarn build
後的 Vue 打包成 Docker image。
第 1 行
FROM nginx:alpine
使用最新版 nginx:alpine
為基底建立 image。
建議使用
nginx:*-alpine
為 production image,size 會小很多
第 2 行
COPY dist /usr/share/nginx/html
將 dist
目錄下所有檔案複製到 image 內的 /usr/share/nginx/html
目錄下,此為 Nginx 放 HTML 之處。
Docker Compose
docker-compose.yml
version: "3"
services:
graphql:
image: gql-server:${GQL_TAG}
container_name: MyGraphQL
restart: always
ports:
- ${GQL_PORT}:4000
vue:
image: vue-gql:${VUE_TAG}
container_name: MyVue
restart: always
ports:
- ${VUE_PORT}:80
使用 docker-compose.yml
同時啟動 Apollo GraphQL 與 Vue。
.env
GQL_TAG=0.1.0
VUE_TAG=0.1.0
GQL_PORT=4000
VUE_PORT=8080
設定 docker-compose.yml
的環境變數。
Start All Server
$ yarn all
使用 yarn all
同時啟動 Vue 與 Apollo GraphQL。
Conclusion
- 本文以 Vue CLI 為基礎,另外在 Apollo GraphQL 加上 Babel,適合 Vue + Apollo GraphQL 的 fullstack 開發
Sample Code
完整範例可在我的 GitHub 上找到