参考: GraphQL官网
与 RESTful API 对比
RESTful:服务端决定有哪些数据获取方式,客户端只能挑选使用,如果数据过于冗余也只能默默接收再对数据进行处理;而数据不能满足需求则需要请求更多的接口。
GraphQL:给客户端自主选择数据内容的能力,客户端完全自主决定获取信息的内容,服务端负责精确的返回目标数据。
为什么要用 GraphQL
说人话就是:减少工作量,后端一个接口前端进行查询
专业一点:提供一种更严格、可扩展、可维护的数据查询方式
优点
1.能为老板节省几个亿的流量(由前端定义需要哪些字段
2.再也不需要对接口的文档进行维护了(自动生成文档,代码里定义的结构就是文档
3.再也不用加班了(真正做到一个接口适用多个场景
4.再也不用改bug了(强类型,自动校验入参出参类型
5.新人再也不用培训了(所有的接口都在一颗数下,一目了然
6.再也不用前端去写假数据了(代码里定义好结构之后自动生成mock接口
7.再不用痛苦的联调了(代码里定义好结构之后,自动生成接口在线调试工具,直接8.在界面里写请求语句来调试返回,而且调试的时候各种自动补全
9.react/vue/express/koa无缝接入(relay方案/apollo方案
10.更容易写底层的工具去监控每个接口的请求统计信息(都在同一个端点的请求下
11.不限语言,除了官方提供的js实现,其他所有的语言都有社区的实现
12.生态是真的好啊,有各种方便易用的开发者工具
ps: graphql 的方案完美的解决了以上所有问题,连大名鼎鼎GitHub也抛弃了自己非常优秀的REST API接口,全面拥抱graphql了。
GraphQL存在的问题
graphQl也不是没有缺点,主要有以下几个缺点:
改造成本
要使用GraphQL对数据源进行管理,相当于要对整个服务端进行一次换血。你需要考虑的不仅仅是需要针对现有数据源建立一套GraphQL的类型系统,同时需要改造服务端暴露数据的方式,这对业务久远的产品无疑是一场灾难,让人望而却步。
查询性能
GraphQL查询的每个字段如果都有自己的resolve方法,可能导致一次查询操作对数据库跑了大量了query,数据库里一趟select+join就能完成的事情在这里看来会产生大量的数据库查询操作,虽然网络层面的请求数被优化了,但是数据库查询可能会成为性能瓶颈。但是这个其实是可以优化,反正就算是用rest api,同时处理多个请求的时候,也总要运行那些数据库语句的。
1. Environment setting up
请提前全局安装 nodemon
, 执行代码npm i -g nodemon
,新建文件夹,扔入 package.json
如下,然后安装依赖 npm i
目录如下:
为了方便,直接贴出 package.json
,
{
"name": "server",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "nodemon app.js"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"express": "^4.16.4",
"express-graphql": "^0.8.0",
"graphql": "^14.2.1",
"lodash": "^4.17.11",
"mongoose": "^5.5.4"
}
}
2. Express app setting up
app.js
代码如下:
// app.js
const express = require('express');
const app = express();
app.listen(8087, (err) =>{
if(err) throw 'err';
console.log('listening on port 8087')
})
运行执行 npm start
,能见到打印信息,即已经成功启动
3. GraphQL setting up
前期已经安装过一系列 graphQL
相关依赖,这节开始将派上用场
3.1 Book schema setting up
先来一个简单查询的,查找书的 name
, 或者 genre
- 首先在根目录下创建 schema 文件夹,准备
schema.js
文件
// schema.js
const {
GraphQLObjectType,
GraphQLString,
GraphQLID,
GraphQLSchema
} = require('graphql');
const _ = require('lodash');
const books = [{
id: '1',
name: 'Javascript',
genre: '计算机'
},{
id: '2',
name: 'vue',
genre: '框架'
},{
id: '3',
name: '毛主席语录',
genre: '爱国'
}]
// 定义数据模型
const BookType = new GraphQLObjectType({
name: 'book',
fields: () => ({
id: {
type: GraphQLID
},
name: {
type: GraphQLString
},
genre: {
type: GraphQLString
}
})
})
// 定义query模型
const RootQuery = new GraphQLObjectType({
name: 'query',
fields: {
book: {
type: BookType,
args: {
id: {
type: GraphQLID
}
},
resolve(parent, args) {
// 数据来源,比如数据库或者其他来源
// lodash 模拟查找
return _.find(books, {
id: args.id
});
}
},
}
})
// 导出 GraphQL 数据模型
module.exports = new GraphQLSchema({
query: RootQuery
})
- 改造
app.js
,将schema.js
和 graphQL 应用于上
// app.js
const express = require('express');
const graphqlHTTP = require('express-graphql');
const schema = require('./schema/schema1.js');
const app = express();
// 定义路由
app.use('/graphql', graphqlHTTP({
// 引入数据模型
schema,
// 启用类似于 graphQL 的 playground
graphiql: true,
}));
app.listen(8087, (err) => {
if (err) throw 'err';
console.log('listen on port 8087');
});
- 启动项目,
npm start
, 在浏览器端输入地址,当前使用的是localhost:8087
3.2 Author schema setting up
有了书籍, 再来加个作者的
跟创建书籍的方式类似,直接 copy 了改改,如下:
// schema.js
const {
GraphQLObjectType,
GraphQLString,
GraphQLID,
GraphQLInt,
GraphQLSchema
} = require('graphql');
const _ = require('lodash');
const books = [{
id: '1',
name: 'Javascript',
genre: '计算机'
},{
id: '2',
name: 'vue',
genre: '框架'
},{
id: '3',
name: '毛主席语录',
genre: '爱国'
}]
const authors = [
{
id: '1',
name: 'red',
age: 34
},
{
id: '2',
name: 'blue',
age: 54
},
{
id: '3',
name: '毛主席',
age: 65
}
]
// 定义数据模型
const BookType = new GraphQLObjectType({
name: 'book',
fields: () => ({
id: {
type: GraphQLID
},
name: {
type: GraphQLString
},
genre: {
type: GraphQLString
}
})
})
const AuthorType = new GraphQLObjectType({
name: 'author',
fields: () => ({
id: {
type: GraphQLID
},
name: {
type: GraphQLString
},
age: {
type: GraphQLInt
}
})
})
// 定义query模型
const RootQuery = new GraphQLObjectType({
name: 'query',
fields: {
book: {
type: BookType,
args: {
id: {
type: GraphQLID
}
},
resolve(parent, args) {
// 数据来源,比如数据库或者其他来源
// lodash 模拟查找
return _.find(books, {
id: args.id
});
}
},
author: {
type: AuthorType,
args: {
id: {
type: GraphQLID
}
},
resolve(parent, args) {
// 数据来源,比如数据库或者其他来源
// lodash 模拟查找
return _.find(authors, {
id: args.id
});
}
},
}
})
// 导出 GraphQL 数据模型
module.exports = new GraphQLSchema({
query: RootQuery
})
运行如下,即为成功
3.3 Find book's author
稍微深入一点,找到书的作者
- 首先我们得对 books 数组进行改写,增加一个作者的字段
authorId
const books = [{
id: '1',
name: 'Javascript',
genre: '计算机',
authorId: '1'
},{
id: '2',
name: 'vue',
genre: '框架',
authorId: '2'
},{
id: '3',
name: '毛主席语录',
genre: '爱国',
authorId: '3'
}]
既然要找到书的作者,那么 BookType
中就要新增一个 author
字段了
const BookType = new GraphQLObjectType({
name: 'book',
fields: () => ({
id: {
type: GraphQLID
},
name: {
type: GraphQLString
},
genre: {
type: GraphQLString
},
author: {
type: AuthorType,
resolve(parent, args) {
return _.find(authors, {id: parent.authorId});
}
}
})
})
这个
BookType
的作用是什么呢 ?
个人观点,就是想得到的返回值的集合,可以只取其中的一个值,比如书的名称name
,或者取其中你想要的值,也就是说你可以自主选择数据内容,服务端负责精确返回
Ok,贴出完整的代码
// schema.js
const {
GraphQLObjectType,
GraphQLString,
GraphQLID,
GraphQLInt,
GraphQLSchema
} = require('graphql');
const _ = require('lodash');
const books = [{
id: '1',
name: 'Javascript',
genre: '计算机',
authorId: '1'
},{
id: '2',
name: 'vue',
genre: '框架',
authorId: '2'
},{
id: '3',
name: '毛主席语录',
genre: '爱国',
authorId: '3'
}]
const authors = [
{
id: '1',
name: 'red',
age: 34
},
{
id: '2',
name: 'blue',
age: 54
},
{
id: '3',
name: '毛主席',
age: 65
}
]
// 定义数据模型
const BookType = new GraphQLObjectType({
name: 'book',
fields: () => ({
id: {
type: GraphQLID
},
name: {
type: GraphQLString
},
genre: {
type: GraphQLString
},
author: {
type: AuthorType,
resolve(parent, args) {
console.log(parent);// 打印出来的为 {id:xx, name:xx, genre: xx, authorId: xx}
return _.find(authors, {id: parent.authorId});
}
}
})
})
const AuthorType = new GraphQLObjectType({
name: 'author',
fields: () => ({
id: {
type: GraphQLID
},
name: {
type: GraphQLString
},
age: {
type: GraphQLInt
}
})
})
// 定义query模型
const RootQuery = new GraphQLObjectType({
name: 'query',
fields: {
book: {
type: BookType,
args: {
id: {
type: GraphQLID
}
},
resolve(parent, args) {
// 数据来源,比如数据库或者其他来源
// lodash 模拟查找
return _.find(books, {
id: args.id
});
}
},
author: {
type: AuthorType,
args: {
id: {
type: GraphQLID
}
},
resolve(parent, args) {
// 数据来源,比如数据库或者其他来源
// lodash 模拟查找
return _.find(authors, {
id: args.id
});
}
},
}
})
// 导出 GraphQL 数据模型
module.exports = new GraphQLSchema({
query: RootQuery
})
运行完成后,就能得到
3.4 Find author's books
好,能顺利查到书的作者是谁了,但还是很多情况下,一个作者会写很多本书,那么怎么根据作者来查他写了多少书呢?再来进阶一下
- 首先,我们得再多编几本书
const books = [{
id: '1',
name: 'Javascript',
genre: '计算机',
authorId: '1'
},{
id: '2',
name: 'vue',
genre: '框架',
authorId: '2'
},{
id: '3',
name: '毛主席语录1',
genre: '爱国',
authorId: '3'
}, {
id: '4',
name: 'react',
genre: '框架',
authorId: '2'
}, {
id: '5',
name: '毛主席语录2',
genre: '爱国',
authorId: '3'
}]
那么跟上述的根据书查找作者类似,我们要在 AuthorType
中新增一个字段 books
, 既然是 books
那么就该是一个数组,引入使用 GraphQLList
const AuthorType = new GraphQLObjectType({
name: 'author',
fields: () => ({
id: {
type: GraphQLID
},
name: {
type: GraphQLString
},
age: {
type: GraphQLInt
},
books: {
type: new GraphQLList(BookType),
resolve(parent, args){
}
}
})
})
Ok, 贴出完整代码
// schema.js
const {
GraphQLObjectType,
GraphQLString,
GraphQLID,
GraphQLInt,
GraphQLSchema,
GraphQLList
} = require('graphql');
const _ = require('lodash');
const books = [{
id: '1',
name: 'Javascript',
genre: '计算机',
authorId: '1'
},{
id: '2',
name: 'vue',
genre: '框架',
authorId: '2'
},{
id: '3',
name: '毛主席语录1',
genre: '爱国',
authorId: '3'
}, {
id: '4',
name: 'react',
genre: '框架',
authorId: '2'
}, {
id: '5',
name: '毛主席语录2',
genre: '爱国',
authorId: '3'
}]
const authors = [
{
id: '1',
name: 'red',
age: 34
},
{
id: '2',
name: 'blue',
age: 54
},
{
id: '3',
name: '毛主席',
age: 65
}
]
// 定义数据模型
const BookType = new GraphQLObjectType({
name: 'book',
fields: () => ({
id: {
type: GraphQLID
},
name: {
type: GraphQLString
},
genre: {
type: GraphQLString
},
author: {
type: AuthorType,
resolve(parent, args) {
console.log(parent);
return _.find(authors, {id: parent.authorId});
}
}
})
})
const AuthorType = new GraphQLObjectType({
name: 'author',
fields: () => ({
id: {
type: GraphQLID
},
name: {
type: GraphQLString
},
age: {
type: GraphQLInt
},
books: {
type: new GraphQLList(BookType),
resolve(parent, args){
return _.filter(books, {authorId: parent.id});
}
}
})
})
// 定义query模型
const RootQuery = new GraphQLObjectType({
name: 'query',
fields: {
book: {
type: BookType,
args: {
id: {
type: GraphQLID
}
},
resolve(parent, args) {
// 数据来源,比如数据库或者其他来源
// lodash 模拟查找
return _.find(books, {
id: args.id
});
}
},
author: {
type: AuthorType,
args: {
id: {
type: GraphQLID
}
},
resolve(parent, args) {
// 数据来源,比如数据库或者其他来源
// lodash 模拟查找
return _.find(authors, {
id: args.id
});
}
},
}
})
// 导出 GraphQL 数据模型
module.exports = new GraphQLSchema({
query: RootQuery
})
4 Pratice with MongoDB
之前都是用死的数据,以及 js 的一些方法来模拟数据的搜索,这节开始,会加上数据库的操作。关于 MongoDB 的安装和操作,请自行查阅,后续可能会独立出来更新
- 首先我们要做的是连接数据库,使用
mongoose
来进行,完整代码如下:
// app.js
const express = require('express');
const graphqlHTTP = require('express-graphql');
const schema = require('./schema/schema1.js');
const app = express();
const mongoose = require('mongoose');
// 连接数据库,现在数据库中创建表 名为graphql
mongoose.connect("mongodb://localhost:27017/graphql", {useNewUrlParser: true});
// 监听数据库是不是连接成功
mongoose.connection.once('open', () => console.log('数据库连接成功'));
// 定义路由
app.use('/graphql', graphqlHTTP({
// 引入数据模型
schema,
// 启用类似于 graphQL 的 playground
graphiql: true,
}));
app.listen(8087, (err) => {
if (err) throw 'err';
console.log('listen on port 8087');
});
启动项目,当你连接成功后,终端会打印内容
ok, 准备工作做完了,目前数据库的 graphql
表还是空的,那么我们要插入一些数据
- 插入数据(手动版),先来一个手动插入数据的,后面会来一个初始化数据库的教程
- 创建 MongoDB schema 模型,新建
model
文件夹, 新建book.model.js
文件
// book.model.js
const mongoose = require('mongoose');
const Schema = mongoose.Schema;
const bookSchema = new Schema({
name: String,
gerne: String,
authorId: String
})
module.exports = mongoose.model('Book', bookSchema);
再来个 author.model.js
, copy 一下上面的,改改就好了
// author.model.js
const mongoose = require('mongoose');
const Schema = mongoose.Schema;
const authorSchema = new Schema({
name: String,
age: Number
})
module.exports = mongoose.model('Author', authorSchema);
两个模型建好了,那么,为了每次导出方便,我们在 model
文件夹下,创建一个 index.js
文件用于统一管理内部的文件
// index.js
const Author = require('./author.model');
const Book = require('./book.model');
module.exports = {
Author,
Book
}
- Ok, 到此为止,我们已经把 model 准备好了,在
app.js
中使用 schema
// app.js
const express = require('express');
const graphqlHTTP = require('express-graphql');
// 使用数据模型
const schema = require('./schema/schema1.js');
const app = express();
const mongoose = require('mongoose');
// 连接数据库,现在数据库中创建表 名为graphql
mongoose.connect("mongodb://localhost:27017/graphql", {useNewUrlParser: true});
// 监听数据库是不是连接成功
mongoose.connection.once('open', () => console.log('数据库连接成功'));
// 定义路由
app.use('/graphql', graphqlHTTP({
// 引入数据模型
schema,
// 启用类似于 graphQL 的 playground
graphiql: true,
}));
app.listen(8087, (err) => {
if (err) throw 'err';
console.log('listen on port 8087');
});
- 改写
schema.js
来进行数据插入, 新增一个mutation
// schema.js
const {
GraphQLObjectType,
GraphQLString,
GraphQLID,
GraphQLInt,
GraphQLSchema,
GraphQLList
} = require('graphql');
const _ = require('lodash');
const Model = require('../model');
const books = [{
id: '1',
name: 'Javascript',
genre: '计算机',
authorId: '1'
},{
id: '2',
name: 'vue',
genre: '框架',
authorId: '2'
},{
id: '3',
name: '毛主席语录1',
genre: '爱国',
authorId: '3'
}, {
id: '4',
name: 'react',
genre: '框架',
authorId: '2'
}, {
id: '5',
name: '毛主席语录2',
genre: '爱国',
authorId: '3'
}]
const authors = [
{
id: '1',
name: 'red',
age: 34
},
{
id: '2',
name: 'blue',
age: 54
},
{
id: '3',
name: '毛主席',
age: 65
}
]
// 定义数据模型
const BookType = new GraphQLObjectType({
name: 'book',
fields: () => ({
id: {
type: GraphQLID
},
name: {
type: GraphQLString
},
gerne: {
type: GraphQLString
},
author: {
type: AuthorType,
resolve(parent, args) {
console.log(parent);
return _.find(authors, {id: parent.authorId});
}
}
})
})
const AuthorType = new GraphQLObjectType({
name: 'author',
fields: () => ({
id: {
type: GraphQLID
},
name: {
type: GraphQLString
},
age: {
type: GraphQLInt
},
books: {
type: new GraphQLList(BookType),
resolve(parent, args){
return _.filter(books, {authorId: parent.id});
}
}
})
})
// 定义query模型
const RootQuery = new GraphQLObjectType({
name: 'query',
fields: {
book: {
type: BookType,
args: {
id: {
type: GraphQLID
}
},
resolve(parent, args) {
// 数据来源,比如数据库或者其他来源
// lodash 模拟查找
return _.find(books, {
id: args.id
});
}
},
author: {
type: AuthorType,
args: {
id: {
type: GraphQLID
}
},
resolve(parent, args) {
// 数据来源,比如数据库或者其他来源
// lodash 模拟查找
return _.find(authors, {
id: args.id
});
}
},
}
})
const Mutation = new GraphQLObjectType({
name: 'Mutation',
fields: {
addAuthor: {
type: AuthorType,
args: {
name: {
type: GraphQLString
},
age: {
type: GraphQLInt
}
},
resolve(parent, args) {
let author = new Model.Author({
name: args.name,
age: args.age
});
return author.save();
}
},
addBook: {
type: BookType,
args: {
name: {
type: GraphQLString
},
gerne: {
type: GraphQLString
},
authorId: {
type: GraphQLID
}
},
resolve(parent, args) {
console.log(args);
let book = new Model.Book({
name: args.name,
gerne: args.gerne,
authorId: args.authorId
});
return book.save();
}
}
}
})
// 导出 GraphQL 数据模型
module.exports = new GraphQLSchema({
query: RootQuery,
mutation: Mutation
})
- 将原先模拟的数据,全部改为数据库查询语句
// schema.js
const {
GraphQLObjectType,
GraphQLString,
GraphQLID,
GraphQLInt,
GraphQLSchema,
GraphQLList
} = require('graphql');
const _ = require('lodash');
const Model = require('../model');
const books = [{
id: '1',
name: 'Javascript',
genre: '计算机',
authorId: '1'
},{
id: '2',
name: 'vue',
genre: '框架',
authorId: '2'
},{
id: '3',
name: '毛主席语录1',
genre: '爱国',
authorId: '3'
}, {
id: '4',
name: 'react',
genre: '框架',
authorId: '2'
}, {
id: '5',
name: '毛主席语录2',
genre: '爱国',
authorId: '3'
}]
const authors = [
{
id: '1',
name: 'red',
age: 34
},
{
id: '2',
name: 'blue',
age: 54
},
{
id: '3',
name: '毛主席',
age: 65
}
]
// 定义数据模型
const BookType = new GraphQLObjectType({
name: 'book',
fields: () => ({
id: {
type: GraphQLID
},
name: {
type: GraphQLString
},
genre: {
type: GraphQLString
},
author: {
type: AuthorType,
resolve(parent, args) {
// console.log(parent);
// return _.find(authors, {id: parent.authorId});
return Model.Author.findById(parent.authorId);
}
}
})
})
const AuthorType = new GraphQLObjectType({
name: 'author',
fields: () => ({
id: {
type: GraphQLID
},
name: {
type: GraphQLString
},
age: {
type: GraphQLInt
},
books: {
type: new GraphQLList(BookType),
resolve(parent, args){
// return _.filter(books, {authorId: parent.id});
return Model.Book.find({authorId: parent.id});
}
}
})
})
// 定义query模型
const RootQuery = new GraphQLObjectType({
name: 'query',
fields: {
book: {
type: BookType,
args: {
id: {
type: GraphQLID
}
},
resolve(parent, args) {
// 数据来源,比如数据库或者其他来源
// lodash 模拟查找
// return _.find(books, {
// id: args.id
// });
return Model.Book.findById(args.id);
}
},
author: {
type: AuthorType,
args: {
id: {
type: GraphQLID
}
},
resolve(parent, args) {
// 数据来源,比如数据库或者其他来源
// lodash 模拟查找
// return _.find(authors, {
// id: args.id
// });
return Model.Book.findById(args.id);
}
},
books: {
type: new GraphQLList(BookType),
resolve(parent, args) {
return Model.Book.find({});
}
},
authors: {
type: new GraphQLList(AuthorType),
resolve(parent, args) {
return Model.Author.find({});
}
}
}
})
const Mutation = new GraphQLObjectType({
name: 'Mutation',
fields: {
addAuthor: {
type: AuthorType,
args: {
name: {
type: GraphQLString
},
age: {
type: GraphQLInt
}
},
resolve(parent, args) {
let author = new Model.Author({
name: args.name,
age: args.age
});
return author.save();
}
},
addBook: {
type: BookType,
args: {
name: {
type: GraphQLString
},
genre: {
type: GraphQLString
},
authorId: {
type: GraphQLID
}
},
resolve(parent, args) {
console.log(args);
let book = new Model.Book({
name: args.name,
genre: args.genre,
authorId: args.authorId
});
return book.save();
}
}
}
})
// 导出 GraphQL 数据模型
module.exports = new GraphQLSchema({
query: RootQuery,
mutation: Mutation
})