初识GraphQL
GraphQL 是一种 API 查询语言,可以让客户端按需请求需要的数据,避免了 REST API 中的过度请求和响应数据的情况,虽然已经出现不少年份了,但一直没有去尝试使用过,最近有空学习了一下,稍作整理。
参考
graphql官方文档,国际惯例,先看官方文档
背景
在 REST API 中,客户端发送 HTTP 请求到服务器,服务器返回整个资源的响应。但有时候客户端只需要获取资源中的一部分,这时候就会请求多次才能获取所需的数据,导致了过度请求和响应数据的问题。
GraphQL 的思路是:客户端发送查询语句到服务器,查询语句只包含需要获取的数据字段,服务器返回查询语句中请求的数据,而不是整个资源。这样,客户端可以根据需要获取所需的数据,避免了过度请求和响应数据的情况,也提高了应用性能。
先来看一个简单的例子:查询用户id为123用户的的名称和邮箱,以及发表的10篇文章,每篇文章返回对应的标题和内容。
在REST API中,如果后端为了解耦,定义了每个接口代表一种资源,那么这查询就需要多个接口来完成
type UserInfo = {
name:string,
email:string
}
type Post = {
title: sting,
content: string,
}
await Promise.all([
fetchUserInfo<BaseResponse<{UserInfo>>({id:123}),
fetchUserPosts<BaseResponse<Post[]>>({id:123})
// ... 其他查询接口
])
前端需要自己从多个接口中组装需要的数据,还需要维护每个接口之前的请求关系,考虑Race Condition
等情况。
此外,接口返回的数据完全是由后端的模型控制的,比如后端定义的Post
模型可能还有其他的字段
type Post = {
title: sting,
content: string,
createDate: string,
id: string
}
即使前端不需要Post数据上面的createDate
和id
字段,后端也会在这个接口里面一起返回(除非后端单独提供了一个只查询title
和content
的接口,或者在接口参数里面增加指定字段相关参数之类的功能),这样就会带来数据的冗余,增加接口的流量成本和传输时间。
后端控制数据响应的另外一个问题是需要考虑API版本兼容的问题,比如在版本1中fetchUserInfo
接口返回了name
和email
字段,后面版本迭代,只返回name
接口字段,那些使用了老版本接口的客户端,由于依赖了email
字段,出于兼容性的考虑,就无法直接在原本的fetchUserInfo
上修改,而是需要使用fetchUserInfo/v2
之类的接口。由于App升级都有版本覆盖率的问题,无法保证所有用户百分之百升级到最新版本,因此这种接口兼容的在App开发中更为常见。
再来看看通过GraphQL 查询(具体的语法细节后面会提到)
query {
user(id: 123) {
name
email
posts(limit: 10) {
title
content
}
// ... 其他查询数据
}
}
只需要一个接口就能拿到所有预期的数据,数据格式整好是查询语句定义的格式,没有接口竞态、没有冗余数据!
此外,所有数据都是有客户端自己定义的,也就彻底消除了API兼容的问题。
GraphQL 还提供了强类型系统,可以在编译时检查查询语句的正确性,并提供了详细的文档和工具支持。这样客户端的查询语句里面如果指定了错误的字段,就会在编译阶段进行提示,这使得客户端和服务器可以更好地协作,从而更快、更高效地构建和维护 API。
示例代码
社区提供了丰富的工具,满足在各种项目中使用GraphQL的需求。
对前端来说,流行的GraphQL 客户端包括 Apollo Client、Relay、urql ,可以非常方便地查询数据。
后端项目语言繁多,主流语言如Java、PHP、GO、NodeJS等都有相关的GraphQL服务端工具,如graphqljs、apollo-server、graphql-yoga等。
先来一段代码体验在NodeJS后端服务中提供GraphQL查询的功能。
服务端
首先,需要安装依赖包 express、express-graphql 和 graphql
npm install express express-graphql graphql
然后,创建一个 index.js
文件,编写以下代码
const express = require('express');
const { graphqlHTTP } = require('express-graphql');
const { buildSchema } = require('graphql');
// 定义 GraphQL Schema
const schema = buildSchema(`
type Query {
hello: String
}
`);
// 定义 GraphQL Resolver
const root = {
hello: () => 'Hello World!'
};
// 创建 Express 实例
const app = express();
// 添加 GraphQL 中间件
app.use('/graphql', graphqlHTTP({
schema: schema,
rootValue: root,
graphiql: true
}));
// 启动服务
app.listen(3000, () => {
console.log('GraphQL API Server is running at http://localhost:3000/graphql');
});
启动服务node index.js
,然后访问http://localhost:3000/graphql
,就可以看见对应的可视化查询界面
客户端
测试一下,输入查询语句然后运行
{
msg
}
打开控制台可以看见实际上是发送了POST请求出去,对应的参数
{"query":"query {\n hello\n}","variables":null}
在页面右侧可以看见接口响应
{
"data": {
"hello": "Hello World"
}
}
从客户端的角度来看,需要接口返回什么数据,完全掌握在自己的手中了。
核心概念
接下来我们来看看GraphQL 的主要概念:
Schema:GraphQL 的核心是由一个 Schema 定义的类型系统,它描述了 API 支持的数据类型、操作和数据之间的关系
- Type Definitions:类型定义,可以定义对象类型、标量类型、枚举类型和接口类型等
- Resolver:负责将 Query 和 Mutation 转换为实际数据的函数
Operation Type:操作类型
- Query:用于从服务器获取数据的 GraphQL 请求,对应
CURD
中的R
,GraphQL主要的功能是用于查询 - Mutation:用于在服务器上更改数据的 GraphQL 请求,对应
CURD
中的CUD
- Substription:当数据发生更改,进行消息推送
- Query:用于从服务器获取数据的 GraphQL 请求,对应
Fragment:用于将查询分解为可重用部分的 GraphQL 特性。
一个完整的 GraphQL 查询一般需要经过三个步骤:描述数据、请求数据和得到结果,从概念上来看即
- 服务端通过Type Definitions 定义数据类型
- 客户端通过Query描述要查询的数据
- 服务端通过Resolver解析请求获得数据,然后返回
接下来我们来细看一下相关的概念
Type Definitions
参考:Schema 和类型
GraphQL Schema的定义通常使用GraphQL Schema Definition Language(SDL)进行,SDL使用一种类似于GraphQL查询语言的语法来描述类型、字段和关系,引入SDL的好处是:GraphQL服务可以用任何的服务端编程语言来实现~。
Schema描述了API中的所有数据类型以及数据之间的关系,包括查询(Query)、变更(Mutation)、订阅(Subscription)等操作的类型和输入参数,定义了客户端可以查询的所有字段以及它们的类型和返回值,从而使客户端能够精确地了解服务器上哪些数据是可用的。
在定义Schema之后,客户端就可以使用GraphQL查询语言来查询API,并从服务器上获取数据。
一个 GraphQL schema 中的最基本的组件是对象类型,每个对象类型都由一个或多个字段组成,而每个字段都具有一个名称、一个返回类型以及可能的参数
type User {
id: Int!, // !表示非空
name: String,
email: String,
posts: [Post]
}
type Post {
title: String,
content: String
}
对象类型定义看起来跟TypeScript非常像,
Int
、String
都是内置的标量类型,此外还有Boolean
、Float
、ID
User
、Post
是自定义的对象类型[Post]
表示的是列表类型,返回的是Array<Post>
数组
枚举类型,枚举类型表示一组可能的值
enum Colors {
RED
GREEN
BLUE
}
接口类型,接口类型定义了一组相关对象类型的共同字段。
interface UserInfo {
id: ID!
name: String!
}
// User1 和 User2类型都具备了接口的所有字段,然后各自包含自己的字段
type User1 implements UserInfo {
id: ID!
name: String!
emmail: String
}
type User2 implements UserInfo {
id: ID!
name: String!
address: String
}
接口类型在需要返回一组不同的类型时比较有用。此外还有联合类型和输出类型,这里不再展开。
数据类型定义完成之后,就需要定义查询和变更类型了,Query
和Mutation
是内置的两种特殊类型
schema {
query: Query
mutation: Mutation
}
每一个 GraphQL 服务都有一个 query
类型,可能有一个 mutation
类型,
下面展示了一个简单的查询语句声明
type Query {
msg: String,
user(id: Int): User
}
有了这个声明之后,客户端就可以用对应的Query来查询数据了
Query
GraphQL 查询语言使用类似 JSON 的语法,上面我们已经展示过一段查询语言
query QueryUserInfoAndPosts {
user(id: 123) {
name
email
posts(limit: 10) {
title
content
}
}
}
其中
- query是操作类型关键字,这里是查询,还可以是
mutation 或
subscription` QueryUserInfoAndPosts
是操作名称,一个语义化的名字,有具体的含义,也更容易调试和日志追踪- 第一个
{}
里面的部分就是查询部分
查询部分具有以下基本语法:
查询字段:可以在查询中指定要获取的字段
{
user {
name
email
}
}
返回的结果
{
"data": {
"user": {
"name": "this is my name",
"email": "xx@xx.com"
}
}
}
参数:可以使用参数来限制查询结果,下面展示了查询id为123的用户的昵称
{
user(id: 123) {
name
}
}
别名:使用别名为字段分配自定义名称
{
user1: user(id: 1) {
name
}
user2: user(id: 2) {
name
}
}
在处理多个同名字段时很有用
{
"data": {
"user1": {
"name": "this is my name",
},
"user2": {
"name": "this is my name2",
}
}
}
嵌套查询:可以使用嵌套查询来获取与根查询相关的附加信息,客户端需要的字段,都可以放在query里面
{
user(id: 123) {
name
posts(limit: 10) {
title
}
}
}
片段:可以使用片段来重用查询中的字段集,这样就不用重复写相同的结构了
fragment userInfo on User {
name
email
phone
friends {
name,
}
}
{
user1: user(id: 1) {
...userInfo
}
user2: user(id: 2) {
...userInfo
}
}
变量:可以使用变量来将查询参数化,上面的user(id: 1)
是通过字面量的形式声明的查询参数,如果要动态传递id,拼接查询字符串并不是一个很合理的做法,更常规的做法是使用变量
query QueryUserInfoAndPosts($uid: Number) {
user(id: $uid) {
name
}
}
指令:指令用于动态控制某些字段是否需要查询返回,
query QueryUserInfoAndPosts($uid: Number, $withPosts:Boolean!) {
user(id: $uid) {
name
posts @include(if: $withPosts) {
name
}
}
}
只有在$withPosts
变量为true时才会返回posts相关字段
Resolvers
GraphQL Schema由两部分组成:类型定义(Type Definitions)和解析器(Resolvers)。类型定义描述了所有的GraphQL类型以及它们的字段和关系,而解析器则实现了这些类型和字段的具体行为。
解析器是一组函数,它们将GraphQL请求中的字段映射到具体的数据源,并返回相应的结果。每个字段都有一个解析器函数,该函数负责从数据源中获取字段的值。解析器还可以处理参数、进行验证和过滤,以及执行与数据源相关的任何其他逻辑。
比如对于下面这个查询
query {
hello
}
对应的解析函数应该是
Query: {
hello (parent, args, context, info) {
return ...
}
}
每个参数的含义
parent
:当前上一个解析函数的返回值args
:查询中传入的参数context
:提供给所有解析器的上下文信息info
:一个保存与当前查询相关的字段特定信息以及 schema 详细信息的值
不同的graphql工具库提供的定义解析函数的方式可能有区别,但大致都遵循这些基本参数,比如上面示例代码中的express-graphql
// 定义 GraphQL Resolver
const root = {
// hello world
hello: () => 'Hello World!',
// 带查询参数的resolver
user({id}){
// 也可以从其他各种数据库中根据参数查找数据
const list = [{id:1,name:'user1',email:'1@xx.com'}, {id:2,name:'user2',email:'2@xx.com'}]
return list.find(row=>row.id===id)
},
users(){
return Db.user.findAll();
}
};
小结
至此,对于GraphQL就有了基本的
- 对于前端而言,需要了解查询语法、类型定义,然后选择项目合适的graphql客户端工具
- 对于后端而言,需要了解类型定义、解析器实现等,然后选择醒目合适的graphql服务端工具
可见如果想要在项目中落地GraphQL,需要前后端共同配置来完成,GraphQL这两年并没有如预期那样飞速发展起来,感觉主要的原因包括
- 虽然提升了查询效率,减少了查询次数,但数据库的查询可能会成为性能瓶颈,对数据库的查询次数很多,需要合并优化方案
- 利好前端、但需要后端进行大规模改造,在分工明确的大团队可能不太容易执行
最后,GraphQL并不是用来替代REST API的,具体的技术选型,还是得看业务合不合适,业务优先为主。
你要请我喝一杯奶茶?
版权声明:自由转载-非商用-保持署名和原文链接。
本站文章均为本人原创,参考文章我都会在文中进行声明,也请您转载时附上署名。