阅读 558

GraphQL 前端接入指南

什么是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个步骤:

  1. HTTP请求体:描述前端所需要的数据结构(根据Schema文件内有的信息组合而成)
  2. 请求到达服务端,解析器首先解析query下的第一个层user,在这个解析器内可以编写对user的处理逻辑。
  3. 解析器继续深入user下的id字段,对id字段进行解析。
  4. 解析器对与id平级的name字段进行解析。
  5. 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文件

  1. 安装 graphql-cli,通过命令帮助我们有效管理schema文件、端点信息、静态检测等。
  2. 在项目的根目录下新建.graphqlconfig.yaml文件,配置graphql,配置项参考:graphql-config
{
  "projects": {
    // 应用名
    "app": {
      // schame文件在项目中的位置
      "schemaPath": "schema.graphql",
      // 检测包含的文件 默认 .graphql
      "includes": ["*.gql"],
      "extensions": {
        // 后端站点
        "endpoints": {
          // 开发环境
          "dev": "http://localhost:3003/graphql",
          // 生产环境
          "prod": {
          }
        }
      }
    }
  }
}
复制代码
  1. 通过在根目录下执行graphql get-schema命令即可新增|更新schema文件。
  2. IDE支持 Vscode安装vscode-graphql,Webstorm可安装 js-graphql-intellij-plugin

提示&静态检查

在编写graphql的请求查询体时,如果没有自动提示或字段检测 是一个非常糟糕的体验,很容易和schema文件内的结构体不一致,导致查询失败。可以通过以下方式处理:

  1. 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'
       ]
     }
    复制代码
  2. 执行Eslint的检测时记得包含.gql和.graphql后缀的文件哦 eslint . --ext .js --ext .gql --ext .graphql

  3. 由于在.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
})
复制代码
配置项强制类型说明
AjaxRequesttrueObject请求框架,可以接入任意 例如:axios、flyio等
reqFunctrueString请求框架 中处理GraphQL请求的方法名
debugfalseBoolean是否开启调试信息

在模块中配置

graphql: {
    ['自定义名称']: {
        // 各配置项
    }
}
复制代码

我们只关心 各配置项就好,['自定义名称']在后面手动调用时 用到。

配置项强制类型说明
querytrueObject对象内 key为响应数据赋值的key,会自动设置到data对象内,value为GraphQL的查询语句
variablesfalseObject对应GraphQL查询语句的 参数变量,会自动设置到data内,同时会watch 用于自动查询。
注意:noWatch下的key不会设置到watch下。
isImmediatefalseBoolean是否立即执行
debouncefalseBoolean | Object是否开启防抖 通过 {wait: 1000} 开启
throttlefalseBoolean | Object是否开启节流 通过 {gapTime: 1000} 开启
ajaxOptions.catchSuccessfalsefunction获取成功的回调,以绑定到当前模块,可以愉快使用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自定义处理,根据前端传入的单位格式 进行相应的返回,如下图所示:

可以将各种计量单位(长度、面积、体积......)都定义出来,在使用的时候统一处理。

参考

  1. GraphQL官方文档
  2. Apollo GraphQL
  3. Vue Apollo
  4. GraphQL-Config
文章分类
前端
文章标签