用了GraphQL之后,就不想再用RESTful了。本文将使用Node.js+Docker+GraphQL+MongoDB构建一个具有CRUD功能的完整微服务。运行代码,需要安装d
用了GraphQL之后,就不想再用RESTful了。
本文将使用Node.js+Docker+GraphQL+MongoDB构建一个具有CRUD功能的完整微服务。
运行代码,需要安装docker和docker-compose。
完整代码见:leinue/node-mongodb-graphql-docker
注:阅读本文需要有GraphQL基础。
Docker
dockerfile
FROM node
WORKDIR /app
EXPOSE 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@2 import koaRouter from 'koa-router'; // koa-router@next import koaBody from 'koa-bodyparser'; // koa-bodyparser@next import { 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
- mutations
- add.js
- index.js
- remove.js
- update.js
- queries
- 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