使用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

聲明了以下任務:

  1. 聲明user_service服務和user_db服務
  2. user_service:
    1. 從當前目錄的dockerfile構建
    2. 與user_db連接
    3. 在啟動時執行node index.js和/bin/bash
    4. 掛載當前目錄到/app目錄下
    5. 複製一份
    6. 在失敗時重啟
    7. 暴露容器內的5555埠到宿主機的5555埠
  3. user_db
    1. 從mongo構建
    2. 將容器內的/data/db掛載到宿主機上的/tmp/db上(數據持久化)
    3. 總是重啟

運行命令

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需要分成三部分:

  1. mutations
    1. 修改/刪除/增加操作
  2. queries
    1. 查詢操作
  3. types
    1. 數據類型定義(輸入/輸出)

以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測試界面: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哪本書最好?

TAG:Docker | GraphQL | Nodejs |