12.拓展-GraphQL简介

1.GraphQL

1.1 介绍

GraphQL 是一种 API 查询语言
GraphQL 是一种为 API 接口和查询已有数据运行时环境的查询语言. 它提供了一套完整的和易于理解的 API 接口数据描述, 给客户端权力去精准查询他们需要的数据, 而不用再去实现其他更多的代码, 使 API 接口开发变得更简单高效, 支持强大的开发者工具.

友情链接:

https://graphql.org

https://graphql.js.cool/

1.1.1 What is GraphQL?

正如副标题所说,GraphQL 是由 Facebook 创造的用于描述复杂数据模型的一种查询语言。这里查询语言所指的并不是常规意义上的类似 sql 语句的查询语言,而是一种用于前后端数据查询方式的规范。

1.1.2 Why using GraphQL?

当今客户端和服务端主要的交互方式有 2 种,分别是 REST 和 ad hoc 端点。GraphQL 官网指出了它们的不足之处主要在于:当需求或数据发生变化时,它们都需要建立新的接口来适应变化,而不断添加的接口,会造成服务器代码的不断增长,即使通过增加接口版本,也并不能够完全限制服务器代码的增长。

GraphQL 特性
  • 首先,它是声明式的。查询的结果格式由请求方(即客户端)决定而非响应方(即服务器端)决定,也就是说,一个 GraphQL 查询结果的返回是同客户端请求时的结构一样的,不多不少,不增不减。
  • 其次,它是可组合的。一个 GraphQL 的查询结构是一个有层次的字段集,它可以任意层次地进行嵌套或组合,也就是说它可以通过对字段进行组合、嵌套来满足需求。
  • 第三,它是强类型的。强类型保证,只有当一个 GraphQL 查询满足所设定的查询类型,那么查询的结果才会被执行。

BP

1.2 Node.js 环境简单用法

1.下载

GraphQL 规范的参考实现, 专为在 Node.js 环境中运行 GraphQL 而设计.

通过 命令行 执行 GraphQL.js hello world 脚本:

1
npm install graphql

2.创建 hello.js

然后执行 node hello.js.

hello.js源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
var { graphql, buildSchema } = require('graphql');

var schema = buildSchema(`
type Query {
hello: String
}
`);

var root = { hello: () => 'Hello world!' };

graphql(schema, '{ hello }', root).then((response) => {
console.log(response);
});

1.3 Express 运行

通过 Express 实现的 GraphQL 参考实现, 你可以联合 Express 运行或独立运行.

为了方便下面的步骤,我们在graphql文件夹中手动创建下面的目录结构,并安装指定的依赖

BP

运行一个 express-graphql 安装项目依赖

1
npm install express express-graphql graphql

1.index.js代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const express = require('express')
const expressGraphql = require('express-graphql')
const app = express()
// 配置路由
app.use('/graphql', expressGraphql(req => {
return {
schema: require('./schema'), // graphql相关代码主目录
graphiql: true // 是否开启可视化工具
// ... 此处还有很多参数,为了简化文章,此处就一一举出, 具体可以看 刚才开篇提到的 express文档,
// 也可以在文章末尾拉取项目demo进行查阅
}
}))
// 服务使用3000端口
app.listen(3000, () => {
console.log("graphql server is ok open http://localhost:3000/graphql");
});

2.graphql/schema.js代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const {
GraphQLSchema,
GraphQLObjectType
} = require('graphql')
// 规范写法,声明query(查询类型接口) 和 mutation(修改类型接口)
module.exports = new GraphQLSchema({
query: new GraphQLObjectType({
name: 'Query',
description: '查询数据',
fields: () => ({
// 查询类型接口方法名称
fetchObjectData: require('./queries/fetchObjectData')
})
}),
mutation: new GraphQLObjectType({
name: 'Mutation',
description: '修改数据',
fields: () => ({
// 修改类型接口方法名称
updateData: require('./mutations/updateData')
})
})
})

3.graphql/queries/fetchObjectData.js代码

