主题
graphQL实践
一. graphQL介绍
GraphQL是API查询语言,类似于数据库中的SQL,是Facebook于 2012 年在内部开发的数据查询语言,在 2015 年开源,旨在提供RESTful架构体系的替代方案。 不同的是sql的查询的是数据库,而GraphQL查询的是数据源,这个数据源可以是HTTP接口、数据库查询集合、静态json文件、另外一个api的数据源,特别的灵活。
官方资料
1. graphql与restful
GraphQL和RESTful一样,都是一种网站架构,一种前后端通信规范,不涉及语言,不同语言有不同的实现方案。
REST 的 API 配合JSON格式的数据交换,使得前后端分离、数据交互变得非常容易,而且也已经成为了目前Web领域最受欢迎的软件架构设计模式。 但是result缺点也非常明显,
1.1 restful缺点
- a. 滥用REST接口,导致大量相似度很高(具有重复性)的API越来越冗余;
- b. 前端和后端对于接口的控制权是交叉冲突的,往往一方改动不算,前端改动一个字段,连带着后端也需要改动,反之亦是。
- c. 前端对于真正用到的字段是没有直观映像的,仅仅通过url地址,无法预测也无法回忆返回的字段数目和字段是否有效,接口返回50个字段,但却只用5个字段,造成字段冗余,扩展性差,单个RESTful接口返回数据越来越臃肿
- d. API聚合问题,某个前端展现,实际需要调用多个独立的RESTful API才能获取到足够的数据。
- e. 前后端字段频繁改动,导致类型不一致,错误的数据类型可能会导致网站出错,尤其是在业务多变的场景中,很难在保证工程质量的同时快速满足业务需求
1.2 graphql优势
- a. 前端按需获取数据,每次请求接口时候,前端可以自由控制,一些不需要的字段可以不用请求回来,前端只需要获取需要的字段,加快传输速度,节约带宽,服务端只负责定义哪些资源是可用的,由客户端自己决定需要得到什么资源
- b. 接口聚合,渲染一个页面需要的数据,若用restful接口,可能会层层嵌套,上一个接口没回来, 下一个接口就不能用上一个接口的数据请求页面需要的数据,而graphql可以只用一个接口,拿到渲染一个页面所需要的数据
- c. graphql内的数据都定义了类型,前后端再对接graphql接口时候,数据的类型必须一致,否则直接报错,开发中可以很好的排除一些潜在错误。
1.3 graphql的缺陷
GraphQL 是在 REST 的基础上构建的,因此其设计是向前的迭代。 REST 一直是现代网络中最有影响力的基础构建块之一,没有它REST,GraphQL 就不会存在;
graphql有如下缺陷:
- a. HTTP 缓存 在一个项目中,使用一个后端接口地址就能满足需要(RESTful用不同的URL来区分资源,GraphQL用类型区分资源),因为无法使用固定规范的 HTTP 请求,也就无法把数据缓存在 HTTP 层,即使做再多的 GraphQL 服务端缓存,也无法解决网络级别的通信量拥堵问题,目前社区提供了一些客户端级别的缓存方案来解决一部分问题,比如使用 Apollo Client 、Relay,但是这些当然也不是免费的,需要开发者持续投入精力,增加了不少成本。
- b. HTTP Status 正常情况下 GraphQL 只会返回 Status Code 200,无论当前数据请求是成功或失败,这样传统方法的通讯状态判断和逻辑就无法使用,虽然开发者可以自定义一套错误处理逻辑,但也增加了复杂度。
1.4 总结
REST 和 GraphQL 都是基于 HTTP 的数据传输解决方案, GraphQL 可以显著的节省网络传输资源,在带宽紧张的环境中(例如移动端),这将发挥巨大的作用。
尽管 GraphQL 相比 REST 有很多显著的优点和升级,但在真实场景中,它并不一定是最适合你的实现。
总结来说,如果你希望做的应用追求简单而敏捷,且没有什么特殊考量,那就没什么必要使用 GraphQL,
REST 可靠、经济、不易出错;
反而言之,如果应用的关键点在于组织复杂数据逻辑,请求存在较多 Over-fetching、Under-fetching 的情况,或者对于网络环境敏感,那么 GraphQL 会是一发银弹。
二. graphQL基本示例
打开官方而文档:https://graphql.cn/code/#javascript,可以看到,javascript部分如下,graphql包含客户端和服务端,以及一些工具
1、Express + graphql 演示
(1)安装依赖
bash
npm i express express-graphql graphql --save
(2)编写代码
js
const express = require("express");
// graphql主要用于构建schema
const { buildSchema } = require("graphql");
// express-graphql中间件主要用于处理graphql请求
const { graphqlHTTP } = require("express-graphql");
const app = express();
app.use(express.static("/public"));
// 1 构建schema,注意模板字符串
const schema = buildSchema(`
type Query {
hello: String
}
`);
// 2. root根节点用于提供所有api对应的解析函数,是api入口的端点
const root = {
hello() {
return "hello grqphql";
}
};
// 3.用express-graphql中间件来处理graphql请求
app.use(
"/graphql",
graphqlHTTP({
schema: schema,
rootValue: root,
graphiql: true // true表示要启用浏览器调试界面
})
);
app.listen(3000, () => {
console.log("serve runing at http://127.0.0.1:3000");
});
(3)调试预览 若graphiql: true
,则,可以在浏览器访问http://127.0.0.1:3000/graphql
,可以得到如下界面
(4)用fetch请求
graphql必须为
post
请求,若使用axios,不需要手动序列化,因为axios会默认序列化,参数为:data:{query:"{hello}"}
js
fetch('http://127.0.0.1:3000/graphql', {
method: 'get',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
body: JSON.stringify({ query: "{ hello }" }) // 多个参数以引文逗号隔开,或空格隔开都行
}).then(r=>r.json())
.then(res=>{
console.log(res);
})
结果如下:
2、graphql客户端示例
graphql拥有自己的客户端,但是,开发web应用并不需要额外的复杂客户端,用ajax
,或者fetch
完全满足要求, 上面的示例中,我们用了fetch
在html内发请求,获取到了grqphQL的数据,当然也可以用其他的,例如axios或者原生ajax。
对于web来说,所有的接口请求地址都是服务端设定好的某个路由,比如,上面示例中的api路由为/graphql
,所有的graphQL请求都指向这一个地址,且请求方法为post,在express-graphql中,中间件会提供一个graphqlHTTP
方法,来根据前端传入的类型区分某个请求应该走向哪个根节点的解析函数。
开发阶段,可以给graphiql
设置为true
,表示要开启浏览器调试界面,拥有这个调试界面,可以很方便的调试、对接后端的接口,左边书写前端需要某个接口数据字段,结构,当然,这些需要服务端支持,前端获取到的数据类型就和后端返回的一模一样,
当要将服务发布上线,应该关闭graphiql
为false
三. graphQL类型
概述
包括
String
、Int
、Float
、Boolean
和ID
大多数情况下,你所需要做的只是使用 GraphQL schema language 指定你的 API 需要的类型,然后作为参数传给 buildSchema 函数。
GraphQL schema language **支持的标量类型有 String、Int、Float、Boolean 和 ID,因此你可以在传给 buildSchema 的 schema 中直接使用这些类型。
默认情况下,每个类型都是可以为空的 —— 意味着所有的标量类型都可以返回 null。使用感叹号可以标记一个类型不可为空,如 String! 表示非空字符串
如果是列表类型,使用方括号将对应类型包起来,如**[Int] 就表示一个整数列表。**
1、基本类型
html
String 表示字符串
Int 表示整数 (grqphQL里面没有Number)
Float 表示浮点数(小数)
Boolean 表示布尔值(true/false)
ID 表示ID, 值必须是唯一的,否则或报错
若是加上
!
,则表示不能为null
, 如:String!
表示类型必须有值,且为String
2、数组
html
[Int] 表示整数组成的数组 如:[1,2,3]
[Int!] 表示数组不能为空数组,[null]都不行 否则报错
[Int]! 表示必须有数组,至少为[],不能为null
[Int!]! 表示必须有数组,且得有不为null的元素,如[123]
其他类型数组也是类似的操作...
3、对象
当你对一个返回对象类型的 API 发出 GraphQL 查询时,你可以通过嵌套 GraphQL 字段名来一次性调用对象上的多个方法。例如:
graphql
type UserInfo {
name:String!
age:Int
avatar:String
}
type Query {
Book:{
id:ID!
bookName:String
img:String
page:Int
author:UserInfo ## 书的作者信息,引用了上面定义好的UserInfo类型,客户端可以选择性的获取对象里面的某些字段
}
}
这种定义对象类型的方式通常会比传统的 REST 风格的 API 会带来更多的好处。你可以只用一次请求就能获取到所有信息,而不是一次请求只能获取到一个对象的相关信息,然后还要请求一系列 API 才能获取到其他对象的信息。这样不仅节省了带宽、让你的应用跑得更快,同时也简化了你客户端应用的逻辑。
数组里面套对象的类型表示也是如此,例如:
[UserInfo]
表示用户组成的数组,每一项都为一个用户
四. graphQL增删改查
1、接口传递参数
就像 REST API 一样,在 GraphQL API 中,通常向入口端点传入参数,在 schema language 中定义参数,并自动进行类型检查,每一个参数必须有名字和数据类型。
1.1 服务端示例
js
const express = require("express");
// graphql主要用于构建schema
const { buildSchema } = require("graphql");
// express-graphql中间件主要用于处理graphql请求
const { graphqlHTTP } = require("express-graphql");
const { v4: UUIDV4 } = require("uuid"); // 用于生成唯一的ID
const app = express();
app.use(express.static("./public"));
// 1 构建schema,注意模板字符串
const schema = buildSchema(`
type Article {
id:ID!
title:String!
auth:String!
content:String!
createdAt:Int
updatedAt:Int
}
type Query {
getArticle(title:String, auth:String):[Article]
}
`);
// 虚假数据源
let source = [
{id: UUIDV4(),title: "西游记",auth: "吴承恩",content: "孙悟空三大白骨金",createdAt: Date.now(),updatedAt: Date.now()},
{id: UUIDV4(),title: "水浒传",auth: "施耐庵",content: "十八条好汉上梁山",createdAt: Date.now(),updatedAt: Date.now()},
{id: UUIDV4(),title: "三国演义",auth: "罗贯中",content: "关三爷过五关斩六将,温酒斩华雄",createdAt: Date.now(),updatedAt: Date.now()},
{id: UUIDV4(),title: "红楼梦",auth: "曹雪芹",content: "举止见识出于须眉之上的闺阁佳人的人生百态,展现了真正的人性美和悲剧美",createdAt: Date.now(),updatedAt: Date.now()},
];
// 2. root根节点用于提供所有api对应的解析函数,是api入口的端点
const root = {
getArticle(arg) {
const {title,auth} = arg||{}
return source.filter(item=>{
if(title&&auth){
return item.title === title && item.auth === auth
}else if (title || auth){
return title ? item.title === title : item.auth === auth
}else {
return true
}
})
}
};
// 3.用express-graphql中间件来处理graphql请求
app.use(
"/graphql",
graphqlHTTP({
schema: schema,
rootValue: root,
graphiql: true // true表示要启用浏览器调试界面
})
);
app.listen(3000, () => {
console.log("serve runing at http://127.0.0.1:3000");
});
1.2 调试界面示例
1.3 客户端示例
js
let title = "";
let auth = "";
// query Article($title:String, $auth:String)中,Article是随便取的名字,也可以不写名字,例如query ($title:String, $auth:String){...}
// 使用 $title和 $auth 作为 GraphQL 中的变量,我们无需在客户端对它们进行转义
let query = `query Article($title:String, $auth:String) {
getArticle(title:$title, auth:$auth){
id
title
auth
content
}
}`
axios({
url: "http://127.0.0.1:3000/graphql",
method: "post",
data: {
query,
variables: { title, auth }
}
}).then(res => {
console.log(res);
})
// ------ 另一种写法,不用 variables --------------------------
let title = "";
let auth = "";
// 通过ES6模板子字符串拼接的方式来拼接query参数
let query = `{
getArticle(auth:"${auth}",title:"${title}") {
id
title
auth
content
}
}`
axios({
url: "http://127.0.0.1:3000/graphql",
method: "post",
data: {
query,
}
}).then(res => {
console.log(res);
})
效果:
❤特别注意
- ① 客户端:query参数中的
query Article
这个Article
是随便取的名字,可以写别的,也可以不写 - ② 客户端:当你在代码中传递参数时,最好避免自己构建整个查询语句。你可以使用
$
语法来定义一条查询中的变量,并将变量作为单独映射来传递。 - ③ query 参数或 schema 里面的空格一定不能是中文空格,否则解析时会报错
- ④ 使用
$title
和$auth
作为GraphQL
中的变量,我们无需在客户端对它们进行转义 - ⑤ schema中字段不能出现中横线
-
,可以使用下划线,否则报错 - ⑥ 服务端:schema中只能有一个
type Query
,也只能有一个type Mutation
,否则报错。
1.4 input类型
在定义schema时候,有时候入参会很多,这时可以使用“输入类型”来简化 schema,使用 input 关键字而不是 type 关键字即可
服务端修改
(1)在schema内新增input类型,用于入参
graphQL
## 查询接口入参类型
input SearchInput {
title:String
auth:String
}
(2)修改schema的query的入参类型,用input接收参数
graphQL
type Query {
getArticle(input:SearchInput):[Article]
}
(3)修改root节点内的对应实践处理函数接收参数的形式
js
getArticle(arg) {
console.log(arg);
const {input:{ auth, title }} = arg || {}; // 接收到的参数内只有input字段
return source.filter(item => {
if (title && auth) {
return item.title === title && item.auth === auth;
} else if (title || auth) {
return title ? item.title === title : item.auth === auth;
} else {
return true;
}
});
},
服务端用input类型的完整示例<span id="SERVICE"></span>
js
const express = require("express");
// graphql主要用于构建schema
const { buildSchema } = require("graphql");
// express-graphql中间件主要用于处理graphql请求
const { graphqlHTTP } = require("express-graphql");
const { v4: UUIDV4 } = require("uuid"); // 用于生成唯一的ID
const app = express();
app.use(express.static("./public"));
// 1 构建schema,注意模板字符串
const schema = buildSchema(`
input SearchInput {
title:String
auth:String
}
input ArticleInput {
title:String!
auth:String!
content:String!
}
type Article {
id:ID!
title:String!
auth:String!
content:String!
createdAt:Int
updatedAt:Int
}
type Query {
getArticle(input:SearchInput):[Article]
}
type Mutation {
createArticle(input: ArticleInput):Article
}
`);
// 虚假数据源
let source = [
{id: UUIDV4(),title: "西游记",auth: "吴承恩",content: "孙悟空三大白骨金",createdAt: Date.now(),updatedAt: Date.now()},
{id: UUIDV4(),title: "水浒传",auth: "施耐庵",content: "十八条好汉上梁山",createdAt: Date.now(),updatedAt: Date.now()},
{id: UUIDV4(),title: "三国演义",auth: "罗贯中",content: "关三爷过五关斩六将,温酒斩华雄",createdAt: Date.now(),updatedAt: Date.now()},
{id: UUIDV4(),title: "红楼梦",auth: "曹雪芹",content: "举止见识出于须眉之上的闺阁佳人的人生百态,展现了真正的人性美和悲剧美",createdAt: Date.now(),updatedAt: Date.now()}
];
// 2. root根节点用于提供所有api对应的解析函数,是api入口的端点
const root = {
// 搜索文章
getArticle(arg) {
console.log(arg);
const {input:{ auth, title }} = arg || {};
return source.filter(item => {
if (title && auth) {
return item.title === title && item.auth === auth;
} else if (title || auth) {
return title ? item.title === title : item.auth === auth;
} else {
return true;
}
});
},
// 添加文章
createArticle({ input: { title, auth, content } }) {
let article = {
id: UUIDV4(),
title,
auth,
content,
createdAt: Date.now(),
updatedAt: Date.now()
};
source.push(article);
return article;
}
};
// 3.用express-graphql中间件来处理graphql请求
app.use(
"/graphql",
graphqlHTTP({
schema: schema,
rootValue: root,
graphiql: true // true表示要启用浏览器调试界面
})
);
app.listen(3000, () => {
console.log("serve runing at http://127.0.0.1:3000");
});
客户端修改
(1)修改query参数
js
// $param 这个名字随便写,但是上下得一致,input参数需要和后端保持一致,SearchInput为后端定义的查询参数input类型
let query = `query getArticle($param:SearchInput){
getArticle(input:$param) {
id
title
auth
content
}
}`
(2)修改body里面的variables
js
axios({
url: "http://127.0.0.1:3000/graphql",
method: "post",
data: {
query,
variables: {
param:{ title, auth }
}
}
}).then(res => {
console.log(res);
})
客户端input类型的完整示例<span id="CLIENT"></span>
js
let title = "";
let auth = "";
let query = `query getArticle($param:SearchInput){
getArticle(input:$param) {
id
title
auth
content
}
}`
axios({
url: "http://127.0.0.1:3000/graphql",
method: "post",
data: {
query,
variables: {
param:{ title, auth }
}
}
}).then(res => {
console.log(res);
})
调试界面input类型的完整示例
1.5 传参方式总结
(1)传参时候直接写{}
,默认表示query
(2)若要写修改、新增、删除,需要显式的注明mutation
,query也可以显式的注明
(3)也可以给query
或者 mutation
起一个名字
(4)若参数都是非必填的,前端写参数时候,可以不写参数,直接选择需要返回的字段即可
(5)前端并行请求传参示例 前端最好显式的注明式query还是mutation
2、查询 数据
graphQL用query作为入口端点来进行查询数据,没有显式声明query时候,默认为query,且一个schema里面只能有一个query入口端点。
2.1 普通查询
服务端定义好schema和root节点内的处理函数,客户端通过axios或者fetch以post请求的方式发请求,得到结果。 具体示例请看上面 服务端示例 、客户端示例
2.2 查询别名
示例:
别名的好处: 1)同一个查询中,可多次查同一个数据,以不同的别名命名 2)可将合并的数据分开成为各自的列表值对象,方便前端取值处理,后端不用改动。上图就是这样, 3)当一个数据对象的名字很长时候,用别名的方式,返回时候取值方便,方便前端渲染。
2.3 聚合查询
场景:一个博客系统,在服务端,一般情况下,用户信息是都单独存在一个用户表,文章评论也是存的评论表,当我们需要查询文章详情时候,文章详情中返回的数据中,用户和评论字段的值不是用户名和评论信息,而是用户Id和文章ID,若前端需要知道用户信息和评论信息,队医restful来说,后端可能会提供对应查询用户信息和评论信息的接口,在文章详情接口返回后,需要再分别请求用户和评论接口来获取对应的数据,若是有BBF(node中间层)的话,可以在node端将数据拼装好返回给前端渲染
这对于graphQL来说,解决起来就很方便,具体看graphql-tools章节
2.4 分片查询
分片查询,其实就是将多个query里面的公共字段类型进行提取出来,然后再用展开运算符进行引用。
例如:后端定义了一个User的schema类型,前端调用时候,可以将里面的字段提取出来,当同时调用多次时候,就可以引用
后端schema代码:
graphql
type User {
name:String
age:Int
sex:String
}
type Query {
getUser():User
}
客户端代码:
如下:其中User是在后端定义的要返回的类型,这里根据User,用fragment
分片了一个post出来,
下面我要查询相关字段时直接使用post就可以了(这里使用了es6的展开运算符)
graphql
fragment post on User {
name
age
sex
}
query {
getUser {
...post
}
}
3、增、删、改 数据
向数据库中插入数据或修改已有数据,在 GraphQL 中,你应该将这个入口端点做为
Mutation
而不是Query
。这十分简单,只需要将这个入口端点做成Mutation
类型顶层的一部份即可。
增删改都是Mutation
,写法类似,所以这里就以新增为例做演示:
3.1 服务端
js
const express = require("express");
// graphql主要用于构建schema
const { buildSchema } = require("graphql");
// express-graphql中间件主要用于处理graphql请求
const { graphqlHTTP } = require("express-graphql");
const { v4: UUIDV4 } = require("uuid"); // 用于生成唯一的ID
const app = express();
app.use(express.static("./public"));
// 1 构建schema,注意模板字符串
const schema = buildSchema(`
input ArticleInput {
title:String!
auth:String!
content:String!
}
type Article {
id:ID!
title:String!
auth:String!
content:String!
createdAt:Int
updatedAt:Int
}
type Mutation {
createArticle(input: ArticleInput):Article
}
`);
// 虚假数据源
let source = [
{id: UUIDV4(),title: "西游记",auth: "吴承恩",content: "孙悟空三大白骨金",createdAt: Date.now(),updatedAt: Date.now()},
{id: UUIDV4(),title: "水浒传",auth: "施耐庵",content: "十八条好汉上梁山",createdAt: Date.now(),updatedAt: Date.now()},
{id: UUIDV4(),title: "三国演义",auth: "罗贯中",content: "关三爷过五关斩六将,温酒斩华雄",createdAt: Date.now(),updatedAt: Date.now()},
{id: UUIDV4(),title: "红楼梦",auth: "曹雪芹",content: "举止见识出于须眉之上的闺阁佳人的人生百态,展现了真正的人性美和悲剧美",createdAt: Date.now(),updatedAt: Date.now()}
];
// 2. root根节点用于提供所有api对应的解析函数,是api入口的端点
const root = {
// 添加文章
createArticle({ input: { title, auth, content } }) {
let article = {
id: UUIDV4(),
title,
auth,
content,
createdAt: Date.now(),
updatedAt: Date.now()
};
source.push(article);
return article;
}
};
// 3.用express-graphql中间件来处理graphql请求
app.use(
"/graphql",
graphqlHTTP({
schema: schema,
rootValue: root,
graphiql: true // true表示要启用浏览器调试界面
})
);
app.listen(3000, () => {
console.log("serve runing at http://127.0.0.1:3000");
});
3.2 客户端
js
let title = "稻香";
let auth = "周杰伦";
let content = "窗外的麻雀"
let query = `mutation createArticle($article:ArticleInput){
createArticle(input:$article) {
id
title
auth
content
}
}`
axios({
url: "http://127.0.0.1:3000/graphql",
method: "post",
data: {
query,
variables: {
article: { title, auth, content }
}
}
}).then(res => {
console.log(res);
})
3.3 运行效果
服务端 客户端
调试界面
请求结果
4、合并请求(聚合)
若我们需要一次请求多个接口,可以像restful那样,同时发起多个请求
4.1 服务端
服务端只需要定义多个请求即可,例如,schema中有对个请求, root节点也有对应的请求处理行数即可。
4.2 客户端
js
let title = "";
let auth = "";
let query = `
query ($input:SearchInput){
## 请求1
getArticle(input:$input){
id
auth
}
## 请求2
getArticleTitle
}
`
axios({
method: "post",
data: {
query,
variables: {
input: { title, auth }
}
}
}).then(res => {
console.log(res.data.data);
})
4.3 运行结果
五. 调试器使用技巧
1、查看schema细节
后端写好接口后,前端不知道后端提供的接口入参有哪些字段,每个字段的类型是什么,是否是必传字段,此时,graphQL提供的开发调试界面,为我们解决了这一问题
打开调试界面 点击右侧的Docs
按钮菜单,打开右侧边栏, 可以看到 ROOT TYPES
下有一些选项,query
和mutation
,这些就是后端定义的schema,前端可以恒方便的点击查看每个schema,知道入参,出参,参数类型,是否必穿等信息,方便对接
当然,GraphQL是可自省的,也就是说你可以通过查询一个GraphQL知道它自己的schema细节。 自省查询可能是你在GraphQL中唯一的GET请求。不管是query还是mutation,如果你要传递请求体,GraphQL请求方式都应该是POST 例如:查询__schema以列出所有该schema中定义的类型,并获取每一个的细节:
query {
__schema {
types {
name
kind
description
fields {
name
}
}
}
}
再例如,查询__type以获取任意类型的细节:
query {
__type(name: "Query") {
name
kind
description
fields {
name
}
}
}
2、代码提示
在调试器写schema时候,一般都会有代码提示,来提示参数有那些,可以输入1
来查看具体的参数 例如:
六. graphQL下Token验证
七. 生成接口文档
1、用双引号""
用英文双引号在需要说明的字段、方法、type上方写上说明。(必须是上方,其他位置不行,会报错) ""
添加好处是注释可以不和被加注释的字段等仅仅挨着
然后再调试面板右侧打开DOCS
,可以看到文档