什么是GraphQL?
官网定义:
一种用于API的查询语言 GraphQL 为数据通信而生。客户端需要告知服务器 需要哪些数据,服务器需要用实际的数据来满足客户端的数据需求。GraphQL 是此种通信方式的中介。
白话翻译:
前端同学只要 描述清楚你所需要的数据,通过HTTP发送给服务器,服务器就会返回给你响应的数据。就像你写SQL语句查询数据库一样方便快捷。
为什么要使用GraphQL?
单个入口
- Rest API 不论前后端都需要对API做管理,版本和路径的管理,非常麻烦。
- (1)GraphQL 只有一个入口,相当于访问一个数据库,我们将查询语句发送给它,它就会返回指定的结果。
(2)我们将通过查看schema获取相关信息,并组装有效的查询语句。
(3)版本的管理可以通过schema文件变更实现,一般会兼容老的版本,只增加新版本所需的属性字段即可。
数据聚合
如果要实现上面这样的界面展示,我们分别需要 banner 、brand 和 column 这三块的信息,对于GraphQL和传统的REST API分别是怎么实现的呢?
- GraphQL本身就支持数据聚合,通过请求时传递
{ banner { .. } brand{ .. } column{ .. } }
这样一段描述给后端,即可在响应体内得到想要的数据。 - REST API (1)前端请求多个接口获取数据 (2)后端根据UI界面 封装数据 提供一个接口,返回数据。
按需返回
很多时候,同一组数据在不同的客户端(web、wap、移动端......)有不同的表现形式,为了适配各个终端,后端通常会 通过使用一个接口 将所有的信息统一返回,导致有些字段在特别客户端根本用不到这么多字段,这不仅增加了网络传输的时间、响应冗余,同时增加了 维护成本。
然而 使用GraphQL 按我们所需取值即可,需要什么就描述什么,最终就得到什么。
强类型
GraphQL 的类型系统定义了包括 Int, Float, String, Boolean, ID, Object, List, Non-Null 等数据类型,并且支持自定义类型。 Schema文件内各个数据结构中,每个字段都有类型定义,通过 graphql-schema-typescript 库可以将schema文件转为typescript,在编写查询或使用结果响应时 能非常清晰知道每个字段的类型,在对响应数据的使用过程中避免不清楚字段类型带来的问题。
文档
如何高效的维护一份完整细致的API文档 一直是后端头痛,前端头大的事情。使用GraphQL基本不需要考虑文档问题,你定义数据库表结构时一定会给每个字段加注释吧,GraphQL的schema就和你的数据库表一样,你定义维护好每个字段的注释即可。在GraphQL服务启动后,通过访问 /graphiql
即可进入到 自动生成的文档页面,如下图所示:
GraphQL基础
先一起来了解一下GraphQL内重要的基础知识。
Schema
使用GraphQL开发前,后端服务会提供本服务所有的数据结构图,我们称它为schema。有了这个schema文件,就可以根据文件内的各个数据结构 来组合我们所需的查询或更新。
查询过程
有了Schema之后,客户端会根据Schema内的数据结构组合查询,那么后端处理查询的过程是怎么样的呢?
上图中从 1到5 展示了GraphQL一个请求到响应的过程,分别介绍上图中的5个步骤:
- HTTP请求体:描述前端所需要的数据结构(根据Schema文件内有的信息组合而成)
- 请求到达服务端,解析器首先解析query下的第一个层user,在这个解析器内可以编写对user的处理逻辑。
- 解析器继续深入user下的id字段,对id字段进行解析。
- 解析器对与id平级的name字段进行解析。
- HTTP响应体:返回处理后的结果。
从整个查询的处理过程可以看出,针对客户端所需的每个结构 每个字段,后端同学都可以自行处理或默认返回,增加了可玩性。比如:后端可以解析name字段时判断是否需要返回全大写、全小写或首字母大写等 名称格式,客户端传对应参数即可获取所需的name格式,这一点在时间格式、长度格式、面积单位中非常实用。
后端改造接入
要使用GraphQL 必须要推动后端同学和我们一起开发,我们可以从新项目接入 或者 对REST项目进行部分改造。
可将REST项目中一部分进行改造,举例我们可以将已有项目中 用户相关部分进行改造。
这里使用nodejs做代码演示,其它语言可参考Graphql-Code。
第一步:定义Schema文件
以User为数据模型,从REST角度将接口转换为GraphQL的Schema文件。 REST接口:
GET /user:列出所有用户
POST /user:新建一个用户
GET /user/ID:获取某个指定用户的信息
PUT /user/ID:更新某个指定用户的信息(提供该用户的全部信息)
PATCH /user/ID:更新某个指定动物园的信息(提供该用户的部分信息)
DELETE /user/ID:删除某个用户
转为 GraphQL Query:
# 日期标量
scalar DateTime
# 长度单位
enum LengthUnit {
# 米
METER
# 厘米
CM
# 毫米
MM
}
# 用户
type User {
# 用户ID
id: ID!
# 姓名
name: String!
# 出生日期
birthday: DateTime
# 身高
stature(unit: LengthUnit = METER): String
}
# 与REST接口提供的功能保持一致
query {
users():[User!]!
user(id: Int!): User!
}
mutation {
createUser(): User!
updateUser(id: Int): User!
deleteUser(id: Int): User!
}
第二步:Resolver
将REST项目各个接口内的业务或DAO操作 转换 到GraphQL的Resolver(解析函数)中。结合GraphQL基础 -查询过程实现,Resolver的过程就是对query递归遍历处理的过程。
// 1. 定义schema中类型及resolver
const Unit = new GraphQLEnumType({
name: 'Unit',
description: "单位",
values: {
MM: {value: 'MM'},
cm: {value: 'cm'},
mm: {value: 'mm'},
}
})
const User = new GraphQLObjectType({
name: 'User',
description: "用户信息实体",
fields: () => {
return ({
id: {type: new GraphQLNonNull(GraphQLInt)},
name: {type: new GraphQLNonNull(GraphQLString)},
// 处理stature属性 resolve
stature: {
type: GraphQLFloat,
args: {
unit: {type: Unit}
},
resolve: function (user, {unit}) {
if (unit == 'MM') {
return user.stature / 100;
}
if (unit == 'cm') {
return user.stature;
} else if (unit == 'mm') {
return user.stature * 10;
}
}
},
});
},
})
// 2. 构造生成Schema
const Query = new GraphQLObjectType({
name: 'UserQuery',
description: '用户信息查询',
fields: () => ({
user: {
// 返回类型为 User实体
type: User,
description: '根据id查询单个用户',
args: {
id: {type: new GraphQLNonNull(GraphQLInt)}
},
// 对user查询的 解析函数
resolve: async function (source, {id}) {
// 通过sql语句查询数据库user表中数据 select * from user where id=?
return (await util.searchSql($sql.queryAll))[id];
}
},
users: {
type: new GraphQLList(User),
description: '查询全部用户列表',
// 对users查询的 解析函数
resolve: async function () {
// 通过sql语句查询数据库user表中数据 SELECT * FROM `user` WHERE 1=1
return await util.searchSql($sql.queryAll);
}
}
}),
})
const schema = new GraphQLSchema({
query: Query,
// 省略mutation.. 与query一致,只是sql语句变一下就好
mutation: Mutation
})
// 3. 开启服务 graphqlHTTP = require('express-graphql')
app.use('/graphql', graphqlHTTP({
schema,
//启用GraphiQL
graphiql: true
}))
Mock数据
前端开发接入
配置前端开发环境
维护Schema文件
- 安装 graphql-cli,通过命令帮助我们有效管理schema文件、端点信息、静态检测等。
- 在项目的根目录下新建
.graphqlconfig.yaml
文件,配置graphql,配置项参考:graphql-config
{
"projects": {
// 应用名
"app": {
// schame文件在项目中的位置
"schemaPath": "schema.graphql",
// 检测包含的文件 默认 .graphql
"includes": ["*.gql"],
"extensions": {
// 后端站点
"endpoints": {
// 开发环境
"dev": "http://localhost:3003/graphql",
// 生产环境
"prod": {
}
}
}
}
}
}
- 通过在根目录下执行
graphql get-schema
命令即可新增|更新schema文件。 - IDE支持 Vscode安装vscode-graphql,Webstorm可安装 js-graphql-intellij-plugin
提示&静态检查
在编写graphql的请求查询体时,如果没有自动提示或字段检测 是一个非常糟糕的体验,很容易和schema文件内的结构体不一致,导致查询失败。可以通过以下方式处理:
-
Eslint引入graphql规则 eslint-plugin-graphql,配置看起来是这样的:
module.exports = { env: { browser: true, es6: true }, parserOptions: { parser: 'babel-eslint' }, // 省略相关项 ... rules: { // 省略.... 'graphql/template-strings': [ 'error', { // 由于我没有使用任何Graphql框架客户端,使用graphql-config,所以这里直接写literal即可 // 参考:https://github.com/apollographql/eslint-plugin-graphql#example-config-when-using-graphqlconfig env: 'literal' } ] }, plugins: [ 'graphql' ] }
-
执行Eslint的检测时记得包含.gql和.graphql后缀的文件哦
eslint . --ext .js --ext .gql --ext .graphql
。 -
由于在
.graphqlconfig.yaml
文件中 配置了includes,当你在.gql和.graphql文件内编辑 请求体时 将自动根据schema文件的结构体给予提示或报错。
在使用IDE系列(不限于IntelliJ、GoLand、Pyu、Webstorm等)的js-graphql-intellij-plugin插件时,请配置.gql为GraphQL类型的扩展名:然后就可以愉快的看到提示和报错啦:
Vue-GraphQL-Plugin插件
前端JavaScript使用GraphQL有较多的框架可供选择GraphQL-code-Javascript。大部分的前端库对新项目友好,对 部分使用GraphQL 部分使用REST 的前端项目却并不友好,因此开发 vue-graphql-plugin 插件,减少对前端项目的侵入,更好的兼容老项目。
快速接入
插件通过mixin 混入到vue模块内
安装插件 配置项
Vue.use(VueGraphql, {
// 请求框架,可以接入任意 例如:axios、flyio等
AjaxRequest: request,
// 请求框架 中处理GraphQL请求的方法
reqFunc: 'gQLRequest',
// 是否开启调试信息
debug: true
})
配置项 | 强制 | 类型 | 说明 |
---|---|---|---|
AjaxRequest | true | Object | 请求框架,可以接入任意 例如:axios、flyio等 |
reqFunc | true | String | 请求框架 中处理GraphQL请求的方法名 |
debug | false | Boolean | 是否开启调试信息 |
在模块中配置
graphql: {
['自定义名称']: {
// 各配置项
}
}
我们只关心 各配置项就好,['自定义名称']在后面手动调用时 用到。
配置项 | 强制 | 类型 | 说明 |
---|---|---|---|
query | true | Object | 对象内 key为响应数据赋值的key,会自动设置到data对象内,value为GraphQL的查询语句 |
variables | false | Object | 对应GraphQL查询语句的 参数变量,会自动设置到data内,同时会watch 用于自动查询。 注意:noWatch下的key不会设置到watch下。 |
isImmediate | false | Boolean | 是否立即执行 |
debounce | false | Boolean | Object | 是否开启防抖 通过 {wait: 1000} 开启 |
throttle | false | Boolean | Object | 是否开启节流 通过 {gapTime: 1000} 开启 |
ajaxOptions.catchSuccess | false | function | 获取成功的回调,以绑定到当前模块,可以愉快使用this |
兼容REST风格
可以通过this.$graphql.query({query, variables, ajaxOptions})
或 this.$graphql.mutation({query, variables, ajaxOptions})
方式进行数据获取或更新。
数据和组件
通常对同一数据模型的查询或更新会散落在不同的子组件中,如下图所示:
上图中表现出 有不同的子组件对用户信息进行了查询,相同的代码片段我们可以通过graphql-tag将不同的结构类型抽离出来,方便更好的组织和维护,类似于ORM(对象-关系映射)。
请将graphql-tag引入项目,并且将graphql-tag/loader配置到webpack。
相关实践
快速展示
在实际项目中有些页面需要加载大量数据,导致请求时间较长,用户体验差。比如首屏渲染 因为首屏需要请求更多内容,通常情况下 比原来多了更多HTTP的往返时间(RTT),这造成了白屏,如果白屏时间过长,用户体验会大打折扣。使用GraphQL可轻松解决这个问题。 比如我们页面需要的数据如下:
# 用户信息
user {
id
name
age
profile
mobile
avatar
}
实际上在第一次请求时 我们可以只取关键信息 用来快速展示界面
# 用户信息
user {
id
name
}
这样的话 用户先看到界面后,我们再次请求剩下的信息即可,将按需返回 应用解决实际问题。
高效利用 Resolve 解析函数
利用好Resolve函数对每一个字段都可处理的特性,可以让我们更好的定义每个字段前端所需的类型。
比如:用户身高信息 在 有的界面中 显示厘米,有的界面中 显示米。我们可以定义 长度单位LengthUnit
,后端给User中的length进行Resolve自定义处理,根据前端传入的单位格式 进行相应的返回,如下图所示:
可以将各种计量单位(长度、面积、体积......)都定义出来,在使用的时候统一处理。