先在graphql/queries文件夹下创建fetchObjectData.js文件, 并填入以下代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
const {
GraphQLID,
GraphQLInt,
GraphQLFloat,
GraphQLString,
GraphQLBoolean,
GraphQLNonNull,
GraphQLObjectType
} = require('graphql')
// 定义接口返回的数据结构
const userType = new GraphQLObjectType({
name: 'userItem',
description: '用户信息',
fields: () => ({
id: {
type: GraphQLID,
description: '数据唯一标识'
},
username: {
type: GraphQLString,
description: '用户名'
},
age: {
type: GraphQLInt,
description: '年龄'
},
height: {
type: GraphQLFloat,
description: '身高'
},
isMarried: {
type: GraphQLBoolean,
description: '是否已婚',
deprecationReason: "这个字段现在不需要了"
}
})
})
// 定义接口
module.exports = {
type: userType,
description: 'object类型数据例子',
// 定义接口传参的数据结构
args: {
isReturn: {
type: new GraphQLNonNull(GraphQLBoolean),
description: '是否返回数据'
}
},
resolve: (root, params, context) => {
const { isReturn } = params
if (isReturn) {
// 返回的数据与前面定义的返回数据结构一致
return {
"id": "110000199811259999",
"username": "DongWenbin",
"age": 21,
"height": 182.5,
"isMarried": true
}
} else {
return null
}
}
}

4.graphql/mutations/updateData.js代码

先在graphql/mutations文件夹下创建updateData.js文件, 并填入以下代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const {
GraphQLInt
} = require('graphql')

let count = 0

module.exports = {
type: GraphQLInt, // 定义返回的数据类型
description: '修改例子',
args: { // 定义传参的数据结构
num: {
type: GraphQLInt,
description: '数量'
}
},
resolve: (root, params) => {
let { num } = params
count += num
return count
}
}

好了,到此为止,简单的GraphQL服务器就搭建好了,让我们来启动看看

1
node index.js  // 启动项目

然后我们在浏览器打开 http://localhost:3000/graphql 如下图所示

BP

我们可以看到页面分为3栏,左边的是调用api用的,中间是调用api返回的结果 右边实际上就是我们刚才定义接口相关的东西,也就是api文档。
我们在左边粘贴以下代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
query fetchObjectData {
fetchObjectData(
isReturn: true
) {
id
username
age
height
isMarried
}
}

mutation updateData {
updateData(
num: 2
)
}

1.4 语法规范

1、导入GraphQL.js及类型

graphql 无论在定义接口参数和接口返回结果时, 都需要先定义好其中所包含数据结构的类型, 这不难理解,可以理解为我们定义的就是数据模型,其中常用的类型如下。

1
2
3
4
5
6
7
8
9
10
const {
GraphQLList, // 数组列表
GraphQLObjectType, // 对象
GraphQLString, // 字符串
GraphQLInt, // int类型
GraphQLFloat, // float类型
GraphQLEnumType, // 枚举类型
GraphQLNonNull, // 非空类型
GraphQLSchema // schema(定义接口时使用)
} = require('graphql')

2、定义schema

schema实例中,一般规范为
query: 定义查询类的接口
mutation: 定义修改类的接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
new GraphQLSchema({
query: new GraphQLObjectType({
name: 'Query', // 查询实例名称
description: '查询数据', // 接口描述
fields: () => ({
// 查询类型接口方法名称
fetchDataApi1: require('./queries/fetchDataApi1'),
fetchDataApi2: require('./queries/fetchDataApi2'),
fetchDataApi3: require('./queries/fetchDataApi3'),
...
})
}),
mutation: new GraphQLObjectType({
name: 'Mutation',
description: '修改数据',
fields: () => ({
// 修改类型接口方法名称
updateDataApi1: require('./mutations/updateDataApi1'),
updateDataApi2: require('./mutations/updateDataApi2'),
...
})
})
})

3、接口方法定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
// 引用需要用到的数据类型
const {
GraphQLID,
GraphQLString,
GraphQLNonNull,
GraphQLObjectType
} = require('graphql')
// 第一部分 定义接口返回的数据结构
// 不难看出来,下面定义的是
/*
{
id
username
}
*/
const userType = new GraphQLObjectType({
name: 'userItem',
description: '用户信息',
fields: () => ({
id: {
type: GraphQLID,
description: '数据唯一标识'
},
username: {
type: GraphQLString,
description: '用户名'
}
})
})
// 第二部分 定义接口
module.exports = {
type: userType,
description: 'object类型数据例子',
// 定义接口传参的数据结构
args: {
isReturn: {
type: new GraphQLNonNull(GraphQLBoolean),
description: '是否返回数据'
}
},
resolve: (root, params, context) => {
const { isReturn } = params
// 返回的数据与前面定义的返回数据结构一致
return {
"id": "5bce2b8c7fde05hytsdsc12c",
"username": "Davis"
}
}
}

