connect-webやってみた
connect-web の記事が、はてブでトレンドになっていました。気になったので、試してみました。
サンプルコードは、次のリポジトリに置いています。
前置き: gRPC と connect-web の雑な理解
※ このあたり理解がめちゃ浅いです
RPC (Remote Procedure Call) を実現するためのプロトコルとして、gRPC があります。
このプロトコルは、ブラウザ側からは使えない(?)ため、gRPC-Web というブラウザ向けの gRPC というものを使うことになります。
その場合、ブラウザとサーバーとの間に、プロキシを建てる必要があるようです。(たぶん)
そこで、Connect という gRPC 互換の HTTP API を構築するためのライブラリ群が開発されました。
これのおかげで、プロキシを建てる必要がなく、ブラウザ側から gRPC を使うことが可能になります。
上記ページに、バックエンドは connect-go、フロントエンドは connect-web という項目があります。
connect-web は、ブラウザから RPC を動かすための小さなライブラリです。タイプセーフなライブラリなため、
型補完が効きます。
connect-go は、go で Connect のサービスを作ることができます。
そのため、フロントエンドの開発は、connect-web を使うことになります。
以降は、フロントエンドの作業を、紹介します。ちなみに、React を使います。
やってみた
フロントエンド側は、主に、次の 2 つの作業になります。
- Protocol Buffer スキーマから TypeScript ファイルを生成
- 生成された TypeScript ファイルから gRPC クライアントを実装
1. Protocol Buffer スキーマから TypeScript ファイルを生成
gRPC で通信するためのスキーマ、ProtocolBuffer スキーマが必要です。
これは、すでにあるものを使います。
具体的には、次のようなスキーマです。
syntax = "proto3";
service ElizaService {
rpc Say(SayRequest) returns (SayResponse) {}
}
message SayRequest {
string sentence = 1;
}
message SayResponse {
string sentence = 1;
}
TypeScript コードを生成するために、buf
という CLI を使います。
buf で利用する、次の定義ファイルを書きます。
# buf.gen.yaml
# buf.gen.yaml defines a local generation template.
# For details, see https://docs.buf.build/configuration/v1/buf-gen-yaml
version: v1
plugins:
- name: es
path: node_modules/.bin/protoc-gen-es
out: gen
# With target=ts, we generate TypeScript files.
# Use target=js+dts to generate JavaScript and TypeScript declaration files
# like remote generation does.
opt: target=ts
- name: connect-web
path: node_modules/.bin/protoc-gen-connect-web
out: gen
# With target=ts, we generate TypeScript files.
opt: target=ts
これは、後述する buf generate
するときにどういう出力をするかの設定情報です。
codegen の yaml ファイルみたいなものかなと思います。
これを動かすために、次の module をインストールしましょう。
# plugin
yarn add --dev @bufbuild/protoc-gen-connect-web @bufbuild/protoc-gen-es
# runtime
yarn add @bufbuild/connect-web @bufbuild/protobuf
- plugin
- protoc-gen-es
- リクエストやレスポンスメッセージのような基本型を生成
- protoc-gen-connect-web
- Protocol Buffer スキーマからサービスを生成
- protoc-gen-es
- runtime
- bufbuild/connect-web
- Connect および gRPC-web プロトコルのクライアントを提供
- bufbuild/protobuf
- 基本型に対するシリアライズなどを提供
- bufbuild/connect-web
次に、buf
をインストールしましょう。
私は、brew でインストールしました。
brew install bufbuild/buf/buf
# ref: https://github.com/bufbuild/buf#installation
では、ProtocolBuffer スキーマから TypeScript ファイルを生成しましょう。
buf generate --template buf.gen.yaml buf.build/bufbuild/eliza
成功すると、次の 2 つの TypeScript ファイルが生成されます。
- gen/buf/connect/demo/eliza/v1/eliza_connectweb.ts
- gen/buf/connect/demo/eliza/v1/eliza_pb.ts
eliza_connectweb.ts
は、次のコードが含まれています。
// eliza_connectweb.ts
import { SayRequest, SayResponse } from "./eliza_pb.js";
import { MethodKind } from "@bufbuild/protobuf";
export const ElizaService = {
typeName: "ElizaService",
methods: {
say: {
name: "Say",
I: SayRequest,
O: SayResponse,
kind: MethodKind.Unary,
},
},
} as const;
eliza_pb.ts
は、次のコードが含まれています。
export class SayRequest extends Message<SayRequest> {
/**
* @generated from field: string sentence = 1;
*/
sentence = "";
constructor(data?: PartialMessage<SayRequest>) {
super();
proto3.util.initPartial(data, this);
}
static readonly runtime = proto3;
static readonly typeName = "buf.connect.demo.eliza.v1.SayRequest";
# ... 省略 ...
}
/**
* SayResponse describes the sentence responded by the ELIZA program.
*
* @generated from message buf.connect.demo.eliza.v1.SayResponse
*/
export class SayResponse extends Message<SayResponse> {
/**
* @generated from field: string sentence = 1;
*/
sentence = "";
constructor(data?: PartialMessage<SayResponse>) {
super();
proto3.util.initPartial(data, this);
}
static readonly runtime = proto3;
static readonly typeName = "buf.connect.demo.eliza.v1.SayResponse";
# ... 省略 ...
}
これで、準備はできました。
2. 生成された TypeScript ファイルから gRPC クライアントを実装
では、gRPC のクライアントを実装しましょう。
gRPC のクライント生成は、createPromiseClient
でできます。
生成時の引数に、サービスとトランスポート(?)というものを渡す必要があります。
コードを見たほうがわかりやすいと思うので、次のコードを見てください。
// client.ts
import { useMemo } from "react";
import { ServiceType } from "@bufbuild/protobuf";
import {
createConnectTransport,
createPromiseClient,
PromiseClient,
Transport,
} from "@bufbuild/connect-web";
const transport = createConnectTransport({
baseUrl: "https://demo.connect.build", # バックエンド側のURL
});
export function useClient<T extends ServiceType>(service: T): PromiseClient<T> {
return useMemo(() => createPromiseClient(service, transport), [service]);
}
このクライアントを、使ってみましょう。
// App.tsx
import { createConnectTransport, Interceptor } from "@bufbuild/connect-web";
import { ElizaService } from "../gen/buf/connect/demo/eliza/v1/eliza_connectweb";
import { useClient } from "./client";
function App() {
const client = useClient(ElizaService);
client
.say({
sentence: "hello",
})
.then(({ sentence }) => {
console.log(sentence);
});
// ...
}
このように、ProtocolBuffers の ElizaService が、型補完として使えるようになります。
良い感じです!
終わりに
意外とあっさり動いて、びっくりしました。
自分のブログの宣伝です、よければどうぞ。
Discussion