用了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
- 初始化query和mutation
- mutations
- add.js
- 执行增加操作
- index.js
- 将增加/修改/删除操作引用到一起
- remove.js
- 执行删除操作
- update.js
- 执行更新操作
- add.js
- queries
- hello.js
- 执行查询操作
- index.js
- 将查询操作引用到一起
- hello.js
- types
- Hello.js
- 定义返回结果的数据类型
- HelloAddInput.js
- 定义增加操作时输入结构的数据类型
- HelloUpdateInput.js
- 定义更新操作时输入结构的数据类型
- HelloFields.js
- 定义HelloSchema的通用数据结构(和HelloModel内容相同)
- Hello.js
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的测试器右侧可以查看数据类型:
评论