1.5 前后端交互(入门)

1、准备

1
npm i --save express express-graphql graphql cors

2.服务器端代码app.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
var express = require('express');
var graphqlHTTP = require('express-graphql');
const { buildSchema } = require('graphql');
const cors = require('cors'); // 用来解决跨域问题

// 创建 schema,需要注意到:
// 1. 感叹号 ! 代表 not-null
// 2. rollDice 接受参数
const schema = buildSchema(`
type Query {
username: String
age: Int!
}
`)
const root = {
username: () => {
return '董文斌'
},
age: () => {
return Math.ceil(Math.random() * 100)
},
}
const app = express();
app.use(cors());
app.use('/graphql', graphqlHTTP({
schema: schema,
rootValue: root,
graphiql: true
}))

app.listen(3300);
console.log('Running a GraphQL API server at http://localhost:3300/graphql')

3. 客户端代码index.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>graphql demo</title>
</head>

<body>
<button class="test">获取当前用户数据</button>
<p class="username"></p>
<p class="age"></p>
</body>
<script>
var test = document.querySelector('.test');
test.onclick = function () {
var username = document.querySelector('.username');
var age = document.querySelector('.age');
fetch('http://localhost:3300/graphql', {
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
},
method: 'POST',
body: JSON.stringify({
query: `{
username,
age,
}`
}),
mode: 'cors' // no-cors, cors, *same-origin
})
.then(function (response) {
return response.json();
})
.then(function (res) {
console.log('返回结果', res);
username.innerHTML = `姓名:${res.data.username}`;
age.innerHTML = `年龄:${res.data.age}`
})
.catch(err => {
console.error('错误', err);
});
}
</script>

</html>

1.6 总结

目前前后端的结构大概如下图。后端通过 DAO 层与数据库连接,服务于主要处理业务逻辑的 Service 层,为 Controller 层提供数据源并产出 API;前端通过浏览器 URL 进行路由命中获取目标视图状态,而页面视图是由组件嵌套组成,每个组件维护着各自的组件级状态,一些稍微复杂的应用还会使用集中式状态管理的工具,比如 Vuex、Redux、Mobx 等。前后端只通过 API 来交流,这也是现在前后端分离开发的基础。

BP

如果使用 GraphQL,那么后端将不再产出 API,而是将 Controller 层维护为 Resolver,和前端约定一套 Schema,这个 Schema 将用来生成接口文档,前端直接通过 Schema 或生成的接口文档来进行自己期望的请求。

经过几年一线开发者的填坑,已经有一些不错的工具链可以使用于开发与生产,很多语言也提供了对 GraphQL 的支持,比如 Java/Nodejs、Java、PHP、Ruby、Python、Go、C# 等。

一些比较有名的公司比如 Twitter、IBM、Coursera、Airbnb、Facebook、Github、携程等,内部或外部 API 从 RESTful 转为了 GraphQL 风格,特别是 Github,它的 v4 版外部 API 只使用 GraphQL。据一位在 Twitter 工作的大佬说硅谷不少一线二线的公司都在想办法转到 GraphQL 上,但是同时也说了 GraphQL 还需要时间发展,因为将它使用到生产环境需要前后端大量的重构,这无疑需要高层的推动和决心。

正如尤雨溪所说,为什么 GraphQL 两三年前没有广泛使用起来呢,可能有下面两个原因:

  1. GraphQL 的 field resolve 如果按照 naive 的方式来写,每一个 field 都对数据库直接跑一个 query,会产生大量冗余 query,虽然网络层面的请求数被优化了,但数据库查询可能会成为性能瓶颈,这里面有很大的优化空间,但并不是那么容易做。FB 本身没有这个问题,因为他们内部数据库这一层也是抽象掉的,写 GraphQL 接口的人不需要顾虑 query 优化的问题。
  2. GraphQL 的利好主要是在于前端的开发效率,但落地却需要服务端的全力配合。如果是小公司或者整个公司都是全栈,那可能可以做,但在很多前后端分工比较明确的团队里,要推动 GraphQL 还是会遇到各种协作上的阻力。

大约可以概括为性能瓶颈和团队分工的原因,希望随着社区的发展,基础设施的完善,会渐渐有完善的解决方案提出,让广大前后端开发者们可以早日用上此利器。