使用Node.js+Docker+GraphQL+MongoDB構建服務
用了GraphQL之後,就不想再用RESTful了。
本文將使用Node.js+Docker+GraphQL+MongoDB構建一個具有CRUD功能的完整微服務。
運行代碼,需要安裝docker和docker-compose。
完整代碼見:leinue/node-mongodb-graphql-docker
註:閱讀本文需要有GraphQL基礎。
Docker
dockerfile
FROM nodeWORKDIR /appEXPOSE 5555
從node構建,將工作目錄設置為/app,暴露5555埠
docker-compose
version: "3"services: user_service: build: . links: - user_db command: ["node", "index.js", " && /bin/bash"] hostname: user_service_in_container volumes: - ./:/app deploy: replicas: 1 restart_policy: condition: on-failure ports: - "5555:5555" user_db: image: mongo volumes: - "/tmp/db:/data/db" restart: always
聲明了以下任務:
- 聲明user_service服務和user_db服務
- user_service:
- 從當前目錄的dockerfile構建
- 與user_db連接
- 在啟動時執行node index.js和/bin/bash
- 掛載當前目錄到/app目錄下
- 複製一份
- 在失敗時重啟
- 暴露容器內的5555埠到宿主機的5555埠
- user_db
- 從mongo構建
- 將容器內的/data/db掛載到宿主機上的/tmp/db上(數據持久化)
- 總是重啟
運行命令
docker-compose up -d
實現代碼
使用babel編譯為ES6代碼
事實上現在的node v8已經支持大部分ES6代碼了,但是async仍不支持,為了使用async不得不使用babel編譯。
.babelrc
{ "presets": ["es2015"], "plugins": ["syntax-async-generators", "transform-async-generator-functions", "transform-regenerator"]}
需要的babel包
{ "babel-plugin-syntax-async-generators": "^6.13.0", "babel-plugin-transform-async-generator-functions": "^6.24.1", "babel-plugin-transform-regenerator": "^6.26.0" "babel-core": "^6.26.0", "babel-polyfill": "^6.26.0", "babel-preset-es2015": "^6.24.1",}
安裝之後創建index.js
require("babel-core/register");require("babel-polyfill");require("./server.js");
這樣就可以在server.js中使用es6的代碼了。
server.js
server.js用來初始化graphql伺服器、路由和連接mongodb。這裡使用koa和graphql服務端的插件來初始化。
import koa from "koa"; // koa@2import koaRouter from "koa-router"; // koa-router@nextimport koaBody from "koa-bodyparser"; // koa-bodyparser@nextimport { graphqlKoa, graphiqlKoa } from "apollo-server-koa";import cors from "koa-cors";import convert from "koa-convert";import configs from "./configs";import mongoose from "mongoose";const app = new koa();const router = new koaRouter();const db = mongoose.createConnection(["mongodb://", configs.mongodb.ip, "/", configs.mongodb.dbname].join(""));if(db) { console.log("mongodb connected successfully"); global.db = db;}else { console.log("mongodb connected failed");}import schemaRouters from "./routers/schemaRouters";const schemas = schemaRouters().default;router.post("/graphql", koaBody(), graphqlKoa({ schema: schemas.HelloSchema }));router.get("/graphql", graphqlKoa({ schema: schemas.HelloSchema }));router.get("/graphiql", graphiqlKoa({ endpointURL: "/graphql" }));app.use(convert(cors(configs.cors)));app.use(router.routes());app.use(router.allowedMethods());app.listen(configs.port, () => { console.log("app started successfully, listening on port " + configs.port);});
這段初始化代碼將/graphql作為graphql數據收發的路由地址,服務將啟動在5555埠。
構建MongoDB數據類型
import { Schema } from "mongoose";var helloSchema = new Schema({ email: String, lastIP: String,});export default global.db.model("Hello", helloSchema);
初始化一個helloSchema,並將這個model命名為hello
還需要一個index.js將數據類型全部引入:
import HelloModel from "./HelloModel"export default { HelloModel,}
構建GraphQL Schema
一個schema需要分成三部分:
- mutations
- 修改/刪除/增加操作
- queries
- 查詢操作
- types
- 數據類型定義(輸入/輸出)
以HelloSchema為例,其文件結構如下:
.├── index.js├── mutations│ ├── add.js│ ├── index.js│ ├── remove.js│ └── update.js├── queries│ ├── hello.js│ └── index.js└── types ├── Hello.js ├── HelloAddInput.js ├── HelloFields.js └── HelloUpdateInput.js
各文件作用如下:
- index.js
- 初始化query和mutation
- mutations
- add.js
- 執行增加操作
- index.js
- 將增加/修改/刪除操作引用到一起
- remove.js
- 執行刪除操作
- update.js
- 執行更新操作
- queries
- hello.js
- 執行查詢操作
- index.js
- 將查詢操作引用到一起
- types
- Hello.js
- 定義返回結果的數據類型
- HelloAddInput.js
- 定義增加操作時輸入結構的數據類型
- HelloUpdateInput.js
- 定義更新操作時輸入結構的數據類型
- HelloFields.js
- 定義HelloSchema的通用數據結構(和HelloModel內容相同)
index.js
初始化query和mutation
import { GraphQLObjectType, GraphQLSchema, GraphQLList} from "graphql";import mutations from "./mutations";import queries from "./queries";let schema = new GraphQLSchema({ query: new GraphQLObjectType({ name: "Query", fields: queries }), mutation: new GraphQLObjectType({ name: "Mutation", fields: mutations })});export default schema;
queries/hello.js
執行查詢操作
import { GraphQLList, GraphQLString } from "graphql";import HelloType from "../types/Hello.js";import HelloModel from "../../../models/HelloModel";const hello = { type: new GraphQLList(HelloType), async resolve (root, params, options) { var hello = await HelloModel.find({}); return hello; }}export default hello;
定義了返回結果是一個HelloType的數組列表 ,resolve中使用了async函數進行mongodb非同步查詢。
mutations/add.js
執行增加操作
import { GraphQLNonNull } from "graphql";import HelloType from "../types/Hello.js";import HelloAddInput from "../types/HelloAddInput.js";import HelloModel from "../../../models/HelloModel";const add = { type: HelloType, args: { info: { name: "info", type: new GraphQLNonNull(HelloAddInput) } }, async resolve (root, params, options) { const HelloModel = new HelloModel(params.info); const newHello = await HelloModel.save(); if (!newHello) { return false; } return newHello; }};export default add;
注意其中args,其類型是HelloAddInput
HelloAddInput定義如下:
import { GraphQLInputObjectType, GraphQLString, GraphQLID, GraphQLNonNull} from "graphql";export default new GraphQLInputObjectType({ name: "HelloAddInput", fields: { email: { type: GraphQLString }, lastIP: { type: GraphQLString } }});
聲明了在執行添加操作時需要輸入email和lastIP參數。
mutations/update.js
執行更新操作
import { GraphQLNonNull } from "graphql";import HelloType from "../types/Hello.js";import HelloModel from "../../../models/HelloModel";import HelloUpdateInput from "../types/HelloUpdateInput.js";const update = { type: HelloType, args: { options: { name: "options", type: new GraphQLNonNull(HelloUpdateInput) } }, async resolve (root, params, options) { const updated = await HelloModel.findOneAndUpdate({ _id: params.options._id }, params.options); const hello = await HelloModel.findOne({ _id: params.options._id }); return hello; }};export default update
注意其中的參數options,其數據類型為HelloUpdateInput。
HelloUpdateInput定義如下:
import { GraphQLObjectType, GraphQLInputObjectType, GraphQLNonNull, GraphQLString, GraphQLID, GraphQLInt, GraphQLBoolean} from "graphql";import HelloFields from "./HelloFields";export default new GraphQLInputObjectType({ name: "HelloUpdateInput", fields: HelloFields});
定義了在更新欄位時可以使用HelloFields內的任意欄位,HelloFields定義如下:
import { GraphQLString, GraphQLInt, GraphQLBoolean} from "graphql";export default { _id: { type: GraphQLString }, email: { type: GraphQLString }, lastIP: { type: GraphQLString }}
其和model是一樣的
mutations/remove.js
執行刪除操作
import { GraphQLList, GraphQLString } from "graphql";import HelloType from "../types/Hello.js";import HelloModel from "../../../models/HelloModel";const remove = { type: new GraphQLList(HelloType), args: { ids: { name: "ids", type: new GraphQLList(GraphQLString) } }, async resolve (root, params, options) { let removedList = []; for (var i = 0; i < params.ids.length; i++) { const _id = params.ids[i]; const removed = await HelloModel.findOneAndRemove({ _id }); if(removed) { removedList.push(removed) } }; return removedList; }}export default remove
注意其中的ids是一個GraphQLList字元串數組,返回結果是HelloType對象。
執行GraphQL查詢
啟動程序
docker-compose up -d
打開GraphQL測試界面:http://localhost:5555/graphiql
先來執行一個查詢:
可以看到結果返回空。
再執行一個增加操作:
可以看到右側返回了新增的數據及其id
我們再將emai修改為「fuck_shit」:
右側成功返回了修改之後的數據。
最後,我們刪除這條數據:
返回了被刪除的id。
再最後執行一次查詢:
可以看到結果又變為空了。
至此,整個GraphQL+MongoDB的CRUD操作測試成功。
小Tips
graphql的測試器右側可以查看數據類型:
完整代碼見:leinue/node-mongodb-graphql-docker
推薦閱讀:
※Hyper:一款新推出的免費容器(類vps)
※超輕量級「虛擬機」—— Docker 初識
※如何評價docker?
※如何看待Docker改名為Moby?
※學習Docker哪本書最好?