GraphQL入门

参考: 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
目录如下:

folder.png

为了方便,直接贴出 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,能见到打印信息,即已经成功启动

start.png

3. GraphQL setting up

前期已经安装过一系列 graphQL 相关依赖,这节开始将派上用场

3.1 Book schema setting up

先来一个简单查询的,查找书的 name, 或者 genre

  1. 首先在根目录下创建 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
})
  1. 改造 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');
});
  1. 启动项目, npm start, 在浏览器端输入地址,当前使用的是 localhost:8087
    image.png

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
})

运行如下,即为成功


image.png

3.3 Find book's author

稍微深入一点,找到书的作者

  1. 首先我们得对 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
})

运行完成后,就能得到


image.png

3.4 Find author's books

好,能顺利查到书的作者是谁了,但还是很多情况下,一个作者会写很多本书,那么怎么根据作者来查他写了多少书呢?再来进阶一下

  1. 首先,我们得再多编几本书
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
})
image.png

4 Pratice with MongoDB

之前都是用死的数据,以及 js 的一些方法来模拟数据的搜索,这节开始,会加上数据库的操作。关于 MongoDB 的安装和操作,请自行查阅,后续可能会独立出来更新

  1. 首先我们要做的是连接数据库,使用 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');
});

启动项目,当你连接成功后,终端会打印内容


image.png

ok, 准备工作做完了,目前数据库的 graphql 表还是空的,那么我们要插入一些数据

  1. 插入数据(手动版),先来一个手动插入数据的,后面会来一个初始化数据库的教程
  • 创建 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
})
image.png
image.png