February 22, 2016

森的妖精 GraphQL

最(好几个)近(月之前), 在公司前端同学的推荐下接触了GraphQL, 感觉一阵清爽. 如今使用了一段时间, 可以汇报一下使用感受.

GraphQL?

物如其名, GraphQL 是一种 Query Language, 由世界上最好语言的领军者 Facebook 公司发明. 它的出现是为了满足服务端向客户端灵活输出数据的需要.

Restful API 不行吗?

当然行. Restful 是包括我在内, 各位后端同学都非常熟悉的朋友. 从早年凭感觉写的jsp, asp们, 到后来做的 WebService API, 再到有哲学思想指导的 Restful API. 可以很明显的感觉到, 这些前后端的数据交互都是由后端在主导. 后端决定了数据格式, 决定了每个请求所能得到的数据.

可是! 时代不再是做个门户, 做个页面这么简单了. 如今更注重的是客户端侧的数据交互. 看看隔壁js们的各种前端框架, SPA, 数据绑定, MVVM 搞得热火朝天. 虽然后端依然承载着业务处理, 数据安全这样核心的功能. 但是客户端才是数据的需求方, 前端更清楚, 需要什么样的数据进行呈现, 使用什么样的数据进行交互.

这样一来写接口的的时候就会有这样的经历, 随着前端页面或是客户端的快速改版, 对于字段的需求是在不断变化, 于是一个 API 不断地 v1, v2, v3...vN. 最后也改烦了: 干脆把每个 model 的字段, 可以展示的都展示出去好了! 牺牲了服务器的带宽和客户的流量, 不过总算是不用再繁琐的更改接口定义了.

何不让客户端主导?

就好像 ElasticSearch 的接口里一样, 可以告诉服务端我要请求什么字段. 我这里有个叫 Uesr 的 model, 它一对多拥有许多的 Messages. 一个请求 Messages 列表的 Request 看起来就像下面这样:

GET /messages
{
  "since": "20XX-YY-ZZTAA:BB:CC",
  "fields": [
    "id",
    "title",
    "content",
    "created_at",
    {
      "user": [
        "id",
        "name",
        "account"
      ]
    }
  ]
}

其实也挺好看的对不对...

GraphQL 的出现, 就是定义了一套更为清晰更为灵活的标准, 来满足客户端对于数据灵活的需要.

In GraphQL

定义结构

GraphQL 的核心是两个概念. 一个是 Type, 他定义了数据的结构.

type Message {
  id: String
  title: String
  content: String
  user: User
}

type User {
  id: String
  name: String
  account: String
  messages: [Message]
}

这是 GraphQL Spec 中定义 Type 用的语言. (虽然实际上我也没见到任何一个 GraphQL 的库用到过这些语言, 包括 Facebook 自己官方提供的例子... 不过这种细节不用在意, 这就是为了方便沟通用的东西.)
在这个 Type 定义中, 定义了两个 Type, 在 {} 中间的定义的东西叫做 Field. 也就是 GraphQL 两个核心中的另一个. Type 和 Field 的定义形成了 GraphQL 的 Schema.

当然只有散户的话是没有入口的. GraphQL 的默认 Schema 自带了两个 Type 作为入口. 一个是 QueryRoot, 一个是 MutationRoot. 下文再慢慢讲.

我们可以在 QueryRoot 里把 Message 挂上去.

type QueryRoot {
  messages(since: Date): [Message]
}

[] 代表列表, () 里的是接受的参数.

执行查询

有了上面的 Schema 之后, 就能进行 query 了.

query messages(since: "20XX-YY-ZZTAA:BB:CC") {
  id
  title
  content
  user {
    id
    name
    account
  }
}

看起来比restful版本的可选字段请求清楚很多了. user 作为关联对象, 也很自然的出现在相同的结构中.

还有个 Mutation 呢?

上面提到过, GraphQL 的 Schema 最上级有两个 Root Type, 一个是 QueryRoot, 一个是 MutationRoot. Mutation, 根据 spec 中的定义, 是"会让服务端数据发生变化的 query", 也就是有副作用的 query .
比如定义一个发消息的 mutation

type MutationRoot {
  createMessage(title: String, content: String): Message
}

这里 createMessage 是一个 mutation, 它返回的数据 Type 为 Message.

mutation createMessage(title: "森的妖精 GraphQL", content: "blahblahblahblah") {
  id
}

这样就能创建一个 Message, 并且指定服务端返回创建后的 Message ID.

GraphQL 的基本用法就是上面这些了. 详细的还会有 Interface, Union 等东西, 主要是为了解决多态和复用.

总结?

在 GraphQL 中服务端的职责在于定义数据结构和准备数据, 进行业务处理. 而数据的获取, 展示则由更接近用户的客户端来主导. 对客户端和服务端都是很大的解放. 对于 Restful 在呈现端的不灵活是一个很大的补充.

不足

要说讨厌的地方, 那就是 Mutation 了. 根据 Facebook 所提供的 spec, 所有的 Mutation 都挂载在 MutationRoot 之下. 用我们 coding 所习惯的说法, 都是同一个类的全局方法... 感觉很难受.

于是我们在开发中, 前后端慢慢形成了一种默契. 在 Type 中定义对象的 Mutation...
类似这样

type Fish {
  id: String
  name: String
  Price: Float
  inStock: Int
  mutation: FishMutation
}
type FishMutation {
  update: UpdateFishMutation
  sell: SellFishMutation
}

于是就能

query {
  fish(id: "1") {
    id
    name
    price
    mutation {
      sell(weight: 100) {
        inStock
      }
    }
  }
}

这样看上去稍微舒服点...