前言
作为前端,你会抱怨请求接口字段冗余,你会抱怨需要请求多个接口才能得到想要的数据,甚至痛恨后端接口文档写的很烂。作为后端,你也无法忍受前端对你接口的指指点点,抱怨为什么前端不自己去取想要的值...类似的开发互怼场景已经司空见惯,GraphQL就此隆重出世了。
什么是GraphQL
GraphQL是FackBook开源的一套数据交互方案,是一种为API接口和查询已有数据运行时环境的 查询语句。它提供了一套完整和易于理解的API接口数据描述,给客户端权力去精准查询他们需要的数据,而不用去实现其他更多的代码。让API接口开发变得更简单高效,支持强大的开发者 工具
遇到的问题
- 接口数量众多,维护成本高
为了接口的通用性,后端通常会构建较小粒度的数据接口,再根据具体业务产品对数据接口进行组合,对外暴露业务接口.
即使这样前端接口还是很多,因为业务总是多变的,当前端需求变化,涉及到改动旧需求时,会有以下情况。
做加法 产品需求增加,页面需要增加功能,数据也相应增加显示时,这种增加接口是无可厚非的。
做减法 产品需求减少,页面需求减少功能或减少某些信息的显示
- 前端不与后端沟通,前端按照需求只展示对应字段
- 另一种做法,前端要求后端要么开发新接口,要么修改旧接口,删除冗余字段
- 接口字段冗余,多次请求浪费带宽
为了方便管理接口,后端通常只开发一套API,适应不同端(web/h5/ios/android)的数据请求
- 不同端对展示数据要求有差异。特别在开发移动端时,前端需求后端尽量少的返回字段,以避免带宽浪费,通常后端再开发一套API已适应移动端请求
- 页面展示需要多个发起多个请求,这时候后端封装Action,多维护一个API还是前端忍受多个HTTP请求呢?
渲染一个页面需要发送四个请求,获取对应的数据
- 不确定的数据结构 前端开发随着spa全面普及,组件化开发也是大势所趋,各组件独立管理着自己的状态。但是组件化也给前端带来了一些烦恼,因为一些公共组件从服务器拉取数据后,需要分发到各个子组件或者通知父组件。 这种通信带来数据结构的复杂度,不明朗的数据,不知道从何订阅数据响应,使得杂乱无章等。
- 接口字段类型不统一 对于javascript,php等弱类型语言,前后端数据交互时字符串与数字类型,会造成前后端bug。
GraphQL解决方案
1、自由组合接口
可以组合和连接多个GraphQL API,合并为一个,减少请求次数
2、按需取值,不返回多余字段
3、GraphQL是强类型的,可以在执行之前验证 GraphQL 类型系统中的查询, 它帮助我们构建更强大的 API
GraphQL如何实现
谈到GraphQL实现之前,我们先看看传统的请求数据交互。
传统REST请求
一个接口请求对应的api,获取对应的数据
GraphQL请求
GraphQL将多个请求合并,指定获取用户列表,科目列表,水果列表对应的字段。这一实现,是客户端发送一个特定的请求体**(我们称之为GraphQL客户端),服务端接收请求,根据解析器(我们称之为GraphQL服务端)**解析,返回指定的数据给前端,而这一部分的请求体和响应体,我们称它为schema,如下图
schema 定义前端需求获取那些数据和对应的字段,后端根据GraphQL解析会将schema解析成document AST语法树,进行调用对应Action,跟Modal的查询,下面我们会详细解析schema
Schema
schema 可以理解为一种协议,用来定义描述接口,type 的标量类型有String,Int,Float,Boolean,Enum,ID等,操作类型主要有两种类型,Query(即是增删改查中的查),Mutation(代表你要执行的动作是增删改)
query操作
我们在schema定义一个获取用户列表接口,返回用户名字,年龄,创建用户时间等信息
//...
// 定义用户信息
type UserInfo{
name!: String, // 定义姓名为字符串
age!: Int, // !表示此自动非空,GraphQL 服务保证当你查询这个字段后总会给你返回一个值
createTime: Float
}
Query {
// 定义用户列表,返回用户信息
getUserList: [UserInfo]
}
//
mutation 操作 定义一个增加用户操作,name,age为必填字段,返回接口响应码ret和响应信息
//...
// 定义用户信息
type UserInfo{
name: String, // 定义姓名为字符串
age!: Int, // !表示此自动非空,GraphQL 服务保证当你查询这个字段后总会给你返回一个值
createTime: Float
}
type Ret{
ret: Int,
msg: String
}
Mutation {
// 定义用户列表,返回用户信息
addUser: (input: UserInfo): Ret
}
//
前后端如何实现
- 首先要设计数据模型,用来描述数据对象,它的作用可以看做是VO,用于告知GraphQL如何来描述定义的数据,为下一步查询返回做准备;
- 前端使用模式查询语言(Schema)来描述需要请求的数据对象类型和具体需要的字段(称之为声明式数据获取);
- 后端GraphQL通过前端传过来的请求,根据需要,自动组装数据字段,返回给前端;
后端 GraphQL官方提供了 C# / .NET、Go、Java、JavaScript、PHP、Python、Ruby、Swift等十多种语言工具库,这里我们以JavaScript为例说明。
GraphQL 规范的参考实现,设计用于在 Node.js 环境中运行。创建index.js,安装依赖graphql库,定义schema,运行node index.js,我们可以访问hello,getUserInfo,addUser等接口
var { graphql, buildSchema } = require('graphql');
var schema = buildSchema(`
type UserInfo {
name: String,
age: Int,
createTime: Float
}
type Ret {
ret: Int,
msg: String
}
type Query {
hello: String,
getUserList: [UserInfo],
}
Mutation {
addUser: (input: UserInfo): Ret
}
`);
var root = { hello: () => 'Hello world!' };
graphql(schema, '{ hello }', root).then((response) => {
console.log(response);
});
//
GraphQL与node库结合开发
结合node express库,提供了express-graphql
npm install express-graphql graphql
var express = require('express');
var graphqlHTTP = require('express-graphql');
var { buildSchema } = require('graphql');
var schema = buildSchema(`
type UserInfo {
name: String,
age: Int,
createTime: Float
}
type Ret {
ret: Int,
msg: String
}
type Query {
hello: String,
getUserList: [UserInfo],
}
Mutation {
addUser: (input: UserInfo): Ret
}
`);
var root = {
hello: () => 'Hello world!',
getUserList: [
{
name: '小明',
age: 8,
createTime: 451647414144
},
{
name: 'kenny',
age: 18,
createTime: 812344314710
}
]
// ...
};
var app = express();
app.use('/graphql', graphqlHTTP({
schema: schema,
rootValue: root,
graphiql: true,
}));
app.listen(4000, () => console.log('Now browse to localhost:4000/graphql'));
前端 前端只要是通过GraphQL提交的请求库,或者发送符合schema语法的ajax请求,这里我们借助graphql-request请求,跟常见axios,fetch等用法类似
npm install graphql-request
import { GraphQLClient } from "graphql-request"
// 实例化一个GraphQL
const graphqlClient = new GraphQLClient("/test/graphql/api", {
credentials: "same-origin",
})
// 定义获取科目列表,返回id,name,abstract,create_time,update_time
const query = `
query querySubjectList{
querySubjectList {
id
name
abstract
create_time
update_time
}
}
`
graphqlClient
.request(query, vars = {})
.then(res => {
resolve(res)
})
.catch(err => {
console.log("err", err)
rej(err)
})
友好前端的schema玩法之别名 spa开发中常遇到一个组件要适配不同的接口字段,比如我们定义了一个Select组件,渲染列表支持的valueMap是value和label,如果后端接口返回的是id,name等,通常前端需要给组件适配器,已适配不同字段的列表能正常使用组件,但是GraphQL的别名可以支持我们定义返回字段名,如下图,我们将id转为value,label,这可以让你重命名结果中的字段为任意你想到的名字
query querySubjectList{
querySubjectList {
value: id
label: name
abstract
create_time
update_time
}
}
友好前端的schema玩法之指令(Directives)
我们上面讨论的变量使得我们可以避免手动字符串插值构建动态查询。传递变量给参数解决了一大堆这样的问题,但是我们可能也需要一个方式使用变量动态地改变我们查询的结构。譬如我们假设有个 UI 组件,其有概括视图和详情视图,后者比前者拥有更多的字段
{
"episode": "JEDI",
"withFriends": false
}
query Hero($episode: Episode, $withFriends: Boolean!) {
hero(episode: $episode) {
name
friends @include(if: $withFriends) {
name
}
}
}
返回结果:
{
"data": {
"hero": {
"name": "R2-D2"
}
}
}
更多操作可以在GraphQL中学习
总结
在开发服务端的时候,我们的 service 可以更原子化,不用关心前端到底需要什么字段,一切都可以面向后端的最佳实践,根据 GraphQL Schema 来编写 service;而前端则可以根据 Schema 来自由组合数据和服务,不必再频繁要求后端增减字段或接口。
GraphQL解除了接口和数据之间的绑定,对业务数据模型做了抽象和整理,以图的方式来明确模型之间的关系,通过这些关系,具体业务场景可以定制自己的数据,不同的业务场景只要基于同样一套基础数据模型就可以复用过往的接口。
更多好处,请在开发实践中慢慢体会吧!