阅读 179

GraphQL真香,快来体验一下吧

GraphQL是一种查询语言,前后端开发都要熟练掌握用法

平时写业务接口时,经常会遇到以下这些情况:

  1. 情况一:用户有姓名、头像、电话、资质证书(多张)、兴趣爱好等信息,在用户列表页只希望显示姓名、头像和电话这些基本信息,在用户详情页希望显示全部完整的信息,写接口时通常会定义一个用户DTO,里面包含用户的所有信息,在列表接口和详情接口里都生成这个DTO对象,但是列表接口返回资质证书和兴趣爱好是多余的,且会额外增加查询和网络带宽的消耗
  2. 情况二:查询订单列表接口需要返回订单号、总价和下单用户信息,其中下单用户信息并不在订单服务中,如果写接口的时候遍历查询出的订单列表去用户服务查询用户信息,肯定是不可行的;如果让前端自己去查询每个订单的用户信息,这增加了前端的工作量,前端也不会愿意

带着以上的问题,我们来看看使用GraphQL能够带来哪些好处:

  1. 由调用方设定接口要返回哪些字段,对于上面提到的情况一,可以实现列表页1只要姓名和头像,列表页2只要姓名、电话和资质证书,列表页3只要姓名、电话和兴趣爱好,不需要调整后端接口的代码
  2. 对于情况二,订单列表接口返回下单用户ID或唯一标识,由GraphQL去用户服务查询用户信息,并且在GraphQL查询后做下缓存,后端接口和前端调用都不用修改代码,真正的前端后端你好我好大家好

类型定义

基本类型

类型说明
String字符串类型
Int有符号的32位整型
Float有符号的双精度浮点型
Boolean布尔
IDID类型,用于唯一标识

自定义类型

可以像下面这样定义一个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可以让我们后端开发的每一个接口只负责各自单一的工作,像本文的例子需要后端提供两个接口:

  1. 查询用户列表
  2. 根据用户ID查询资质证书列表

不需要用户列表接口里面先查出当前页的用户,再遍历去查询出这些用户的资质证书列表,如果资质证书放在一个专门负责文件管理的服务中,通过接口遍历查询的方式就需要进行远程调用了

文章分类
后端
文章标签