TypeScript の型定義を観点に gRPC-Web の実装ライブラリを比較する
gRPC-Web の実装を調べてみると、どうやら複数の実装があるみたいです。 それぞれ関連ライブラリの組み合わせが決まっており、きちんと区別しないと混乱しそうなのでまとめてみます。
比較のために作成したプロジェクトは以下に置いてあります。
gRPC-Web の実装を簡易比較
- grpc/grpc-web (star: 700)
- grpc のオーガニゼーション(以下本家)にある...と思ってこれを選ぶとまだ成熟しておらず後悔する
- 特に TypeScript の型定義は完全にサポートされていない
- アクセッサの戻り型が
{}
で潰されていたりする
- アクセッサの戻り型が
- improbable-eng/grpc-web (star: 1.7k)
- 本家よりも勢いがあって利用者も多そう
- TypeScript の型定義は本家よりも充実している
なので gRPC の本家本元のリポジトリにも grpc/grpc-web という実装はあるけれど、 improbable-eng/grpc-web の方が人気があり実装状況もマルという状況ですね。
以下、両者を細かく見ていきます。 比較の題材にした Protobuf 定義は以下の通りです。
syntax = "proto3"; package hello; service Greeting { rpc Hello(HelloRequest) returns (HelloResponse) {} } message HelloRequest { string message = 1; } message HelloResponse { string message = 1; Profile profile = 2; } message Profile { string name = 1; }
grpc/grpc-web
本家お膝元のリポジトリです。 しかしもう一つのリポジトリと比較すると実装に開きがあります。
- protoc プラグイン: protoc-gen-grpc-web
- web ランタイム: npm/grpc-web
protoc \ --js_out=import_style=commonjs:${OUT_DIR} \ --grpc-web_out=import_style=commonjs,mode=grpcwebtext:${OUT_DIR} \ hello.proto
のように生成します。 これで以下のようなファイルが生成されます。
out/grpc-web-by-grpc/proto ├── hello_grpc_web_pb.d.ts ├── hello_grpc_web_pb.js ├── hello_pb.d.ts └── hello_pb.js
上で書いた「TypeScript の型が {}
で潰される」とは以下のように Profile 型が返ってきてほしいところが {}
になっているところを指しています。
export class HelloResponse { constructor (); getMessage(): string; setMessage(a: string): void; getProfile(): {}; setProfile(a: {}): void; serializeBinary(): Uint8Array; static deserializeBinary: (bytes: {}) => HelloResponse; }
おそらくまだ実装途中なのだと思います。
toObject
の型が定義されていないのも物足りないところですね。
improbable-eng/grpc-web
本家よりも勢いのあるリポジトリです。
- protoc プラグイン: protoc-gen-ts (ts-protoc-gen)
- web ランタイム: npm/grpc-web-client
protoc \ --js_out="import_style=commonjs,binary:${OUT_DIR}" \ --ts_out="service=true:${OUT_DIR}" \ hello.proto
のように生成します。これで以下のようなファイルが生成されます。
out/grpc-web-by-improbable-eng/proto ├── hello_pb.d.ts ├── hello_pb.js ├── hello_pb_service.d.ts └── hello_pb_service.js
微妙にファイルの命名が違っていますが、hello_pb.js の型付けのために hello_pb.d.ts が生成されているのは同じですね。
ただこちらの方が Message
の型付け度合いは以下のように充実しています。
export class HelloResponse extends jspb.Message { getMessage(): string; setMessage(value: string): void; hasProfile(): boolean; clearProfile(): void; getProfile(): Profile | undefined; setProfile(value?: Profile): void; serializeBinary(): Uint8Array; toObject(includeInstance?: boolean): HelloResponse.AsObject; static toObject(includeInstance: boolean, msg: HelloResponse): HelloResponse.AsObject; static extensions: {[key: number]: jspb.ExtensionFieldInfo<jspb.Message>}; static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo<jspb.Message>}; static serializeBinaryToWriter(message: HelloResponse, writer: jspb.BinaryWriter): void; static deserializeBinary(bytes: Uint8Array): HelloResponse; static deserializeBinaryFromReader(message: HelloResponse, reader: jspb.BinaryReader): HelloResponse; }
toObject
にも型が設定されているのが嬉しいですね。現状で選ぶならこちら一択だと思います。
protoc のオプション js_out の提供元
余談的なものになりますが、ts-protoc-gen や protoc-gen-grpc-web を比較しているときに両者が共通して利用している --js_out が気になりました。
protoc
のデフォルトのオプションに含まれているので、元から含まれているものでしょうか。
となると JavaScript のコードジェネレータの実装も protoc に含まれているということなのでしょうか。
https://github.com/protocolbuffers/protobuf/tree/master/js がそれっぽいですね。
ただ gRPC のコードは含まれておらず単純にバイナリと JavaScript のデータの変換処理のみが行われているようです。 具体的には Protobuf の Message ごとに以下のメソッドを生やしているようです。
toObject
deserializeBinary
deserializeBinaryFromReader
serializeBinary
serializeBinaryToWriter
これ以外の処理は生成されないようなので完全にデータ変換のみの実装ですね。 なので protoc に含まれている JavaScript のコードジェネレータで生成されない gRPC 関係の service/server/client などの実装は ts-protoc-gen や protoc-gen-grpc-web などが独自に生成している、という理解をしました。