阅读 187

[API翻译]gRPC-Web。从REST+JSON走向类型安全的Web APIs

原文地址:www.improbable.io/blog/grpc-w…

原文作者:

发布时间:2017年4月26日

Michal Witkowski是Improbable在SpatialOS方面的主要技术负责人,Marcus Longmuir是Improbable Webtools团队的技术负责人。

REST+JSON是构建Web应用和API服务器之间交互的事实标准方式。然而,一旦你过了最初的简易原型阶段,它就会显示出维护手工制作的客户端代码、调试网络协议问题和缺乏类型安全的问题。这篇博文介绍了我们如何将微服务和客户端库的通用语言gRPC的使用扩展到浏览器Web Apps中使用。我们还将展示这一举措,以及我们对TypeScript的采用,如何使我们最终在类型安全的Web App中涅槃。

旁白:当初为什么要用gRPC?

在Improbable,我们正在构建SpatialOS,这是一个用于空间感知模拟的PaaS产品,它利用现有的模拟模型、游戏引擎和用多种语言编写的可视化客户端。在我们平台开发的早期,我们就决定了强类型化、良好文档化的API的重要性。最初,我们选择了REST和HAL,但是为大量的语言(C++、Java、Go、C#、Objective-C)重新实现和维护客户端的负担开始侵蚀我们做我们真正喜欢的事情的时间,即解决我们客户的实际问题,因此我们决定寻找其他方法。

大约在同一时间,Google开源了gRPC,这是他们内部的下一代RPC库,基于当时全新的HTTP2协议,支持用多种语言(C++、Java、Go、C#、Objective-C、NodeJS......)生成客户端和服务器存根的代码。后者完全符合我们的要求,经过初步的刺探和验证,我们决定在全公司范围内采用。很快,为gRPC服务提供模式的.proto定义就成为了思考服务的标准方式:它们是我们在设计讨论中首先汇聚在一起的东西,也是事物工作原理的规范文档。

这使得拥有不同专业领域(SDK、后端、CLI)和不同语言背景的团队可以用一种单一的方式来表达合同和期望。通过利用协议缓冲区的前后兼容,"我到底需不需要添加这个字段?"或 "这个字段接受什么?"的问题不再出现。

Web是个例外

由于不支持从浏览器使用gRPC,Web应用团队是这种文化转变的一个例外。同时,他们的目的是消费与我们的客户端和CLI工具相同的gRPC平台API,所以我们需要找到一种方法来暴露它们。

幸运的是,有了gRPC REST Gateway,存在一个基于.proto文件注释的gRPC apis的REST+JSON API绑定的代码生成器。我们采取的方法与这里描述的方法非常相似。

虽然这个解决方案使我们不必再手工编写API服务器端代码,但它仍然有许多缺点。我们的Web App团队仍然需要编写JSON解析代码和客户端对象表示,用正确的调用约定编写HTTP客户端库,偶尔还要偷看浏览器的Network Console来找出一个奇怪的网络错误。但更重要的是:新的API(方法、字段)只有在平台API重新生成并与gRPC REST网关一起重新部署时,Web Apps才能看到。这对于一个做独立和频繁推出的工程组织来说是一个巨大的障碍。

黑客和非黑客的原型。

我们开始考虑从我们的API堆栈中完全取消gRPC REST网关。它主要做了两件事:将protobuf消息翻译成JSON,并提供一个HTTP/1.1兼容的API和可选的RESTful语义。

这让我们想到:现代的浏览器是非常......现代的。它们当然可以处理原始字节的protobuf,而不仅仅是JSON,而且它们也会说HTTP2。为什么不能让浏览器说protobuf二进制,以及基于HTTP2的gRPC本身呢?

于是,在2016年不太阳光明媚的伦敦夏天即将结束的时候,我们决定把事情做出来:使用官方的proto3 JavaScript codegen,对Fetch API进行干净的改造,再加上一点Go端的HTTP2黑客技术......我们让一个浏览器对一个基于Go的gRPC服务器说gRPC。

后来我们了解到,Google的gRPC团队正在内部开发gRPC-Web规范。他们的方法与我们的方法非常相似,于是我们决定将我们的经验贡献给上游的gRPC-Web规范(目前处于早期访问模式,仍有可能改变)。

同时,我们将所有的Web应用重新基于TypeScript。尽管最初的高峰确认了这一努力的价值,但只有在我们完成迁移后,才发现我们低估了编译时类型验证将给我们的Web应用团队带来的长期生产力提升。

适用于TypeScript和Go的gRPC-Web。

所以在这个时候,我们已经准备好扣动扳机,将我们的原型生产化。我们的目标很简单:在现代类型安全的Web App(主要是TypeScript)中轻松地使用gRPC APIs(主要用Go编写)。

我们构建了一个生产就绪的实现,其中有四个组件是实现这个目标所必需的。

  • grpcweb - 一个Go包,它包裹了一个现有的Go gRPC服务器,并使其成为gRPC-Web。
  • grpcwebproxy - 一个用于传统gRPC服务器的gRPC-Web独立反向代理(如Java或C++)。
  • ts-protoc-gen - 协议缓冲编译器(protoc)的TypeScript插件,为上游protoc生成的标准JavaScript对象生成TypeScript服务定义和TypeScript声明。
  • grpc-web-client - 一个用于浏览器的TypeScript gRPC-Web客户端库,它从用户和代码生成的类中抽象出网络(Fetch API或XHR)。

image.png

有了这些组件,我们服务的.proto定义会自动生成使用我们平台API所需的TypeScript对象,所有的TypeScript类型安全荣耀都从.proto文件中保留下来。此外,grpc-web-client是为了向后兼容。它不仅支持非Fetch API浏览器,而且如果需要的话,还可以回落到HTTP1.1。目前我们的测试矩阵可以一直追溯到。

  • IE10+
  • Edge 13+
  • Firefox 38+
  • Chrome 41+
  • Safari 8+

给我画一张代码图

假设你使用下面的.proto文件为一个假设的BookService定义一个RPC和一个服务器流RPC。

image.png

import pb_library "../_proto/examplecom/library"

type bookService struct{
books []*pb_library.Book
}

func (s *bookService) GetBook(ctx context.Context, bookQuery *pb_library.GetBookRequest) (*pb_library.Book, error) {
    for _, book := range s.books {
        if book.Isbn == bookQuery.Isbn {
            return book, nil
        }
    }
    return nil, grpc.Errorf(codes.NotFound, "Book could not be found")
}

func (s *bookService) QueryBooks(bookQuery *pb_library.QueryBooksRequest, stream pb_library.BookService_QueryBooksServer) error {
    for _, book := range s.books {
        if strings.HasPrefix(s.book.Author, bookQuery.AuthorPrefix) {
            stream.Send(book)
        }
    }
    return nil
}
复制代码

在代码生成JavaScript/TypeScript消息和方法存根后,你可以使用下面的代码示例片段从浏览器中调用代码。

import {grpc, BrowserHeaders} from "grpc-web-client";

// Import code-generated data structures.
import {BookService} from "../_proto/examplecom/library/book_service_pb_service";
import {QueryBooksRequest, Book, GetBookRequest} from "../_proto/examplecom/library/book_service_pb";

const queryBooksRequest = new QueryBooksRequest();
queryBooksRequest.setAuthorPrefix("Geor");

grpc.invoke(BookService.QueryBooks, {
  request: queryBooksRequest,
  host: "https://my.grpc.server.example.com",
  onMessage: (message: Book) => {
    console.log("got book: ", message.toObject());
  },
  onEnd: (code: grpc.Code, msg: string | undefined, trailers: BrowserHeaders) => {
    if code == grpc.Code.OK { console.log("all ok") } else { console.log("hit an error", code, msg, trailers); }
  }
});
复制代码

Web App开发的未来

尽管我们开发gRPC-Web的动机主要是为了消除不断重建和重新部署gRPC REST网关的需求,但我们认为我们已经偶然发现了一个潜在的Web App开发游戏规则。我们使用gRPC Web的经验对我们的Web App团队来说是一个变革。

  • 不再需要寻找API文档--.proto是API合同的标准格式,就像在其他团队中一样
  • 不再有手工制作的JSON调用对象--所有的请求和响应都是强类型化和代码生成的,并在IDE中提供提示。
  • 不再需要处理方法、头文件、主体和低级网络--一切都在grpc-web-client中被处理和抽象化了。
  • 不再猜测HTTP错误代码的含义--gRPC状态代码是表示API中问题的一种规范方式。
  • 在服务器上不再有手工制作的大块编码流媒体的疯狂,gRPC-Web支持1:1的RPC和1:多的服务器端流媒体请求。
  • 当推出新的二进制文件时,不再有数据解析错误--协议缓冲区保证了请求和响应的前后兼容性

简而言之,gRPC Web将前端代码和微服务之间的交互从手工制作的HTTP请求领域转移到定义良好的用户逻辑方法上。这对我们的Web App团队的生产力是一个巨大的福音:他们可以专注于构建有价值的客户端逻辑,而不是手工制作REST客户端。

开源和贡献

为了表彰开源技术对Improbable的成功所做的贡献,我们决定不仅仅是通过错误报告和补丁来回馈。Improbable Engineering现在有一个GitHub组织 improbable-eng,我们将在这里开源我们有趣的技术, improbable-eng/grpc-web只是其中之一。

值得注意的是 improbable-eng/grpc-web 并不是一个正式的实现。然而,我们期待着与云原生计算基金会(CNCF)旗下的gRPC团队合作,并期待着在上游贡献TypeScript协议缓冲区插件、TypeScript gRPC客户端库和Go中间件。

听起来很有趣?查看我们当前的工程角色,包括建立gRPC-Web库的Web和基础设施团队。

我们制造SpatialOS - 了解更多关于我们平台的信息。


通过www.DeepL.com/Translator(免费版)翻译

文章分类
前端
文章标签