GraphQL是一种查询语言,前后端开发都要熟练掌握用法
平时写业务接口时,经常会遇到以下这些情况:
- 情况一:用户有姓名、头像、电话、资质证书(多张)、兴趣爱好等信息,在用户列表页只希望显示姓名、头像和电话这些基本信息,在用户详情页希望显示全部完整的信息,写接口时通常会定义一个用户DTO,里面包含用户的所有信息,在列表接口和详情接口里都生成这个DTO对象,但是列表接口返回资质证书和兴趣爱好是多余的,且会额外增加查询和网络带宽的消耗
- 情况二:查询订单列表接口需要返回订单号、总价和下单用户信息,其中下单用户信息并不在订单服务中,如果写接口的时候遍历查询出的订单列表去用户服务查询用户信息,肯定是不可行的;如果让前端自己去查询每个订单的用户信息,这增加了前端的工作量,前端也不会愿意
带着以上的问题,我们来看看使用GraphQL能够带来哪些好处:
- 由调用方设定接口要返回哪些字段,对于上面提到的情况一,可以实现列表页1只要姓名和头像,列表页2只要姓名、电话和资质证书,列表页3只要姓名、电话和兴趣爱好,不需要调整后端接口的代码
- 对于情况二,订单列表接口返回下单用户ID或唯一标识,由GraphQL去用户服务查询用户信息,并且在GraphQL查询后做下缓存,后端接口和前端调用都不用修改代码,真正的前端后端你好我好大家好
类型定义
基本类型
类型 | 说明 |
---|---|
String | 字符串类型 |
Int | 有符号的32位整型 |
Float | 有符号的双精度浮点型 |
Boolean | 布尔 |
ID | ID类型,用于唯一标识 |
自定义类型
可以像下面这样定义一个User类型
type User {
name: String
age: Int
mobileNumber: String
address: String
}
复制代码
数组
在类型外面添加[],如[Int]
[String]
[User]
感叹号
在类型右边添加感叹号表示不能为空,如Int!
String!
User!
创建GraphQL服务
我们采用NodeJS来创建服务,新建一个目录,初始化并下载需要用到的库
mkdir graphql-test
cd graphql-test
npm init -y
npm install express graphql node-fetch dataloader apollo-server -S
复制代码
先以一个最简单的示例来了解GraphQL
const fetch = require('node-fetch')
const DataLoader = require('dataloader')
const { ApolloServer, gql } = require('apollo-server')
// 定义后端接口URL
const BASE_URL = 'http://localhost:8088/api'
// 定义GraphQL的请求和类型
const typeDefs = gql`
type Query {
findUser(id: ID!): User
}
type User {
id: ID
name: String
age: Int
}
`;
// 定义GraphQL处理器
const resolvers = {
Query: {
findUser: resolveFindUser
}
}
function resolveFindUser(_, { id }, context) {
return {
id: id,
name: 'TodayNie ' + id,
age: 18
};
}
// 启动服务
const server = new ApolloServer({ typeDefs, resolvers });
server.listen().then(({ url }) => {
console.log(`GraphQL服务已启动,访问地址:${url}`);
});
复制代码
typeDefs
这是定义查询方法和自定义类型
type Query {
findUser(id: ID!): User
}
复制代码
上面是定义一个findUser方法,必须传入id作为查询参数,返回类型为User,所有的查询方法都要定义在type Query {}里面
type User {
id: ID
name: String
age: Int
}
复制代码
上面是定义一个自定义类型
resolvers
这是查询方法对应的业务逻辑
const resolvers = {
Query: {
findUser: resolveFindUser
}
}
复制代码
意思是typeDefs中Query里面定义的查询方法怎么样处理,findUser查询方法交由resolveFindUser方法来处理
function resolveFindUser(_, { id }, context) {
return {
id: id,
name: 'TodayNie ' + id,
age: 18
};
}
复制代码
resolveFindUser方法第一个入参是root,第二个入参是查询参数,第三个入参是context,root和context下文会说,本例中简化逻辑,只是返回一个固定的对象
现在来启动这个简单的例子
node index.js
复制代码
启动后访问http://localhost:4000/
,看到如下界面:
左边是查询界面,右边是查询结果界面,在左边输入查询内容
query {
findUser(id: 1) {
name
age
}
}
复制代码
查询ID=1的用户信息,只返回name和age两个字段
请求后端接口的示例
假设后端返回的json格式如下:
{
"code":0,
"msg":"",
"data":{
"id":"1",
"name":"todaynie",
"age":18
}
}
复制代码
修改resolveFindUser方法
function resolveFindUser(_, { id }, context) {
return fetch(`${BASE_URL}/index/test?id=${id}`).then(res => res.json()).then(data => data.data);
}
复制代码
请求后端接口时可能需要打印后端返回的结果,可以修改resolveFindUser方法,将后端返回结果打印出来便于调试
return fetch(`${BASE_URL}/index/test?id=${id}`).then(res => res.text()).then(data => console.log(data));
复制代码
分页查询
typeDefs
const typeDefs = gql`
type Query {
findUser(id: ID!): User
findUsers(page: Int, pageSize: Int): UserPage
}
type User {
id: ID
name: String
age: Int
}
type UserPage {
list: [User]
pager: Pager
}
type Pager {
page: Int
pageSize: Int
pageCount: Int
recordCount: Int
}
`;
复制代码
resolvers
function resolveFindUsers(_, { page, pageSize }, context) {
return {
list: [
{ id: 1, name: 'todaynie1', age: 18 },
{ id: 2, name: 'todaynie2', age: 19 },
{ id: 3, name: 'todaynie3', age: 20 },
],
pager: {
page: 1,
pageSize: 10,
pageCount: 100,
recordCount: 1000
},
};
}
复制代码
查询
query {
findUsers(page: 1, pageSize: 10) {
list {
name
}
pager {
page
pageCount
}
}
}
复制代码
嵌套查询
假设查询用户列表接口,根据返回的用户ID再查询各自的资质证书,或者是用户详情页,调用用户基本信息接口后再查询资质证书,这种查询需要上一次查询的结果,就需要用到上面说的root参数了
对typeDef进行修改,在User中添加certs: [File]
type User {
id: ID
name: String
age: Int
certs: [File]
}
type File {
name: String
url: String
}
复制代码
调整resolvers
const resolvers = {
Query: {
findUser: resolveFindUser,
findUsers: resolveFindUsers,
},
// 添加了User.certs,指定当需要查询User的certs字段时,交由resolveUserCerts处理
User: {
certs: resolveUserCerts,
}
}
function resolveUserCerts(root, {}, context) {
// 这里可以打印root,这里的root就是User对象
console.log(root);
return [
{ name: 'xxx证书', url: 'http://xxx' },
{ name: 'xxx证书', url: 'http://xxx' },
{ name: 'xxx证书', url: 'http://xxx' },
];
}
复制代码
查询
query {
findUser(id: 2) {
name
age
certs {
name
}
}
}
复制代码
很简单就实现了嵌套查询
header
GraphQL作为中间层,前端调用一个GraphQL查询,GraphQL调用一个或多个后端接口得到数据返回给前端,在这个过程中后端接口可能要求在header中携带token才允许访问,这就需要前端将token传给GraphQL,GraphQL再传给后端,context参数就派上用场了
修改index.js,添加header到context
const server = new ApolloServer({ typeDefs, resolvers });
server.context = async ({ req }) => {
return {
headers: req.headers,
};
}
server.listen().then(({ url }) => {
console.log(`GraphQL服务已启动,访问地址:${url}`);
});
复制代码
修改resolvers
function resolveFindUser(_, { id }, context) {
// 在这里打印header,只要能打印出正确的结果,那么在fetch方法调用后端时把header传入就可以了
console.log(context.headers);
return fetch(`${BASE_URL}/index/test?id=${id}`).then(res => res.json()).then(data => data.data);
}
复制代码
查询界面输入header
查询后查看控制台,能够得到token
缓存
缓存可以使用DataLoader来实现,这个在网上搜索一下能找到用法,就是把fetch得到的结果放缓存里,下次再查询时可以从缓存里直接取出
总结
GraphQL可以让我们后端开发的每一个接口只负责各自单一的工作,像本文的例子需要后端提供两个接口:
- 查询用户列表
- 根据用户ID查询资质证书列表
不需要用户列表接口里面先查出当前页的用户,再遍历去查询出这些用户的资质证书列表,如果资质证书放在一个专门负责文件管理的服务中,通过接口遍历查询的方式就需要进行远程调用了