1.REST vs GraphQL
REST 是一个很流行的前后端交互形式的约定, 在这种约定下, 前端专注于页面, 同时与后端进行数据交互; 而后端则专注于提供 API 接口. RESTful API 开发中遇到的问题:
扩展性 : 随着 API 数量的不断增加, RESTful API 的接口会变得越来臃肿.
无法按需获取 : 一个返回 id,name,age, city, addr, email 的接口, 如果仅获取部分信息, 如 name,age, 却必须返回接口的全部信息, 然后从中提取自己需要的. 坏处是不仅会增加网络传输量, 而且不便于 client 处理数据.
RESTful API 不好处理的问题 : 比如确保 client 提供的参数是类型安全的, 如何从代码生成 API 的文档等.
一个请求无法获取所需全部资源 : 例如 client 需要显示一篇文章的内容, 同时要显示评论, 作者信息, 那么就需要调用文章, 评论, 用户的接口. 坏处是造成服务的的维护困难, 以及响应时间变长 .
RESTful API 通常由多个端点组成, 每个端点代表一种资源. 所以当 client 需要多个资源是, 它需要向 RESTful API 发起多个请求, 才能获取到所需要的数据.
Facebook 开源的 GraphQL , 在 Twitter,GitHub 等大公司已做实践, GraphQL 是一种数据查询语言, 提供以下的性质:
请求你的数据不多不少 :GraphQL 查询总是能准确获得你想要的数据, 不多不少, 所以返回的结果是可预测的.
获取多个资源只用一个请求 :GraphQL 查询不仅能够获得资源的属性, 还能沿着资源间进一步查询, 所以 GraphQL 可以通过一次请求就获取你应用所需的所有数据.
描述所有的可能类型系统: GraphQL API 基于类型和字段的方式进行组成, 使用类型来保证应用只请求可能的类型, 同时提供了清晰的辅助性错误信息.
使用你现有的数据和代码: GraphQL 让你的整个应用共享一套 API, 通过 GraphQL API 能够更好的利用你的现有数据和代码. GraphQL 引擎已经有多种语言实现, GraphQL 不限于某一特定数据库, 可以使用已经存在的数据, 代码, 甚至可以连接第三方的 APIs.
API 演进无需划分版本: 给 GraphQL API 添加字段和类型而无需影响现有查询. 老旧字段可以废弃, 从工具中隐藏.
2. GraphQL 介绍
官网给出的定义: GraphQL 既是一种用于 API 的查询语言 也是一个满足你数据查询的运行时 .GraphQL 对你的 API 中的数据提供了一套易于理解的完整描述 , 使得客户端能够准确地获得它需要的数据 , 而且没有任何冗余, 也让 API 更容易地随着时间推移而演进, 还能用于构建强大的开发者工具.
API 不是用来调用的吗? 是的, 这正是 GraphQL 的强大之处, 引用官方文档的一句话
- ask exactly what you want
- .
本质上来说 GraphQL 是一种查询语言.
上述的定义比较抽象很难理解, 实践过 GraphQL 的使用后能够更加深刻的理解.
数据在本质上是分层的, 同时也是关系图. GraphQL 的核心目标就是表达这种关系图.
在 GraphQL 中, 通过定义 Schema 和声明 Type 来达到上述描述的功能, 需要学习:
对于数据模型的抽象是通过 Type 来描述的 , 那么如何定义 Type?
对于接口获取数据的逻辑是通过 schema 来描述的 , 那么如何定义 schema?
2.1 如何定义 Type
对于数据模型的抽象是通过 Type 来描述的, 每一个 Type 有若干 Field 组成, 每个 Field 又分别指向某个 Type.
GraphQL 的 Type 简单可以分为两种, 一种是 scalar type(标量类型) , 另一种是 object type(对象类型).
2.1.1 scalar type
GraphQL 中的内建的标量包含 String,Int,Float,Boolean,Enum, 标量是 GraphQL 类型系统中最小的颗粒.
2.1.2 object type
仅有标量是不够抽象一些复杂的数据模型, 需要使用对象类型. 通过对象类型来构建 GraphQL 中关于一个数据模型的形状, 同时还可以声明各个模型之间的内在关联(一对多, 一对一或多对多).
一对一模型展示:
- type Article {
- id: ID
- text: String
- isPublished: Boolean
- author: User
- }
上述代码, 声明了一个 Article 类型, 它有三个 Field, 分别是 id(ID 类型),text(String 类型),isPublished(Boolean 类型)以及 author(新建的对象类型 User),User 类型的声明如下:
- type User {
- id: ID
- name: String
- }
- 2.1.3 Type Modifier
类型修饰符, 当前的类型修饰符有两种, 分别是 List 和 Required , 语法分别为 [Type] 和 Type! , 两者可以组合使用:
[Type]! : 列表本身为必填项, 但内部元素可以为空
[Type!] : 列表本身可以为空, 但是其内部元素为必填
[Type!]! : 列表本身和内部元素均为必填
2.2 如何定义 Schema
schema 用来描述对于接口获取数据逻辑 ,GraphQL 中使用 Query 来抽象数据的查询逻辑, 分为三种, 分别是 query(查询),mutation(更改),subscription(订阅) .API 的接口概括起来有 CRUD(创建, 获取, 更改, 删除)四类, query 可以覆盖 R(获取)的功能, mutation 可以覆盖 ( CUD 创建, 更改, 删除) 的功能.
注意: Query 特指 GraphQL 中的查询(包含三种类型),query 指 GraphQL 中的查询类型(仅指查询类型).
2.2.1 Query
query(查询): 当获取数据时, 选择 query 类型
mutation(更改): 当尝试修改数据时, 选择 mutation 类型
subscription(订阅): 当希望数据更改时, 可以进行消息推送, 使用 subscription 类型(针对当前的日趋流行的 real-time 应用提出的).
以 Article 为数据模型, 分别以 REST 和 GraphQL 的角度, 编写 CURD 的接口
Rest 接口
- GET /api/v1/articles/
- GET /api/v1/article/:id/
- POST /api/v1/article/
- DELETE /api/v1/article/:id/
- PATCH /api/v1/article/:id/
- GraphQL Query
- query {
- articles():[Article!]!
- article(id: Int!): Article!
- }
- mutation {
- createArticle(): Article!
- updateArticle(id: Int): Article!
- deleteArticle(id: Int): Article!
- }
注意:
GraphQL 是按照类型来划分职能的 query,mutation,subscription, 同时必须明确声明返回的数据类型.
2.2.2 Resolver
上述的描述并未说明如何返回相关操作 ( query,mutation,subscription ) 的数据逻辑. 所有此处引入一个更核心的概念 Resolver(解析函数).
GraphQL 中默认有这样的约定, Query(包括 query,mutation,subscription )和与之对应的 Resolver 是同名的, 比如关于
articles(): [Articles!]!
这个 query, 它的 Resolver 的名字必然叫做 articles.
以已经声明的 articles 的 query 为例, 解释下 GraphQL 的内部工作机制:
- Query {
- articles {
- id
- author {
- name
- }
- comments {
- id
- desc
- author
- }
- }
- }
按照如下步骤进行解析:
首先进行第一次解析, 当前的类型是 query 类型, 同时 Resolver 的名字为 articles.
下一步会使用 articles 的 Resolver 获取解析数据, 第一层解析完毕.
下一步对第一层解析的返回值, 进行第二层解析, 当前 articles 包含三个子 query , 分别是 id,author 和 comments.
id 在 Author 类型中为标量类型, 解析结束.
author 在 articles 类型中为对象类型 User, 尝试使用 User 的 Resolver 获取数据, 当前 field 解析完毕.
下一步对第二层解析的返回值, 进行第三层解析, 当前 author 还包含一个 query,name 是标量类型, 解析结束.
comments 解析同上.
概括总结 GraphQL 大体解析流程就是遇见一个 Query 之后, 尝试使用它的 Resolver 取值, 之后再对返回值进行解析, 这个过程是递归的, 直到所有解析 Field 类型是 Scalar Type(标量类型)为止. 整个解析过程可以想象为一个很长的 Resolver Chain(解析链). GraphQL 在实际使用中常常作为中间层来使用, 数据的获取通过 Resolver 来封装, 内部数据获取的实现可能基于 RPC,REST,WS,SQL 等多种不同的方式.
3.GraphQL 例子
下面这部分将会展示一个用 https://github.com/graphql-go/graphql 库实现的用户管理的例子, 包括获取全部用户信息, 获取指定用户信息, 修改用户名称, 删除用户的功能, 以及如何创建枚举类型的功能 完整代码在这里 https://github.com/darker11/graphql :
3.1 生成的 schema 文件内容如下:
- //mutation 操作可完成 C(创建),U(更新),D(删除)
- type Mutation {
- """[用户管理] 修改用户名称""" // 操作注释信息
- changeUserName(
- """用户 ID""" // 参数注释信息, 必传一个 Int 类型的值
- userId: Int!
- """用户名称"""
- userName: String!
- ): Boolean
- """[用户管理] 创建用户"""
- createUser(
- """用户名称"""
- userName: String!
- """用户邮箱"""
- email: String!
- """用户密码"""
- pwd: String!
- """用户联系方式"""
- phone: Int
- ): Boolean
- """[用户管理] 删除用户"""
- deleteUser(
- """用户 ID"""
- userId: Int!
- ): Boolean
- }
- //query 操作, 可完成 R(查询)
- type Query {
- """[用户管理] 获取指定用户的信息"""
- UserInfo(
- """用户 ID"""
- userId: Int!
- ): userInfo
- """[用户管理] 获取全部用户的信息"""
- UserListInfo: [userInfo]!
- }
- //object type 说明
- """用户信息描述"""
- type userInfo {
- """用户 email"""
- email: String // 字段说明
- """用户名称"""
- name: String
- """用户手机号"""
- phone: Int
- """用户密码"""
- pwd: String
- """用户状态"""
- status: UserStatusEnum
- """用户 ID"""
- userID: Int
- }
- // 枚举类型在 schema 文件中的展示
- """用户状态信息"""
- enum UserStatusEnum {
- """用户可用"""
- EnableUser
- """用户不可用"""
- DisableUser
- }
注意
GraphQL 基于 golang 实现的例子比较少 .
GraphQL 的 schema 可以自动生成, 具体操作可查看 graphq-cli https://github.com/graphql-cli/graphql-cli 文档, 步骤大致包括 npm 包的安装, graphql-cli 工具的安装, 配置文件的更改(此处需要指定服务对外暴露的地址) , 执行 graphql get-schema 命令.
3.2 GraphQL 的 object type 定义
- type UserInfo struct {
- UserID uint64 `json:"userID"`
- Name string `json:"name"`
- Email string `json:"email"`
- Phone int64 `json:"phone"`
- Pwd string `json:"pwd"`
- Status model.UserStatusType `json:"status"`
- }
- // 这段内容是如何使用 GraphQL 定义枚举类型
- var UserStatusEnumType = graphql.NewEnum(graphql.EnumConfig{
- Name: "UserStatusEnum",
- Description: "用户状态信息",
- Values: graphql.EnumValueConfigMap{
- "EnableUser": &graphql.EnumValueConfig{
- Value: model.EnableStatus,
- Description: "用户可用",
- },
- "DisableUser": &graphql.EnumValueConfig{
- Value: model.DisableStatus,
- Description: "用户不可用",
- },
- },
- })
- // 定义 object type, 前端可以按需获取该类型中包含的字段
- var UserInfoType = graphql.NewObject(graphql.ObjectConfig{
- Name: "userInfo",
- Description: "用户信息描述",
- Fields: graphql.Fields{
- "userID": &graphql.Field{
- Description: "用户 ID",
- Type: graphql.Int,
- },
- "name": &graphql.Field{
- Description: "用户名称",
- Type: graphql.String,
- },
- "email": &graphql.Field{
- Description: "用户 email",
- Type: graphql.String,
- },
- "phone": &graphql.Field{
- Description: "用户手机号",
- Type: graphql.Int,
- },
- "pwd": &graphql.Field{
- Description: "用户密码",
- Type: graphql.String,
- },
- "status": &graphql.Field{
- Description: "用户状态",
- Type: UserStatusEnumType,
- },
- },
- })
3.3 query 与 mutation 的定义
- var MutationType = graphql.NewObject(graphql.ObjectConfig{
- Name: "Mutation",
- Fields: graphql.Fields{
- "createUser": &graphql.Field{
- Type: graphql.Boolean,
- Description: "[用户管理] 创建用户",
- Args: graphql.FieldConfigArgument{
- "userName": &graphql.ArgumentConfig{
- Description: "用户名称",
- Type: graphql.NewNonNull(graphql.String),
- },
- "email": &graphql.ArgumentConfig{
- Description: "用户邮箱",
- Type: graphql.NewNonNull(graphql.String),
- },
- "pwd": &graphql.ArgumentConfig{
- Description: "用户密码",
- Type: graphql.NewNonNull(graphql.String),
- },
- "phone": &graphql.ArgumentConfig{
- Description: "用户联系方式",
- Type: graphql.Int,
- },
- },
- Resolve: func(p graphql.ResolveParams) (interface{}, error) {
- userId, _ := strconv.Atoi(GenerateID())
- user := &model.User{
- // 展示如何解析传入的参数, 传入参数必须符合断言
- Name: p.Args["userName"].(string),
- Email: sql.NullString{
- String: p.Args["email"].(string),
- Valid: true,
- },
- Pwd: p.Args["pwd"].(string),
- Phone: int64(p.Args["phone"].(int)),
- UserID: uint64(userId),
- Status: int64(model.EnableStatus),
- }
- ......
- return true, nil
- },
- },
- },
- })
- var QueryType = graphql.NewObject(graphql.ObjectConfig{
- Name: "Query",
- Fields: graphql.Fields{
- "UserListInfo": &graphql.Field{
- Description: "[用户管理] 获取指定用户的信息",
- // 定义了非空的 list 类型
- Type: graphql.NewNonNull(graphql.NewList(UserInfoType)),
- Resolve: func(p graphql.ResolveParams) (interface{}, error) {
- users, err := model.GetUsers()
- if err != nil {
- log.WithError(err).Error("[query.UserInfo] invoke InserUser() failed")
- return false, err
- }
- usersList := make([]*UserInfo, 0)
- for _, v := range users {
- userInfo := new(UserInfo)
- userInfo.Name = v.Name
- userInfo.Email = v.Email.String
- userInfo.Phone = v.Phone
- userInfo.Pwd = v.Pwd
- userInfo.Status = model.UserStatusType(v.Status)
- usersList = append(usersList, userInfo)
- }
- return usersList, nil
- },
- },
- },
- })
注意:
此处仅展示了部分例子.
此处笔者仅列举了 query,mutation 类型的定义.
3.4 如何定义服务 main 函数
- func main() {
- ......
- //new graphql schema
- schema, err := graphql.NewSchema(
- graphql.SchemaConfig{
- Query: object.QueryType,
- Mutation: object.MutationType,
- },
- )
- // 此次从 http 请求的 header 中获取 user_id 的值, 然后通过 context 向后续操作传递
- http.HandleFunc("/graphql", func(w http.ResponseWriter, r *http.Request) {
- ctx := context.Background()
- //read user_id from gateway
- userIDStr := r.Header.Get("user_id")
- if len(userIDStr)> 0 {
- userID, err := strconv.Atoi(userIDStr)
- if err != nil {
- w.WriteHeader(http.StatusBadRequest)
- w.Write([]byte(err.Error()))
- return
- }
- ctx = context.WithValue(ctx, "ContextUserIDKey", userID)
- }
- h.ContextHandler(ctx, w, r)
- })
- log.Fatal(http.ListenAndServe(svrCfg.Addr, nil))
- }
4. 总结
笔者在实践 GraphQL 的过程中, 发现存在以下问题:
除了 Facebook 官方的 Node.js 版本支持的比较好, 其他版本的文档和实践都比较少.
https://github.com/graphql-go/graphql 库亲测存在 n + 1 问题, 建议使用 graph-gophers 下的 graphql-go 的库, 因为 Features 里明确写着
- parallel execution of resolvers
- .
Rest 和 GraphQL 都是服务端承载的系统对外的接口, 二者是可以共存的.
GraphQL 更容易造成拒绝服务攻击, 在使用时要特别小心.
GraphQL 的利好主要是在于前端的开发效率, 但落地却需要服务端的全力配合.
如果是一家没有技术包袱的小公司, 根据接口变动频繁等的业务的特性, 考虑选择使用 GraphQL 完全可以理解; 但如果是一个大公司, 对外已经全部使用 RESTful API , 基于人力成本的考虑不使用 GraphQL 也是合理的. 无论 GraphQL 还是 RESTful API , 因地制宜选择适合的才是好的. 虽然 GraphQL 对于 Facebook 这样的公司是合适的, 但不可能其他所有公司的业务需求都跟 Facebook 相同.
笔者初次接触 GraphQL , 不免有理解有误的地方, 欢迎指出.
来源: https://juejin.im/entry/5b4d8625e51d4519213fdca1