个人前端故事----我为何用GraphQL

背景

今年我在作一个有关商户的app,这是一个包含商户从入网到审核、从驳回提交到入网维护的完整的生命周期线下推广人员使用的客户端软件,但故事并无这么简单。。。前端

疑问

随着app的逐渐完善,遇到的问题也渐渐多了起来,界面加载太久,初始化页面请求次数过多等各类各样的小毛病开始凸显了出来。因而我开始了优化之路,第一步即是从api请求入手,仔细查看了每一个api返回的内容,一直奇怪为何接口老是返回不少的数据回来,好比我须要一个商户的详细信息,可接口却会把这个商户相关的门店信息、全部人信息等其它各类各样的信息一块儿返回回来,若是是例如商户详情页面也就罢了,但是在商户列表这个接口下依旧返回如此之多的数据,可想而知这个列表有多大多复杂了。node

后来问事后端的同窗才知道是为了兼容 web 端的需求,一个接口须要同时为多个平台提供内容,这大大增长了接口的返回内容和处理逻辑,并且需求也常常改动,因此还不如把能用到的字段全都输出出来,省得每次改需求都要先后端一块儿联动。web

反思

得知告终果,这确实是一个有“充分”理由的处理结果,但是真的只能这样了嘛?有没有什么更好的解决办法呢?咱们先来总结一下如今遇到的问题:数据库

  • 兼容多平台致使字段冗余
  • 一个页面须要屡次调用 API 聚合数据
  • 需求常常改动致使接口很难为单一接口精简逻辑

以上三个问题看起来并不复杂,按照以往的逻辑其实也是很好解决的,就拿第一个来讲,遇到多平台须要兼容时其实能够经过提供不一样平台的接口来解决,例如这样:npm

http://api.xxx.com/web/getUserInfo/:uid
http://api.xxx.com/app/getUserInfo/:uid
http://api.xxx.com/mobile/getUserInfo/:uid

又或者是经过不一样的参数去控制:后端

http://api.xxx.com/getUserInfo/:uid?platfrom=web

虽然这是一个方便的解决方案,但带来的实际上是后端逻辑的增长,须要为不一样平台维护不一样的逻辑代码。api

再说第二个问题,一个页面须要屡次调用接口来聚合数据这块也能够经过多加接口的方式来解决:restful

http://api.xxx.com/getIndexInfo

或者是经过 http2 来复用请求,但这些方法不是增长工做量就是有兼容性问题,那么还有没有其余的方法呢?数据结构

若是你们还记得数据库的知识的话,就发现其实咱们能够用 SQL 的思路去解决这些事情,那若是把后端抽象成一个数据库会怎么样呢?app

我想要什么字段就 SELECT 什么字段就行咯,若是一个页面须要多个数据源的内容来填充那不就是组合 SQL 语句嘛。这样不就解决了上面提出的三个问题了嘛,并且不管前端需求如何变动,只要咱们维持一个数据的超集,那么每次只要让前端改动查询语句就能够了,后端这里也不须要同步的去给某个接口增长字段什么的了,那么解决方案有了,那该怎么把后端抽象成一个数据库呢?

解决

既然思路有了,那么办法也会有的~这就是 Facebook 在2015年开源的 GraphQL

这又是一个什么东西呢?具体的介绍直接看它的官网就行了,我在这里就很少说了,直接来看看如何使用吧。

因为个人中间层是基于 Koa2 的,因此就在 koa2 上面作演示了,手写咱们先安装依赖:

npm install graphql koa-graphql --save

这样咱们就能够在 koa 中使用 graphql 了,而后就是配置路由了,按照文档上面的例子,咱们能够这样写:

"use strict";
const router = require('koa-router')();
const graphqlHTTP = require('koa-graphql');
const GraphQLSchema = require('./graphql');
const renderGraphiQL = require('../utils/render_graphiQL');
const graphqlModule = graphqlHTTP((request) => ({
    schema: GraphQLSchema,
    graphiql: false,
    context: {token: request.header.authorization, platform: request.query.platform},
    formatError: error => ({
        type: 'graphql',
        path: error.path,
        message: error.message,
        locations: error.locations ? error.locations[0] : null
    })
}));
router.all('/graphql', graphqlModule);

咱们来看看 graphqlModule 对象中都包含写什么吧,首先是 schema,这个是咱们主要的解析逻辑,全部经过 graphql 的请求都会被传入这里进行解析和处理,graphiql 这个是 koa-graphql 自带的一个图形界面版的测试地址,后面我再单独介绍这个插件,context 是咱们的上下文,若是咱们须要在每一个解析函数内获取到例如用户 token时就能够在这里赋值,需求说明的是这个必须是一个 object 对象,而且若是咱们不指定的话会默认传整个 request 对象,接下来就是最后一个经常使用的属性了,formatError 是格式化错误的属性,咱们能够根据业务的需求自定义咱们的错误返回。

