## 1 引言

每当项目进入联调阶段，或者提前约定接口时，前后端就会聚在一起热火朝天的讨论起来。可能 99% 的场景都在约定 Http 接口，讨论 URL 是什么，入参是什么，出参是什么。

有的团队前后端接口约定更加高效，后端会拿出接口定义代码，前端会转换成（或自动转成）Typescript 定义文件。

但这些工作都针对于 Http 接口，今天通过 [when-to-use-what-rest-graphql-webhooks-grpc](https://nordicapis.com/when-to-use-what-rest-graphql-webhooks-grpc/) 一文，抛开联调时千遍一律的 Http 接口，一起看看接口还可以怎么约定，分别适用于哪些场景，你现在处于哪个场景。

## 2 概述

本文主要讲了四种接口设计方案，分别是：REST、gRPC、GraphQL、Webhooks，下面分别介绍一下。

### REST

REST 也许是最通用，也是最常用的接口设计方案，它是 **无状态的**，以资源为核心，针对如何操作资源定义了一系列 URL 约定，而操作类型通过 `GET` `POST` `PUT` `DELETE` 等 HTTP Methods 表示。

REST 基于原生 HTTP 接口，因此改造成本很小，而且其无状态的特性，降低了前后端耦合程度，利于快速迭代。

随着未来发展，REST 可能更适合提供微服务 API。

使用举例：

```bash
curl -v -X GET https://api.sandbox.paypal.com/v1/activities/activities?start_time=2012-01-01T00:00:01.000Z&amp;end_time=2014-10-01T23:59:59.999Z&amp;page_size=10 \
-H "Content-Type: application/json" \
-H "Authorization: Bearer Access-Token"
```

### gRPC

gRPC 是对 RPC 的一个新尝试，最大特点是使用 protobufs 语言格式化数据。

RPC 主要用来做服务器之间的方法调用，影响其性能最重要因素就是 序列化/反序列化 效率。RPC 的目的是打造一个高效率、低消耗的服务调用方式，因此比较适合 IOT 等对资源、带宽、性能敏感的场景。而 gRPC 利用 protobufs 进一步提高了序列化速度，降低了数据包大小。

使用举例：

gRPC 主要用于服务之间传输，这里拿 Nodejs 举例：

1. 定义接口。由于 gRPC 使用 protobufs，所以接口定义文件就是 `helloword.proto`:

```protobufs
// The greeting service definition.
service Greeter {
  // Sends a greeting
  rpc SayHello (HelloRequest) returns (HelloReply) {}
  // Sends another greeting
  rpc SayHelloAgain (HelloRequest) returns (HelloReply) {}
}

// The request message containing the user's name.
message HelloRequest {
  string name = 1;
}

// The response message containing the greetings
message HelloReply {
  string message = 1;
}
```

这里定义了服务 `Greeter`，拥有两个方法：`SayHello` 与 `SayHelloAgain`，通过 `message` 关键字定义了入参与出参的结构。

事实上利用 protobufs，传输数据时仅传送很少的内容，作为代价，双方都要知道接口定义规则才能序列化/反序列化。

2. 定义服务器：

```js
function sayHello(call, callback) {
  callback(null, { message: "Hello " + call.request.name });
}

function sayHelloAgain(call, callback) {
  callback(null, { message: "Hello again, " + call.request.name });
}

function main() {
  var server = new grpc.Server();
  server.addProtoService(hello_proto.Greeter.service, {
    sayHello: sayHello,
    sayHelloAgain: sayHelloAgain
  });
  server.bind("0.0.0.0:50051", grpc.ServerCredentials.createInsecure());
  server.start();
}
```

我们在 `50051` 端口支持了 gRPC 服务，并注册了服务 `Greeter`，并对 `sayHello` `sayHelloAgain` 方法做了一些业务处理，并返回给调用方一些数据。

3. 定义客户端：

```js
function main() {
  var client = new hello_proto.Greeter(
    "localhost:50051",
    grpc.credentials.createInsecure()
  );
  client.sayHello({ name: "you" }, function(err, response) {
    console.log("Greeting:", response.message);
  });
  client.sayHelloAgain({ name: "you" }, function(err, response) {
    console.log("Greeting:", response.message);
  });
}
```

可以看到，客户端和服务端同时需要拿到 proto 结构，客户端数据发送也要依赖 proto 包提供的方法，框架会内置做掉序列化/反序列化的工作。

> 也有一些额外手段将 gRPC 转换为 http 服务，让网页端也享受到其高效、低耗的好处。但是不要忘了，RPC 最常用的场景是 IOT 等硬件领域，网页场景也许不会在乎节省几 KB 的流量。

### GraphQL

GraphQL 不是 REST 的替代品，而是另一种交互形式：前端决定后端的返回结果。

GraphQL 带来的最大好处是精简请求响应内容，不会出现冗余字段，前端可以决定后端返回什么数据。但要注意的是，前端的决定权取决于后端支持什么数据，因此 GraphQL 更像是精简了返回值的 REST，而后端接口也可以一次性定义完所有功能，而不需要逐个开发。

再次强调，相比 REST 和 gRPC，GraphQL 是由前端决定返回结果的反模式。

使用举例：

原文推荐参考 [GitHub GraphQL API](https://developer.github.com/v4/)

比如查询某个组织下的成员，REST 风格接口可能是：

```bash
curl -v https://api.github.com/orgs/:org/members
```

含义很明确，但问题是返回结果不明确，必须实际调试才知道。换成等价的 GraphQL 是这样的

```graphql
query {
  organization(login: "github") {
    members(first: 100) {
      edges {
        node {
          name
          avatarUrl
        }
      }
    }
  }
}
```

返回的结果和约定的格式结构一致，且不会有多余的字段：

```json
{
  "data": {
    "organization": {
      "members": {
        "edges": [
          {
            "node": {
              "name": "Chris Wanstrath",
              "avatarUrl": "https://avatars0.githubusercontent.com/u/2?v=4"
            }
          },
          {
            "node": {
              "name": "Justin Palmer",
              "avatarUrl": "https://avatars3.githubusercontent.com/u/25?v=4"
            }
          }
        ]
      }
    }
  }
}
```

但是能看出来，这样做需要一个系统帮助你写 `query`，很多框架都提供这个功能，比如 [apollo-client](https://github.com/apollographql/apollo-client)。

### Webhooks

如果说 GraphQL 颠覆了前后端交互模式，那 Webhooks 可以说是彻头彻尾的反模式了，因为其定义就是，前端不主动发送请求，完全由后端推送。

它最适合解决轮询问题。或者说轮询就是一种妥协的行为，当后端不支持 Webhooks 模式时。

使用举例：

Webhooks 本身也可以由 REST 或者 gRPC 实现，所以就不贴代码了。举个常用例子，比如你的好友发了一条朋友圈，后端将这条消息推送给所有其他好友的客户端，就是 Webhooks 的典型场景。

---

最后作者给出的结论是，这四个场景各有不同使用场景，无法相互替代：

- REST：无状态的数据传输结构，适用于通用、快速迭代和标准化语义的场景。
- gRPC：轻量的传输方式，特殊适合对性能高要求或者环境苛刻的场景，比如 IOT。
- GraphQL: 请求者可以自定义返回格式，某些程度上可以减少前后端联调成本。
- Webhooks: 推送服务，主要用于服务器主动更新客户端资源的场景。

## 3 精读

### REST 并非适用所有场景

本文给了我们一个更大的视角看待日常开发中的接口问题，对于奋战在一线的前端同学，接触到 90% 的接口都是非 REST 规则的 Http 接口，能真正落实 REST 的团队其实非常少。这其实暴露了一个重要问题，就是 REST 所带来的好处，在整套业务流程中到底占多大的比重？

不仅接口设计方案的使用要分场景，针对某个接口方案的重要性也要再继续细分：在做一个开放接口的项目，提供 Http 接口给第三方使用，这时必须好好规划接口的语义，所以更容易让大家达成一致使用 REST 约定；而开发一个产品时，其实前后端不关心接口格式是否规范，甚至在开发内网产品时，性能和冗余都不会考虑，效率放在了第一位。所以第一点启示是，不要埋冤当前团队业务为什么没有使用某个更好的接口约定，因为接口约定很可能是业务形态决定的，而不是凭空做技术对比从而决定的。

### gRPC 是服务端交互的首选

前端同学转 node 开发时，很喜欢用 Http 方式进行服务器间通讯，但可能会疑惑，为什么公司内部 Java 或者 C++ 写的服务都不提供 Http 方式调用，而是另外一个名字。了解 gRPC 后，可以认识到这些平台都是对 RPC 方式的封装，服务器间通信对性能和延时要求非常高，所以比较适合专门为性能优化的 gRPC 等服务。

### GraphQL 需要配套

GraphQL 不是 REST 的替代品，所以不要想着团队从 Http 接口迁移到 GraphQL 就能提升 X% 的开发效率。GraphQL 方案是一种新的前后端交互约定，所以上手成本会比较高，同时，为了方便前端同学拼 query，等于把一部分后端工作量转移给了前端，如果此时没有一个足够好用的平台快速查阅、生成、维护这些定义，开发效率可能不升反降。

总的来说，对外开放 API 或者拥有完整配套的场景，使用 GraphQL 是比较理想的，但对于快速迭代，平台又不够成熟的团队，继续使用标准 Http 接口可以更快完成项目。

### Webhooks 解决特殊场景问题

对于第三方平台验权、登陆等 **没有前端界面做中转的场景，或者强安全要求的支付场景等**，适合用 Webhooks 做数据主动推送。说白了就是在前端无从参与，或者因为前端安全问题不适合参与时，就是 Webhooks 的场景。很显然 Webhooks 也不是 Http 的替代品，不过的确是一种新的前后端交互方式。

对于慢查询等场景，前端普遍使用轮询完成，这和 Socket 相比体验更弱，但无状态的特性反而会降低服务器负担，所以慢查询和即时通讯要区分对待，用户对消息及时性的敏感程度决定了使用哪种方案。

## 4 总结

最后，上面总结的内容一定还有许多疏漏，欢迎补充。

## 5 更多讨论

> 讨论地址是：[精读《REST, GraphQL, Webhooks, & gRPC 如何选型》 · Issue #102 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/102)

**如果你想参与讨论，请[点击这里](https://github.com/dt-fe/weekly)，每周都有新的主题，周末或周一发布。**
