GraphQL —— 新一代Web API设计模式

目前,程序界对于前后端分离的应用,最常用的接口设计模式是RESTful API。当然,作为被业界广泛接受的设计模式,在当初被设计出来后的很长一段时间RESTful API都是前后端分离的Web API设计模式的最好选择。它的优点我就不赘述了,不了解的可以看看阮一峰老师的这篇文章。今天我要介绍的是接口设计模式的新贵 GraphQL,在介绍GraphQL之前,我们先结合实例说说RESTful API 的局限性。

场景

在某个业务中,我们需要以下数据内容a:

{
  user(id: 1) {
    id,
    name,
    isFollowed
  }
}

在另一个业务中,我们需要数据内容b:

{
  user(id: 1) {
    id,
    name,
    university: {
      id,
      name,
      profile
    }
  }
}

对比一下两个数据内容,两者间只相差一个字段。

在RESTful API中,这一资源一般会被设计为:/user/:id(GET) ,接口返回数据为:

{
  user(id: 1) {
    id,
    name,
    isFollowed,
    university: {
      id,
      name,
      profile
    }
  }
}

那么就存在一个问题,假如业务比较复杂的时候,这个接口要做到通用,可能需要返回user(id: 1)的所有字段以及关联数据,然而在多数业务中,接口返回的大部分数据都是多余的。

有人会说,那我们可以通过增加参数,如 /user/:id?extend=university(GET) 这样的方式来约束接口的返回内容,但是这样的做法在复杂的业务面前同样存在很大的问题,一个简单的获取user资源的接口业务代码会非常臃肿。

如果我们通过写多个接口来解决这个问题(这其实已经不符合RESTful API的设计规范),同样会使得后台代码非常的臃肿,而且没营养的业务代码会非常多。

好,那既然问题存在,自然会有相应的解决方案,接下来我们就来看看Facebook在2015年开源的GraphQL是怎么解决这样的问题的。

解决方案

还记得上文中是用什么结构表示业务所需数据内容吗

{
  user(id: 1) {
    id,
    name,
    isFollowed
  }
}

这不是json,但是我们还是能很清晰的看出要查询的数据内容,它表示我们要获得id为1的用户的[id, name, isFollowed]字段。

没错,GraphQL就是使用这样的查询语句。根据上面的场景,我写了一个简单的demo:

// index.js
const express = require('express')
const parser = require('body-parser')
const { graphql } = require('graphql')
const UserSchema = require('./schema/user')

const app = express()

app.use(parser.text({ type: 'application/graphql' }))

app.get('/user', (req, res) => {
  graphql(UserSchema, req.query.query).then((result) => {
    res.json(result)
  })
})

let server = app.listen(3000, function() {
  let host = server.address().address
  let port = server.address().port

  console.log('Server listening at http://%s:%s', host, port)
})
// ./schema/user.js
const { GraphQLObjectType, GraphQLSchema, GraphQLInt, GraphQLString, GraphQLBoolean } = require('graphql')
const user = {
  id: 1,
  name: 'test',
  isFollowed: false
}
const university = {
  id: 2,
  name: 'test-university'
}
const UniversityType = new GraphQLObjectType({
  name: 'University',
  fields: {
    id: {
      type: GraphQLInt
    },
    name: {
      type: GraphQLString,
      args: {
        id: GraphQLInt
      },
      resolve: async (id) => {
        return await UserService.getName(id)
      }
    }
  }
})
const UserType = new GraphQLSchema({
  query: new GraphQLObjectType({
    name: 'rootQueryType',
    fields: {
      id: {
        type: GraphQLInt,
        resolve: () => {
          return user.id
        }
      },
      name: {
        type: GraphQLString,
        resolve: () => {
          return user.name
        }
      },
      isFollowed: {
        type: GraphQLBoolean,
        resolve: () => {
          return user.isFollowed
        }
      },
      university: {
        type: UniversityType,
        resolve: () => {
          return university
        }
      }
    }
  })
})

module.exports = UserType

在终端运行demo:

$ node index.js
Server listening at http://:::3000

接下来我们使用postman来示例,上文中提到的场景是如何完美解决的。(请求Headers设置 Content-Type: application/graphql

针对业务数据内容a:

// 传入参数
query: {id, name, isFollowed}

针对业务数据内容b:

// 传入参数
query: {id, name, gender, university{id, name}}

GraphQL还提供了很多特性

  • 别名

假如我们在业务a中需要把返回数据的name改为nickname,那么可以这样设置query

// 传入参数
query: {id, nickname:name, isFollowed}

  • 片段(Fragment)

假如存在这样的数据内容

// User
{
  id,
  name,
  profile,
  father {
    id,
    name,
    profile
  },
  mother {
    id,
    name,
    profile
  }
}

片段中{id, name, profile}是可复用的,这时候就可以使用片段来简化:

// fragment
fragment userFields on User {
  id,
  name,
  profile
}

// User
{
  ...userFields,
  father {
    ...userFields
  },
  mother {
    ...userFields
  }
}

关于数据的修改,GraphQL是使用 mutation 来实现的,具体实现我在这里就不多说了,有兴趣的可以去了解一下。

GraphQL 使用强类型来约束数据内容,而Node却是一门弱类型语言,因此在 Node 中使用 GraphQL ,可能有些人会不适应。但是,我觉得这是未来的趋势,就比如现在正火的typescript,也是强类型的。使用它可以大大提高接口的复用性、可维护性,同时方便前后端接口对接,对于大型项目是一个很好的选择。

我觉得再过几年,GraphQL API 会和 RESTful API 一样成为API设计的首选,甚至更受欢迎。具体案例:原先使用RESTful API, 全世界最大的同性交友网站 gayhub(github) 在新版本上也已经使用 GraphQL API 了。

0%