接下来去看看从最简单的 hello world 开始,而后完成一个最基础的 demo。

首先咱们在客户端发起一个 post 请求,而后在请求 body 中带上咱们的查询语句,在 Graphql 中有两种类型的查询,一直是 query 开头的查询操做,一种是 mutation 开头的修改操做。

query {hello}

这是一个最简单的查询,那么这个查询是如何经过解析的呢?上文说到所有的 Graphql 查询都会经过 schema 来进行解析,那咱们看看上面定义的GraphQLSchema对象是个什么吧。

module.exports = new GraphQLSchema({
    query: new GraphQLObjectType({
        name: 'rootQueryType',
        description: '查询操做',
        fields: {
            hello: {
                type: GraphQLString,
                description: '演示 demo',
                args: {
                    name: {type: GraphQLString, description: '演示参数'}
                },
                resolve(it, args, context) {
                    return args.name;
                }
            }
        }
    }),
    mutation: new GraphQLObjectType({
        name: 'rootMutationType',
        description: '新增或修改操做',
        fields: {}
    })
});

咱们一步步来讲,首先在这个文件中导出的是一个 GraphQLSchema 对象,这是 Graphql 的基础对象,里面包含了咱们须要的两种类型,而后看 query 属性,它返回的是一个GraphQLObjectType对象,这是 Graphql 中对于 object 的基本类型,这个对象中包含 name:名称(全局惟一),description: 描述(这会自动的显示在文档中,虽然是非必须的,可是我仍是强烈建议每一个 Graphql 节点都写上,这样在后面的维护和查询中都很是有利),最后就是fields属性了,一个 Graphql 语句能查到什么就全靠这里写了什么了,在开始的例句中咱们查询 query{hello},其实就是说咱们要查根节点下的 hello 属性,因此这里咱们就须要在 query 的 fields 中写上 hello 属性了,不然这条查询语句就没法生效了。

接下来咱们看看这个 hello 属性中又包含了什么呢?首先咱们须要指定它的类型,这很关键,这个类型是 hello 的返回类型,在这里我指定它返回的是一个字符串,除此以外还有 Int,Boolean 等 js 的基础类型可供选择,具体可去查看文档,固然了,在复杂状况下也能够返回 GraphQLObjectType 这种对象类型,而后就是对 hello 的描述字段description,接下来是args 属性,若是咱们须要给此次查询传入参数的话就靠这个了,接下来就是最关键的 resolve 函数了,这个函数接受三个参数,第一个是上层的返回值,这在循环嵌套的状况下会常用,好比说若是 hello 还有子属性的话,那么子属性的这个参数就会是 args.name,第二个参数即是查询属性,第三个是咱们一开始说的贯穿整个请求的上下文。下面是一个完整的例子:

Request: query{hello(name: "world")}

Response: {"hello": "world"}

讲完了 query 操做,其实 mutation 操做也是相似的,我就再也不展开说了。

总结

最后来总结一下这个解决方案吧,其实这个方案你说是否是最佳解呢?也未必,仍是要看具体的业务场景的,在我遇到的场景中各类数据的关系是明确的,或者说是能够抽象成模型的,我认为这是可否使用 Graphql 的关键,从上面的实例中咱们其实能够发现经过 Graphql 咱们把每一个数据都规范了起来,指定了类型,肯定了嵌套关系,这在以 JavaScript 为基础的 node 环境中显得那么格格不入,自己 JavaScript 是弱类型的,基于此咱们能够在 node 服务中灵活的修改数据,从而不须要关心返回值和参数值,可是 Graphql 用一种强类型的观念来强制咱们设计每一个数据,也许会有些前端的同窗接受不了,可是我我的认为这种思路实际上是很是合理的,而且 Graphql 这种还支持嵌套查询,只须要 fields 属性中有这个对象就好了,所以咱们能够把每一个数据类型尽量的抽象和分离出来,举个例子,店长这个角色不就是用户对象加上商户对象的组合嘛,这不只从关系上明确了逻辑,也方便了更多可能性的组合条件。

对应我一开始遇到的那几个问题,Graphql 看上去彷佛完美的解决了个人问题,可是对于更加复杂的场景呢?或者说对于老项目的改形成本是否划算呢?虽然我没有遇到,可是我以为只要认真梳理数据结构,最终均可以的,但那个时候是否还须要 Graphql 呢?那就不知道了,这篇博客不是介绍 Graphql 如何使用的中文文档,我想表达的是这种思路对于这种场景下的一个解决思路,如今它只是解决了个人这些问题,那么从这个思路的身上能不能挖掘出更多的惊喜呢?它仍是太新了,也许过几年回头再看,它说不定就和 restful 同样是 API 的标配了也说不定呢,毕竟 GitHub 今年也推出了他们的 Graphql API 了呢。

相关文章
相关标签/搜索