點燈坊

失くすものさえない今が強くなるチャンスよ

使用 Babel 打造 GraphQL Fullstack 開發環境

Sam Xiao's Avatar 2019-11-14

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 。

babel001

是否安裝 sample code ? 直接按 選擇 N 不安裝。

babel002

是否安裝 GraphQL Server ? 直接按 選擇 N 不安裝。

babel003

是否設定 Apollo Engine ? 直接按 選擇 N 不設定。

babel004

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-servergraphql

$ 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

babel005

Babel Configuration

server/.babelrc

{
  "presets": [
    "@babel/preset-env"
  ]
}

server 目錄下建立 .babelrc,使用剛剛安裝的 @babel/preset-env 的設定轉譯 ES6+。

babel006

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

watchext 可視實際需求加以修改

babel007

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 進 ApolloServergql

第 3 行

let data = [
  { title: 'FP in JavaScript', category: 'FP'},
  { title: 'RxJS in Action', category: 'FRP'},
  { title: 'Speaking JavaScript', category: 'JS'}
]

原始資料為 array of object,包含 titlecategory 兩個 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 內的 titlecategory,其中 category 型別為 BookCategory enum。

19 行

enum BookCategory {
  FP
  FRP
  JS
}

定義 BookCategory enum,包含 FPFRPJS 三個 enumeration。

28 行

let resolvers = {
  Query: {
    books
  }
}

resolvers 內定義 books query。

26 行

let books = (_, { category }) => data.filter(x => x.category === category)

實現 books query,resolver 依序有 4 個 argument: parentargscontextinfo,讀取 argument 只會用到第二個 args,而 parent 目前用不到可用 _ 表示, contextinfo 目前可忽略。

直接從 args destructure 出 category 使用。

34 行

let apolloServer = new ApolloServer({ typeDefs, resolvers })

使用 ApolloServer 建立 apollo object,將 typeDefsresolvers 組合成 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_ENVproduction,且依序執行:

  • yarn clean 刪除 dist 目錄
  • yarn build 使用 Babel 轉譯成 Node 能執行的 js
  • yarn 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:buildyarn prod 類似,差異只在最後 docker:build 包成 image 而非啟動 Apollo GraphQL。

babel008

Summary

雖然 Yarn script 內分的很細,但其實會用到的 script 只有 3 個:

  • yarn serve:development 時使用,存檔後 Nodemon 會自動 Babel 轉譯,以 babel-node 重新啟動 Apollo GraphQL
  • yarn build:development 時使用,以 babelsrc 目錄下所有 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

稍後 COPYRUNCMD 都會在此目錄下。

第 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。

babel009

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。

babel000

Conclusion

  • 本文以 Vue CLI 為基礎,另外在 Apollo GraphQL 加上 Babel,適合 Vue + Apollo GraphQL 的 fullstack 開發

Sample Code

完整範例可在我的 GitHub 上找到